/* * Copyright 2016 Google Inc. * * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ #include "bench/Benchmark.h" #include "include/core/SkCanvas.h" #include "include/core/SkImage.h" #include "include/core/SkSurface.h" #include "include/gpu/GrDirectContext.h" #include "src/gpu/GrDirectContextPriv.h" #include "tools/ToolUtils.h" #include /** These benchmarks were designed to measure changes to GrResourceCache's replacement policy */ ////////////////////////////////////////////////////////////////////////////// // The width/height of the images to draw. The small size underestimates the value of a good // replacement strategy since the texture uploads are quite small. However, the effects are still // significant and this lets the benchmarks complete a lot faster, especially on mobile. static constexpr int kS = 25; static void make_images(sk_sp imgs[], int cnt) { for (int i = 0; i < cnt; ++i) { imgs[i] = ToolUtils::create_checkerboard_image(kS, kS, SK_ColorBLACK, SK_ColorCYAN, 10); } } static void draw_image(SkCanvas* canvas, SkImage* img) { // Make the paint transparent to avoid any issues of deferred tiler blending // optmizations SkPaint paint; paint.setAlpha(0x10); canvas->drawImage(img, 0, 0, SkSamplingOptions(), &paint); } void set_cache_budget(SkCanvas* canvas, int approxImagesInBudget) { // This is inexact but we attempt to figure out a baseline number of resources GrContext needs // to render an SkImage and add one additional resource for each image we'd like to fit. auto context = canvas->recordingContext()->asDirectContext(); SkASSERT(context); context->flushAndSubmit(); context->priv().testingOnly_purgeAllUnlockedResources(); sk_sp image; make_images(&image, 1); draw_image(canvas, image.get()); context->flushAndSubmit(); int baselineCount; context->getResourceCacheUsage(&baselineCount, nullptr); baselineCount -= 1; // for the image's textures. context->setResourceCacheLimits(baselineCount + approxImagesInBudget, 1 << 30); context->priv().testingOnly_purgeAllUnlockedResources(); } ////////////////////////////////////////////////////////////////////////////// /** * Tests repeatedly drawing the same set of images in each frame. Different instances of the bench * run with different cache sizes and either repeat the image order each frame or use a random * order. Every variation of this bench draws the same image set, only the budget and order of * images differs. Since the total fill is the same they can be cross-compared. */ class ImageCacheBudgetBench : public Benchmark { public: /** budgetSize is the number of images that can fit in the cache. 100 images will be drawn. */ ImageCacheBudgetBench(int budgetSize, bool shuffle) : fBudgetSize(budgetSize) , fShuffle(shuffle) , fIndices(nullptr) { float imagesOverBudget = float(kImagesToDraw) / budgetSize; // Make the benchmark name contain the percentage of the budget that is used in each // simulated frame. fName.printf("image_cache_budget_%.0f%s", imagesOverBudget * 100, (shuffle ? "_shuffle" : "")); } bool isSuitableFor(Backend backend) override { return kGPU_Backend == backend; } protected: const char* onGetName() override { return fName.c_str(); } void onPerCanvasPreDraw(SkCanvas* canvas) override { auto context = canvas->recordingContext()->asDirectContext(); SkASSERT(context); fOldBytes = context->getResourceCacheLimit(); set_cache_budget(canvas, fBudgetSize); make_images(fImages, kImagesToDraw); if (fShuffle) { SkRandom random; fIndices.reset(new int[kSimulatedFrames * kImagesToDraw]); for (int frame = 0; frame < kSimulatedFrames; ++frame) { int* base = fIndices.get() + frame * kImagesToDraw; for (int i = 0; i < kImagesToDraw; ++i) { base[i] = i; } for (int i = 0; i < kImagesToDraw - 1; ++i) { int other = random.nextULessThan(kImagesToDraw - i) + i; using std::swap; swap(base[i], base[other]); } } } } void onPerCanvasPostDraw(SkCanvas* canvas) override { auto context = canvas->recordingContext()->asDirectContext(); SkASSERT(context); context->setResourceCacheLimit(fOldBytes); for (int i = 0; i < kImagesToDraw; ++i) { fImages[i].reset(); } fIndices.reset(nullptr); } void onDraw(int loops, SkCanvas* canvas) override { auto dContext = GrAsDirectContext(canvas->recordingContext()); for (int i = 0; i < loops; ++i) { for (int frame = 0; frame < kSimulatedFrames; ++frame) { for (int j = 0; j < kImagesToDraw; ++j) { int idx; if (fShuffle) { idx = fIndices[frame * kImagesToDraw + j]; } else { idx = j; } draw_image(canvas, fImages[idx].get()); } // Simulate a frame boundary by flushing. This should notify GrResourceCache. if (dContext) { dContext->flush(); } } } } private: static constexpr int kImagesToDraw = 100; static constexpr int kSimulatedFrames = 5; int fBudgetSize; bool fShuffle; SkString fName; sk_sp fImages[kImagesToDraw]; std::unique_ptr fIndices; size_t fOldBytes; using INHERITED = Benchmark; }; DEF_BENCH( return new ImageCacheBudgetBench(105, false); ) DEF_BENCH( return new ImageCacheBudgetBench(90, false); ) DEF_BENCH( return new ImageCacheBudgetBench(80, false); ) DEF_BENCH( return new ImageCacheBudgetBench(50, false); ) DEF_BENCH( return new ImageCacheBudgetBench(105, true); ) DEF_BENCH( return new ImageCacheBudgetBench(90, true); ) DEF_BENCH( return new ImageCacheBudgetBench(80, true); ) DEF_BENCH( return new ImageCacheBudgetBench(50, true); ) ////////////////////////////////////////////////////////////////////////////// /** * Similar to above but changes between being over and under budget by varying the number of images * rendered. This is not directly comparable to the non-dynamic benchmarks. */ class ImageCacheBudgetDynamicBench : public Benchmark { public: enum class Mode { // Increase from min to max images drawn gradually over simulated frames and then back. kPingPong, // Alternate between under and over budget every other simulated frame. kFlipFlop }; ImageCacheBudgetDynamicBench(Mode mode) : fMode(mode) {} bool isSuitableFor(Backend backend) override { return kGPU_Backend == backend; } protected: const char* onGetName() override { switch (fMode) { case Mode::kPingPong: return "image_cache_budget_dynamic_ping_pong"; case Mode::kFlipFlop: return "image_cache_budget_dynamic_flip_flop"; } return ""; } void onPerCanvasPreDraw(SkCanvas* canvas) override { auto context = canvas->recordingContext()->asDirectContext(); SkASSERT(context); context->getResourceCacheLimits(&fOldCount, &fOldBytes); make_images(fImages, kMaxImagesToDraw); set_cache_budget(canvas, kImagesInBudget); } void onPerCanvasPostDraw(SkCanvas* canvas) override { auto context = canvas->recordingContext()->asDirectContext(); SkASSERT(context); context->setResourceCacheLimits(fOldCount, fOldBytes); for (int i = 0; i < kMaxImagesToDraw; ++i) { fImages[i].reset(); } } void onDraw(int loops, SkCanvas* canvas) override { auto dContext = GrAsDirectContext(canvas->recordingContext()); int delta = 0; switch (fMode) { case Mode::kPingPong: delta = 1; break; case Mode::kFlipFlop: delta = kMaxImagesToDraw - kMinImagesToDraw; break; } for (int i = 0; i < loops; ++i) { int imgsToDraw = kMinImagesToDraw; for (int frame = 0; frame < kSimulatedFrames; ++frame) { for (int j = 0; j < imgsToDraw; ++j) { draw_image(canvas, fImages[j].get()); } imgsToDraw += delta; if (imgsToDraw > kMaxImagesToDraw || imgsToDraw < kMinImagesToDraw) { delta = -delta; imgsToDraw += 2 * delta; } // Simulate a frame boundary by flushing. This should notify GrResourceCache. if (dContext) { dContext->flush(); } } } } private: static constexpr int kImagesInBudget = 25; static constexpr int kMinImagesToDraw = 15; static constexpr int kMaxImagesToDraw = 35; static constexpr int kSimulatedFrames = 80; Mode fMode; sk_sp fImages[kMaxImagesToDraw]; size_t fOldBytes; int fOldCount; using INHERITED = Benchmark; }; DEF_BENCH( return new ImageCacheBudgetDynamicBench(ImageCacheBudgetDynamicBench::Mode::kPingPong); ) DEF_BENCH( return new ImageCacheBudgetDynamicBench(ImageCacheBudgetDynamicBench::Mode::kFlipFlop); )