/* * Copyright 2013 Google Inc. * * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ #include "BenchTimer.h" #include "LazyDecodeBitmap.h" #include "PictureBenchmark.h" #include "PictureRenderer.h" #include "SkBenchmark.h" #include "SkForceLinking.h" #include "SkGraphics.h" #include "SkStream.h" #include "SkString.h" #include "SkTArray.h" #include "TimerData.h" static const int kNumNormalRecordings = 10; static const int kNumRTreeRecordings = 10; static const int kNumPlaybacks = 1; static const size_t kNumBaseBenchmarks = 3; static const size_t kNumTileSizes = 3; static const size_t kNumBbhPlaybackBenchmarks = 3; static const size_t kNumBenchmarks = kNumBaseBenchmarks + kNumBbhPlaybackBenchmarks; enum BenchmarkType { kNormal_BenchmarkType = 0, kRTree_BenchmarkType, }; struct Histogram { Histogram() { // Make fCpuTime negative so that we don't mess with stats: fCpuTime = SkIntToScalar(-1); } SkScalar fCpuTime; SkString fPath; }; //////////////////////////////////////////////////////////////////////////////// // Defined below. struct BenchmarkControl; typedef void (*BenchmarkFunction) (const BenchmarkControl&, const SkString&, SkPicture*, BenchTimer*); static void benchmark_playback( const BenchmarkControl&, const SkString&, SkPicture*, BenchTimer*); static void benchmark_recording( const BenchmarkControl&, const SkString&, SkPicture*, BenchTimer*); //////////////////////////////////////////////////////////////////////////////// /** * Acts as a POD containing information needed to run a benchmark. * Provides static methods to poll benchmark info from an index. */ struct BenchmarkControl { SkISize fTileSize; BenchmarkType fType; BenchmarkFunction fFunction; SkString fName; /** * Will construct a BenchmarkControl instance from an index between 0 an kNumBenchmarks. */ static BenchmarkControl Make(size_t i) { SkASSERT(kNumBenchmarks > i); BenchmarkControl benchControl; benchControl.fTileSize = GetTileSize(i); benchControl.fType = GetBenchmarkType(i); benchControl.fFunction = GetBenchmarkFunc(i); benchControl.fName = GetBenchmarkName(i); return benchControl; } enum BaseBenchmarks { kNormalRecord = 0, kRTreeRecord, kNormalPlayback, }; static SkISize fTileSizes[kNumTileSizes]; static SkISize GetTileSize(size_t i) { // Two of the base benchmarks don't need a tile size. But to maintain simplicity // down the pipeline we have to let a couple of values unused. if (i < kNumBaseBenchmarks) { return SkISize::Make(256, 256); } if (i >= kNumBaseBenchmarks && i < kNumBenchmarks) { return fTileSizes[i - kNumBaseBenchmarks]; } SkASSERT(0); return SkISize::Make(0, 0); } static BenchmarkType GetBenchmarkType(size_t i) { if (i < kNumBaseBenchmarks) { switch (i) { case kNormalRecord: return kNormal_BenchmarkType; case kNormalPlayback: return kNormal_BenchmarkType; case kRTreeRecord: return kRTree_BenchmarkType; } } if (i < kNumBenchmarks) { return kRTree_BenchmarkType; } SkASSERT(0); return kRTree_BenchmarkType; } static BenchmarkFunction GetBenchmarkFunc(size_t i) { // Base functions. switch (i) { case kNormalRecord: return benchmark_recording; case kNormalPlayback: return benchmark_playback; case kRTreeRecord: return benchmark_recording; } // RTree playbacks if (i < kNumBenchmarks) { return benchmark_playback; } SkASSERT(0); return NULL; } static SkString GetBenchmarkName(size_t i) { // Base benchmark names switch (i) { case kNormalRecord: return SkString("normal_recording"); case kNormalPlayback: return SkString("normal_playback"); case kRTreeRecord: return SkString("rtree_recording"); } // RTree benchmark names. if (i < kNumBenchmarks) { SkASSERT(i >= kNumBaseBenchmarks); SkString name; name.printf("rtree_playback_%dx%d", fTileSizes[i - kNumBaseBenchmarks].fWidth, fTileSizes[i - kNumBaseBenchmarks].fHeight); return name; } else { SkASSERT(0); } return SkString(""); } }; SkISize BenchmarkControl::fTileSizes[kNumTileSizes] = { SkISize::Make(256, 256), SkISize::Make(512, 512), SkISize::Make(1024, 1024), }; static SkPicture* pic_from_path(const char path[]) { SkFILEStream stream(path); if (!stream.isValid()) { SkDebugf("-- Can't open '%s'\n", path); return NULL; } return SkPicture::CreateFromStream(&stream, &sk_tools::LazyDecodeBitmap); } /** * Returns true when a tiled renderer with no bounding box hierarchy produces the given bitmap. */ static bool compare_picture(const SkString& path, const SkBitmap& inBitmap, SkPicture* pic) { SkBitmap* bitmap; sk_tools::TiledPictureRenderer renderer; renderer.setBBoxHierarchyType(sk_tools::PictureRenderer::kNone_BBoxHierarchyType); renderer.init(pic); renderer.setup(); renderer.render(&path, &bitmap); SkAutoTDelete<SkBitmap> bmDeleter(bitmap); renderer.end(); if (bitmap->getSize() != inBitmap.getSize()) { return false; } return !memcmp(bitmap->getPixels(), inBitmap.getPixels(), bitmap->getSize()); } /** * This function is the sink to which all work ends up going. * Renders the picture into the renderer. It may or may not use an RTree. * The renderer is chosen upstream. If we want to measure recording, we will * use a RecordPictureRenderer. If we want to measure rendering, we will use a * TiledPictureRenderer. */ static void do_benchmark_work(sk_tools::PictureRenderer* renderer, int benchmarkType, const SkString& path, SkPicture* pic, const int numRepeats, const char *msg, BenchTimer* timer) { SkString msgPrefix; switch (benchmarkType){ case kNormal_BenchmarkType: msgPrefix.set("Normal"); renderer->setBBoxHierarchyType(sk_tools::PictureRenderer::kNone_BBoxHierarchyType); break; case kRTree_BenchmarkType: msgPrefix.set("RTree"); renderer->setBBoxHierarchyType(sk_tools::PictureRenderer::kRTree_BBoxHierarchyType); break; default: SkASSERT(0); break; } renderer->init(pic); /** * If the renderer is not tiled, assume we are measuring recording. */ bool isPlayback = (NULL != renderer->getTiledRenderer()); // Will be non-null during RTree picture playback. For correctness test. SkBitmap* bitmap = NULL; SkDebugf("%s %s %s %d times...\n", msgPrefix.c_str(), msg, path.c_str(), numRepeats); for (int i = 0; i < numRepeats; ++i) { // Set up the bitmap. SkBitmap** out = NULL; if (i == 0 && kRTree_BenchmarkType == benchmarkType && isPlayback) { out = &bitmap; } renderer->setup(); // Render once to fill caches. Fill bitmap during the first iteration. renderer->render(NULL, out); // Render again to measure timer->start(); bool result = renderer->render(NULL); timer->end(); // We only care about a false result on playback. RecordPictureRenderer::render will always // return false because we are passing a NULL file name on purpose; which is fine. if (isPlayback && !result) { SkDebugf("Error rendering during playback.\n"); } } if (bitmap) { SkAutoTDelete<SkBitmap> bmDeleter(bitmap); if (!compare_picture(path, *bitmap, pic)) { SkDebugf("Error: RTree produced different bitmap\n"); } } } /** * Call do_benchmark_work with a tiled renderer using the default tile dimensions. */ static void benchmark_playback( const BenchmarkControl& benchControl, const SkString& path, SkPicture* pic, BenchTimer* timer) { sk_tools::TiledPictureRenderer renderer; SkString message("tiled_playback"); message.appendf("_%dx%d", benchControl.fTileSize.fWidth, benchControl.fTileSize.fHeight); do_benchmark_work(&renderer, benchControl.fType, path, pic, kNumPlaybacks, message.c_str(), timer); } /** * Call do_benchmark_work with a RecordPictureRenderer. */ static void benchmark_recording( const BenchmarkControl& benchControl, const SkString& path, SkPicture* pic, BenchTimer* timer) { sk_tools::RecordPictureRenderer renderer; int numRecordings = 0; switch(benchControl.fType) { case kRTree_BenchmarkType: numRecordings = kNumRTreeRecordings; break; case kNormal_BenchmarkType: numRecordings = kNumNormalRecordings; break; } do_benchmark_work(&renderer, benchControl.fType, path, pic, numRecordings, "recording", timer); } /** * Takes argc,argv along with one of the benchmark functions defined above. * Will loop along all skp files and perform measurments. * * Returns a SkScalar representing CPU time taken during benchmark. * As a side effect, it spits the timer result to stdout. * Will return -1.0 on error. */ static bool benchmark_loop( int argc, char **argv, const BenchmarkControl& benchControl, SkTArray<Histogram>& histogram) { static const SkString timeFormat("%f"); TimerData timerData(argc - 1); for (int index = 1; index < argc; ++index) { BenchTimer timer; SkString path(argv[index]); SkAutoTUnref<SkPicture> pic(pic_from_path(path.c_str())); if (NULL == pic) { SkDebugf("Couldn't create picture. Ignoring path: %s\n", path.c_str()); continue; } benchControl.fFunction(benchControl, path, pic, &timer); histogram[index - 1].fPath = path; histogram[index - 1].fCpuTime = SkDoubleToScalar(timer.fCpu); } const SkString timerResult = timerData.getResult( /*doubleFormat = */ timeFormat.c_str(), /*result = */ TimerData::kAvg_Result, /*configName = */ benchControl.fName.c_str(), /*timerFlags = */ TimerData::kCpu_Flag); const char findStr[] = "= "; int pos = timerResult.find(findStr); if (-1 == pos) { SkDebugf("Unexpected output from TimerData::getResult(...). Unable to parse.\n"); return false; } SkScalar cpuTime = SkDoubleToScalar(atof(timerResult.c_str() + pos + sizeof(findStr) - 1)); if (cpuTime == 0) { // atof returns 0.0 on error. SkDebugf("Unable to read value from timer result.\n"); return false; } return true; } int tool_main(int argc, char** argv); int tool_main(int argc, char** argv) { SkAutoGraphics ag; SkString usage; usage.printf("Usage: filename [filename]*\n"); if (argc < 2) { SkDebugf("%s\n", usage.c_str()); return -1; } SkTArray<Histogram> histograms[kNumBenchmarks]; for (size_t i = 0; i < kNumBenchmarks; ++i) { histograms[i].reset(argc - 1); bool success = benchmark_loop( argc, argv, BenchmarkControl::Make(i), histograms[i]); if (!success) { SkDebugf("benchmark_loop failed at index %d\n", i); } } // Output gnuplot readable histogram data.. const char* pbTitle = "bbh_shootout_playback.dat"; const char* recTitle = "bbh_shootout_record.dat"; SkFILEWStream playbackOut(pbTitle); SkFILEWStream recordOut(recTitle); recordOut.writeText("# "); playbackOut.writeText("# "); for (size_t i = 0; i < kNumBenchmarks; ++i) { SkString out; out.printf("%s ", BenchmarkControl::GetBenchmarkName(i).c_str()); if (BenchmarkControl::GetBenchmarkFunc(i) == &benchmark_recording) { recordOut.writeText(out.c_str()); } if (BenchmarkControl::GetBenchmarkFunc(i) == &benchmark_playback) { playbackOut.writeText(out.c_str()); } } recordOut.writeText("\n"); playbackOut.writeText("\n"); // Write to file, and save recording averages. SkScalar avgRecord = SkIntToScalar(0); SkScalar avgPlayback = SkIntToScalar(0); SkScalar avgRecordRTree = SkIntToScalar(0); SkScalar avgPlaybackRTree = SkIntToScalar(0); for (int i = 0; i < argc - 1; ++i) { SkString pbLine; SkString recLine; // ==== Write record info recLine.printf("%d ", i); SkScalar cpuTime = histograms[BenchmarkControl::kNormalRecord][i].fCpuTime; recLine.appendf("%f ", cpuTime); avgRecord += cpuTime; cpuTime = histograms[BenchmarkControl::kRTreeRecord][i].fCpuTime; recLine.appendf("%f", cpuTime); avgRecordRTree += cpuTime; avgPlaybackRTree += cpuTime; // ==== Write playback info pbLine.printf("%d ", i); pbLine.appendf("%f ", histograms[2][i].fCpuTime); // Start with normal playback time. avgPlayback += histograms[kNumBbhPlaybackBenchmarks - 1][i].fCpuTime; avgPlaybackRTree += histograms[kNumBbhPlaybackBenchmarks][i].fCpuTime; // Append all playback benchmark times. for (size_t j = kNumBbhPlaybackBenchmarks; j < kNumBenchmarks; ++j) { pbLine.appendf("%f ", histograms[j][i].fCpuTime); } pbLine.remove(pbLine.size() - 1, 1); // Remove trailing space from line. pbLine.appendf("\n"); recLine.appendf("\n"); playbackOut.writeText(pbLine.c_str()); recordOut.writeText(recLine.c_str()); } avgRecord /= argc - 1; avgRecordRTree /= argc - 1; avgPlayback /= argc - 1; avgPlaybackRTree /= argc - 1; SkDebugf("Average base recording time: %.3fms\n", avgRecord); SkDebugf("Average recording time with rtree: %.3fms\n", avgRecordRTree); SkDebugf("Average base playback time: %.3fms\n", avgPlayback); SkDebugf("Average playback time with rtree: %.3fms\n", avgPlaybackRTree); SkDebugf("\nWrote data to gnuplot-readable files: %s %s\n", pbTitle, recTitle); return 0; } #if !defined(SK_BUILD_FOR_IOS) && !defined(SK_BUILD_FOR_NACL) int main(int argc, char** argv) { return tool_main(argc, argv); } #endif