| /* |
| * 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 |