| // 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 <memory> |
| |
| #include "base/command_line.h" |
| #include "base/containers/flat_map.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/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/strings/string_piece.h" |
| #include "base/task/post_task.h" |
| #include "base/task/task_traits.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/metrics/persistent_system_profile.h" |
| #include "components/prefs/pref_registry_simple.h" |
| #include "components/prefs/pref_service.h" |
| |
| namespace metrics { |
| |
| namespace { |
| |
| // These structures provide values used to define how files are opened and |
| // accessed. It obviates the need for multiple code-paths within several of |
| // the methods. |
| struct SourceOptions { |
| // The flags to be used to open a file on disk. |
| int file_open_flags; |
| |
| // The access mode to be used when mapping a file into memory. |
| base::MemoryMappedFile::Access memory_mapped_access; |
| |
| // Indicates if the file is to be accessed read-only. |
| bool is_read_only; |
| }; |
| |
| enum : int { |
| // Opening a file typically requires at least these flags. |
| STD_OPEN = base::File::FLAG_OPEN | base::File::FLAG_READ, |
| }; |
| |
| constexpr SourceOptions kSourceOptions[] = { |
| // SOURCE_HISTOGRAMS_ATOMIC_FILE |
| { |
| // Ensure that no other process reads this at the same time. |
| STD_OPEN | base::File::FLAG_EXCLUSIVE_READ, |
| base::MemoryMappedFile::READ_ONLY, |
| true |
| }, |
| // SOURCE_HISTOGRAMS_ATOMIC_DIR |
| { |
| // Ensure that no other process reads this at the same time. |
| STD_OPEN | base::File::FLAG_EXCLUSIVE_READ, |
| base::MemoryMappedFile::READ_ONLY, |
| true |
| }, |
| // SOURCE_HISTOGRAMS_ACTIVE_FILE |
| { |
| // Allow writing (updated "logged" values) to the file. |
| STD_OPEN | base::File::FLAG_WRITE, |
| base::MemoryMappedFile::READ_WRITE, |
| false |
| } |
| }; |
| |
| enum EmbeddedProfileResult : int { |
| EMBEDDED_PROFILE_ATTEMPT, |
| EMBEDDED_PROFILE_FOUND, |
| EMBEDDED_PROFILE_FALLBACK, |
| EMBEDDED_PROFILE_DROPPED, |
| EMBEDDED_PROFILE_ACTION_MAX |
| }; |
| |
| void RecordEmbeddedProfileResult(EmbeddedProfileResult result) { |
| UMA_HISTOGRAM_ENUMERATION("UMA.FileMetricsProvider.EmbeddedProfileResult", |
| result, EMBEDDED_PROFILE_ACTION_MAX); |
| } |
| |
| void DeleteFileWhenPossible(const base::FilePath& path) { |
| // Open (with delete) and then immediately close the file by going out of |
| // scope. This is the only cross-platform safe way to delete a file that may |
| // be open elsewhere, a distinct possibility given the asynchronous nature |
| // of the delete task. |
| base::File file(path, base::File::FLAG_OPEN | base::File::FLAG_READ | |
| base::File::FLAG_DELETE_ON_CLOSE); |
| } |
| |
| // A task runner to use for testing. |
| base::TaskRunner* g_task_runner_for_testing = nullptr; |
| |
| // Returns a task runner appropriate for running background tasks that perform |
| // file I/O. |
| scoped_refptr<base::TaskRunner> CreateBackgroundTaskRunner() { |
| if (g_task_runner_for_testing) |
| return scoped_refptr<base::TaskRunner>(g_task_runner_for_testing); |
| |
| return base::CreateTaskRunnerWithTraits( |
| {base::MayBlock(), base::TaskPriority::BEST_EFFORT, |
| base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN}); |
| } |
| |
| } // namespace |
| |
| // This structure stores all the information about the sources being monitored |
| // and their current reporting state. |
| struct FileMetricsProvider::SourceInfo { |
| SourceInfo(const Params& params) |
| : type(params.type), |
| association(params.association), |
| prefs_key(params.prefs_key), |
| filter(params.filter), |
| max_age(params.max_age), |
| max_dir_kib(params.max_dir_kib), |
| max_dir_files(params.max_dir_files) { |
| switch (type) { |
| case SOURCE_HISTOGRAMS_ACTIVE_FILE: |
| DCHECK(prefs_key.empty()); |
| FALLTHROUGH; |
| case SOURCE_HISTOGRAMS_ATOMIC_FILE: |
| path = params.path; |
| break; |
| case SOURCE_HISTOGRAMS_ATOMIC_DIR: |
| directory = params.path; |
| break; |
| } |
| } |
| ~SourceInfo() {} |
| |
| struct FoundFile { |
| base::FilePath path; |
| base::FileEnumerator::FileInfo info; |
| }; |
| using FoundFiles = base::flat_map<base::Time, FoundFile>; |
| |
| // How to access this source (file/dir, atomic/active). |
| const SourceType type; |
| |
| // With what run this source is associated. |
| const SourceAssociation association; |
| |
| // Where on disk the directory is located. This will only be populated when |
| // a directory is being monitored. |
| base::FilePath directory; |
| |
| // The files found in the above directory, ordered by last-modified. |
| std::unique_ptr<FoundFiles> found_files; |
| |
| // 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 filter callback for determining what to do with found files. |
| FilterCallback filter; |
| |
| // The maximum allowed age of a file. |
| base::TimeDelta max_age; |
| |
| // The maximum allowed bytes in a directory. |
| size_t max_dir_kib; |
| |
| // The maximum allowed files in a directory. |
| size_t max_dir_files; |
| |
| // 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 and assigned to an |allocator| object. |
| std::unique_ptr<base::PersistentHistogramAllocator> allocator; |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(SourceInfo); |
| }; |
| |
| FileMetricsProvider::Params::Params(const base::FilePath& path, |
| SourceType type, |
| SourceAssociation association, |
| base::StringPiece prefs_key) |
| : path(path), type(type), association(association), prefs_key(prefs_key) {} |
| |
| FileMetricsProvider::Params::~Params() {} |
| |
| FileMetricsProvider::FileMetricsProvider(PrefService* local_state) |
| : task_runner_(CreateBackgroundTaskRunner()), |
| pref_service_(local_state), |
| weak_factory_(this) { |
| base::StatisticsRecorder::RegisterHistogramProvider( |
| weak_factory_.GetWeakPtr()); |
| } |
| |
| FileMetricsProvider::~FileMetricsProvider() {} |
| |
| void FileMetricsProvider::RegisterSource(const Params& params) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| // Ensure that kSourceOptions has been filled for this type. |
| DCHECK_GT(arraysize(kSourceOptions), static_cast<size_t>(params.type)); |
| |
| std::unique_ptr<SourceInfo> source(new SourceInfo(params)); |
| |
| // |prefs_key| may be empty if the caller does not wish to persist the |
| // state across instances of the program. |
| if (pref_service_ && !params.prefs_key.empty()) { |
| source->last_seen = base::Time::FromInternalValue( |
| pref_service_->GetInt64(metrics::prefs::kMetricsLastSeenPrefix + |
| source->prefs_key)); |
| } |
| |
| switch (params.association) { |
| case ASSOCIATE_CURRENT_RUN: |
| case ASSOCIATE_INTERNAL_PROFILE: |
| sources_to_check_.push_back(std::move(source)); |
| break; |
| case ASSOCIATE_PREVIOUS_RUN: |
| case ASSOCIATE_INTERNAL_PROFILE_OR_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::SetTaskRunnerForTesting( |
| const scoped_refptr<base::TaskRunner>& task_runner) { |
| DCHECK(!g_task_runner_for_testing || !task_runner); |
| g_task_runner_for_testing = task_runner.get(); |
| } |
| |
| // static |
| void FileMetricsProvider::RecordAccessResult(AccessResult result) { |
| UMA_HISTOGRAM_ENUMERATION("UMA.FileMetricsProvider.AccessResult", result, |
| ACCESS_RESULT_MAX); |
| } |
| |
| // static |
| bool FileMetricsProvider::LocateNextFileInDirectory(SourceInfo* source) { |
| DCHECK_EQ(SOURCE_HISTOGRAMS_ATOMIC_DIR, source->type); |
| DCHECK(!source->directory.empty()); |
| |
| // Cumulative directory stats. These will remain zero if the directory isn't |
| // scanned but that's okay since any work they would cause to be done below |
| // would have been done during the first call where the directory was fully |
| // scanned. |
| size_t total_size_kib = 0; // Using KiB allows 4TiB even on 32-bit builds. |
| size_t file_count = 0; |
| |
| base::Time now_time = base::Time::Now(); |
| if (!source->found_files) { |
| source->found_files = std::make_unique<SourceInfo::FoundFiles>(); |
| base::FileEnumerator file_iter(source->directory, /*recursive=*/false, |
| base::FileEnumerator::FILES); |
| SourceInfo::FoundFile found_file; |
| |
| // Open the directory and find all the files, remembering the last-modified |
| // time of each. |
| for (found_file.path = file_iter.Next(); !found_file.path.empty(); |
| found_file.path = file_iter.Next()) { |
| found_file.info = file_iter.GetInfo(); |
| |
| // Ignore directories. |
| if (found_file.info.IsDirectory()) |
| continue; |
| |
| // Ignore temporary files. |
| base::FilePath::CharType first_character = |
| found_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 (found_file.path.Extension() != |
| base::PersistentMemoryAllocator::kFileExtension) { |
| continue; |
| } |
| |
| // Process real files. |
| total_size_kib += found_file.info.GetSize() >> 10; |
| base::Time modified = found_file.info.GetLastModifiedTime(); |
| if (modified > source->last_seen) { |
| // This file hasn't been read. Remember it (unless from the future). |
| if (modified <= now_time) |
| source->found_files->emplace(modified, std::move(found_file)); |
| ++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(found_file.path, /*recursive=*/false); |
| } |
| } |
| } |
| |
| // Filter files from the front until one is found for processing. |
| bool have_file = false; |
| while (!source->found_files->empty()) { |
| SourceInfo::FoundFile found = |
| std::move(source->found_files->begin()->second); |
| source->found_files->erase(source->found_files->begin()); |
| |
| bool too_many = |
| source->max_dir_files > 0 && file_count > source->max_dir_files; |
| bool too_big = |
| source->max_dir_kib > 0 && total_size_kib > source->max_dir_kib; |
| bool too_old = |
| source->max_age != base::TimeDelta() && |
| now_time - found.info.GetLastModifiedTime() > source->max_age; |
| if (too_many || too_big || too_old) { |
| base::DeleteFile(found.path, /*recursive=*/false); |
| --file_count; |
| total_size_kib -= found.info.GetSize() >> 10; |
| RecordAccessResult(too_many ? ACCESS_RESULT_TOO_MANY_FILES |
| : too_big ? ACCESS_RESULT_TOO_MANY_BYTES |
| : ACCESS_RESULT_TOO_OLD); |
| continue; |
| } |
| |
| AccessResult result = HandleFilterSource(source, found.path); |
| if (result == ACCESS_RESULT_SUCCESS) { |
| source->path = std::move(found.path); |
| have_file = true; |
| break; |
| } |
| |
| // Record the result. Success will be recorded by the caller. |
| if (result != ACCESS_RESULT_THIS_PID) |
| RecordAccessResult(result); |
| } |
| |
| return have_file; |
| } |
| |
| // static |
| void FileMetricsProvider::FinishedWithSource(SourceInfo* source, |
| AccessResult result) { |
| // Different source types require different post-processing. |
| switch (source->type) { |
| case SOURCE_HISTOGRAMS_ATOMIC_FILE: |
| case SOURCE_HISTOGRAMS_ATOMIC_DIR: |
| // Done with this file so delete the allocator and its owned file. |
| source->allocator.reset(); |
| // Remove the file if has been recorded. This prevents them from |
| // accumulating or also being recorded by different instances of |
| // the browser. |
| if (result == ACCESS_RESULT_SUCCESS || |
| result == ACCESS_RESULT_NOT_MODIFIED || |
| result == ACCESS_RESULT_MEMORY_DELETED || |
| result == ACCESS_RESULT_TOO_OLD) { |
| DeleteFileWhenPossible(source->path); |
| } |
| break; |
| case SOURCE_HISTOGRAMS_ACTIVE_FILE: |
| // Keep the allocator open so it doesn't have to be re-mapped each |
| // time. This also allows the contents to be merged on-demand. |
| break; |
| } |
| } |
| |
| // static |
| void FileMetricsProvider::CheckAndMergeMetricSourcesOnTaskRunner( |
| 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; |
| do { |
| result = CheckAndMapMetricSource(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 && |
| result != ACCESS_RESULT_THIS_PID) { |
| RecordAccessResult(result); |
| } |
| |
| // If there are no files (or no more files) in this source, stop now. |
| if (result == ACCESS_RESULT_DOESNT_EXIST) |
| break; |
| |
| // Mapping was successful. Merge it. |
| if (result == ACCESS_RESULT_SUCCESS) { |
| // Metrics associated with internal profiles have to be fetched directly |
| // so just keep the mapping for use by the main thread. |
| if (source->association == ASSOCIATE_INTERNAL_PROFILE) |
| break; |
| |
| MergeHistogramDeltasFromSource(source.get()); |
| DCHECK(source->read_complete); |
| } |
| |
| // All done with this source. |
| FinishedWithSource(source.get(), result); |
| |
| // If it's a directory, keep trying until a file is successfully opened. |
| // When there are no more files, ACCESS_RESULT_DOESNT_EXIST will be |
| // returned and the loop will exit above. |
| } while (result != ACCESS_RESULT_SUCCESS && !source->directory.empty()); |
| |
| // If the set of known files is empty, clear the object so the next run |
| // will do a fresh scan of the directory. |
| if (source->found_files && source->found_files->empty()) |
| source->found_files.reset(); |
| } |
| } |
| |
| // 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::CheckAndMapMetricSource( |
| SourceInfo* source) { |
| // If source was read, clean up after it. |
| if (source->read_complete) |
| FinishedWithSource(source, ACCESS_RESULT_SUCCESS); |
| source->read_complete = false; |
| DCHECK(!source->allocator); |
| |
| // If the source is a directory, look for files within it. |
| if (!source->directory.empty() && !LocateNextFileInDirectory(source)) |
| return ACCESS_RESULT_DOESNT_EXIST; |
| |
| // Do basic validation on the file metadata. |
| 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; |
| if (source->max_age != base::TimeDelta() && |
| base::Time::Now() - info.last_modified > source->max_age) { |
| return ACCESS_RESULT_TOO_OLD; |
| } |
| |
| // Non-directory files still need to be filtered. |
| if (source->directory.empty()) { |
| AccessResult result = HandleFilterSource(source, source->path); |
| if (result != ACCESS_RESULT_SUCCESS) |
| return result; |
| } |
| |
| // A new file of metrics has been found. |
| base::File file(source->path, kSourceOptions[source->type].file_open_flags); |
| if (!file.IsValid()) |
| return ACCESS_RESULT_NO_OPEN; |
| |
| std::unique_ptr<base::MemoryMappedFile> mapped(new base::MemoryMappedFile()); |
| if (!mapped->Initialize(std::move(file), |
| kSourceOptions[source->type].memory_mapped_access)) { |
| 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. |
| const bool read_only = kSourceOptions[source->type].is_read_only; |
| if (!base::FilePersistentMemoryAllocator::IsFileAcceptable(*mapped, |
| read_only)) { |
| return ACCESS_RESULT_INVALID_CONTENTS; |
| } |
| |
| // Map the file and validate it. |
| std::unique_ptr<base::PersistentMemoryAllocator> memory_allocator = |
| std::make_unique<base::FilePersistentMemoryAllocator>( |
| std::move(mapped), 0, 0, base::StringPiece(), read_only); |
| if (memory_allocator->GetMemoryState() == |
| base::PersistentMemoryAllocator::MEMORY_DELETED) { |
| return ACCESS_RESULT_MEMORY_DELETED; |
| } |
| if (memory_allocator->IsCorrupt()) |
| return ACCESS_RESULT_DATA_CORRUPTION; |
| |
| // Create an allocator for the mapped file. Ownership passes to the allocator. |
| source->allocator = std::make_unique<base::PersistentHistogramAllocator>( |
| std::move(memory_allocator)); |
| |
| // Check that an "independent" file has the necessary information present. |
| if (source->association == ASSOCIATE_INTERNAL_PROFILE && |
| !PersistentSystemProfile::GetSystemProfile( |
| *source->allocator->memory_allocator(), nullptr)) { |
| return ACCESS_RESULT_NO_PROFILE; |
| } |
| |
| return ACCESS_RESULT_SUCCESS; |
| } |
| |
| // static |
| void FileMetricsProvider::MergeHistogramDeltasFromSource(SourceInfo* source) { |
| DCHECK(source->allocator); |
| SCOPED_UMA_HISTOGRAM_TIMER("UMA.FileMetricsProvider.SnapshotTime.File"); |
| base::PersistentHistogramAllocator::Iterator histogram_iter( |
| source->allocator.get()); |
| |
| const bool read_only = kSourceOptions[source->type].is_read_only; |
| int histogram_count = 0; |
| while (true) { |
| std::unique_ptr<base::HistogramBase> histogram = histogram_iter.GetNext(); |
| if (!histogram) |
| break; |
| if (read_only) { |
| source->allocator->MergeHistogramFinalDeltaToStatisticsRecorder( |
| histogram.get()); |
| } else { |
| source->allocator->MergeHistogramDeltaToStatisticsRecorder( |
| histogram.get()); |
| } |
| ++histogram_count; |
| } |
| |
| source->read_complete = true; |
| DVLOG(1) << "Reported " << histogram_count << " histograms from " |
| << source->path.value(); |
| } |
| |
| // static |
| void FileMetricsProvider::RecordHistogramSnapshotsFromSource( |
| base::HistogramSnapshotManager* snapshot_manager, |
| SourceInfo* source) { |
| DCHECK_NE(SOURCE_HISTOGRAMS_ACTIVE_FILE, source->type); |
| |
| 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; |
| snapshot_manager->PrepareFinalDelta(histogram.get()); |
| ++histogram_count; |
| } |
| |
| source->read_complete = true; |
| DVLOG(1) << "Reported " << histogram_count << " histograms from " |
| << source->path.value(); |
| } |
| |
| FileMetricsProvider::AccessResult FileMetricsProvider::HandleFilterSource( |
| SourceInfo* source, |
| const base::FilePath& path) { |
| if (!source->filter) |
| return ACCESS_RESULT_SUCCESS; |
| |
| // Alternatively, pass a Params object to the filter like what was originally |
| // used to configure the source. |
| // Params params(path, source->type, source->association, source->prefs_key); |
| FilterAction action = source->filter.Run(path); |
| switch (action) { |
| case FILTER_PROCESS_FILE: |
| // Process the file. |
| return ACCESS_RESULT_SUCCESS; |
| |
| case FILTER_ACTIVE_THIS_PID: |
| // Even the file for the current process has to be touched or its stamp |
| // will be less than "last processed" and thus skipped on future runs, |
| // even those done by new instances of the browser if a pref key is |
| // provided so that the last-uploaded stamp is recorded. |
| case FILTER_TRY_LATER: { |
| // Touch the file with the current timestamp making it (presumably) the |
| // newest file in the directory. |
| base::Time now = base::Time::Now(); |
| base::TouchFile(path, /*accessed=*/now, /*modified=*/now); |
| if (action == FILTER_ACTIVE_THIS_PID) |
| return ACCESS_RESULT_THIS_PID; |
| return ACCESS_RESULT_FILTER_TRY_LATER; |
| } |
| |
| case FILTER_SKIP_FILE: |
| switch (source->type) { |
| case SOURCE_HISTOGRAMS_ATOMIC_FILE: |
| case SOURCE_HISTOGRAMS_ATOMIC_DIR: |
| // Only "atomic" files are deleted (best-effort). |
| DeleteFileWhenPossible(path); |
| break; |
| case SOURCE_HISTOGRAMS_ACTIVE_FILE: |
| // File will presumably get modified elsewhere and thus tried again. |
| break; |
| } |
| return ACCESS_RESULT_FILTER_SKIP_FILE; |
| } |
| |
| // Code never gets here but some compilers don't realize that and so complain |
| // that "not all control paths return a value". |
| NOTREACHED(); |
| return ACCESS_RESULT_SUCCESS; |
| } |
| |
| void FileMetricsProvider::ScheduleSourcesCheck() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| 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::BindOnce( |
| &FileMetricsProvider::CheckAndMergeMetricSourcesOnTaskRunner, |
| base::Unretained(check_list)), |
| base::BindOnce(&FileMetricsProvider::RecordSourcesChecked, |
| weak_factory_.GetWeakPtr(), base::Owned(check_list))); |
| } |
| |
| void FileMetricsProvider::RecordSourcesChecked(SourceInfoList* checked) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| // Sources that still have an allocator at this point are read/write "active" |
| // files that may need their contents merged on-demand. If there is no |
| // allocator (not a read/write file) but a read was done on the task-runner, |
| // try again immediately to see if more is available (in a directory of |
| // files). Otherwise, remember the source for checking again at a later time. |
| bool did_read = false; |
| for (auto iter = checked->begin(); iter != checked->end();) { |
| auto temp = iter++; |
| SourceInfo* source = temp->get(); |
| if (source->read_complete) { |
| RecordSourceAsRead(source); |
| did_read = true; |
| } |
| if (source->allocator) { |
| if (source->association == ASSOCIATE_INTERNAL_PROFILE) { |
| sources_with_profile_.splice(sources_with_profile_.end(), *checked, |
| temp); |
| } else { |
| sources_mapped_.splice(sources_mapped_.end(), *checked, temp); |
| } |
| } else { |
| sources_to_check_.splice(sources_to_check_.end(), *checked, temp); |
| } |
| } |
| |
| // If a read was done, schedule another one immediately. In the case of a |
| // directory of files, this ensures that all entries get processed. It's |
| // done here instead of as a loop in CheckAndMergeMetricSourcesOnTaskRunner |
| // so that (a) it gives the disk a rest and (b) testing of individual reads |
| // is possible. |
| if (did_read) |
| ScheduleSourcesCheck(); |
| } |
| |
| void FileMetricsProvider::DeleteFileAsync(const base::FilePath& path) { |
| task_runner_->PostTask(FROM_HERE, |
| base::BindOnce(DeleteFileWhenPossible, path)); |
| } |
| |
| void FileMetricsProvider::RecordSourceAsRead(SourceInfo* source) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| // Persistently record the "last seen" timestamp of the source file to |
| // ensure that the file is never read again unless it is modified again. |
| if (pref_service_ && !source->prefs_key.empty()) { |
| pref_service_->SetInt64( |
| metrics::prefs::kMetricsLastSeenPrefix + source->prefs_key, |
| source->last_seen.ToInternalValue()); |
| } |
| } |
| |
| void FileMetricsProvider::OnDidCreateMetricsLog() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| // 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 a MayBlock() TaskRunner 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. |
| for (const std::unique_ptr<SourceInfo>& source : sources_for_previous_run_) |
| DeleteFileAsync(source->path); |
| sources_for_previous_run_.clear(); |
| } |
| |
| bool FileMetricsProvider::ProvideIndependentMetrics( |
| SystemProfileProto* system_profile_proto, |
| base::HistogramSnapshotManager* snapshot_manager) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| while (!sources_with_profile_.empty()) { |
| SourceInfo* source = sources_with_profile_.begin()->get(); |
| DCHECK(source->allocator); |
| |
| bool success = false; |
| RecordEmbeddedProfileResult(EMBEDDED_PROFILE_ATTEMPT); |
| if (PersistentSystemProfile::GetSystemProfile( |
| *source->allocator->memory_allocator(), system_profile_proto)) { |
| RecordHistogramSnapshotsFromSource(snapshot_manager, source); |
| success = true; |
| RecordEmbeddedProfileResult(EMBEDDED_PROFILE_FOUND); |
| } else { |
| RecordEmbeddedProfileResult(EMBEDDED_PROFILE_DROPPED); |
| |
| // TODO(bcwhite): Remove these once crbug/695880 is resolved. |
| |
| int histogram_count = 0; |
| base::PersistentHistogramAllocator::Iterator histogram_iter( |
| source->allocator.get()); |
| while (histogram_iter.GetNext()) { |
| ++histogram_count; |
| } |
| UMA_HISTOGRAM_COUNTS_10000( |
| "UMA.FileMetricsProvider.EmbeddedProfile.DroppedHistogramCount", |
| histogram_count); |
| } |
| |
| // Regardless of whether this source was successfully recorded, it is never |
| // read again. |
| source->read_complete = true; |
| RecordSourceAsRead(source); |
| sources_to_check_.splice(sources_to_check_.end(), sources_with_profile_, |
| sources_with_profile_.begin()); |
| ScheduleSourcesCheck(); |
| |
| if (success) |
| return true; |
| } |
| |
| return false; |
| } |
| |
| bool FileMetricsProvider::HasPreviousSessionData() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| // 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 = CheckAndMapMetricSource(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 then-previous run which would no longer |
| // be the run from which it came. |
| if (result != ACCESS_RESULT_SUCCESS) { |
| DCHECK(!source->allocator); |
| RecordSourceAsRead(source); |
| DeleteFileAsync(source->path); |
| sources_for_previous_run_.erase(temp); |
| continue; |
| } |
| |
| DCHECK(source->allocator); |
| |
| // If the source should be associated with an existing internal profile, |
| // move it to |sources_with_profile_| for later upload. |
| if (source->association == ASSOCIATE_INTERNAL_PROFILE_OR_PREVIOUS_RUN) { |
| if (PersistentSystemProfile::HasSystemProfile( |
| *source->allocator->memory_allocator())) { |
| RecordEmbeddedProfileResult(EMBEDDED_PROFILE_ATTEMPT); |
| RecordEmbeddedProfileResult(EMBEDDED_PROFILE_FALLBACK); |
| sources_with_profile_.splice(sources_with_profile_.end(), |
| sources_for_previous_run_, temp); |
| } |
| } |
| } |
| |
| return !sources_for_previous_run_.empty(); |
| } |
| |
| void FileMetricsProvider::RecordInitialHistogramSnapshots( |
| base::HistogramSnapshotManager* snapshot_manager) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| 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->read_complete); |
| 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. |
| RecordSourceAsRead(source.get()); |
| } |
| } |
| |
| void FileMetricsProvider::MergeHistogramDeltas() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| for (std::unique_ptr<SourceInfo>& source : sources_mapped_) { |
| MergeHistogramDeltasFromSource(source.get()); |
| } |
| } |
| |
| } // namespace metrics |