From f9e45d1d818ae0956ba77ed598b7040cfecca553 Mon Sep 17 00:00:00 2001 From: Derek Sollenberger Date: Thu, 1 Jun 2017 13:07:39 -0400 Subject: [PATCH] Implement CacheManager for the Skia pipelines. The core of the implementation is complete and provides heuristic cache sizing based on the size of the surface being used. This CL will also be used to add the following features in the future... 1) Support Vulkan pipeline reporting on the size of the surface. 2) Complete the VectorDrawableAtlas stub code 3) Automatic purging of stale resources for low memory devices. Test: hwui_unit_tests (new test added) and CtsUiRendering Bug: 62260637 Change-Id: Ib85159cca28b646fe249f2190b07f1b7e0f50d8f --- libs/hwui/Android.bp | 2 + libs/hwui/pipeline/skia/SkiaPipeline.cpp | 3 +- libs/hwui/renderstate/RenderState.h | 2 + libs/hwui/renderthread/CacheManager.cpp | 187 +++++++++++++++++++++ libs/hwui/renderthread/CacheManager.h | 92 ++++++++++ libs/hwui/renderthread/CanvasContext.cpp | 40 ++++- libs/hwui/renderthread/EglManager.cpp | 2 +- libs/hwui/renderthread/RenderProxy.cpp | 13 +- libs/hwui/renderthread/RenderThread.cpp | 47 ++++++ libs/hwui/renderthread/RenderThread.h | 6 +- libs/hwui/tests/unit/CacheManagerTests.cpp | 75 +++++++++ 11 files changed, 444 insertions(+), 25 deletions(-) create mode 100644 libs/hwui/renderthread/CacheManager.cpp create mode 100644 libs/hwui/renderthread/CacheManager.h create mode 100644 libs/hwui/tests/unit/CacheManagerTests.cpp diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp index 07ff27de794f0..23a207ed8e978 100644 --- a/libs/hwui/Android.bp +++ b/libs/hwui/Android.bp @@ -147,6 +147,7 @@ cc_defaults { "renderstate/Scissor.cpp", "renderstate/Stencil.cpp", "renderstate/TextureState.cpp", + "renderthread/CacheManager.cpp", "renderthread/CanvasContext.cpp", "renderthread/OpenGLPipeline.cpp", "renderthread/DrawFrameTask.cpp", @@ -299,6 +300,7 @@ cc_test { "tests/unit/BakedOpRendererTests.cpp", "tests/unit/BakedOpStateTests.cpp", "tests/unit/BitmapTests.cpp", + "tests/unit/CacheManagerTests.cpp", "tests/unit/CanvasContextTests.cpp", "tests/unit/CanvasStateTests.cpp", "tests/unit/ClipAreaTests.cpp", diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.cpp b/libs/hwui/pipeline/skia/SkiaPipeline.cpp index 88293db193f88..bbbbd5ce51b28 100644 --- a/libs/hwui/pipeline/skia/SkiaPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaPipeline.cpp @@ -50,8 +50,7 @@ TaskManager* SkiaPipeline::getTaskManager() { } void SkiaPipeline::onDestroyHardwareResources() { - // No need to flush the caches here. There is a timer - // which will flush temporary resources over time. + mRenderThread.cacheManager().trimStaleResources(); } bool SkiaPipeline::pinImages(std::vector& mutableImages) { diff --git a/libs/hwui/renderstate/RenderState.h b/libs/hwui/renderstate/RenderState.h index 787946f79f6be..4b7a86580621a 100644 --- a/libs/hwui/renderstate/RenderState.h +++ b/libs/hwui/renderstate/RenderState.h @@ -45,6 +45,7 @@ class Layer; class DeferredLayerUpdater; namespace renderthread { +class CacheManager; class CanvasContext; class RenderThread; } @@ -55,6 +56,7 @@ class RenderState { PREVENT_COPY_AND_ASSIGN(RenderState); friend class renderthread::RenderThread; friend class Caches; + friend class renderthread::CacheManager; public: void onGLContextCreated(); void onGLContextDestroyed(); diff --git a/libs/hwui/renderthread/CacheManager.cpp b/libs/hwui/renderthread/CacheManager.cpp new file mode 100644 index 0000000000000..f0d6b3860938e --- /dev/null +++ b/libs/hwui/renderthread/CacheManager.cpp @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * 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 "CacheManager.h" + +#include "Layer.h" +#include "RenderThread.h" +#include "renderstate/RenderState.h" + +#include +#include +#include +#include + +namespace android { +namespace uirenderer { +namespace renderthread { + +// This multiplier was selected based on historical review of cache sizes relative +// to the screen resolution. This is meant to be a conservative default based on +// that analysis. The 4.0f is used because the default pixel format is assumed to +// be ARGB_8888. +#define SURFACE_SIZE_MULTIPLIER (12.0f * 4.0f) +#define BACKGROUND_RETENTION_PERCENTAGE (0.5f) + +// for super large fonts we will draw them as paths so no need to keep linearly +// increasing the font cache size. +#define FONT_CACHE_MIN_MB (0.5f) +#define FONT_CACHE_MAX_MB (4.0f) + +CacheManager::CacheManager(const DisplayInfo& display) + : mMaxSurfaceArea(display.w * display.h) { + mVectorDrawableAtlas.reset(new VectorDrawableAtlas); +} + +void CacheManager::reset(GrContext* context) { + if (context != mGrContext.get()) { + destroy(); + } + + if (context) { + mGrContext = sk_ref_sp(context); + mGrContext->getResourceCacheLimits(&mMaxResources, nullptr); + updateContextCacheSizes(); + } +} + +void CacheManager::destroy() { + // cleanup any caches here as the GrContext is about to go away... + mGrContext.reset(nullptr); + mVectorDrawableAtlas.reset(new VectorDrawableAtlas); +} + +void CacheManager::updateContextCacheSizes() { + mMaxResourceBytes = mMaxSurfaceArea * SURFACE_SIZE_MULTIPLIER; + mBackgroundResourceBytes = mMaxResourceBytes * BACKGROUND_RETENTION_PERCENTAGE; + + mGrContext->setResourceCacheLimits(mMaxResources, mMaxResourceBytes); +} + +void CacheManager::configureContext(GrContextOptions* contextOptions) { + contextOptions->fAllowPathMaskCaching = true; + + float screenMP = mMaxSurfaceArea / 1024.0f / 1024.0f; + float fontCacheMB = 0; + float decimalVal = std::modf(screenMP, &fontCacheMB); + + // This is a basic heuristic to size the cache to a multiple of 512 KB + if (decimalVal > 0.8f) { + fontCacheMB += 1.0f; + } else if (decimalVal > 0.5f) { + fontCacheMB += 0.5f; + } + + // set limits on min/max size of the cache + fontCacheMB = std::max(FONT_CACHE_MIN_MB, std::min(FONT_CACHE_MAX_MB, fontCacheMB)); + + // We must currently set the size of the text cache based on the size of the + // display even though we like to be dynamicallysizing it to the size of the window. + // Skia's implementation doesn't provide a mechanism to resize the font cache due to + // the potential cost of recreating the glyphs. + contextOptions->fGlyphCacheTextureMaximumBytes = fontCacheMB * 1024 * 1024; +} + +void CacheManager::trimMemory(TrimMemoryMode mode) { + if (!mGrContext) { + return; + } + + mGrContext->flush(); + + switch (mode) { + case TrimMemoryMode::Complete: + mVectorDrawableAtlas.reset(new VectorDrawableAtlas); + mGrContext->freeGpuResources(); + break; + case TrimMemoryMode::UiHidden: + mGrContext->purgeUnlockedResources(mMaxResourceBytes - mBackgroundResourceBytes, true); + break; + } +} + +void CacheManager::trimStaleResources() { + if (!mGrContext) { + return; + } + mGrContext->flush(); + mGrContext->purgeResourcesNotUsedInMs(std::chrono::seconds(30)); +} + +VectorDrawableAtlas* CacheManager::acquireVectorDrawableAtlas() { + LOG_ALWAYS_FATAL_IF(mVectorDrawableAtlas.get() == nullptr); + LOG_ALWAYS_FATAL_IF(mGrContext == nullptr); + + /** + * TODO LIST: + * 1) compute the atlas based on the surfaceArea surface + * 2) identify a way to reuse cache entries + * 3) add ability to repack the cache? + * 4) define memory conditions where we clear the cache (e.g. surface->reset()) + */ + + return mVectorDrawableAtlas.release(); +} +void CacheManager::releaseVectorDrawableAtlas(VectorDrawableAtlas* atlas) { + LOG_ALWAYS_FATAL_IF(mVectorDrawableAtlas.get() != nullptr); + mVectorDrawableAtlas.reset(atlas); + mVectorDrawableAtlas->isNewAtlas = false; +} + +void CacheManager::dumpMemoryUsage(String8& log, const RenderState* renderState) { + if (!mGrContext) { + log.appendFormat("No valid cache instance.\n"); + return; + } + + size_t bytesCached; + mGrContext->getResourceCacheUsage(nullptr, &bytesCached); + + log.appendFormat("Caches:\n"); + log.appendFormat(" Current / Maximum\n"); + log.appendFormat(" VectorDrawableAtlas %6.2f kB / %6.2f kB (entries = %zu)\n", + 0.0f, 0.0f, (size_t)0); + + if (renderState) { + if (renderState->mActiveLayers.size() > 0) { + log.appendFormat(" Layer Info:\n"); + } + + size_t layerMemoryTotal = 0; + for (std::set::iterator it = renderState->mActiveLayers.begin(); + it != renderState->mActiveLayers.end(); it++) { + const Layer* layer = *it; + const char* layerType = layer->getApi() == Layer::Api::OpenGL ? "GlLayer" : "VkLayer"; + log.appendFormat(" %s size %dx%d\n", layerType, + layer->getWidth(), layer->getHeight()); + layerMemoryTotal += layer->getWidth() * layer->getHeight() * 4; + } + log.appendFormat(" Layers Total %6.2f kB (numLayers = %zu)\n", + layerMemoryTotal / 1024.0f, renderState->mActiveLayers.size()); + } + + + log.appendFormat("Total memory usage:\n"); + log.appendFormat(" %zu bytes, %.2f MB (%.2f MB is purgeable)\n", + bytesCached, bytesCached / 1024.0f / 1024.0f, + mGrContext->getResourceCachePurgeableBytes() / 1024.0f / 1024.0f); + + +} + +} /* namespace renderthread */ +} /* namespace uirenderer */ +} /* namespace android */ diff --git a/libs/hwui/renderthread/CacheManager.h b/libs/hwui/renderthread/CacheManager.h new file mode 100644 index 0000000000000..43d58f2d58a83 --- /dev/null +++ b/libs/hwui/renderthread/CacheManager.h @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * 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. + */ + +#ifndef CACHEMANAGER_H +#define CACHEMANAGER_H + +#include +#include +#include +#include +#include + +namespace android { + +class Surface; + +namespace uirenderer { + +class RenderState; + +namespace renderthread { + +class IRenderPipeline; +class RenderThread; + +struct VectorDrawableAtlas { + sk_sp surface; + bool isNewAtlas = true; +}; + +class CacheManager { +public: + enum class TrimMemoryMode { + Complete, + UiHidden + }; + + void configureContext(GrContextOptions* context); + void trimMemory(TrimMemoryMode mode); + void trimStaleResources(); + void dumpMemoryUsage(String8& log, const RenderState* renderState = nullptr); + + VectorDrawableAtlas* acquireVectorDrawableAtlas(); + void releaseVectorDrawableAtlas(VectorDrawableAtlas*); + + size_t getCacheSize() const { return mMaxResourceBytes; } + size_t getBackgroundCacheSize() const { return mBackgroundResourceBytes; } + +private: + friend class RenderThread; + + CacheManager(const DisplayInfo& display); + + + void reset(GrContext* grContext); + void destroy(); + void updateContextCacheSizes(); + + const size_t mMaxSurfaceArea; + sk_sp mGrContext; + + int mMaxResources = 0; + size_t mMaxResourceBytes = 0; + size_t mBackgroundResourceBytes = 0; + + struct PipelineProps { + const void* pipelineKey = nullptr; + size_t surfaceArea = 0; + }; + + std::unique_ptr mVectorDrawableAtlas; +}; + +} /* namespace renderthread */ +} /* namespace uirenderer */ +} /* namespace android */ + +#endif /* CACHEMANAGER_H */ + diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp index e2a4a2a8dec26..a79bf359913b2 100644 --- a/libs/hwui/renderthread/CanvasContext.cpp +++ b/libs/hwui/renderthread/CanvasContext.cpp @@ -585,15 +585,37 @@ void CanvasContext::destroyHardwareResources() { } void CanvasContext::trimMemory(RenderThread& thread, int level) { - // No context means nothing to free - if (!thread.eglManager().hasEglContext()) return; - - ATRACE_CALL(); - if (level >= TRIM_MEMORY_COMPLETE) { - thread.renderState().flush(Caches::FlushMode::Full); - thread.eglManager().destroy(); - } else if (level >= TRIM_MEMORY_UI_HIDDEN) { - thread.renderState().flush(Caches::FlushMode::Moderate); + auto renderType = Properties::getRenderPipelineType(); + switch (renderType) { + case RenderPipelineType::OpenGL: { + // No context means nothing to free + if (!thread.eglManager().hasEglContext()) return; + ATRACE_CALL(); + if (level >= TRIM_MEMORY_COMPLETE) { + thread.renderState().flush(Caches::FlushMode::Full); + thread.eglManager().destroy(); + } else if (level >= TRIM_MEMORY_UI_HIDDEN) { + thread.renderState().flush(Caches::FlushMode::Moderate); + } + break; + } + case RenderPipelineType::SkiaGL: + case RenderPipelineType::SkiaVulkan: { + // No context means nothing to free + if (!thread.getGrContext()) return; + ATRACE_CALL(); + if (level >= TRIM_MEMORY_COMPLETE) { + thread.cacheManager().trimMemory(CacheManager::TrimMemoryMode::Complete); + thread.eglManager().destroy(); + thread.vulkanManager().destroy(); + } else if (level >= TRIM_MEMORY_UI_HIDDEN) { + thread.cacheManager().trimMemory(CacheManager::TrimMemoryMode::UiHidden); + } + break; + } + default: + LOG_ALWAYS_FATAL("canvas context type %d not supported", (int32_t) renderType); + break; } } diff --git a/libs/hwui/renderthread/EglManager.cpp b/libs/hwui/renderthread/EglManager.cpp index 53d42a2b9c43f..ecf686c5b40cd 100644 --- a/libs/hwui/renderthread/EglManager.cpp +++ b/libs/hwui/renderthread/EglManager.cpp @@ -138,7 +138,7 @@ void EglManager::initialize() { GrContextOptions options; options.fGpuPathRenderers &= ~GrContextOptions::GpuPathRenderers::kDistanceField; - options.fAllowPathMaskCaching = true; + mRenderThread.cacheManager().configureContext(&options); mRenderThread.setGrContext(GrContext::Create(GrBackend::kOpenGL_GrBackend, (GrBackendContext)glInterface.get(), options)); } diff --git a/libs/hwui/renderthread/RenderProxy.cpp b/libs/hwui/renderthread/RenderProxy.cpp index 5a4695f1315ed..ec56c313f62a5 100644 --- a/libs/hwui/renderthread/RenderProxy.cpp +++ b/libs/hwui/renderthread/RenderProxy.cpp @@ -469,18 +469,7 @@ uint32_t RenderProxy::frameTimePercentile(int p) { } CREATE_BRIDGE2(dumpGraphicsMemory, int fd, RenderThread* thread) { - args->thread->jankTracker().dump(args->fd); - - FILE *file = fdopen(args->fd, "a"); - if (Caches::hasInstance()) { - String8 cachesLog; - Caches::getInstance().dumpMemoryUsage(cachesLog); - fprintf(file, "\nCaches:\n%s\n", cachesLog.string()); - } else { - fprintf(file, "\nNo caches instance.\n"); - } - fprintf(file, "\nPipeline=FrameBuilder\n"); - fflush(file); + args->thread->dumpGraphicsMemory(args->fd); return nullptr; } diff --git a/libs/hwui/renderthread/RenderThread.cpp b/libs/hwui/renderthread/RenderThread.cpp index 0554583970235..13af2c4d15e85 100644 --- a/libs/hwui/renderthread/RenderThread.cpp +++ b/libs/hwui/renderthread/RenderThread.cpp @@ -202,6 +202,45 @@ void RenderThread::initThreadLocals() { mRenderState = new RenderState(*this); mJankTracker = new JankTracker(mDisplayInfo); mVkManager = new VulkanManager(*this); + mCacheManager = new CacheManager(mDisplayInfo); +} + +void RenderThread::dumpGraphicsMemory(int fd) { + jankTracker().dump(fd); + + String8 cachesOutput; + String8 pipeline; + auto renderType = Properties::getRenderPipelineType(); + switch (renderType) { + case RenderPipelineType::OpenGL: { + if (Caches::hasInstance()) { + cachesOutput.appendFormat("Caches:\n"); + Caches::getInstance().dumpMemoryUsage(cachesOutput); + } else { + cachesOutput.appendFormat("No caches instance."); + } + pipeline.appendFormat("FrameBuilder"); + break; + } + case RenderPipelineType::SkiaGL: { + mCacheManager->dumpMemoryUsage(cachesOutput, mRenderState); + pipeline.appendFormat("Skia (OpenGL)"); + break; + } + case RenderPipelineType::SkiaVulkan: { + mCacheManager->dumpMemoryUsage(cachesOutput, mRenderState); + pipeline.appendFormat("Skia (Vulkan)"); + break; + } + default: + LOG_ALWAYS_FATAL("canvas context type %d not supported", (int32_t) renderType); + break; + } + + FILE *file = fdopen(fd, "a"); + fprintf(file, "\n%s\n", cachesOutput.string()); + fprintf(file, "\nPipeline=%s\n", pipeline.string()); + fflush(file); } Readback& RenderThread::readback() { @@ -228,6 +267,14 @@ Readback& RenderThread::readback() { return *mReadback; } +void RenderThread::setGrContext(GrContext* context) { + mCacheManager->reset(context); + if (mGrContext.get()) { + mGrContext->releaseResourcesAndAbandonContext(); + } + mGrContext.reset(context); +} + int RenderThread::displayEventReceiverCallback(int fd, int events, void* data) { if (events & (Looper::EVENT_ERROR | Looper::EVENT_HANGUP)) { ALOGE("Display event receiver pipe was closed or an error occurred. " diff --git a/libs/hwui/renderthread/RenderThread.h b/libs/hwui/renderthread/RenderThread.h index 4b5601c5abc43..d9842572d7cd3 100644 --- a/libs/hwui/renderthread/RenderThread.h +++ b/libs/hwui/renderthread/RenderThread.h @@ -20,6 +20,7 @@ #include "RenderTask.h" #include "../JankTracker.h" +#include "CacheManager.h" #include "TimeLord.h" #include @@ -102,11 +103,13 @@ public: const DisplayInfo& mainDisplayInfo() { return mDisplayInfo; } GrContext* getGrContext() const { return mGrContext.get(); } - void setGrContext(GrContext* cxt) { mGrContext.reset(cxt); } + void setGrContext(GrContext* cxt); + CacheManager& cacheManager() { return *mCacheManager; } VulkanManager& vulkanManager() { return *mVkManager; } sk_sp allocateHardwareBitmap(SkBitmap& skBitmap); + void dumpGraphicsMemory(int fd); protected: virtual bool threadLoop() override; @@ -161,6 +164,7 @@ private: Readback* mReadback = nullptr; sk_sp mGrContext; + CacheManager* mCacheManager; VulkanManager* mVkManager; }; diff --git a/libs/hwui/tests/unit/CacheManagerTests.cpp b/libs/hwui/tests/unit/CacheManagerTests.cpp new file mode 100644 index 0000000000000..6115162c8f814 --- /dev/null +++ b/libs/hwui/tests/unit/CacheManagerTests.cpp @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * 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 + +#include "renderthread/CacheManager.h" +#include "renderthread/EglManager.h" +#include "tests/common/TestUtils.h" + +using namespace android; +using namespace android::uirenderer; +using namespace android::uirenderer::renderthread; + +static size_t getCacheUsage(GrContext* grContext) { + size_t cacheUsage; + grContext->getResourceCacheUsage(nullptr, &cacheUsage); + return cacheUsage; +} + +RENDERTHREAD_SKIA_PIPELINE_TEST(CacheManager, trimMemory) { + DisplayInfo displayInfo = renderThread.mainDisplayInfo(); + GrContext* grContext = renderThread.getGrContext(); + ASSERT_TRUE(grContext != nullptr); + + // create pairs of offscreen render targets and images until we exceed the backgroundCacheSizeLimit + std::vector> surfaces; + + while (getCacheUsage(grContext) <= renderThread.cacheManager().getBackgroundCacheSize()) { + SkImageInfo info = SkImageInfo::MakeA8(displayInfo.w, displayInfo.h); + sk_sp surface = SkSurface::MakeRenderTarget(grContext, SkBudgeted::kYes, info); + surface->getCanvas()->drawColor(SK_AlphaTRANSPARENT); + + grContext->flush(); + + surfaces.push_back(surface); + } + + ASSERT_TRUE(1 < surfaces.size()); + + // attempt to trim all memory while we still hold strong refs + renderThread.cacheManager().trimMemory(CacheManager::TrimMemoryMode::Complete); + ASSERT_TRUE(0 == grContext->getResourceCachePurgeableBytes()); + + // free the surfaces + for (size_t i = 0; i < surfaces.size(); i++) { + ASSERT_TRUE(surfaces[i]->unique()); + surfaces[i].reset(); + } + + // verify that we have enough purgeable bytes + const size_t purgeableBytes = grContext->getResourceCachePurgeableBytes(); + ASSERT_TRUE(renderThread.cacheManager().getBackgroundCacheSize() < purgeableBytes); + + // UI hidden and make sure only some got purged + renderThread.cacheManager().trimMemory(CacheManager::TrimMemoryMode::UiHidden); + ASSERT_TRUE(0 < grContext->getResourceCachePurgeableBytes()); + ASSERT_TRUE(renderThread.cacheManager().getBackgroundCacheSize() > getCacheUsage(grContext)); + + // complete and make sure all get purged + renderThread.cacheManager().trimMemory(CacheManager::TrimMemoryMode::Complete); + ASSERT_TRUE(0 == grContext->getResourceCachePurgeableBytes()); +}