Commit 5cec364d authored by Gang Wu's avatar Gang Wu Committed by Commit Bot

Implement Disk Cache for Feed Image Loader.

This is disk caching part of Feed Image Loader, and other parts of Feed
Image Loader will come later.
leveldb_proto is used as database. This disk cache need to save images
when Image Loader fetched images from network, and next time when Image
Loader received a same URL request, disk cache can provided without
downloading. Also this disk chache will do timestamp-base garbage
collection when Image Loader requested.

Bug:807359, 807457

Change-Id: I66db2411304012cf6f04a7380fc34fb1f5306d2f
Reviewed-on: https://chromium-review.googlesource.com/981298
Commit-Queue: Gang Wu <gangwu@chromium.org>
Reviewed-by: default avatarTommy Nyquist <nyquist@chromium.org>
Reviewed-by: default avatarIlya Sherman <isherman@chromium.org>
Reviewed-by: default avatarFilip Gorski <fgorski@chromium.org>
Cr-Commit-Position: refs/heads/master@{#548230}
parent 4be1c8bc
...@@ -24,47 +24,9 @@ static_library("feature_list") { ...@@ -24,47 +24,9 @@ static_library("feature_list") {
] ]
} }
static_library("feed") {
sources = [
"core/feed_networking_host.cc",
"core/feed_networking_host.h",
]
public_deps = [
"//base",
"//components/keyed_service/core",
"//net",
]
deps = [
"//components/data_use_measurement/core",
"//components/variations",
"//components/variations/net",
"//components/variations/service",
"//google_apis",
"//services/identity/public/cpp",
"//services/network/public/cpp",
"//services/network/public/mojom",
"//third_party/zlib/google:compression_utils",
]
}
source_set("unit_tests") { source_set("unit_tests") {
testonly = true testonly = true
sources = [
"core/feed_networking_host_unittest.cc",
]
deps = [ deps = [
":feed", "core:core_unit_tests",
"//base",
"//base/test:test_support",
"//net:test_support",
"//services/identity/public/cpp",
"//services/identity/public/cpp:test_support",
"//services/network:test_support",
"//services/network/public/cpp",
"//services/network/public/mojom",
"//third_party/zlib/google:compression_utils",
] ]
} }
include_rules = [ include_rules = [
"+components/data_use_measurement/core", "+components/data_use_measurement/core",
"+components/leveldb_proto",
"+components/variations", "+components/variations",
"+components/version_info", "+components/version_info",
"+net/base", "+net/base",
......
# Copyright 2018 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.
source_set("feed_core") {
sources = [
"feed_image_database.cc",
"feed_image_database.h",
"feed_networking_host.cc",
"feed_networking_host.h",
"time_serialization.cc",
"time_serialization.h",
]
public_deps = [
"//base",
"//components/feed/core/proto",
"//components/keyed_service/core",
"//components/leveldb_proto",
"//net",
]
deps = [
"//components/data_use_measurement/core",
"//components/variations",
"//components/variations/net",
"//components/variations/service",
"//google_apis",
"//services/identity/public/cpp",
"//services/network/public/cpp",
"//services/network/public/mojom",
"//third_party/zlib/google:compression_utils",
]
}
source_set("core_unit_tests") {
testonly = true
sources = [
"feed_image_database_unittest.cc",
"feed_networking_host_unittest.cc",
]
deps = [
":feed_core",
"//base",
"//base/test:test_support",
"//components/leveldb_proto:test_support",
"//net:test_support",
"//services/identity/public/cpp",
"//services/identity/public/cpp:test_support",
"//services/network:test_support",
"//services/network/public/cpp",
"//services/network/public/mojom",
"//third_party/zlib/google:compression_utils",
]
}
// Copyright 2018 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/feed/core/feed_image_database.h"
#include "base/bind.h"
#include "base/logging.h"
#include "base/sequenced_task_runner.h"
#include "base/sys_info.h"
#include "base/task_scheduler/post_task.h"
#include "base/task_scheduler/task_traits.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/time/time.h"
#include "components/feed/core/proto/cached_image.pb.h"
#include "components/feed/core/time_serialization.h"
#include "components/leveldb_proto/proto_database_impl.h"
namespace feed {
namespace {
// Statistics are logged to UMA with this string as part of histogram name. They
// can all be found under LevelDB.*.FeedImageDatabase. Changing this needs to
// synchronize with histograms.xml, AND will also become incompatible with older
// browsers still reporting the previous values.
const char kImageDatabaseUMAClientName[] = "FeedImageDatabase";
const char kImageDatabaseFolder[] = "images";
const size_t kDatabaseWriteBufferSizeBytes = 512 * 1024;
const size_t kDatabaseWriteBufferSizeBytesForLowEndDevice = 128 * 1024;
} // namespace
FeedImageDatabase::FeedImageDatabase(const base::FilePath& database_dir)
: FeedImageDatabase(
database_dir,
std::make_unique<leveldb_proto::ProtoDatabaseImpl<CachedImageProto>>(
base::CreateSequencedTaskRunnerWithTraits(
{base::MayBlock(), base::TaskPriority::BACKGROUND,
base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN}))) {}
FeedImageDatabase::FeedImageDatabase(
const base::FilePath& database_dir,
std::unique_ptr<leveldb_proto::ProtoDatabase<CachedImageProto>>
image_database)
: database_status_(UNINITIALIZED),
image_database_(std::move(image_database)),
weak_ptr_factory_(this) {
leveldb_env::Options options = leveldb_proto::CreateSimpleOptions();
if (base::SysInfo::IsLowEndDevice()) {
options.write_buffer_size = kDatabaseWriteBufferSizeBytesForLowEndDevice;
} else {
options.write_buffer_size = kDatabaseWriteBufferSizeBytes;
}
base::FilePath image_dir = database_dir.AppendASCII(kImageDatabaseFolder);
image_database_->Init(
kImageDatabaseUMAClientName, image_dir, options,
base::BindOnce(&FeedImageDatabase::OnDatabaseInitialized,
weak_ptr_factory_.GetWeakPtr()));
}
FeedImageDatabase::~FeedImageDatabase() = default;
bool FeedImageDatabase::IsInitialized() {
return INITIALIZED == database_status_;
}
void FeedImageDatabase::SaveImage(const std::string& url,
const std::string& image_data) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// If database is not ready, ignore the request.
if (!IsInitialized())
return;
CachedImageProto image_proto;
image_proto.set_url(url);
image_proto.set_data(image_data);
image_proto.set_last_used_time(ToDatabaseTime(base::Time::Now()));
SaveImageImpl(url, std::move(image_proto));
}
void FeedImageDatabase::LoadImage(const std::string& url,
FeedImageDatabaseCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
switch (database_status_) {
case INITIALIZED:
case INIT_FAILURE:
LoadImageImpl(url, std::move(callback));
break;
case UNINITIALIZED:
pending_image_callbacks_.emplace_back(url, std::move(callback));
break;
default:
NOTREACHED();
}
}
void FeedImageDatabase::GarbageCollectImages(base::Time expired_time) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// If database is not initiailzed yet, ignore the request.
if (!IsInitialized())
return;
image_database_->LoadEntries(
base::BindOnce(&FeedImageDatabase::GarbageCollectImagesImpl,
weak_ptr_factory_.GetWeakPtr(), expired_time));
}
void FeedImageDatabase::OnDatabaseInitialized(bool success) {
DCHECK_EQ(database_status_, UNINITIALIZED);
if (success) {
database_status_ = INITIALIZED;
} else {
database_status_ = INIT_FAILURE;
DVLOG(1) << "FeedImageDatabase init failed.";
}
ProcessPendingImageLoads();
}
void FeedImageDatabase::ProcessPendingImageLoads() {
DCHECK_NE(database_status_, UNINITIALIZED);
for (auto& image_callback : pending_image_callbacks_)
LoadImageImpl(image_callback.first, std::move(image_callback.second));
pending_image_callbacks_.clear();
}
void FeedImageDatabase::SaveImageImpl(const std::string& url,
CachedImageProto image_proto) {
auto entries_to_save = std::make_unique<ImageKeyEntryVector>();
entries_to_save->emplace_back(url, std::move(image_proto));
image_database_->UpdateEntries(
std::move(entries_to_save), std::make_unique<std::vector<std::string>>(),
base::BindOnce(&FeedImageDatabase::OnImageUpdated,
weak_ptr_factory_.GetWeakPtr()));
}
void FeedImageDatabase::OnImageLoaded(std::string url,
FeedImageDatabaseCallback callback,
bool success,
std::unique_ptr<CachedImageProto> entry) {
if (!success || !entry) {
DVLOG_IF(1, !success) << "FeedImageDatabase load failed.";
std::move(callback).Run(std::string());
return;
}
DCHECK_EQ(url, entry->url());
std::move(callback).Run(entry->data());
// Update timestamp for image.
entry->set_last_used_time(ToDatabaseTime(base::Time::Now()));
SaveImageImpl(url, std::move(*entry));
}
void FeedImageDatabase::LoadImageImpl(const std::string& url,
FeedImageDatabaseCallback callback) {
DCHECK_NE(database_status_, UNINITIALIZED);
if (IsInitialized()) {
image_database_->GetEntry(
url, base::BindOnce(&FeedImageDatabase::OnImageLoaded,
weak_ptr_factory_.GetWeakPtr(), url,
std::move(callback)));
} else {
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(std::move(callback), std::string()));
}
}
void FeedImageDatabase::OnImageUpdated(bool success) {
DVLOG_IF(1, !success) << "FeedImageDatabase update failed.";
}
void FeedImageDatabase::GarbageCollectImagesImpl(
base::Time expired_time,
bool load_entries_success,
std::unique_ptr<std::vector<CachedImageProto>> image_entries) {
if (!load_entries_success) {
DVLOG(1) << "FeedImageDatabase garbage collection failed.";
return;
}
int64_t expired_database_time = ToDatabaseTime(expired_time);
auto keys_to_remove = std::make_unique<std::vector<std::string>>();
for (const CachedImageProto& image : *image_entries) {
if (image.last_used_time() < expired_database_time)
keys_to_remove->emplace_back(image.url());
}
if (keys_to_remove->empty())
return;
image_database_->UpdateEntries(
std::make_unique<ImageKeyEntryVector>(), std::move(keys_to_remove),
base::BindOnce(&FeedImageDatabase::OnImageUpdated,
weak_ptr_factory_.GetWeakPtr()));
}
} // namespace feed
// Copyright 2018 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.
#ifndef COMPONENTS_FEED_CORE_FEED_IMAGE_DATABASE_H_
#define COMPONENTS_FEED_CORE_FEED_IMAGE_DATABASE_H_
#include "base/memory/weak_ptr.h"
#include "components/leveldb_proto/proto_database.h"
namespace feed {
class CachedImageProto;
// FeedImageDatabase is leveldb backed store for feed's image data.
// FeedImageDatabase keeps images identified by URLs.
// Save and Load operations are asynchronous, every load operation will update
// last_used_time for the image for garbage collection purpose.
class FeedImageDatabase {
public:
enum State {
UNINITIALIZED,
INITIALIZED,
INIT_FAILURE,
};
// Returns the resulting raw image data as std::string of a |LoadImage| call.
using FeedImageDatabaseCallback = base::OnceCallback<void(std::string)>;
// Initializes the database with |database_dir|.
explicit FeedImageDatabase(const base::FilePath& database_dir);
// Initializes the database with |database_dir|. Creates storage using the
// given |image_database| for local storage. Useful for testing.
FeedImageDatabase(
const base::FilePath& database_dir,
std::unique_ptr<leveldb_proto::ProtoDatabase<CachedImageProto>>
image_database);
~FeedImageDatabase();
// Returns true if initialization has finished successfully, else false.
// While this is false, initialization may already started, or initialization
// failed.
bool IsInitialized();
// Adds or updates the image data for the |url|.
// If the database is not initialized or in some error status, the call will
// be ignored.
void SaveImage(const std::string& url, const std::string& image_data);
// Loads the image data for the |url| and passes it to |callback|.
// |callback| will be called in the same thread as this function called.
// If the image cannot be found in database, or database error, returns an
// empty CachedImageProto. If the database is not initialized yet, the
// request will be pending until the database has been initialized.
void LoadImage(const std::string& url, FeedImageDatabaseCallback callback);
// Deletes all images which are older than |expired_time|.
// If database is not initialized, the call will be ignored.
void GarbageCollectImages(base::Time expired_time);
private:
friend class FeedImageDatabaseTest;
using ImageKeyEntryVector =
leveldb_proto::ProtoDatabase<CachedImageProto>::KeyEntryVector;
// Initialization
void OnDatabaseInitialized(bool success);
void ProcessPendingImageLoads();
// Saving
void SaveImageImpl(const std::string& url, CachedImageProto image_proto);
void OnImageUpdated(bool success);
// Loading
void LoadImageImpl(const std::string& url,
FeedImageDatabaseCallback callback);
void OnImageLoaded(std::string url,
FeedImageDatabaseCallback callback,
bool success,
std::unique_ptr<CachedImageProto> entry);
// Garbage collection
void GarbageCollectImagesImpl(
base::Time expired_time,
bool load_entries_success,
std::unique_ptr<std::vector<CachedImageProto>> image_entries);
State database_status_;
std::unique_ptr<leveldb_proto::ProtoDatabase<CachedImageProto>>
image_database_;
std::vector<std::pair<std::string, FeedImageDatabaseCallback>>
pending_image_callbacks_;
SEQUENCE_CHECKER(sequence_checker_);
base::WeakPtrFactory<FeedImageDatabase> weak_ptr_factory_;
DISALLOW_COPY_AND_ASSIGN(FeedImageDatabase);
};
} // namespace feed
#endif // COMPONENTS_FEED_CORE_FEED_IMAGE_DATABASE_H_
// Copyright 2018 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/feed/core/feed_image_database.h"
#include <map>
#include "base/test/scoped_task_environment.h"
#include "components/feed/core/proto/cached_image.pb.h"
#include "components/feed/core/time_serialization.h"
#include "components/leveldb_proto/testing/fake_db.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
using leveldb_proto::test::FakeDB;
using testing::Mock;
using testing::_;
namespace feed {
namespace {
const std::string kImageURL = "http://pie.com/";
const std::string kImageData = "pie image";
} // namespace
class FeedImageDatabaseTest : public testing::Test {
public:
FeedImageDatabaseTest() : image_db_(nullptr) {}
void CreateDatabase() {
// The FakeDBs are owned by |feed_db_|, so clear our pointers before
// resetting |feed_db_| itself.
image_db_ = nullptr;
// Explicitly destroy any existing database before creating a new one.
feed_db_.reset();
auto image_db =
std::make_unique<FakeDB<CachedImageProto>>(&image_db_storage_);
image_db_ = image_db.get();
feed_db_ = std::make_unique<FeedImageDatabase>(base::FilePath(),
std::move(image_db));
}
int64_t GetImageLastUsedTime(const std::string& url) {
return image_db_storage_[kImageURL].last_used_time();
}
void InjectImageProto(const std::string& url,
const std::string& data,
base::Time time) {
CachedImageProto image_proto;
image_proto.set_url(url);
image_proto.set_data(data);
image_proto.set_last_used_time(ToDatabaseTime(time));
image_db_storage_[url] = image_proto;
}
FakeDB<CachedImageProto>* image_db() { return image_db_; }
FeedImageDatabase* db() { return feed_db_.get(); }
void RunUntilIdle() { scoped_task_environment_.RunUntilIdle(); }
MOCK_METHOD1(OnImageLoaded, void(std::string));
private:
base::test::ScopedTaskEnvironment scoped_task_environment_;
std::map<std::string, CachedImageProto> image_db_storage_;
// Owned by |feed_db_|.
FakeDB<CachedImageProto>* image_db_;
std::unique_ptr<FeedImageDatabase> feed_db_;
DISALLOW_COPY_AND_ASSIGN(FeedImageDatabaseTest);
};
TEST_F(FeedImageDatabaseTest, Init) {
ASSERT_FALSE(db());
CreateDatabase();
EXPECT_FALSE(db()->IsInitialized());
image_db()->InitCallback(true);
EXPECT_TRUE(db()->IsInitialized());
}
TEST_F(FeedImageDatabaseTest, LoadBeforeInitSuccess) {
CreateDatabase();
EXPECT_FALSE(db()->IsInitialized());
// Start an image load before the database is initialized.
db()->LoadImage(kImageURL,
base::BindOnce(&FeedImageDatabaseTest::OnImageLoaded,
base::Unretained(this)));
EXPECT_CALL(*this, OnImageLoaded(_));
image_db()->InitCallback(true);
EXPECT_TRUE(db()->IsInitialized());
image_db()->GetCallback(true);
}
TEST_F(FeedImageDatabaseTest, LoadBeforeInitFailed) {
CreateDatabase();
EXPECT_FALSE(db()->IsInitialized());
// Start an image load before the database is initialized.
db()->LoadImage(kImageURL,
base::BindOnce(&FeedImageDatabaseTest::OnImageLoaded,
base::Unretained(this)));
EXPECT_CALL(*this, OnImageLoaded(_));
image_db()->InitCallback(false);
EXPECT_FALSE(db()->IsInitialized());
RunUntilIdle();
}
TEST_F(FeedImageDatabaseTest, LoadAfterInitSuccess) {
CreateDatabase();
EXPECT_FALSE(db()->IsInitialized());
EXPECT_CALL(*this, OnImageLoaded(_)).Times(0);
image_db()->InitCallback(true);
EXPECT_TRUE(db()->IsInitialized());
Mock::VerifyAndClearExpectations(this);
EXPECT_CALL(*this, OnImageLoaded(_));
db()->LoadImage(kImageURL,
base::BindOnce(&FeedImageDatabaseTest::OnImageLoaded,
base::Unretained(this)));
image_db()->GetCallback(true);
}
TEST_F(FeedImageDatabaseTest, LoadAfterInitFailed) {
CreateDatabase();
EXPECT_FALSE(db()->IsInitialized());
EXPECT_CALL(*this, OnImageLoaded(_)).Times(0);
image_db()->InitCallback(false);
EXPECT_FALSE(db()->IsInitialized());
Mock::VerifyAndClearExpectations(this);
EXPECT_CALL(*this, OnImageLoaded(_));
db()->LoadImage(kImageURL,
base::BindOnce(&FeedImageDatabaseTest::OnImageLoaded,
base::Unretained(this)));
RunUntilIdle();
}
TEST_F(FeedImageDatabaseTest, Save) {
CreateDatabase();
image_db()->InitCallback(true);
ASSERT_TRUE(db()->IsInitialized());
// Store an image.
db()->SaveImage(kImageURL, kImageData);
image_db()->UpdateCallback(true);
// Make sure they're there.
EXPECT_CALL(*this, OnImageLoaded(kImageData));
db()->LoadImage(kImageURL,
base::BindOnce(&FeedImageDatabaseTest::OnImageLoaded,
base::Unretained(this)));
image_db()->GetCallback(true);
}
TEST_F(FeedImageDatabaseTest, SavePersist) {
CreateDatabase();
image_db()->InitCallback(true);
ASSERT_TRUE(db()->IsInitialized());
// Store an image.
db()->SaveImage(kImageURL, kImageData);
image_db()->UpdateCallback(true);
// They should still exist after recreating the database.
CreateDatabase();
image_db()->InitCallback(true);
ASSERT_TRUE(db()->IsInitialized());
EXPECT_CALL(*this, OnImageLoaded(kImageData));
db()->LoadImage(kImageURL,
base::BindOnce(&FeedImageDatabaseTest::OnImageLoaded,
base::Unretained(this)));
image_db()->GetCallback(true);
}
TEST_F(FeedImageDatabaseTest, LoadUpdatesTime) {
CreateDatabase();
image_db()->InitCallback(true);
ASSERT_TRUE(db()->IsInitialized());
// Store an image.
InjectImageProto(kImageURL, kImageData, base::Time::UnixEpoch());
int64_t old_time = GetImageLastUsedTime(kImageURL);
// Make sure they're there.
EXPECT_CALL(*this, OnImageLoaded(kImageData));
db()->LoadImage(kImageURL,
base::BindOnce(&FeedImageDatabaseTest::OnImageLoaded,
base::Unretained(this)));
image_db()->GetCallback(true);
image_db()->UpdateCallback(true);
EXPECT_TRUE(old_time != GetImageLastUsedTime(kImageURL));
}
TEST_F(FeedImageDatabaseTest, GarbageCollectImagesTest) {
CreateDatabase();
image_db()->InitCallback(true);
ASSERT_TRUE(db()->IsInitialized());
base::Time now = base::Time::Now();
base::Time expired_time = now - base::TimeDelta::FromDays(30);
base::Time very_old_time = now - base::TimeDelta::FromDays(100);
// Store images.
InjectImageProto("url1", "data1", very_old_time);
InjectImageProto("url2", "data2", now);
InjectImageProto("url3", "data3", very_old_time);
// Garbage collect all except the second.
db()->GarbageCollectImages(expired_time);
// This will first load all images, then delete the expired ones.
image_db()->LoadCallback(true);
image_db()->UpdateCallback(true);
// Make sure the images are gone.
EXPECT_CALL(*this, OnImageLoaded(std::string()));
db()->LoadImage("url1", base::BindOnce(&FeedImageDatabaseTest::OnImageLoaded,
base::Unretained(this)));
image_db()->GetCallback(true);
EXPECT_CALL(*this, OnImageLoaded(std::string()));
db()->LoadImage("url3", base::BindOnce(&FeedImageDatabaseTest::OnImageLoaded,
base::Unretained(this)));
image_db()->GetCallback(true);
// Make sure the second still exists.
EXPECT_CALL(*this, OnImageLoaded("data2"));
db()->LoadImage("url2", base::BindOnce(&FeedImageDatabaseTest::OnImageLoaded,
base::Unretained(this)));
image_db()->GetCallback(true);
}
} // namespace feed
# Copyright 2018 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.
import("//third_party/protobuf/proto_library.gni")
proto_library("proto") {
sources = [
"cached_image.proto",
]
}
// Copyright 2018 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.
syntax = "proto2";
option optimize_for = LITE_RUNTIME;
package feed;
message CachedImageProto {
// The URL of the original source, ex. https://www.chromium.org/image.png.
optional string url = 1;
// Raw image data fetched from network.
optional bytes data = 2;
// Last used time (in microseconds since the origin (or "zero") point.).
optional int64 last_used_time = 3;
}
// Copyright 2018 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/feed/core/time_serialization.h"
namespace feed {
int64_t ToDatabaseTime(base::Time time) {
return time.since_origin().InMicroseconds();
}
base::Time FromDatabaseTime(int64_t serialized_time) {
return base::Time() + base::TimeDelta::FromMicroseconds(serialized_time);
}
} // namespace feed
// Copyright 2018 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.
#ifndef COMPONENTS_FEED_CORE_TIME_SERIALIZATION_H_
#define COMPONENTS_FEED_CORE_TIME_SERIALIZATION_H_
#include <stdint.h>
#include "base/time/time.h"
namespace feed {
int64_t ToDatabaseTime(base::Time time);
base::Time FromDatabaseTime(int64_t serialized_time);
} // namespace feed
#endif // COMPONENTS_FEED_CORE_TIME_SERIALIZATION_H_
...@@ -109274,6 +109274,7 @@ http://cs/file:chrome/histograms.xml - but prefer this file for new entries. ...@@ -109274,6 +109274,7 @@ http://cs/file:chrome/histograms.xml - but prefer this file for new entries.
label="Database for FeatureEngagementTracker events."/> label="Database for FeatureEngagementTracker events."/>
<suffix name="FeatureEngagementTrackerAvailabilityStore" <suffix name="FeatureEngagementTrackerAvailabilityStore"
label="Database for FeatureEngagementTracker feature availability."/> label="Database for FeatureEngagementTracker feature availability."/>
<suffix name="FeedImageDatabase" label="Databases for Feed Image Loader."/>
<affected-histogram name="LevelDB.Open"/> <affected-histogram name="LevelDB.Open"/>
</histogram_suffixes> </histogram_suffixes>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment