| // Copyright 2017 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/viz/client/client_resource_provider.h" |
| |
| #include <algorithm> |
| #include <utility> |
| |
| #include "base/debug/stack_trace.h" |
| #include "base/feature_list.h" |
| #include "base/functional/bind.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/task/bind_post_task.h" |
| #include "base/trace_event/trace_event.h" |
| #include "build/build_config.h" |
| #include "components/viz/common/features.h" |
| #include "components/viz/common/gpu/raster_context_provider.h" |
| #include "components/viz/common/resources/returned_resource.h" |
| #include "gpu/GLES2/gl2extchromium.h" |
| #include "gpu/command_buffer/client/context_support.h" |
| #include "gpu/command_buffer/client/gles2_interface.h" |
| #include "gpu/command_buffer/client/raster_interface.h" |
| #include "gpu/command_buffer/client/shared_image_interface.h" |
| #include "gpu/command_buffer/common/capabilities.h" |
| #include "gpu/command_buffer/common/sync_token.h" |
| |
| namespace viz { |
| |
| namespace { |
| |
| void CustomUmaHistogramMemoryKB(const char* name, int sample) { |
| // Based on UmaHistogramMemoryKB, but with a starting bucket for under 1 MB. |
| // This gives granularity for smaller resources that are held around. Vs 0 KB |
| // representing an estimation error. |
| base::UmaHistogramCustomCounts(name, sample, 0, 500000, 50); |
| } |
| |
| void ReportResourceSourceUsage(TransferableResource::ResourceSource source, |
| size_t usage) { |
| const size_t usage_in_kb = usage / 1024u; |
| switch (source) { |
| case TransferableResource::ResourceSource::kUnknown: |
| CustomUmaHistogramMemoryKB( |
| "Memory.Renderer.EvictedLockedResources.Unknown", usage_in_kb); |
| break; |
| case TransferableResource::ResourceSource::kAR: |
| CustomUmaHistogramMemoryKB("Memory.Renderer.EvictedLockedResources.AR", |
| usage_in_kb); |
| break; |
| case TransferableResource::ResourceSource::kCanvas: |
| CustomUmaHistogramMemoryKB( |
| "Memory.Renderer.EvictedLockedResources.Canvas", usage_in_kb); |
| break; |
| case TransferableResource::ResourceSource::kDrawingBuffer: |
| CustomUmaHistogramMemoryKB( |
| "Memory.Renderer.EvictedLockedResources.DrawingBuffer", usage_in_kb); |
| break; |
| case TransferableResource::ResourceSource::kExoBuffer: |
| CustomUmaHistogramMemoryKB( |
| "Memory.Renderer.EvictedLockedResources.ExoBuffer", usage_in_kb); |
| break; |
| case TransferableResource::ResourceSource::kHeadsUpDisplay: |
| CustomUmaHistogramMemoryKB( |
| "Memory.Renderer.EvictedLockedResources.HeadsUpDisplay", usage_in_kb); |
| break; |
| case TransferableResource::ResourceSource::kImageLayerBridge: |
| CustomUmaHistogramMemoryKB( |
| "Memory.Renderer.EvictedLockedResources.ImageLayerBridge", |
| usage_in_kb); |
| break; |
| case TransferableResource::ResourceSource::kPPBGraphics3D: |
| CustomUmaHistogramMemoryKB( |
| "Memory.Renderer.EvictedLockedResources.PPBGraphics3D", usage_in_kb); |
| break; |
| case TransferableResource::ResourceSource::kPepperGraphics2D: |
| CustomUmaHistogramMemoryKB( |
| "Memory.Renderer.EvictedLockedResources.PepperGraphics2D", |
| usage_in_kb); |
| break; |
| case TransferableResource::ResourceSource::kViewTransition: |
| CustomUmaHistogramMemoryKB( |
| "Memory.Renderer.EvictedLockedResources.ViewTransition", usage_in_kb); |
| break; |
| case TransferableResource::ResourceSource::kStaleContent: |
| CustomUmaHistogramMemoryKB( |
| "Memory.Renderer.EvictedLockedResources.StaleContent", usage_in_kb); |
| break; |
| case TransferableResource::ResourceSource::kTest: |
| CustomUmaHistogramMemoryKB("Memory.Renderer.EvictedLockedResources.Test", |
| usage_in_kb); |
| break; |
| case TransferableResource::ResourceSource::kTileRasterTask: |
| CustomUmaHistogramMemoryKB( |
| "Memory.Renderer.EvictedLockedResources.TileRasterTask", usage_in_kb); |
| break; |
| case TransferableResource::ResourceSource::kUI: |
| CustomUmaHistogramMemoryKB("Memory.Renderer.EvictedLockedResources.UI", |
| usage_in_kb); |
| break; |
| case TransferableResource::ResourceSource::kVideo: |
| CustomUmaHistogramMemoryKB("Memory.Renderer.EvictedLockedResources.Video", |
| usage_in_kb); |
| break; |
| case TransferableResource::ResourceSource::kWebGPUSwapBuffer: |
| CustomUmaHistogramMemoryKB( |
| "Memory.Renderer.EvictedLockedResources.WebGPUSwapBuffer", |
| usage_in_kb); |
| break; |
| } |
| } |
| |
| class ScopedBatchResourcesReleaseImpl |
| : public ClientResourceProvider::ScopedBatchResourcesRelease { |
| public: |
| using ScopedBatchResourcesRelease::ScopedBatchResourcesRelease; |
| explicit ScopedBatchResourcesReleaseImpl( |
| base::OnceClosure batch_release_callback); |
| }; |
| |
| ScopedBatchResourcesReleaseImpl::ScopedBatchResourcesReleaseImpl( |
| base::OnceClosure batch_release_callback) |
| : ScopedBatchResourcesRelease(std::move(batch_release_callback)) {} |
| |
| } // namespace |
| |
| struct ClientResourceProvider::ImportedResource { |
| TransferableResource resource; |
| ReleaseCallback impl_release_callback; |
| ReleaseCallback main_thread_release_callback; |
| int exported_count = 0; |
| bool marked_for_deletion = false; |
| |
| gpu::SyncToken returned_sync_token; |
| bool returned_lost = false; |
| |
| ResourceEvictedCallback evicted_callback; |
| |
| #if DCHECK_IS_ON() |
| base::debug::StackTrace stack_trace; |
| #endif |
| |
| ImportedResource(ResourceId id, |
| const TransferableResource& resource, |
| ReleaseCallback impl_release_callback, |
| ReleaseCallback main_thread_release_callback, |
| ResourceEvictedCallback evicted_callback) |
| : resource(resource), |
| impl_release_callback(std::move(impl_release_callback)), |
| main_thread_release_callback(std::move(main_thread_release_callback)), |
| // If the resource is immediately deleted, it returns the same SyncToken |
| // it came with. The client may need to wait on that before deleting the |
| // backing or reusing it. |
| returned_sync_token(resource.sync_token()), |
| evicted_callback(std::move(evicted_callback)) { |
| // We should never have no ReleaseCallback. |
| CHECK(this->impl_release_callback || this->main_thread_release_callback); |
| // Replace the |resource| id with the local id from this |
| // ClientResourceProvider. |
| this->resource.id = id; |
| } |
| ~ImportedResource() = default; |
| |
| ImportedResource(ImportedResource&&) = default; |
| ImportedResource& operator=(ImportedResource&&) = default; |
| |
| void RunReleaseCallbacks() { |
| if (impl_release_callback) { |
| std::move(impl_release_callback).Run(returned_sync_token, returned_lost); |
| } |
| // We intend for `main_thread_release_callback` to be run on |
| // `main_tast_runner_`. This is done in |
| // `ClientResourceProvider::BatchMainReleaseCallbacks`, in response to |
| // resources being returned. However the client can also remove resources |
| // independently. Since we currently do not know when these removals would |
| // start/stop, we cannot batch them. Instead maintain previous behaviour |
| // of just calling these directly. |
| if (main_thread_release_callback) { |
| std::move(main_thread_release_callback) |
| .Run(returned_sync_token, returned_lost); |
| } |
| } |
| }; |
| |
| ClientResourceProvider::ScopedBatchResourcesRelease:: |
| ScopedBatchResourcesRelease( |
| ClientResourceProvider::ScopedBatchResourcesRelease&& other) = default; |
| |
| ClientResourceProvider::ScopedBatchResourcesRelease:: |
| ~ScopedBatchResourcesRelease() { |
| if (batch_release_callback_) { |
| std::move(batch_release_callback_).Run(); |
| } |
| } |
| |
| ClientResourceProvider::ScopedBatchResourcesRelease:: |
| ScopedBatchResourcesRelease( |
| base::OnceCallback<void()> batch_release_callback) |
| : batch_release_callback_(std::move(batch_release_callback)) {} |
| |
| ClientResourceProvider::ClientResourceProvider() { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| } |
| |
| ClientResourceProvider::ClientResourceProvider( |
| scoped_refptr<base::SequencedTaskRunner> main_task_runner, |
| scoped_refptr<base::SequencedTaskRunner> impl_task_runner, |
| ResourceFlushCallback resource_flush_callback) |
| : main_task_runner_(main_task_runner), |
| impl_task_runner_(impl_task_runner), |
| resource_flush_callback_(std::move(resource_flush_callback)), |
| threaded_release_callbacks_supported_( |
| main_task_runner_ && impl_task_runner_ && |
| main_task_runner_ != impl_task_runner_ && resource_flush_callback_) {} |
| |
| ClientResourceProvider::~ClientResourceProvider() { |
| // If this fails, there are outstanding resources exported that should be |
| // lost and returned by calling ShutdownAndReleaseAllResources(), or there |
| // are resources that were imported without being removed by |
| // RemoveImportedResource(). In either case, calling |
| // ShutdownAndReleaseAllResources() will help, as it will report which |
| // resources were imported without being removed as well. |
| DCHECK(imported_resources_.empty()); |
| |
| // It is possible that we were deleted while a `ScopedBatchResourcesRelease` |
| // was still being held. This ensures the callbacks are ran. |
| BatchResourceRelease(); |
| } |
| |
| void ClientResourceProvider::PrepareSendToParent( |
| const std::vector<ResourceId>& export_ids, |
| std::vector<TransferableResource>* list, |
| gpu::SharedImageInterface* shared_image_interface) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| |
| // This function goes through the array multiple times, store the resources |
| // as pointers so we don't have to look up the resource id multiple times. |
| // Make sure the maps do not change while these vectors are alive or they |
| // will become invalid. |
| std::vector<ImportedResource*> imports; |
| imports.reserve(export_ids.size()); |
| for (const ResourceId id : export_ids) { |
| auto it = imported_resources_.find(id); |
| CHECK(it != imported_resources_.end()); |
| imports.push_back(&it->second); |
| } |
| |
| if (shared_image_interface) { |
| // Note for this lamdba -> gpu::SyncToken& is required as implicit auto |
| // decays removing the reference |
| static auto get_sync_token = |
| [](ImportedResource* imported_resource) -> gpu::SyncToken& { |
| CHECK(imported_resource); |
| return imported_resource->resource.mutable_sync_token(); |
| }; |
| |
| shared_image_interface->VerifySyncTokens(imports, get_sync_token); |
| } else { |
| static auto check_is_verified = |
| [](const ImportedResource* imported_resource) { |
| CHECK(imported_resource); |
| return !imported_resource->resource.sync_token().verified_flush(); |
| }; |
| |
| DCHECK(std::none_of(imports.begin(), imports.end(), check_is_verified)); |
| } |
| |
| list->reserve(list->size() + imports.size()); |
| for (ImportedResource* imported : imports) { |
| list->push_back(imported->resource); |
| imported->exported_count++; |
| } |
| } |
| |
| void ClientResourceProvider::ReceiveReturnsFromParent( |
| std::vector<ReturnedResource> resources) { |
| TRACE_EVENT0("viz", __PRETTY_FUNCTION__); |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| |
| std::vector<base::OnceClosure> impl_release_callbacks; |
| impl_release_callbacks.reserve(resources.size()); |
| std::vector<base::OnceClosure> main_impl_release_callbacks; |
| main_impl_release_callbacks.reserve(resources.size()); |
| |
| for (const auto& returned : resources) { |
| auto imported_it = imported_resources_.find(returned.id); |
| if (imported_it == imported_resources_.end()) { |
| // TODO(zmo): In theory, everything being returned should already be in |
| // the imported list. We should figure out why this happens. |
| continue; |
| } |
| auto& imported = imported_it->second; |
| |
| DCHECK_GE(imported.exported_count, returned.count); |
| imported.exported_count -= returned.count; |
| imported.returned_lost |= returned.lost; |
| |
| if (imported.exported_count) { |
| // Can't remove the imported yet so go to the next, looking for the next |
| // returned resource. |
| continue; |
| } |
| |
| // Save the sync token only when the exported count is going to 0. Or IOW |
| // drop all by the last returned sync token. |
| if (returned.sync_token.HasData()) { |
| imported.returned_sync_token = returned.sync_token; |
| } |
| |
| if (!imported.marked_for_deletion) { |
| // Not going to remove the imported yet so go to the next, looking for the |
| // next returned resource. |
| continue; |
| } |
| |
| if (imported.main_thread_release_callback) { |
| main_impl_release_callbacks.push_back( |
| base::BindOnce(std::move(imported.main_thread_release_callback), |
| imported.returned_sync_token, imported.returned_lost)); |
| } |
| |
| if (imported.impl_release_callback) { |
| impl_release_callbacks.push_back( |
| base::BindOnce(std::move(imported.impl_release_callback), |
| imported.returned_sync_token, imported.returned_lost)); |
| } |
| |
| imported_resources_.erase(imported_it); |
| } |
| |
| for (auto& cb : impl_release_callbacks) { |
| std::move(cb).Run(); |
| } |
| |
| if (!main_impl_release_callbacks.empty()) { |
| BatchMainReleaseCallbacks(std::move(main_impl_release_callbacks)); |
| } |
| } |
| |
| ResourceId ClientResourceProvider::ImportResource( |
| const TransferableResource& resource, |
| ReleaseCallback impl_release_callback, |
| ReleaseCallback main_thread_release_callback, |
| ResourceEvictedCallback evicted_callback) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| |
| // Clients are not allowed to import any empty resource. |
| CHECK(!resource.is_empty()); |
| ResourceId id = id_generator_.GenerateNextId(); |
| auto result = imported_resources_.emplace( |
| id, ImportedResource(id, resource, std::move(impl_release_callback), |
| std::move(main_thread_release_callback), |
| std::move(evicted_callback))); |
| CHECK(result.second); // If false, the id was already in the map. |
| return id; |
| } |
| |
| void ClientResourceProvider::RemoveImportedResource(ResourceId id) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| auto it = imported_resources_.find(id); |
| CHECK(it != imported_resources_.end()); |
| ImportedResource& imported = it->second; |
| imported.marked_for_deletion = true; |
| // We clear the callback here, as we will hold onto `imported` until it has |
| // been returned. Which could occur after the lifetime of the importer. |
| imported.evicted_callback = ResourceEvictedCallback(); |
| if (imported.exported_count == 0) { |
| TakeOrRunResourceReleases(batch_release_callbacks_, imported); |
| imported_resources_.erase(it); |
| } |
| } |
| |
| void ClientResourceProvider::ReleaseAllExportedResources(bool lose) { |
| const bool batch = |
| base::FeatureList::IsEnabled(features::kBatchResourceRelease); |
| if (batch) { |
| batch_main_release_callbacks_.reserve(imported_resources_.size()); |
| } |
| |
| for (auto it = imported_resources_.begin(); |
| it != imported_resources_.end();) { |
| ImportedResource& imported = it->second; |
| if (!imported.exported_count) { |
| // Not exported, not up for consideration to be returned here. |
| it++; |
| continue; |
| } |
| imported.exported_count = 0; |
| imported.returned_lost |= lose; |
| if (!imported.marked_for_deletion) { |
| // Was exported, but not removed by the client, so don't return it |
| // yet. |
| it++; |
| continue; |
| } |
| |
| TakeOrRunResourceReleases(batch, imported); |
| // Was exported and removed by the client, so erase it. |
| it = imported_resources_.erase(it); |
| } |
| |
| BatchResourceRelease(); |
| } |
| |
| void ClientResourceProvider::ShutdownAndReleaseAllResources() { |
| const bool batch = |
| base::FeatureList::IsEnabled(features::kBatchResourceRelease); |
| if (batch) { |
| batch_main_release_callbacks_.reserve(imported_resources_.size()); |
| } |
| |
| for (auto& pair : imported_resources_) { |
| ImportedResource& imported = pair.second; |
| |
| #if DCHECK_IS_ON() |
| // If this is false, then the resource has not been removed via |
| // RemoveImportedResource(), and all resources should be removed before |
| // we resort to marking resources as lost during shutdown. |
| DCHECK(imported.marked_for_deletion) |
| << "id: " << pair.first << " from:\n" |
| << imported.stack_trace.ToString() << "==="; |
| DCHECK(imported.exported_count) << "id: " << pair.first << " from:\n" |
| << imported.stack_trace.ToString() << "==="; |
| #endif |
| |
| imported.returned_lost = true; |
| TakeOrRunResourceReleases(batch, imported); |
| } |
| imported_resources_.clear(); |
| BatchResourceRelease(); |
| } |
| |
| void ClientResourceProvider::ValidateResource(ResourceId id) const { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| DCHECK(id); |
| DCHECK(imported_resources_.find(id) != imported_resources_.end()); |
| } |
| |
| bool ClientResourceProvider::InUseByConsumer(ResourceId id) { |
| auto it = imported_resources_.find(id); |
| CHECK(it != imported_resources_.end()); |
| ImportedResource& imported = it->second; |
| return imported.exported_count > 0 || imported.returned_lost; |
| } |
| |
| void ClientResourceProvider::SetEvicted(bool evicted) { |
| if (evicted_ == evicted) { |
| return; |
| } |
| evicted_ = evicted; |
| HandleEviction(); |
| } |
| |
| void ClientResourceProvider::SetVisible(bool visible) { |
| if (visible_ == visible) { |
| return; |
| } |
| visible_ = visible; |
| HandleEviction(); |
| } |
| |
| ClientResourceProvider::ScopedBatchResourcesRelease |
| ClientResourceProvider::CreateScopedBatchResourcesRelease() { |
| // Typically `batch_release_callbacks_` will remain `true` until the callback |
| // `BatchResourceRelease` is called. |
| // |
| // However other internal batching can lead to this being `false` as bot |
| // `ReleaseAllExportedResources` and `ShutdownAndReleaseAllResources`. |
| batch_release_callbacks_ = |
| base::FeatureList::IsEnabled(features::kBatchResourceRelease); |
| return ScopedBatchResourcesReleaseImpl( |
| base::BindOnce(&ClientResourceProvider::BatchResourceRelease, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| void ClientResourceProvider::HandleEviction() { |
| // The eviction and visibility change messages are racy. The Renderer |
| // Main-thread can be slow enough that we are still considered visible when |
| // the eviction signal is received. We do not count the locked resources |
| // solely when evicted. Instead we await the visibility change, as the Main |
| // thread may be in the process of unlocking resources, and the visibility |
| // change itself will attempt to free more resources. |
| if (!evicted_ || visible_) { |
| return; |
| } |
| int locked = 0; |
| size_t total_mem = 0u; |
| std::unordered_map<TransferableResource::ResourceSource, size_t> |
| mem_per_source; |
| std::vector<ResourceId> ids_to_unlock; |
| for (auto& [id, imported] : imported_resources_) { |
| if (!imported.marked_for_deletion) { |
| ++locked; |
| auto resource_source = imported.resource.resource_source; |
| size_t resource_mem = imported.resource.GetFormat().EstimatedSizeInBytes( |
| imported.resource.GetSize()); |
| total_mem += resource_mem; |
| mem_per_source[resource_source] += resource_mem; |
| |
| if (!base::FeatureList::IsEnabled(features::kEvictionUnlocksResources) || |
| !imported.evicted_callback) { |
| continue; |
| } |
| // We can't call imported.evicted_callback here, nor can we save them and |
| // them in a for-loop after. This is because callbacks may trigger |
| // ClientResourceProvider::RemoveImportedResource(). |
| ids_to_unlock.push_back(id); |
| } |
| } |
| |
| for (const auto id : ids_to_unlock) { |
| auto imported = imported_resources_.find(id); |
| if (imported == imported_resources_.end()) { |
| continue; |
| } |
| std::move(imported->second.evicted_callback).Run(); |
| } |
| |
| // Only report when there are locked resources. Evictions where all resources |
| // can be released are not interesting |
| if (!locked) { |
| return; |
| } |
| size_t total_mem_in_kb = total_mem / 1024u; |
| CustomUmaHistogramMemoryKB("Memory.Renderer.EvictedLockedResources.Total", |
| total_mem_in_kb); |
| for (auto& [source, size] : mem_per_source) { |
| if (size) { |
| ReportResourceSourceUsage(source, size); |
| } |
| } |
| } |
| |
| void ClientResourceProvider::BatchMainReleaseCallbacks( |
| std::vector<base::OnceClosure> release_callbacks) { |
| if (threaded_release_callbacks_supported_) { |
| main_task_runner_->PostTask( |
| FROM_HERE, |
| base::BindOnce( |
| [](std::vector<base::OnceClosure> release_callbacks, |
| scoped_refptr<base::SequencedTaskRunner> impl_task_runner, |
| base::OnceClosure completed_callback) { |
| for (auto& cb : release_callbacks) { |
| std::move(cb).Run(); |
| } |
| std::move(completed_callback).Run(); |
| }, |
| std::move(release_callbacks), impl_task_runner_, |
| base::BindPostTask(impl_task_runner_, resource_flush_callback_))); |
| } else { |
| for (auto& cb : release_callbacks) { |
| std::move(cb).Run(); |
| } |
| } |
| } |
| |
| void ClientResourceProvider::BatchResourceRelease() { |
| batch_release_callbacks_ = false; |
| if (!batch_main_release_callbacks_.empty()) { |
| BatchMainReleaseCallbacks(std::move(batch_main_release_callbacks_)); |
| } |
| batch_main_release_callbacks_.clear(); |
| } |
| |
| void ClientResourceProvider::TakeOrRunResourceReleases( |
| bool batch, |
| ImportedResource& imported) { |
| if (batch) { |
| if (imported.impl_release_callback) { |
| std::move(imported.impl_release_callback) |
| .Run(imported.returned_sync_token, imported.returned_lost); |
| } |
| if (imported.main_thread_release_callback) { |
| batch_main_release_callbacks_.push_back( |
| base::BindOnce(std::move(imported.main_thread_release_callback), |
| imported.returned_sync_token, imported.returned_lost)); |
| } |
| } else { |
| imported.RunReleaseCallbacks(); |
| } |
| } |
| |
| size_t ClientResourceProvider::num_resources_for_testing() const { |
| return imported_resources_.size(); |
| } |
| |
| } // namespace viz |