| // 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_); |
| } |
| } |
| |
| net::LoadTimingInfo |
| CachedResourceBase::OnLoadedCallbackHandler::GetLoadTimingInfo() { |
| return cached_resource_->GetLoadTimingInfo(); |
| } |
| |
| 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 |
| // maximum 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_); |
| |
| if (loader_ != nullptr) { |
| load_timing_info_ = loader_->get_load_timing_info(); |
| } |
| |
| // 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 |