| // Copyright 2024 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/metrics/structured/arena_event_buffer.h" |
| |
| #include <iterator> |
| #include <memory> |
| #include <string_view> |
| #include <utility> |
| |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/system/sys_info.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/task/task_traits.h" |
| #include "base/task/thread_pool.h" |
| #include "base/types/expected.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "components/metrics/structured/histogram_util.h" |
| #include "components/metrics/structured/lib/histogram_util.h" |
| #include "components/metrics/structured/structured_metrics_features.h" |
| #include "third_party/protobuf/src/google/protobuf/message_lite.h" |
| |
| namespace metrics::structured { |
| namespace { |
| using google::protobuf::RepeatedPtrField; |
| |
| uint64_t GetFreeDiskSpace(const base::FilePath& path) { |
| if (int64_t size = base::SysInfo::AmountOfFreeDiskSpace(path); size != -1) { |
| return static_cast<uint64_t>(size); |
| } |
| return 0; |
| } |
| |
| base::expected<FlushedKey, FlushError> WriteEvents(const base::FilePath& path, |
| std::string content) { |
| if (!base::WriteFile(path, content)) { |
| if (GetFreeDiskSpace(path) < content.size()) { |
| return base::unexpected(kDiskFull); |
| } |
| // Leaving proto content intact to let the caller handle cleanup. |
| return base::unexpected(kWriteError); |
| } |
| |
| base::File::Info info; |
| if (!base::GetFileInfo(path, &info)) { |
| return base::unexpected(kWriteError); |
| } |
| |
| return FlushedKey{ |
| .size = static_cast<int64_t>(content.size()), |
| .path = path, |
| .creation_time = info.creation_time, |
| }; |
| } |
| |
| } // namespace |
| |
| ArenaEventBuffer::ArenaEventBuffer(const base::FilePath& path, |
| base::TimeDelta write_delay, |
| uint64_t max_size_bytes) |
| : EventBuffer(ResourceInfo(max_size_bytes)), |
| task_runner_(base::ThreadPool::CreateSequencedTaskRunner( |
| {base::TaskPriority::BEST_EFFORT, base::MayBlock(), |
| base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN})) { |
| events_ = std::make_unique<ArenaPersistentProto<EventsProto>>( |
| path, write_delay, |
| base::BindOnce(&ArenaEventBuffer::OnEventRead, |
| weak_factory_.GetWeakPtr()), |
| base::BindRepeating(&ArenaEventBuffer::OnEventWrite, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| ArenaEventBuffer::~ArenaEventBuffer() = default; |
| |
| Result ArenaEventBuffer::AddEvent(StructuredEventProto event) { |
| if (!proto()) { |
| pre_init_events_.emplace_back(std::move(event)); |
| return Result::kOk; |
| } |
| |
| const uint64_t event_size = EstimateEventSize(event); |
| |
| if (!resource_info_.HasRoom(event_size)) { |
| return Result::kFull; |
| } |
| |
| proto()->mutable_events()->Add(std::move(event)); |
| |
| resource_info_.Consume(event_size); |
| |
| // What would be a good heuristic here to determine if the buffer should |
| // flush. |
| // TODO(b/333938940): Investigate if using an event count is sufficient. If |
| // so, then we can produce the ShouldFlush result. |
| return Result::kOk; |
| } |
| |
| void ArenaEventBuffer::Purge() { |
| resource_info_.used_size_bytes = 0; |
| events_->Purge(); |
| } |
| |
| uint64_t ArenaEventBuffer::Size() { |
| return proto() ? proto()->events_size() : 0; |
| } |
| |
| RepeatedPtrField<StructuredEventProto> ArenaEventBuffer::Serialize() { |
| // Performance: performs a deep copy. Investigate an alternative to improve |
| // performance. |
| // TODO(b/339905988): Implement an optimization where two Persistent Protos |
| // are used for staged and active that are swapped when a flush occurs. |
| return proto()->events(); |
| } |
| |
| // This flushing for an ArenaEventBuffer will write the content |
| void ArenaEventBuffer::Flush(const base::FilePath& path, |
| FlushedCallback callback) { |
| const base::FilePath proto_path = events_->path(); |
| |
| std::string content; |
| if (!proto()->SerializeToString(&content)) { |
| std::move(callback).Run(base::unexpected(kSerializationFailed)); |
| return; |
| } |
| |
| // Cleanup the in-memory events. |
| Purge(); |
| |
| // Write the events to disk. |callback| is expected to handle the key. |
| task_runner_->PostTaskAndReplyWithResult( |
| FROM_HERE, base::BindOnce(&WriteEvents, path, std::move(content)), |
| std::move(callback)); |
| } |
| |
| void ArenaEventBuffer::ProfileAdded(const Profile& profile) { |
| base::FilePath path = profile.GetPath() |
| .Append(FILE_PATH_LITERAL("structured_metrics")) |
| .Append(FILE_PATH_LITERAL("storage")) |
| .Append(FILE_PATH_LITERAL("arena-events")); |
| UpdatePath(path); |
| } |
| |
| void ArenaEventBuffer::UpdatePath(const base::FilePath& path) { |
| events_->UpdatePath(path, |
| base::BindOnce(&ArenaEventBuffer::OnEventRead, |
| weak_factory_.GetWeakPtr()), |
| /*remove_existing=*/true); |
| } |
| |
| // static |
| uint64_t ArenaEventBuffer::EstimateEventSize( |
| const StructuredEventProto& event) { |
| return sizeof(StructuredEventProto) + |
| event.metrics_size() * sizeof(StructuredEventProto::Metric) + |
| sizeof(StructuredEventProto) * event.has_event_sequence_metadata(); |
| } |
| |
| void ArenaEventBuffer::OnEventRead(const ReadStatus status) { |
| switch (status) { |
| case ReadStatus::kOk: |
| // Update the used sized of the proto if a file was successfully loaded. |
| resource_info_.used_size_bytes = (*events_)->ByteSizeLong(); |
| break; |
| case ReadStatus::kMissing: |
| break; |
| case ReadStatus::kReadError: |
| LogInternalError(StructuredMetricsError::kEventReadError); |
| break; |
| case ReadStatus::kParseError: |
| LogInternalError(StructuredMetricsError::kEventParseError); |
| break; |
| } |
| |
| // Once the proto has been read, any pre-init events need to be added to |
| // the storage. The result is ignored. |pre_init_events_| shouldn't be very |
| // large, inlining this operation should be fine. |
| if (!pre_init_events_.empty()) { |
| for (auto begin = std::make_move_iterator(pre_init_events_.begin()), |
| end = std::make_move_iterator(pre_init_events_.end()); |
| begin != end; ++begin) { |
| AddEvent(std::move(*begin)); |
| } |
| |
| // Clear |pre_init_events_| such that it is using as little memory as |
| // possible. |
| std::vector<StructuredEventProto> temp; |
| pre_init_events_.swap(temp); |
| } |
| |
| if (!backup_timer_.IsRunning()) { |
| backup_timer_.Start(FROM_HERE, GetBackupTimeDelta(), |
| base::BindRepeating(&ArenaEventBuffer::BackupTask, |
| weak_factory_.GetWeakPtr())); |
| } |
| } |
| |
| void ArenaEventBuffer::OnEventWrite(const WriteStatus status) { |
| switch (status) { |
| case WriteStatus::kOk: |
| break; |
| case WriteStatus::kWriteError: |
| LogInternalError(StructuredMetricsError::kEventWriteError); |
| break; |
| case WriteStatus::kSerializationError: |
| LogInternalError(StructuredMetricsError::kEventSerializationError); |
| break; |
| } |
| } |
| |
| void ArenaEventBuffer::BackupTask() { |
| // This task isn't started until after the OnReadComplete has been called so |
| // we do not need to check if the proto has been created. |
| events_->QueueWrite(); |
| } |
| |
| } // namespace metrics::structured |