blob: 8fc9f15a5bc380c41a8e5b4dcd69c0f8eea2b71a [file] [log] [blame]
// 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 "components/metrics/structured/storage_manager.h"
#include <algorithm>
#include <cstdint>
#include <memory>
#include <utility>
#include <vector>
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/test/bind.h"
#include "base/test/run_until.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/threading/thread.h"
#include "base/time/time.h"
#include "chrome/browser/metrics/structured/storage_manager_impl.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile_manager.h"
#include "components/metrics/structured/lib/event_buffer.h"
#include "components/metrics/structured/storage_manager.h"
#include "components/metrics/structured/structured_metrics_features.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/metrics_proto/structured_data.pb.h"
namespace metrics::structured {
namespace {
using google::protobuf::RepeatedPtrField;
class TestStorageDelegate : public StorageManager::StorageDelegate {
public:
using FlushedCallback = base::RepeatingCallback<void(const FlushedKey&)>;
using DeletedCallback =
base::RepeatingCallback<void(const FlushedKey&, DeleteReason)>;
TestStorageDelegate() = default;
~TestStorageDelegate() override = default;
void SetFlushedCallback(FlushedCallback flushed) {
flushed_callback_ = std::move(flushed);
}
void SetDeletedCallback(DeletedCallback deleted) {
delete_callback_ = std::move(deleted);
}
void OnFlushed(const FlushedKey& key) override {
flushed_count_ += 1;
flushed_key_ = key;
if (flushed_callback_) {
flushed_callback_.Run(key);
}
}
void OnDeleted(const FlushedKey& key, DeleteReason reason) override {
delete_count_ += 1;
if (delete_callback_) {
delete_callback_.Run(key, reason);
}
}
int flushed_count() const { return flushed_count_; }
const std::optional<FlushedKey>& flushed_key() { return flushed_key_; }
int delete_count() const { return delete_count_; }
private:
std::optional<FlushedKey> flushed_key_;
int flushed_count_ = 0;
int delete_count_ = 0;
FlushedCallback flushed_callback_;
DeletedCallback delete_callback_;
};
StructuredEventProto BuildTestEvent(
uint64_t id = 0,
const std::vector<int64_t>& metrics = std::vector<int64_t>()) {
StructuredEventProto event;
event.set_device_project_id(id);
int metric_id = 0;
for (int64_t metric : metrics) {
auto* m = event.add_metrics();
m->set_name_hash(metric_id++);
m->set_value_int64(metric);
}
return event;
}
EventsProto BuildTestEventsProto(int num, int start_id = 0) {
EventsProto events;
for (int i = 0; i < num; ++i) {
events.mutable_events()->Add(BuildTestEvent(start_id + i, {start_id}));
}
return events;
}
StorageManagerConfig CreateManagerConfig(int32_t buffer_max_bytes,
int32_t disk_max_bytes) {
return StorageManagerConfig{
.buffer_max_bytes = buffer_max_bytes,
.disk_max_bytes = disk_max_bytes,
};
}
EventsProto ReadFlushedEvents(const base::FilePath& path) {
std::string content;
EXPECT_TRUE(base::ReadFileToString(path, &content));
EventsProto list;
EXPECT_TRUE(list.MergeFromString(content));
return list;
}
void WriteEventsProto(const base::FilePath& dir,
std::string_view name,
EventsProto&& events) {
std::string content;
ASSERT_TRUE(events.SerializeToString(&content));
ASSERT_TRUE(base::WriteFile(dir.Append(name), content));
}
} // namespace
class StorageManagerTest : public testing::Test {
public:
StorageManagerTest()
: storage_delegate_(std::make_unique<TestStorageDelegate>()),
profile_manager_(TestingBrowserProcess::GetGlobal()) {}
void SetUp() override {
ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
ASSERT_TRUE(profile_manager_.SetUp());
}
base::FilePath GetArenaPath() {
return temp_dir_.GetPath().Append(FILE_PATH_LITERAL("arena_file"));
}
base::FilePath GetFlushDir() {
return temp_dir_.GetPath().Append(FILE_PATH_LITERAL("flushed_dir"));
}
void Wait() { task_environment_.RunUntilIdle(); }
std::unique_ptr<StorageManager> CreateManager(
const StorageManagerConfig& config) {
auto manager = std::make_unique<StorageManagerImpl>(config, GetArenaPath(),
GetFlushDir());
manager->set_delegate(storage_delegate_.get());
return manager;
}
void SortEvents(RepeatedPtrField<StructuredEventProto>& events) {
std::sort(events.begin(), events.end(),
[](const StructuredEventProto& l,
const StructuredEventProto& r) -> bool {
return l.device_project_id() < r.device_project_id();
});
}
private:
base::test::TaskEnvironment task_environment_{
base::test::TaskEnvironment::MainThreadType::UI,
base::test::TaskEnvironment::ThreadPoolExecutionMode::QUEUED};
base::ScopedTempDir temp_dir_;
protected:
std::unique_ptr<TestStorageDelegate> storage_delegate_;
TestingProfileManager profile_manager_;
};
TEST_F(StorageManagerTest, StoreAndProvideEventsInMemory) {
std::unique_ptr<StorageManager> manager = CreateManager(
CreateManagerConfig(/*buffer_max_bytes=*/1024, /*disk_max_bytes=*/1024));
Wait();
// Add event.
manager->AddEvent(BuildTestEvent(1, {1, 2, 3}));
// Expect it to be recorded
EXPECT_EQ(manager->RecordedEventsCount(), 1);
// Expect nothing to be flushed.
EXPECT_EQ(base::ComputeDirectorySize(GetFlushDir()), 0l);
// Provide events.
RepeatedPtrField<StructuredEventProto> events = manager->TakeEvents();
EXPECT_EQ(manager->RecordedEventsCount(), 0);
EXPECT_EQ(events.size(), 1);
EXPECT_EQ(events[0].device_project_id(), 1ul);
}
TEST_F(StorageManagerTest, FlushEvents) {
std::unique_ptr<StorageManager> manager = CreateManager(
CreateManagerConfig(/*buffer_max_bytes=*/512, /*disk_max_bytes=*/1024));
Wait();
storage_delegate_->SetFlushedCallback(base::BindLambdaForTesting(
[&](const FlushedKey& key) { EXPECT_TRUE(base::PathExists(key.path)); }));
manager->AddEvent(BuildTestEvent(1, {1, 2, 3, 4}));
EXPECT_EQ(manager->RecordedEventsCount(), 1);
// A flush should occur and the event should be added.
manager->AddEvent(BuildTestEvent(2, {1, 2, 3, 4}));
Wait();
ASSERT_EQ(storage_delegate_->flushed_count(), 1);
ASSERT_TRUE(storage_delegate_->flushed_key().has_value());
EventsProto events =
ReadFlushedEvents(storage_delegate_->flushed_key()->path);
ASSERT_EQ(events.events_size(), 1);
const auto& event = events.events(0);
EXPECT_EQ(event.device_project_id(), 1ul);
EXPECT_EQ(event.metrics_size(), 4);
}
TEST_F(StorageManagerTest, FullBuffer) {
std::unique_ptr<StorageManager> manager = CreateManager(
CreateManagerConfig(/*buffer_max_bytes=*/512, /*disk_max_bytes=*/1024));
Wait();
storage_delegate_->SetFlushedCallback(base::BindLambdaForTesting(
[&](const FlushedKey& key) { EXPECT_TRUE(base::PathExists(key.path)); }));
manager->AddEvent(BuildTestEvent(1, {1, 2, 3, 4}));
EXPECT_EQ(manager->RecordedEventsCount(), 1);
// A flush should occur and the event should be added.
manager->AddEvent(BuildTestEvent(1, {1, 2, 3, 4}));
Wait();
EXPECT_EQ(manager->RecordedEventsCount(), 1);
EXPECT_EQ(storage_delegate_->flushed_count(), 1);
}
// Tests the ability of StorageManager to collect on-disk events and return them
// to be uploaded.
TEST_F(StorageManagerTest, ProvideFlushedEvents) {
// This isn't needed in other tests because FlushedMap creates the directory
// if it doesn't exist. Here we populate it before hand.
ASSERT_TRUE(base::CreateDirectory(GetFlushDir()));
WriteEventsProto(GetFlushDir(), "events1", BuildTestEventsProto(/*num=*/3));
WriteEventsProto(GetFlushDir(), "events2",
BuildTestEventsProto(/*num=*/3, /*start_id=*/3));
WriteEventsProto(GetFlushDir(), "events3",
BuildTestEventsProto(/*num=*/3, /*start_id=*/6));
std::unique_ptr<StorageManager> manager =
CreateManager(CreateManagerConfig(/*buffer_max_bytes=*/512,
/*disk_max_bytes=*/1024));
Wait();
// Returns the number of on-disk files if there are no events in memory.
ASSERT_EQ(manager->RecordedEventsCount(), 3);
storage_delegate_->SetDeletedCallback(base::BindLambdaForTesting(
[&](const FlushedKey& key, DeleteReason reason) {
EXPECT_EQ(reason, DeleteReason::kUploaded);
EXPECT_FALSE(base::PathExists(key.path));
}));
RepeatedPtrField<StructuredEventProto> events = manager->TakeEvents();
Wait();
// Since on-disk events are being read, it is expected they are deleted.
EXPECT_EQ(storage_delegate_->delete_count(), 3);
// 3 events from each of the 3 files.
EXPECT_EQ(events.size(), 9);
// Sort the events to validating all events are present easier.
SortEvents(events);
uint64_t expected_id = 0;
for (const auto& event : events) {
EXPECT_EQ(event.device_project_id(), expected_id++);
}
}
TEST_F(StorageManagerTest, ProvideFlushedAndInMemoryEvents) {
// This isn't needed in other tests because FlushedMap creates the directory
// if it doesn't exist. Here we populate it before hand.
ASSERT_TRUE(base::CreateDirectory(GetFlushDir()));
WriteEventsProto(GetFlushDir(), "events1", BuildTestEventsProto(/*num=*/3));
WriteEventsProto(GetFlushDir(), "events2",
BuildTestEventsProto(/*num=*/3, /*start_id=*/3));
WriteEventsProto(GetFlushDir(), "events3",
BuildTestEventsProto(/*num=*/3, /*start_id=*/6));
std::unique_ptr<StorageManager> manager =
CreateManager(CreateManagerConfig(/*buffer_max_bytes=*/1024,
/*disk_max_bytes=*/1024));
Wait();
manager->AddEvent(BuildTestEvent(9, {9}));
manager->AddEvent(BuildTestEvent(10, {10}));
manager->AddEvent(BuildTestEvent(11, {11}));
ASSERT_EQ(manager->RecordedEventsCount(), 3);
storage_delegate_->SetDeletedCallback(base::BindLambdaForTesting(
[&](const FlushedKey& key, DeleteReason reason) {
EXPECT_EQ(reason, DeleteReason::kUploaded);
EXPECT_FALSE(base::PathExists(key.path));
}));
RepeatedPtrField<StructuredEventProto> events = manager->TakeEvents();
Wait();
// Since on-disk events are being read, it is expected they are deleted.
EXPECT_EQ(storage_delegate_->delete_count(), 3);
// 3 events from each of the 3 files and 3 in-memory events.
EXPECT_EQ(events.size(), 12);
// Sort the events to validating all events are present easier.
SortEvents(events);
uint64_t expected_id = 0;
for (const auto& event : events) {
EXPECT_EQ(event.device_project_id(), expected_id++);
}
}
TEST_F(StorageManagerTest, Purge) {
// This isn't needed in other tests because FlushedMap creates the directory
// if it doesn't exist. Here we populate it before hand.
ASSERT_TRUE(base::CreateDirectory(GetFlushDir()));
WriteEventsProto(GetFlushDir(), "events1", BuildTestEventsProto(/*num=*/3));
WriteEventsProto(GetFlushDir(), "events2",
BuildTestEventsProto(/*num=*/3, /*start_id=*/3));
WriteEventsProto(GetFlushDir(), "events3",
BuildTestEventsProto(/*num=*/3, /*start_id=*/6));
std::unique_ptr<StorageManager> manager =
CreateManager(CreateManagerConfig(/*buffer_max_bytes=*/1024,
/*disk_max_bytes=*/1024));
Wait();
manager->AddEvent(BuildTestEvent(9, {9}));
manager->AddEvent(BuildTestEvent(10, {10}));
manager->AddEvent(BuildTestEvent(11, {11}));
ASSERT_EQ(manager->RecordedEventsCount(), 3);
manager->Purge();
ASSERT_EQ(manager->RecordedEventsCount(), 0);
EXPECT_FALSE(
base::PathExists(GetFlushDir().Append(FILE_PATH_LITERAL("events1"))));
EXPECT_FALSE(
base::PathExists(GetFlushDir().Append(FILE_PATH_LITERAL("events2"))));
EXPECT_FALSE(
base::PathExists(GetFlushDir().Append(FILE_PATH_LITERAL("events3"))));
std::optional<int64_t> size = base::GetFileSize(GetArenaPath());
ASSERT_TRUE(size.has_value());
EXPECT_EQ(size.value(), 0l);
}
TEST_F(StorageManagerTest, FlushedQuotaExceeded) {
std::unique_ptr<StorageManager> manager = CreateManager(
CreateManagerConfig(/*buffer_max_bytes=*/512, /*disk_max_bytes=*/64));
Wait();
storage_delegate_->SetFlushedCallback(base::BindLambdaForTesting(
[&](const FlushedKey& key) { EXPECT_TRUE(base::PathExists(key.path)); }));
manager->AddEvent(BuildTestEvent(1, {1, 2, 3, 4}));
EXPECT_EQ(manager->RecordedEventsCount(), 1);
// A flush should occur and the event should be added.
manager->AddEvent(BuildTestEvent(1, {1, 2, 3, 4}));
Wait();
// Only the previously added event should be represented as recorded.
EXPECT_EQ(manager->RecordedEventsCount(), 1);
// Expect 1 batch of events to be written to disk.
EXPECT_EQ(storage_delegate_->flushed_count(), 1);
FlushedKey previous_key = *storage_delegate_->flushed_key();
std::vector<FlushedKey> dropped_keys;
storage_delegate_->SetDeletedCallback(base::BindLambdaForTesting(
[&](const FlushedKey& key, DeleteReason reason) {
EXPECT_EQ(reason, DeleteReason::kExceededQuota);
// The path that was flushed first (only 2) should be the key that is
// flushed. Only one is expected so to be deleted, if more happen this
// will fail.
EXPECT_EQ(previous_key.path, key.path);
dropped_keys.push_back(key);
}));
// A flush should occur but our quota has been reached.
manager->AddEvent(BuildTestEvent(1, {1, 2, 3, 4}));
Wait();
// After the second flush, the first file should be deleted.
EXPECT_EQ(storage_delegate_->delete_count(), 1);
// Deleting of the files is asynchronous, checking once all tasks have
// completed.
for (const FlushedKey& key : dropped_keys) {
EXPECT_FALSE(base::PathExists(key.path));
}
}
} // namespace metrics::structured