// Copyright 2019 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/thumbnail/cc/thumbnail_cache.h"
#include <algorithm>
#include <cmath>
#include <utility>
#include <vector>
#include "base/android/application_status_listener.h"
#include "base/android/path_utils.h"
#include "base/big_endian.h"
#include "base/containers/adapters.h"
#include "base/containers/contains.h"
#include "base/containers/flat_set.h"
#include "base/feature_list.h"
#include "base/files/file.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/metrics/field_trial_params.h"
#include "base/metrics/histogram_functions.h"
#include "base/no_destructor.h"
#include "base/rand_util.h"
#include "base/ranges/algorithm.h"
#include "base/strings/string_number_conversions.h"
#include "base/task/bind_post_task.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "gpu/config/gpu_finch_features.h"
#include "skia/ext/image_operations.h"
#include "third_party/android_opengl/etc1/etc1.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/core/SkCanvas.h"
#include "third_party/skia/include/core/SkData.h"
#include "third_party/skia/include/core/SkImage.h"
#include "third_party/skia/include/core/SkMallocPixelRef.h"
#include "third_party/skia/include/core/SkPixelRef.h"
#include "ui/android/resources/ui_resource_provider.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/gfx/codec/jpeg_codec.h"
#include "ui/gfx/geometry/size_conversions.h"
namespace thumbnail {
namespace {
constexpr float kApproximationScaleFactor = 4.f;
constexpr base::TimeDelta kDefaultCaptureMinRequestTimeMs(
base::Milliseconds(1000));
constexpr int kKiB = 1024;
// Indicates whether we prefer to have more free CPU memory over GPU memory.
constexpr bool kPreferCPUMemory = true;
// Borrowed from GetDelayForNextMemoryLog() in browser_metrics.cc.
//
// A Poisson distributed delay with a mean of `mean_time` for computing time
// delta between recording memory metrics.
base::TimeDelta ComputeDelay(base::TimeDelta mean_time) {
double uniform = base::RandDouble();
return -std::log(1 - uniform) * mean_time;
}
} // anonymous namespace
ThumbnailCache::ThumbnailCache(size_t default_cache_size,
size_t compression_queue_max_size,
size_t write_queue_max_size,
bool save_jpeg_thumbnails)
: etc1_file_sequenced_task_runner_(
base::ThreadPool::CreateSequencedTaskRunner({base::MayBlock()})),
jpeg_file_sequenced_task_runner_(
base::ThreadPool::CreateSequencedTaskRunner(
{base::MayBlock(), base::TaskPriority::USER_VISIBLE})),
etc1_helper_(GetCacheDirectory(), etc1_file_sequenced_task_runner_),
jpeg_helper_(GetCacheDirectory(), jpeg_file_sequenced_task_runner_),
compression_queue_max_size_(compression_queue_max_size),
write_queue_max_size_(write_queue_max_size),
save_jpeg_thumbnails_(save_jpeg_thumbnails),
capture_min_request_time_ms_(kDefaultCaptureMinRequestTimeMs),
compression_tasks_count_(0),
write_tasks_count_(0),
read_in_progress_(false),
cache_(default_cache_size),
ui_resource_provider_(nullptr) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
memory_pressure_ = std::make_unique<base::MemoryPressureListener>(
FROM_HERE, base::BindRepeating(&ThumbnailCache::OnMemoryPressure,
base::Unretained(this)));
ScheduleRecordCacheMetrics(base::Minutes(1));
}
ThumbnailCache::~ThumbnailCache() {
SetUIResourceProvider(nullptr);
}
void ThumbnailCache::SetUIResourceProvider(
base::WeakPtr<ui::UIResourceProvider> ui_resource_provider) {
if (ui_resource_provider_.get() == ui_resource_provider.get()) {
return;
}
cache_.Clear();
ui_resource_provider_ = ui_resource_provider;
}
void ThumbnailCache::AddThumbnailCacheObserver(
ThumbnailCacheObserver* observer) {
if (!observers_.HasObserver(observer)) {
observers_.AddObserver(observer);
}
}
void ThumbnailCache::RemoveThumbnailCacheObserver(
ThumbnailCacheObserver* observer) {
if (observers_.HasObserver(observer)) {
observers_.RemoveObserver(observer);
}
}
void ThumbnailCache::Put(
TabId tab_id,
std::unique_ptr<ThumbnailCaptureTracker, base::OnTaskRunnerDeleter> tracker,
const SkBitmap& bitmap,
float thumbnail_scale) {
if (!ui_resource_provider_ || bitmap.empty() || thumbnail_scale <= 0) {
tracker->MarkCaptureFailed();
return;
}
if (thumbnail_meta_data_.find(tab_id) == thumbnail_meta_data_.end()) {
DVLOG(1) << "Thumbnail meta data was removed for tab id " << tab_id;
tracker->MarkCaptureFailed();
return;
}
base::Time time_stamp = thumbnail_meta_data_[tab_id].capture_time();
std::unique_ptr<Thumbnail> thumbnail = Thumbnail::Create(
tab_id, time_stamp, thumbnail_scale, ui_resource_provider_, this);
thumbnail->SetBitmap(bitmap);
RemoveFromReadQueue(tab_id);
if (base::Contains(visible_ids_, tab_id)) {
MakeSpaceForNewItemIfNecessary(tab_id);
cache_.Put(tab_id, std::move(thumbnail));
NotifyObserversOfThumbnailAddedToCache(tab_id);
}
CompressThumbnailIfNecessary(tab_id, std::move(tracker), time_stamp, bitmap,
thumbnail_scale);
}
void ThumbnailCache::Remove(TabId tab_id) {
cache_.Remove(tab_id);
thumbnail_meta_data_.erase(tab_id);
RemoveFromDisk(tab_id);
RemoveFromReadQueue(tab_id);
}
Thumbnail* ThumbnailCache::Get(TabId tab_id, bool force_disk_read) {
Thumbnail* thumbnail = cache_.Get(tab_id);
if (thumbnail) {
thumbnail->CreateUIResource();
return thumbnail;
}
if (force_disk_read && primary_tab_id_ != tab_id &&
base::Contains(visible_ids_, tab_id) &&
!base::Contains(read_queue_, tab_id)) {
read_queue_.push_back(tab_id);
ReadNextThumbnail();
}
return nullptr;
}
void ThumbnailCache::InvalidateThumbnailIfChanged(TabId tab_id,
const GURL& url) {
auto meta_data_iter = thumbnail_meta_data_.find(tab_id);
if (meta_data_iter == thumbnail_meta_data_.end()) {
thumbnail_meta_data_[tab_id] = ThumbnailMetaData(base::Time(), url);
} else if (!url.is_empty() && meta_data_iter->second.url() != url) {
Remove(tab_id);
}
}
base::FilePath ThumbnailCache::GetCacheDirectory() {
static const base::NoDestructor<base::FilePath> cache_dir([] {
base::FilePath path;
base::android::GetThumbnailCacheDirectory(&path);
return path;
}());
return *cache_dir;
}
bool ThumbnailCache::CheckAndUpdateThumbnailMetaData(TabId tab_id,
const GURL& url,
bool force_update) {
base::Time current_time = base::Time::Now();
auto meta_data_iter = thumbnail_meta_data_.find(tab_id);
if (!force_update && meta_data_iter != thumbnail_meta_data_.end() &&
meta_data_iter->second.url() == url &&
(current_time - meta_data_iter->second.capture_time()) <
capture_min_request_time_ms_) {
return false;
}
thumbnail_meta_data_[tab_id] = ThumbnailMetaData(current_time, url);
return true;
}
bool ThumbnailCache::IsInVisibleIds(TabId tab_id) {
return primary_tab_id_ == tab_id || base::Contains(visible_ids_, tab_id);
}
void ThumbnailCache::UpdateVisibleIds(const std::vector<TabId>& priority,
TabId primary_tab_id) {
bool needs_update = false;
if (primary_tab_id_ != primary_tab_id) {
// The primary screen-filling tab (if any) is not pushed onto the read
// queue, under the assumption that it either has a live layer or will have
// one very soon.
primary_tab_id_ = primary_tab_id;
needs_update = true;
}
size_t ids_size = std::min(priority.size(), cache_.MaximumCacheSize());
if (visible_ids_.size() != ids_size) {
needs_update = true;
} else {
// Early out if called with the same input as last time (We only care
// about the first mCache.MaximumCacheSize() entries).
auto visible_iter = visible_ids_.begin();
auto priority_iter = priority.begin();
while (visible_iter != visible_ids_.end() &&
priority_iter != priority.end()) {
if (*priority_iter != *visible_iter || !cache_.Get(*priority_iter)) {
needs_update = true;
break;
}
visible_iter++;
priority_iter++;
}
}
if (!needs_update) {
PruneCache();
return;
}
read_queue_.clear();
visible_ids_.clear();
size_t count = 0;
auto iter = priority.begin();
while (iter != priority.end() && count < ids_size) {
TabId tab_id = *iter;
visible_ids_.push_back(tab_id);
if (!cache_.Get(tab_id) && primary_tab_id_ != tab_id &&
!base::Contains(read_queue_, tab_id)) {
read_queue_.push_back(tab_id);
}
iter++;
count++;
}
ReadNextThumbnail();
PruneCache();
}
void ThumbnailCache::PruneCache() {
// Intentionally ignore `primary_tab_id_` as it should have a live layer. If
// that isn't true or may be slow the caller should include it in
// `visible_ids_`.
base::flat_set<TabId> ids_to_keep(
std::vector<TabId>(visible_ids_.begin(), visible_ids_.end()));
std::vector<TabId> ids_to_remove;
for (const auto& entry : cache_) {
if (!base::Contains(ids_to_keep, entry.first)) {
ids_to_remove.push_back(entry.first);
}
}
for (TabId id : ids_to_remove) {
cache_.Remove(id);
}
}
void ThumbnailCache::ForkToSaveAsJpeg(
base::OnceCallback<void(bool, const SkBitmap&)> callback,
int tab_id,
bool result,
const SkBitmap& bitmap) {
if (result && !bitmap.isNull()) {
SaveAsJpeg(
tab_id,
std::unique_ptr<ThumbnailCaptureTracker, base::OnTaskRunnerDeleter>(
nullptr, base::OnTaskRunnerDeleter(
base::SequencedTaskRunner::GetCurrentDefault())),
bitmap);
}
std::move(callback).Run(result, bitmap);
}
void ThumbnailCache::DecompressEtc1ThumbnailFromFile(
TabId tab_id,
bool save_jpeg,
base::OnceCallback<void(bool, const SkBitmap&)> post_decompress_callback) {
base::OnceCallback<void(bool, const SkBitmap&)> transcoding_callback;
if (save_jpeg && save_jpeg_thumbnails_) {
transcoding_callback = base::BindOnce(
&ThumbnailCache::ForkToSaveAsJpeg, weak_factory_.GetWeakPtr(),
std::move(post_decompress_callback), tab_id);
} else {
transcoding_callback = std::move(post_decompress_callback);
}
auto decompress_task = base::BindOnce(
&thumbnail::Etc1ThumbnailHelper::Decompress, etc1_helper_.GetWeakPtr(),
std::move(transcoding_callback));
etc1_helper_.Read(
tab_id, base::BindPostTaskToCurrentDefault(std::move(decompress_task)));
}
void ThumbnailCache::ScheduleRecordCacheMetrics(base::TimeDelta mean_delay) {
content::GetUIThreadTaskRunner({})->PostDelayedTask(
FROM_HERE,
base::BindOnce(&ThumbnailCache::RecordCacheMetrics,
weak_factory_.GetWeakPtr()),
ComputeDelay(mean_delay));
}
void ThumbnailCache::RecordCacheMetrics() {
base::UmaHistogramCounts100("Android.ThumbnailCache.InMemoryCacheEntries",
cache_.size());
base::UmaHistogramMemoryKB("Android.ThumbnailCache.InMemoryCacheSize",
ComputeCacheSize(cache_) / kKiB);
ScheduleRecordCacheMetrics(base::Minutes(5));
}
// static
size_t ThumbnailCache::ComputeCacheSize(ExpiringThumbnailCache& cache) {
return std::accumulate(
cache.begin(), cache.end(), 0U,
[](size_t acc, const std::pair<TabId, Thumbnail*>& it) {
return acc + it.second->size_in_bytes();
});
}
void ThumbnailCache::RemoveFromDisk(TabId tab_id) {
jpeg_helper_.Delete(tab_id);
etc1_helper_.Delete(tab_id);
}
void ThumbnailCache::WriteEtc1ThumbnailIfNecessary(
TabId tab_id,
sk_sp<SkPixelRef> compressed_data,
float scale,
const gfx::Size& content_size) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!compressed_data || write_tasks_count_ >= write_queue_max_size_) {
return;
}
write_tasks_count_++;
base::OnceClosure post_write_task = base::BindOnce(
&ThumbnailCache::PostWriteEtc1Task, weak_factory_.GetWeakPtr());
etc1_helper_.Write(tab_id, compressed_data, scale, content_size,
std::move(post_write_task));
}
void ThumbnailCache::WriteJpegThumbnailIfNecessary(
TabId tab_id,
std::unique_ptr<ThumbnailCaptureTracker, base::OnTaskRunnerDeleter> tracker,
std::vector<uint8_t> compressed_data) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (compressed_data.empty() || write_tasks_count_ >= write_queue_max_size_) {
if (tracker) {
tracker->MarkJpegFailed();
}
return;
}
write_tasks_count_++;
auto post_write_task =
base::BindOnce(&ThumbnailCache::PostWriteJpegTask,
weak_factory_.GetWeakPtr(), std::move(tracker));
jpeg_helper_.Write(tab_id, std::move(compressed_data),
std::move(post_write_task));
}
void ThumbnailCache::SaveAsJpeg(
TabId tab_id,
std::unique_ptr<ThumbnailCaptureTracker, base::OnTaskRunnerDeleter> tracker,
const SkBitmap& bitmap) {
base::OnceCallback<void(std::vector<uint8_t>)> post_jpeg_compression_task =
base::BindOnce(&ThumbnailCache::WriteJpegThumbnailIfNecessary,
weak_factory_.GetWeakPtr(), tab_id, std::move(tracker));
jpeg_helper_.Compress(bitmap, std::move(post_jpeg_compression_task));
}
void ThumbnailCache::CompressThumbnailIfNecessary(
TabId tab_id,
std::unique_ptr<ThumbnailCaptureTracker, base::OnTaskRunnerDeleter> tracker,
const base::Time& time_stamp,
const SkBitmap& bitmap,
float scale) {
if (compression_tasks_count_ >= compression_queue_max_size_) {
RemoveOnMatchedTimeStamp(tab_id, time_stamp);
tracker->MarkCaptureFailed();
return;
}
compression_tasks_count_++;
base::OnceCallback<void(sk_sp<SkPixelRef>, const gfx::Size&)>
post_compression_task =
base::BindOnce(&ThumbnailCache::PostEtc1CompressionTask,
weak_factory_.GetWeakPtr(), tab_id, time_stamp, scale);
etc1_helper_.Compress(bitmap,
ui_resource_provider_->SupportsETC1NonPowerOfTwo(),
std::move(post_compression_task));
if (save_jpeg_thumbnails_) {
SaveAsJpeg(tab_id, std::move(tracker), bitmap);
}
}
void ThumbnailCache::ReadNextThumbnail() {
if (read_queue_.empty() || read_in_progress_) {
return;
}
TabId tab_id = read_queue_.front();
read_in_progress_ = true;
base::OnceCallback<void(sk_sp<SkPixelRef>, float, const gfx::Size&)>
post_read_task = base::BindOnce(&ThumbnailCache::PostEtc1ReadTask,
weak_factory_.GetWeakPtr(), tab_id);
etc1_helper_.Read(
tab_id, base::BindPostTaskToCurrentDefault(std::move(post_read_task)));
}
void ThumbnailCache::MakeSpaceForNewItemIfNecessary(TabId tab_id) {
if (cache_.Get(tab_id) || !base::Contains(visible_ids_, tab_id) ||
cache_.size() < cache_.MaximumCacheSize()) {
return;
}
TabId key_to_remove;
bool found_key_to_remove = false;
// 1. Find a cached item not in this list
for (auto& item : cache_) {
if (!base::Contains(visible_ids_, item.first)) {
key_to_remove = item.first;
found_key_to_remove = true;
break;
}
}
if (!found_key_to_remove) {
// 2. Find the least important id we can remove.
for (const TabId& id : base::Reversed(visible_ids_)) {
if (cache_.Get(id)) {
key_to_remove = id;
found_key_to_remove = true;
break;
}
}
}
if (found_key_to_remove) {
cache_.Remove(key_to_remove);
}
}
void ThumbnailCache::RemoveFromReadQueue(TabId tab_id) {
auto read_iter = base::ranges::find(read_queue_, tab_id);
if (read_iter != read_queue_.end()) {
read_queue_.erase(read_iter);
}
}
void ThumbnailCache::OnUIResourcesWereEvicted() {
if (visible_ids_.empty()) {
cache_.Clear();
} else {
TabId last_tab = visible_ids_.front();
std::unique_ptr<Thumbnail> thumbnail = cache_.Remove(last_tab);
cache_.Clear();
// Keep the thumbnail for app resume if it wasn't uploaded yet.
if (thumbnail.get() && !thumbnail->ui_resource_id()) {
cache_.Put(last_tab, std::move(thumbnail));
}
}
}
void ThumbnailCache::SetCaptureMinRequestTimeForTesting(int timeMs) {
capture_min_request_time_ms_ = base::Milliseconds(timeMs);
}
void ThumbnailCache::InvalidateCachedThumbnail(Thumbnail* thumbnail) {
DCHECK(thumbnail);
TabId tab_id = thumbnail->tab_id();
cc::UIResourceId uid = thumbnail->ui_resource_id();
Thumbnail* cached_thumbnail = cache_.Get(tab_id);
if (cached_thumbnail && cached_thumbnail->ui_resource_id() == uid) {
cache_.Remove(tab_id);
}
}
void ThumbnailCache::PostWriteJpegTask(
std::unique_ptr<ThumbnailCaptureTracker, base::OnTaskRunnerDeleter> tracker,
bool success) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (tracker) {
if (success) {
tracker->SetWroteJpeg();
} else {
tracker->MarkJpegFailed();
}
}
write_tasks_count_--;
}
void ThumbnailCache::PostWriteEtc1Task() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
write_tasks_count_--;
}
void ThumbnailCache::PostEtc1CompressionTask(TabId tab_id,
const base::Time& time_stamp,
float scale,
sk_sp<SkPixelRef> compressed_data,
const gfx::Size& content_size) {
compression_tasks_count_--;
if (!compressed_data) {
RemoveOnMatchedTimeStamp(tab_id, time_stamp);
return;
}
Thumbnail* thumbnail = cache_.Get(tab_id);
if (thumbnail) {
if (thumbnail->time_stamp() != time_stamp) {
return;
}
thumbnail->SetCompressedBitmap(compressed_data, content_size);
// Don't upload the texture if we are being paused/stopped because
// the context will go away anyways.
if (base::android::ApplicationStatusListener::GetState() ==
base::android::APPLICATION_STATE_HAS_RUNNING_ACTIVITIES) {
thumbnail->CreateUIResource();
NotifyObserversOfThumbnailAddedToCache(tab_id);
}
}
WriteEtc1ThumbnailIfNecessary(tab_id, std::move(compressed_data), scale,
content_size);
}
void ThumbnailCache::PostEtc1ReadTask(TabId tab_id,
sk_sp<SkPixelRef> compressed_data,
float scale,
const gfx::Size& content_size) {
read_in_progress_ = false;
auto iter = base::ranges::find(read_queue_, tab_id);
if (iter == read_queue_.end()) {
ReadNextThumbnail();
return;
}
read_queue_.erase(iter);
if (!cache_.Get(tab_id) && compressed_data) {
auto meta_iter = thumbnail_meta_data_.find(tab_id);
base::Time time_stamp = base::Time::Now();
if (meta_iter != thumbnail_meta_data_.end()) {
time_stamp = meta_iter->second.capture_time();
}
if (base::Contains(visible_ids_, tab_id)) {
MakeSpaceForNewItemIfNecessary(tab_id);
std::unique_ptr<Thumbnail> thumbnail = Thumbnail::Create(
tab_id, time_stamp, scale, ui_resource_provider_, this);
thumbnail->SetCompressedBitmap(std::move(compressed_data), content_size);
if (kPreferCPUMemory) {
thumbnail->CreateUIResource();
}
cache_.Put(tab_id, std::move(thumbnail));
NotifyObserversOfThumbnailAddedToCache(tab_id);
NotifyObserversOfThumbnailRead(tab_id);
}
}
ReadNextThumbnail();
}
void ThumbnailCache::NotifyObserversOfThumbnailAddedToCache(TabId tab_id) {
for (ThumbnailCacheObserver& observer : observers_) {
observer.OnThumbnailAddedToCache(tab_id);
}
}
void ThumbnailCache::NotifyObserversOfThumbnailRead(TabId tab_id) {
for (ThumbnailCacheObserver& observer : observers_) {
observer.OnFinishedThumbnailRead(tab_id);
}
}
void ThumbnailCache::RemoveOnMatchedTimeStamp(TabId tab_id,
const base::Time& time_stamp) {
// We remove the cached version if it matches the tab_id and the time_stamp.
Thumbnail* thumbnail = cache_.Get(tab_id);
if (thumbnail && thumbnail->time_stamp() == time_stamp) {
Remove(tab_id);
}
}
ThumbnailCache::ThumbnailMetaData::ThumbnailMetaData(
const base::Time& current_time,
GURL url)
: capture_time_(current_time), url_(std::move(url)) {}
std::pair<SkBitmap, float> ThumbnailCache::CreateApproximation(
const SkBitmap& bitmap,
float scale) {
DCHECK(!bitmap.empty());
DCHECK_GT(scale, 0);
float new_scale = 1.f / kApproximationScaleFactor;
gfx::Size dst_size = gfx::ScaleToFlooredSize(
gfx::Size(bitmap.width(), bitmap.height()), new_scale);
SkBitmap dst_bitmap;
dst_bitmap.allocPixels(SkImageInfo::Make(dst_size.width(), dst_size.height(),
bitmap.info().colorType(),
bitmap.info().alphaType()));
dst_bitmap.eraseColor(0);
SkCanvas canvas(dst_bitmap);
canvas.scale(new_scale, new_scale);
canvas.drawImage(bitmap.asImage(), 0, 0);
dst_bitmap.setImmutable();
return std::make_pair(dst_bitmap, new_scale * scale);
}
void ThumbnailCache::OnMemoryPressure(
base::MemoryPressureListener::MemoryPressureLevel level) {
if (level == base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_CRITICAL) {
cache_.Clear();
}
}
} // namespace thumbnail