blob: e14e7bb6d6ca4b8696e22f6e64f5da7fb7df0a1e [file] [log] [blame]
// Copyright 2019 The Cobalt Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "cobalt/loader/resource_cache.h"
#include <algorithm>
#include <memory>
#include "base/strings/stringprintf.h"
#include "cobalt/base/console_log.h"
namespace cobalt {
namespace loader {
CachedResourceBase::OnLoadedCallbackHandler::OnLoadedCallbackHandler(
const scoped_refptr<CachedResourceBase>& cached_resource,
const base::Closure& success_callback, const base::Closure& error_callback)
: cached_resource_(cached_resource),
success_callback_(success_callback),
error_callback_(error_callback) {
DCHECK(cached_resource_);
if (!success_callback_.is_null()) {
success_callback_list_iterator_ = cached_resource_->AddCallback(
kOnLoadingSuccessCallbackType, success_callback_);
if (cached_resource_->has_resource_func_.Run()) {
success_callback_.Run();
}
}
if (!error_callback_.is_null()) {
error_callback_list_iterator_ = cached_resource_->AddCallback(
kOnLoadingErrorCallbackType, error_callback_);
}
}
CachedResourceBase::OnLoadedCallbackHandler::~OnLoadedCallbackHandler() {
if (!success_callback_.is_null()) {
cached_resource_->RemoveCallback(kOnLoadingSuccessCallbackType,
success_callback_list_iterator_);
}
if (!error_callback_.is_null()) {
cached_resource_->RemoveCallback(kOnLoadingErrorCallbackType,
error_callback_list_iterator_);
}
}
bool CachedResourceBase::IsLoadingComplete() {
return !loader_ && !retry_timer_;
}
CachedResourceBase::CallbackListIterator CachedResourceBase::AddCallback(
CallbackType callback_type, const base::Closure& callback) {
DCHECK_CALLED_ON_VALID_THREAD(cached_resource_thread_checker_);
CallbackList& callback_list = callback_lists_[callback_type];
callback_list.push_front(callback);
return callback_list.begin();
}
void CachedResourceBase::RemoveCallback(CallbackType type,
CallbackListIterator iterator) {
DCHECK_CALLED_ON_VALID_THREAD(cached_resource_thread_checker_);
CallbackList& callback_list = callback_lists_[type];
callback_list.erase(iterator);
}
void CachedResourceBase::RunCallbacks(CallbackType type) {
DCHECK_CALLED_ON_VALID_THREAD(cached_resource_thread_checker_);
// To avoid the list getting altered in the callbacks.
CallbackList callback_list = callback_lists_[type];
CallbackListIterator callback_iter;
for (callback_iter = callback_list.begin();
callback_iter != callback_list.end(); ++callback_iter) {
callback_iter->Run();
}
}
void CachedResourceBase::EnableCompletionCallbacks() {
are_completion_callbacks_enabled_ = true;
if (!completion_callback_.is_null()) {
completion_callback_.Run();
}
}
void CachedResourceBase::StartLoading() {
DCHECK_CALLED_ON_VALID_THREAD(cached_resource_thread_checker_);
DCHECK(!loader_);
DCHECK(!retry_timer_ || !retry_timer_->IsRunning());
loader_ = start_loading_func_.Run();
}
void CachedResourceBase::ScheduleLoadingRetry() {
DCHECK_CALLED_ON_VALID_THREAD(cached_resource_thread_checker_);
DCHECK(!loader_);
DCHECK(!retry_timer_ || !retry_timer_->IsRunning());
LOG(WARNING) << "Scheduling loading retry for '" << url_ << "'";
on_retry_loading_.Run();
// The delay starts at 1 second and doubles every subsequent retry until the
// maxiumum delay of 1024 seconds (~17 minutes) is reached. After this, all
// additional attempts also wait 1024 seconds.
const int64 kBaseRetryDelayInMilliseconds = 1000;
const int kMaxRetryCountShift = 10;
int64 delay = kBaseRetryDelayInMilliseconds
<< std::min(kMaxRetryCountShift, retry_count_++);
// The retry timer is lazily created the first time that it is needed.
if (!retry_timer_) {
retry_timer_.reset(new base::RetainingOneShotTimer());
}
retry_timer_->Start(
FROM_HERE, base::TimeDelta::FromMilliseconds(delay),
base::Bind(&CachedResourceBase::StartLoading, base::Unretained(this)));
}
void CachedResourceBase::OnLoadingComplete(
const base::Optional<std::string>& error) {
DCHECK_CALLED_ON_VALID_THREAD(cached_resource_thread_checker_);
// Success
if (!error) {
loader_.reset();
retry_timer_.reset();
completion_callback_ =
base::Bind(on_resource_loaded_, kOnLoadingSuccessCallbackType);
if (are_completion_callbacks_enabled_) {
completion_callback_.Run();
}
// Error
} else {
CLOG(WARNING, owner_->debugger_hooks())
<< " Error while loading '" << url_ << "': " << *error;
if (has_resource_func_.Run()) {
LOG(WARNING) << "A resource was produced but there was still an error.";
reset_resource_func_.Run();
}
bool should_retry = are_loading_retries_enabled_func_.Run() &&
loader_->DidFailFromTransientError();
loader_.reset();
if (should_retry) {
ScheduleLoadingRetry();
} else {
retry_timer_.reset();
completion_callback_ =
base::Bind(on_resource_loaded_, kOnLoadingErrorCallbackType);
if (are_completion_callbacks_enabled_) {
completion_callback_.Run();
}
}
}
}
void ResourceCacheBase::SetCapacity(uint32 capacity) {
DCHECK_CALLED_ON_VALID_THREAD(resource_cache_thread_checker_);
cache_capacity_ = capacity;
memory_capacity_in_bytes_ = capacity;
ReclaimMemoryAndMaybeProcessPendingCallbacks(cache_capacity_);
}
void ResourceCacheBase::Purge() {
DCHECK_CALLED_ON_VALID_THREAD(resource_cache_thread_checker_);
ProcessPendingCallbacks();
reclaim_memory_func_.Run(0, true);
}
void ResourceCacheBase::ProcessPendingCallbacks() {
DCHECK_CALLED_ON_VALID_THREAD(resource_cache_thread_checker_);
process_pending_callback_timer_.Stop();
// If callbacks are disabled, simply return.
if (are_callbacks_disabled_) {
return;
}
is_processing_pending_callbacks_ = true;
while (!pending_callback_map_.empty()) {
ResourceCallbackInfo& callback_info = pending_callback_map_.front().second;
// To avoid the last reference of this object getting deleted in the
// callbacks.
auto* cached_resource_ptr = callback_info.cached_resource;
scoped_refptr<CachedResourceBase> holder(cached_resource_ptr);
callback_info.cached_resource->RunCallbacks(callback_info.callback_type);
pending_callback_map_.erase(pending_callback_map_.begin());
}
is_processing_pending_callbacks_ = false;
count_pending_callbacks_ = 0;
}
void ResourceCacheBase::DisableCallbacks() {
DCHECK_CALLED_ON_VALID_THREAD(resource_cache_thread_checker_);
are_callbacks_disabled_ = true;
}
ResourceCacheBase::ResourceCacheBase(
const std::string& name, const base::DebuggerHooks& debugger_hooks,
uint32 cache_capacity, bool are_loading_retries_enabled,
const ReclaimMemoryFunc& reclaim_memory_func)
: name_(name),
debugger_hooks_(debugger_hooks),
are_loading_retries_enabled_(are_loading_retries_enabled),
cache_capacity_(cache_capacity),
reclaim_memory_func_(reclaim_memory_func),
memory_size_in_bytes_(
base::StringPrintf("Memory.%s.Size", name_.c_str()), 0,
"Total number of bytes currently used by the cache."),
memory_capacity_in_bytes_(
base::StringPrintf("Memory.%s.Capacity", name_.c_str()),
cache_capacity_,
"The capacity, in bytes, of the resource cache. "
"Exceeding this results in *unused* resources being "
"purged."),
memory_resources_loaded_in_bytes_(
base::StringPrintf("Memory.%s.Resource.Loaded", name_.c_str()), 0,
"Combined size in bytes of all resources that have been loaded by "
"the cache."),
count_resources_requested_(
base::StringPrintf("Count.%s.Resource.Requested", name_.c_str()), 0,
"The total number of resources that have been requested."),
count_resources_loading_(
base::StringPrintf("Count.%s.Resource.Loading", name_.c_str()), 0,
"The number of resources that are currently loading."),
count_resources_loaded_(
base::StringPrintf("Count.%s.Resource.Loaded", name_.c_str()), 0,
"The total number of resources that have been successfully "
"loaded."),
count_resources_cached_(
base::StringPrintf("Count.%s.Resource.Cached", name_.c_str()), 0,
"The number of resources that are currently in the cache."),
count_pending_callbacks_(
base::StringPrintf("Count.%s.PendingCallbacks", name_.c_str()), 0,
"The number of loading completed resources that have pending "
"callbacks.") {}
void ResourceCacheBase::NotifyResourceLoadingRetryScheduled(
CachedResourceBase* cached_resource) {
DCHECK_CALLED_ON_VALID_THREAD(resource_cache_thread_checker_);
const std::string& url = cached_resource->url().spec();
// Remove the resource from those currently loading. It'll be re-added once
// the retry starts.
// Remove the resource from its loading set. It should exist in exactly one
// of the loading sets.
if (callback_blocking_loading_resource_set_.erase(url)) {
DCHECK(non_callback_blocking_loading_resource_set_.find(url) ==
non_callback_blocking_loading_resource_set_.end());
} else if (!non_callback_blocking_loading_resource_set_.erase(url)) {
DCHECK(false);
}
--count_resources_loading_;
ProcessPendingCallbacksIfUnblocked();
}
void ResourceCacheBase::ReclaimMemoryAndMaybeProcessPendingCallbacks(
uint32 bytes_to_reclaim_down_to) {
reclaim_memory_func_.Run(bytes_to_reclaim_down_to,
false /*log_warning_if_over*/);
// If the current size of the cache is still greater than
// |bytes_to_reclaim_down_to| after reclaiming memory, then process any
// pending callbacks and try again. References to the cached resources are
// potentially being held until the callbacks run, so processing them may
// enable more memory to be reclaimed.
if (memory_size_in_bytes_ > bytes_to_reclaim_down_to) {
ProcessPendingCallbacks();
reclaim_memory_func_.Run(bytes_to_reclaim_down_to,
true /*log_warning_if_over*/);
}
}
void ResourceCacheBase::ProcessPendingCallbacksIfUnblocked() {
// If there are no callback blocking resources, then simply process any
// pending callbacks now; otherwise, start |process_pending_callback_timer_|,
// which ensures that the callbacks are handled in a timely manner while still
// allowing them to be batched.
if (callback_blocking_loading_resource_set_.empty()) {
ProcessPendingCallbacks();
// Now that we've processed the callbacks, if there are any non-blocking
// loading resources, then they're becoming blocking. Simply swap the two
// sets, rather than copying the contents over.
if (!non_callback_blocking_loading_resource_set_.empty()) {
callback_blocking_loading_resource_set_.swap(
non_callback_blocking_loading_resource_set_);
}
} else if (!pending_callback_map_.empty() &&
!process_pending_callback_timer_.IsRunning()) {
// The maximum delay for a pending callback is set to 500ms. After that, the
// callback will be processed regardless of how many callback blocking
// loading resources remain. This specific value maximizes callback batching
// on fast networks while also keeping the callback delay on slow networks
// to a minimum and is based on significant testing.
const int64 kMaxPendingCallbackDelayInMilliseconds = 500;
process_pending_callback_timer_.Start(
FROM_HERE,
base::TimeDelta::FromMilliseconds(
kMaxPendingCallbackDelayInMilliseconds),
base::BindOnce(&ResourceCacheBase::ProcessPendingCallbacks,
base::Unretained(this)));
}
}
} // namespace loader
} // namespace cobalt