blob: 5a6c14a255297d35b1612ba236d4ca9e3d2ba07f [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/metrics/file_metrics_provider.h"
#include "base/command_line.h"
#include "base/files/file.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_util.h"
#include "base/files/memory_mapped_file.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_base.h"
#include "base/metrics/histogram_macros.h"
#include "base/metrics/persistent_histogram_allocator.h"
#include "base/metrics/persistent_memory_allocator.h"
#include "base/task_runner.h"
#include "base/time/time.h"
#include "components/metrics/metrics_pref_names.h"
#include "components/metrics/metrics_service.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
namespace metrics {
// This structure stores all the information about the sources being monitored
// and their current reporting state.
struct FileMetricsProvider::SourceInfo {
SourceInfo(SourceType source_type) : type(source_type) {}
~SourceInfo() {}
// How to access this source (file/dir, atomic/active).
const SourceType type;
// Where on disk the directory is located. This will only be populated when
// a directory is being monitored.
base::FilePath directory;
// Where on disk the file is located. If a directory is being monitored,
// this will be updated for whatever file is being read.
base::FilePath path;
// Name used inside prefs to persistent metadata.
std::string prefs_key;
// The last-seen time of this source to detect change.
base::Time last_seen;
// Indicates if the data has been read out or not.
bool read_complete = false;
// Once a file has been recognized as needing to be read, it is |mapped|
// into memory. If that file is "atomic" then the data from that file
// will be copied to |data| and the mapped file released. If the file is
// "active", it remains mapped and nothing is copied to local memory.
std::vector<uint8_t> data;
std::unique_ptr<base::MemoryMappedFile> mapped;
std::unique_ptr<base::PersistentHistogramAllocator> allocator;
private:
DISALLOW_COPY_AND_ASSIGN(SourceInfo);
};
FileMetricsProvider::FileMetricsProvider(
const scoped_refptr<base::TaskRunner>& task_runner,
PrefService* local_state)
: task_runner_(task_runner),
pref_service_(local_state),
weak_factory_(this) {
}
FileMetricsProvider::~FileMetricsProvider() {}
void FileMetricsProvider::RegisterSource(const base::FilePath& path,
SourceType type,
SourceAssociation source_association,
const base::StringPiece prefs_key) {
DCHECK(thread_checker_.CalledOnValidThread());
std::unique_ptr<SourceInfo> source(new SourceInfo(type));
source->prefs_key = prefs_key.as_string();
switch (source->type) {
case SOURCE_HISTOGRAMS_ATOMIC_FILE:
source->path = path;
break;
case SOURCE_HISTOGRAMS_ATOMIC_DIR:
source->directory = path;
break;
}
// |prefs_key| may be empty if the caller does not wish to persist the
// state across instances of the program.
if (pref_service_ && !prefs_key.empty()) {
source->last_seen = base::Time::FromInternalValue(
pref_service_->GetInt64(metrics::prefs::kMetricsLastSeenPrefix +
source->prefs_key));
}
switch (source_association) {
case ASSOCIATE_CURRENT_RUN:
sources_to_check_.push_back(std::move(source));
break;
case ASSOCIATE_PREVIOUS_RUN:
DCHECK_EQ(SOURCE_HISTOGRAMS_ATOMIC_FILE, source->type);
sources_for_previous_run_.push_back(std::move(source));
break;
}
}
// static
void FileMetricsProvider::RegisterPrefs(PrefRegistrySimple* prefs,
const base::StringPiece prefs_key) {
prefs->RegisterInt64Pref(metrics::prefs::kMetricsLastSeenPrefix +
prefs_key.as_string(), 0);
}
// static
void FileMetricsProvider::CheckAndMapNewMetricSourcesOnTaskRunner(
SourceInfoList* sources) {
// This method has all state information passed in |sources| and is intended
// to run on a worker thread rather than the UI thread.
for (std::unique_ptr<SourceInfo>& source : *sources) {
AccessResult result = CheckAndMapNewMetrics(source.get());
// Some results are not reported in order to keep the dashboard clean.
if (result != ACCESS_RESULT_DOESNT_EXIST &&
result != ACCESS_RESULT_NOT_MODIFIED) {
UMA_HISTOGRAM_ENUMERATION(
"UMA.FileMetricsProvider.AccessResult", result, ACCESS_RESULT_MAX);
}
}
}
// This method has all state information passed in |source| and is intended
// to run on a worker thread rather than the UI thread.
// static
FileMetricsProvider::AccessResult FileMetricsProvider::CheckAndMapNewMetrics(
SourceInfo* source) {
DCHECK(!source->mapped);
DCHECK(source->data.empty());
if (!source->directory.empty() && !LocateNextFileInDirectory(source))
return ACCESS_RESULT_DOESNT_EXIST;
base::File::Info info;
if (!base::GetFileInfo(source->path, &info))
return ACCESS_RESULT_DOESNT_EXIST;
if (info.is_directory || info.size == 0)
return ACCESS_RESULT_INVALID_FILE;
if (source->last_seen >= info.last_modified)
return ACCESS_RESULT_NOT_MODIFIED;
// A new file of metrics has been found. Open it with exclusive access and
// map it into memory.
// TODO(bcwhite): Make this open read/write when supported for "active".
base::File file(source->path, base::File::FLAG_OPEN |
base::File::FLAG_READ |
base::File::FLAG_EXCLUSIVE_READ);
if (!file.IsValid())
return ACCESS_RESULT_NO_EXCLUSIVE_OPEN;
source->mapped.reset(new base::MemoryMappedFile());
if (!source->mapped->Initialize(std::move(file))) {
source->mapped.reset();
return ACCESS_RESULT_SYSTEM_MAP_FAILURE;
}
// Ensure any problems below don't occur repeatedly.
source->last_seen = info.last_modified;
// Test the validity of the file contents.
if (!base::FilePersistentMemoryAllocator::IsFileAcceptable(*source->mapped))
return ACCESS_RESULT_INVALID_CONTENTS;
switch (source->type) {
case SOURCE_HISTOGRAMS_ATOMIC_FILE:
case SOURCE_HISTOGRAMS_ATOMIC_DIR:
// For an "atomic" file, immediately copy the data into local memory
// and release the file so that it is not held open.
source->data.assign(source->mapped->data(),
source->mapped->data() + source->mapped->length());
source->mapped.reset();
break;
}
source->read_complete = false;
return ACCESS_RESULT_SUCCESS;
}
bool FileMetricsProvider::LocateNextFileInDirectory(SourceInfo* source) {
DCHECK_EQ(SOURCE_HISTOGRAMS_ATOMIC_DIR, source->type);
DCHECK(!source->directory.empty());
// Open the directory and find all the files, remembering the oldest that
// has not been read. They can be removed and/or ignored if they're older
// than the last-check time.
base::Time oldest_file_time = base::Time::Now();
base::FilePath oldest_file_path;
base::FilePath file_path;
int file_count = 0;
int delete_count = 0;
base::FileEnumerator file_iter(source->directory, /*recursive=*/false,
base::FileEnumerator::FILES);
for (file_path = file_iter.Next(); !file_path.empty();
file_path = file_iter.Next()) {
base::FileEnumerator::FileInfo file_info = file_iter.GetInfo();
// Ignore directories and zero-sized files.
if (file_info.IsDirectory() || file_info.GetSize() == 0)
continue;
// Ignore temporary files.
base::FilePath::CharType first_character =
file_path.BaseName().value().front();
if (first_character == FILE_PATH_LITERAL('.') ||
first_character == FILE_PATH_LITERAL('_')) {
continue;
}
// Ignore non-PMA (Persistent Memory Allocator) files.
if (file_path.Extension() !=
base::PersistentMemoryAllocator::kFileExtension) {
continue;
}
// Process real files.
base::Time modified = file_info.GetLastModifiedTime();
if (modified > source->last_seen) {
// This file hasn't been read. Remember it if it is older than others.
if (modified < oldest_file_time) {
oldest_file_path = std::move(file_path);
oldest_file_time = modified;
}
++file_count;
} else {
// This file has been read. Try to delete it. Ignore any errors because
// the file may be un-removeable by this process. It could, for example,
// have been created by a privileged process like setup.exe. Even if it
// is not removed, it will continue to be ignored bacuse of the older
// modification time.
base::DeleteFile(file_path, /*recursive=*/false);
++delete_count;
}
}
UMA_HISTOGRAM_COUNTS_100("UMA.FileMetricsProvider.DirectoryFiles",
file_count);
UMA_HISTOGRAM_COUNTS_100("UMA.FileMetricsProvider.DeletedFiles",
delete_count);
// Stop now if there are no files to read.
if (oldest_file_path.empty())
return false;
// Set the active file to be the oldest modified file that has not yet
// been read.
source->path = std::move(oldest_file_path);
return true;
}
void FileMetricsProvider::ScheduleSourcesCheck() {
DCHECK(thread_checker_.CalledOnValidThread());
if (sources_to_check_.empty())
return;
// Create an independent list of sources for checking. This will be Owned()
// by the reply call given to the task-runner, to be deleted when that call
// has returned. It is also passed Unretained() to the task itself, safe
// because that must complete before the reply runs.
SourceInfoList* check_list = new SourceInfoList();
std::swap(sources_to_check_, *check_list);
task_runner_->PostTaskAndReply(
FROM_HERE,
base::Bind(&FileMetricsProvider::CheckAndMapNewMetricSourcesOnTaskRunner,
base::Unretained(check_list)),
base::Bind(&FileMetricsProvider::RecordSourcesChecked,
weak_factory_.GetWeakPtr(), base::Owned(check_list)));
}
void FileMetricsProvider::RecordHistogramSnapshotsFromSource(
base::HistogramSnapshotManager* snapshot_manager,
SourceInfo* source) {
DCHECK(thread_checker_.CalledOnValidThread());
base::PersistentHistogramAllocator::Iterator histogram_iter(
source->allocator.get());
int histogram_count = 0;
while (true) {
std::unique_ptr<base::HistogramBase> histogram = histogram_iter.GetNext();
if (!histogram)
break;
switch (source->type) {
case SOURCE_HISTOGRAMS_ATOMIC_FILE:
case SOURCE_HISTOGRAMS_ATOMIC_DIR:
snapshot_manager->PrepareFinalDeltaTakingOwnership(
std::move(histogram));
break;
#if 0 // Not yet available.
case SOURCE_HISTOGRAMS_ACTIVE:
snapshot_manager->PrepareDeltaTakingOwnership(std::move(histogram));
#endif
}
++histogram_count;
}
DVLOG(1) << "Reported " << histogram_count << " histograms from "
<< source->path.value();
}
void FileMetricsProvider::CreateAllocatorForSource(SourceInfo* source) {
DCHECK(!source->allocator);
// File data was validated earlier. Files are not considered "untrusted"
// as some processes might be (e.g. Renderer) so there's no need to check
// again to try to thwart some malicious actor that may have modified the
// data between then and now.
if (source->mapped) {
DCHECK(source->data.empty());
// TODO(bcwhite): Make this do read/write when supported for "active".
source->allocator.reset(new base::PersistentHistogramAllocator(
base::WrapUnique(new base::FilePersistentMemoryAllocator(
std::move(source->mapped), 0, ""))));
} else {
DCHECK(!source->mapped);
source->allocator.reset(new base::PersistentHistogramAllocator(
base::WrapUnique(new base::PersistentMemoryAllocator(
&source->data[0], source->data.size(), 0, 0, "", true))));
}
}
void FileMetricsProvider::RecordSourcesChecked(SourceInfoList* checked) {
DCHECK(thread_checker_.CalledOnValidThread());
// Move each processed source to either the "to-read" list (for processing)
// or the "to-check" list (for future checking).
for (auto iter = checked->begin(); iter != checked->end();) {
auto temp = iter++;
const SourceInfo* source = temp->get();
if (source->mapped || !source->data.empty())
sources_to_read_.splice(sources_to_read_.end(), *checked, temp);
else
sources_to_check_.splice(sources_to_check_.end(), *checked, temp);
}
}
void FileMetricsProvider::RecordSourceAsSeen(SourceInfo* source) {
DCHECK(thread_checker_.CalledOnValidThread());
source->read_complete = true;
if (pref_service_ && !source->prefs_key.empty()) {
pref_service_->SetInt64(
metrics::prefs::kMetricsLastSeenPrefix + source->prefs_key,
source->last_seen.ToInternalValue());
}
}
void FileMetricsProvider::OnDidCreateMetricsLog() {
DCHECK(thread_checker_.CalledOnValidThread());
// Move finished metric sources back to list of monitored sources.
for (auto iter = sources_to_read_.begin(); iter != sources_to_read_.end();) {
auto temp = iter++;
SourceInfo* source = temp->get();
switch (source->type) {
// Atomic files are read once and then ignored unless they change.
case SOURCE_HISTOGRAMS_ATOMIC_FILE:
case SOURCE_HISTOGRAMS_ATOMIC_DIR:
if (source->read_complete) {
DCHECK(!source->mapped);
source->allocator.reset();
source->data.clear();
}
break;
}
if (!source->allocator && !source->mapped && source->data.empty())
sources_to_check_.splice(sources_to_check_.end(), sources_to_read_, temp);
}
// Schedule a check to see if there are new metrics to load. If so, they
// will be reported during the next collection run after this one. The
// check is run off of the worker-pool so as to not cause delays on the
// main UI thread (which is currently where metric collection is done).
ScheduleSourcesCheck();
// Clear any data for initial metrics since they're always reported
// before the first call to this method. It couldn't be released after
// being reported in RecordInitialHistogramSnapshots because the data
// will continue to be used by the caller after that method returns. Once
// here, though, all actions to be done on the data have been completed.
#if DCHECK_IS_ON()
for (const std::unique_ptr<SourceInfo>& source : sources_for_previous_run_)
DCHECK(source->read_complete);
#endif
sources_for_previous_run_.clear();
}
bool FileMetricsProvider::HasInitialStabilityMetrics() {
DCHECK(thread_checker_.CalledOnValidThread());
// Measure the total time spent checking all sources as well as the time
// per individual file. This method is called during startup and thus blocks
// the initial showing of the browser window so it's important to know the
// total delay.
SCOPED_UMA_HISTOGRAM_TIMER("UMA.FileMetricsProvider.InitialCheckTime.Total");
// Check all sources for previous run to see if they need to be read.
for (auto iter = sources_for_previous_run_.begin();
iter != sources_for_previous_run_.end();) {
SCOPED_UMA_HISTOGRAM_TIMER("UMA.FileMetricsProvider.InitialCheckTime.File");
auto temp = iter++;
SourceInfo* source = temp->get();
// This would normally be done on a background I/O thread but there
// hasn't been a chance to run any at the time this method is called.
// Do the check in-line.
AccessResult result = CheckAndMapNewMetrics(source);
UMA_HISTOGRAM_ENUMERATION("UMA.FileMetricsProvider.InitialAccessResult",
result, ACCESS_RESULT_MAX);
// If it couldn't be accessed, remove it from the list. There is only ever
// one chance to record it so no point keeping it around for later. Also
// mark it as having been read since uploading it with a future browser
// run would associate it with the previous run which would no longer be
// the run from which it came.
if (result != ACCESS_RESULT_SUCCESS) {
RecordSourceAsSeen(source);
sources_for_previous_run_.erase(temp);
}
}
return !sources_for_previous_run_.empty();
}
void FileMetricsProvider::RecordHistogramSnapshots(
base::HistogramSnapshotManager* snapshot_manager) {
DCHECK(thread_checker_.CalledOnValidThread());
// Measure the total time spent processing all sources as well as the time
// per individual file. This method is called on the UI thread so it's
// important to know how much total "jank" may be introduced.
SCOPED_UMA_HISTOGRAM_TIMER("UMA.FileMetricsProvider.SnapshotTime.Total");
for (std::unique_ptr<SourceInfo>& source : sources_to_read_) {
// Skip this source if the data has already been read.
if (source->read_complete)
continue;
SCOPED_UMA_HISTOGRAM_TIMER("UMA.FileMetricsProvider.SnapshotTime.File");
// If the source is mapped or loaded then it needs to have an allocator
// attached to it in order to read histograms out of it.
if (source->mapped || !source->data.empty())
CreateAllocatorForSource(source.get());
// A source should not be under "sources to read" unless it has an allocator
// or is memory-mapped (at which point it will have received an allocator
// above). However, if this method gets called twice before the scheduled-
// sources-check has a chance to clean up, this may trigger. This also
// catches the case where creating an allocator from the source has failed.
if (!source->allocator)
continue;
// Dump all histograms contained within the source to the snapshot-manager.
RecordHistogramSnapshotsFromSource(snapshot_manager, source.get());
// Update the last-seen time so it isn't read again unless it changes.
RecordSourceAsSeen(source.get());
}
}
void FileMetricsProvider::RecordInitialHistogramSnapshots(
base::HistogramSnapshotManager* snapshot_manager) {
DCHECK(thread_checker_.CalledOnValidThread());
// Measure the total time spent processing all sources as well as the time
// per individual file. This method is called during startup and thus blocks
// the initial showing of the browser window so it's important to know the
// total delay.
SCOPED_UMA_HISTOGRAM_TIMER(
"UMA.FileMetricsProvider.InitialSnapshotTime.Total");
for (const std::unique_ptr<SourceInfo>& source : sources_for_previous_run_) {
SCOPED_UMA_HISTOGRAM_TIMER(
"UMA.FileMetricsProvider.InitialSnapshotTime.File");
// The source needs to have an allocator attached to it in order to read
// histograms out of it.
DCHECK(source->mapped || !source->data.empty());
CreateAllocatorForSource(source.get());
DCHECK(source->allocator);
// Dump all histograms contained within the source to the snapshot-manager.
RecordHistogramSnapshotsFromSource(snapshot_manager, source.get());
// Update the last-seen time so it isn't read again unless it changes.
RecordSourceAsSeen(source.get());
}
}
} // namespace metrics