blob: 716facc96748edc31928b38fa4e997b2444484fe [file] [log] [blame]
/*
* Copyright 2023 Google LLC
*
* 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/SkBitmap.h"
#include "include/core/SkCanvas.h"
#include "include/core/SkColorType.h"
#include "include/core/SkGraphics.h"
#include "include/core/SkStream.h"
#include "include/core/SkString.h"
#include "include/core/SkSurface.h"
#include "include/encode/SkPngEncoder.h"
#include "include/private/base/SkAssert.h"
#include "include/private/base/SkTArray.h"
#include "src/base/SkTime.h"
#include "src/utils/SkJSONWriter.h"
#include "src/utils/SkOSPath.h"
#include "tools/AutoreleasePool.h"
#include "tools/ProcStats.h"
#include "tools/Stats.h"
#include "tools/flags/CommandLineFlags.h"
#include "tools/testrunners/benchmark/target/BenchmarkTarget.h"
#include "tools/timer/Timer.h"
#include <cinttypes>
// TODO(lovisolo): Should we check that this is a valid Git hash?
static DEFINE_string(
gitHash,
"",
"Git hash to include in the results.json output file, which can be ingested by Perf.");
static DEFINE_string(issue,
"",
"Changelist ID (e.g. a Gerrit changelist number) to include in the "
"results.json output file, which can be ingested by Perf.");
static DEFINE_string(patchset,
"",
"Patchset ID (e.g. a Gerrit patchset number) to include in the results.json "
"output file, which can be ingested by Perf.");
static DEFINE_string(key,
"",
"Space-separated key/value pairs common to all benchmarks. These will be "
"included in the results.json output file, which can be ingested by Perf.");
static DEFINE_string(
links,
"",
"Space-separated name/URL pairs with additional information about the benchmark execution, "
"for example links to the Swarming bot and task pages, named \"swarming_bot\" and "
"\"swarming_task\", respectively. These links are included in the "
"results.json output file, which can be ingested by Perf.");
// When running under Bazel and overriding the output directory, you might encounter errors
// such as "No such file or directory" and "Read-only file system". The former can happen
// when running on RBE because the passed in output dir might not exist on the remote
// worker, whereas the latter can happen when running locally in sandboxed mode, which is
// the default strategy when running outside of RBE. One possible workaround is to run the
// test as a local subprocess, which can be done by passing flag --strategy=TestRunner=local
// to Bazel.
//
// Reference: https://bazel.build/docs/user-manual#execution-strategy.
static DEFINE_string(outputDir,
"",
"Directory where to write any output JSON and PNG files. "
"Optional when running under Bazel "
"(e.g. \"bazel test //path/to:test\") as it defaults to "
"$TEST_UNDECLARED_OUTPUTS_DIR.");
static DEFINE_string(surfaceConfig,
"",
"Name of the Surface configuration to use (e.g. \"8888\"). This determines "
"how we construct the SkSurface from which we get the SkCanvas that "
"benchmarks will draw on. See file "
"//tools/testrunners/common/surface_manager/SurfaceManager.h for details.");
static DEFINE_bool(
writePNGs,
false,
"Whether or not to write to the output directory any bitmaps produced by benchmarks.");
// Mutually exclusive with --autoTuneLoops.
static DEFINE_int(loops, 0, "The number of benchmark runs that constitutes a single sample.");
// Mutually exclusive with --loops.
static DEFINE_bool(autoTuneLoops,
false,
"Auto-tune (automatically determine) the number of benchmark runs that "
"constitutes a single sample. Timings are only reported when auto-tuning.");
static DEFINE_int(
autoTuneLoopsMax,
1000000,
"Maximum number of benchmark runs per single sample when auto-tuning. Ignored unless flag "
"--autoTuneLoops is set.");
// Mutually exclusive with --ms.
static DEFINE_int(samples, 10, "Number of samples to collect for each benchmark.");
// Mutually exclusive with --samples.
static DEFINE_int(ms, 0, "For each benchmark, collect samples for this many milliseconds.");
static DEFINE_int(flushEvery,
10,
"Flush the results.json output file every n-th run. This file "
"can be ingested by Perf.");
static DEFINE_bool(csv, false, "Print status in CSV format.");
static DEFINE_bool2(quiet, q, false, "if true, do not print status updates.");
static DEFINE_bool2(verbose, v, false, "Enable verbose output from the test runner.");
// TODO(lovisolo): Move these flag validation utilities under //tools/testrunners.
static void validate_string_flag_nonempty(std::string name, CommandLineFlags::StringArray flag) {
if (flag.size() == 0) {
SK_ABORT("Flag %s cannot be empty.\n", name.c_str());
}
}
static void validate_string_flag_single_value(std::string name,
CommandLineFlags::StringArray flag) {
if (flag.size() > 1) {
SK_ABORT("Flag %s takes one single value, got: %d.\n", name.c_str(), flag.size());
}
}
static void validate_string_flag_even(std::string name, CommandLineFlags::StringArray flag) {
if (flag.size() % 2 == 1) {
SK_ABORT(
"Flag %s takes an even number of arguments, got: %d.\n", name.c_str(), flag.size());
}
}
static void validate_int_flag_greater_or_equal(std::string name, int flag, int min) {
if (flag < min) {
SK_ABORT("Flag %s must be greater or equal than %d, got: %d.\n", name.c_str(), min, flag);
}
}
static void validate_flags_all_or_none(std::map<std::string, bool> flags) {
std::string names;
unsigned int numFlagsSet = 0;
for (auto const& [name, isSet] : flags) {
if (names == "") {
names = name;
} else {
names += ", " + name;
}
if (isSet) {
numFlagsSet++;
}
}
if (numFlagsSet != flags.size() && numFlagsSet != 0) {
SK_ABORT("Either all or none of the following flags must be set: %s.\n", names.c_str());
}
}
static void validate_flags_exactly_one(std::map<std::string, bool> flags) {
std::string names;
unsigned int numFlagsSet = 0;
for (auto const& [name, isSet] : flags) {
if (names == "") {
names = name;
} else {
names += ", " + name;
}
if (isSet) {
numFlagsSet++;
}
}
if (numFlagsSet != 1) {
SK_ABORT("Exactly one of the following flags must be set: %s.\n", names.c_str());
}
}
static void validate_flags(bool isBazelTest) {
validate_flags_all_or_none(
{{"--issue", FLAGS_issue.size() > 0}, {"--patchset", FLAGS_patchset.size() > 0}});
validate_string_flag_single_value("--issue", FLAGS_issue);
validate_string_flag_single_value("--patchset", FLAGS_patchset);
validate_string_flag_even("--key", FLAGS_key);
validate_string_flag_even("--links", FLAGS_links);
if (!isBazelTest) {
validate_string_flag_nonempty("--outputDir", FLAGS_outputDir);
}
validate_string_flag_single_value("--outputDir", FLAGS_outputDir);
validate_string_flag_nonempty("--surfaceConfig", FLAGS_surfaceConfig);
validate_string_flag_single_value("--surfaceConfig", FLAGS_surfaceConfig);
validate_flags_exactly_one(
{{"--loops", FLAGS_loops != 0}, {"--autoTuneLoops", FLAGS_autoTuneLoops}});
if (!FLAGS_autoTuneLoops) {
validate_int_flag_greater_or_equal("--loops", FLAGS_loops, 1);
}
validate_int_flag_greater_or_equal("--autoTuneLoopsMax", FLAGS_autoTuneLoopsMax, 1);
validate_flags_exactly_one({{"--samples", FLAGS_samples != 0}, {"--ms", FLAGS_ms != 0}});
if (FLAGS_ms == 0) {
validate_int_flag_greater_or_equal("--samples", FLAGS_samples, 1);
}
if (FLAGS_samples == 0) {
validate_int_flag_greater_or_equal("--ms", FLAGS_ms, 1);
}
}
// Helper class to produce JSON files in Perf ingestion format. The format is determined by Perf's
// format.Format Go struct:
//
// https://skia.googlesource.com/buildbot/+/e12f70e0a3249af6dd7754d55958ee64a22e0957/perf/go/ingest/format/format.go#168
//
// Note that the JSON format produced by this class differs from Nanobench. The latter follows
// Perf's legacy format, which is determined by the format.BenchData Go struct:
//
// https://skia.googlesource.com/buildbot/+/e12f70e0a3249af6dd7754d55958ee64a22e0957/perf/go/ingest/format/leagacyformat.go#26
class ResultsJSONWriter {
public:
// This struct mirrors Perf's format.SingleMeasurement Go struct:
// https://skia.googlesource.com/buildbot/+/e12f70e0a3249af6dd7754d55958ee64a22e0957/perf/go/ingest/format/format.go#31.
struct SingleMeasurement {
std::string value;
double measurement;
};
// This struct mirrors Perf's format.Result Go struct:
// https://skia.googlesource.com/buildbot/+/e12f70e0a3249af6dd7754d55958ee64a22e0957/perf/go/ingest/format/format.go#69.
//
// Note that the format.Result Go struct supports either one single measurement, or multiple
// measurements represented as a dictionary from arbitrary string keys to an array of
// format.SingleMeasurement Go structs. This class focuses on the latter variant.
struct Result {
std::map<std::string, std::string> key;
std::map<std::string, std::vector<SingleMeasurement>> measurements;
};
ResultsJSONWriter(const char* path)
: fFileWStream(path)
, fJson(&fFileWStream, SkJSONWriter::Mode::kPretty)
, fAddingResults(false) {
fJson.beginObject(); // Root object.
fJson.appendS32("version", 1);
}
void addGitHash(std::string gitHash) {
assertNotAddingResults();
fJson.appendCString("git_hash", gitHash.c_str());
}
void addChangelistInfo(std::string issue, std::string patchset) {
assertNotAddingResults();
fJson.appendCString("issue", issue.c_str());
fJson.appendCString("patchset", patchset.c_str());
}
void addKey(std::map<std::string, std::string> key) {
assertNotAddingResults();
fJson.beginObject("key");
for (auto const& [name, value] : key) {
fJson.appendCString(name.c_str(), value.c_str());
}
fJson.endObject();
}
void addLinks(std::map<std::string, std::string> links) {
assertNotAddingResults();
fJson.beginObject("links");
for (auto const& [key, value] : links) {
fJson.appendCString(key.c_str(), value.c_str());
}
fJson.endObject();
}
void addResult(Result result) {
if (!fAddingResults) {
fAddingResults = true;
fJson.beginArray("results"); // "results" array.
}
fJson.beginObject(); // Result object.
// Key.
fJson.beginObject("key"); // "key" dictionary.
for (auto const& [name, value] : result.key) {
fJson.appendCString(name.c_str(), value.c_str());
}
fJson.endObject(); // "key" dictionary.
// Measurements.
fJson.beginObject("measurements"); // "measurements" dictionary.
for (auto const& [name, singleMeasurements] : result.measurements) {
fJson.beginArray(name.c_str()); // <name> array.
for (const SingleMeasurement& singleMeasurement : singleMeasurements) {
// Based on
// https://skia.googlesource.com/skia/+/a063eaeaf1e09e4d6f42e0f44a5723622a46d21c/bench/ResultsWriter.h#51.
//
// Don't record if NaN or Inf.
if (std::isfinite(singleMeasurement.measurement)) {
fJson.beginObject();
fJson.appendCString("value", singleMeasurement.value.c_str());
fJson.appendDoubleDigits("measurement", singleMeasurement.measurement, 16);
fJson.endObject();
}
}
fJson.endArray(); // <name> array.
}
fJson.endObject(); // "measurements" dictionary.
fJson.endObject(); // Result object.
}
void flush() { fJson.flush(); }
~ResultsJSONWriter() {
if (fAddingResults) {
fJson.endArray(); // "results" array;
}
fJson.endObject(); // Root object.
}
private:
void assertNotAddingResults() {
if (fAddingResults) {
SK_ABORT("Cannot perform this operation after addResults() is called.");
}
}
SkFILEWStream fFileWStream;
SkJSONWriter fJson;
bool fAddingResults;
};
// Manages an autorelease pool for Metal. On other platforms, pool.drain() is a no-op.
AutoreleasePool pool;
static double now_ms() { return SkTime::GetNSecs() * 1e-6; }
// Based on
// https://skia.googlesource.com/skia/+/a063eaeaf1e09e4d6f42e0f44a5723622a46d21c/bench/nanobench.cpp#1503.
static void warm_up_test_runner_once(BenchmarkTarget* target, int loops) {
static bool warm = false;
if (warm) {
return;
}
if (FLAGS_ms < 1000) {
// Run the first bench for 1000ms to warm up the test runner if FLAGS_ms < 1000.
// Otherwise, the first few benches' measurements will be inaccurate.
auto stop = now_ms() + 1000;
do {
target->time(loops);
pool.drain();
} while (now_ms() < stop);
}
warm = true;
}
// Collects samples for the given benchmark. Returns a boolean indicating success or failure, the
// number of benchmark runs used for each sample, the samples and any statistics produced by the
// benchmark and/or target.
//
// Based on
// https://skia.googlesource.com/skia/+/a063eaeaf1e09e4d6f42e0f44a5723622a46d21c/bench/nanobench.cpp#1489.
static int sample_benchmark(BenchmarkTarget* target,
int* loops,
skia_private::TArray<double>* samples,
skia_private::TArray<SkString>* statKeys,
skia_private::TArray<double>* statValues) {
target->setup();
if (FLAGS_autoTuneLoops) {
auto [autoTunedLoops, ok] = target->autoTuneLoops();
if (!ok) {
// Can't be timed. A warning has already been printed.
target->tearDown();
return false;
}
*loops = autoTunedLoops;
if (*loops > FLAGS_autoTuneLoopsMax) {
SkDebugf(
"Warning: Clamping loops from %d to %d (per the --autoTuneLoopsMax flag) for "
"benchmark \"%s\".\n",
*loops,
FLAGS_autoTuneLoopsMax,
target->getBenchmark()->getUniqueName());
*loops = FLAGS_autoTuneLoopsMax;
}
} else {
*loops = FLAGS_loops;
}
// Warm up the test runner to increase the chances of getting consistent measurements. Only
// done once for the entire lifecycle of the test runner.
warm_up_test_runner_once(target, *loops);
// Each individual BenchmarkTarget must also be warmed up.
target->warmUp(*loops);
if (FLAGS_ms) {
// Collect as many samples as possible for a specific duration of time.
auto stop = now_ms() + FLAGS_ms;
do {
samples->push_back(target->time(*loops) / *loops);
pool.drain();
} while (now_ms() < stop);
} else {
// Collect an exact number of samples.
samples->reset(FLAGS_samples);
for (int s = 0; s < FLAGS_samples; s++) {
(*samples)[s] = target->time(*loops) / *loops;
pool.drain();
}
}
// Scale each sample to the benchmark's own units, time/unit.
for (double& sample : *samples) {
sample *= (1.0 / target->getBenchmark()->getUnits());
}
target->dumpStats(statKeys, statValues);
target->tearDown();
return true;
}
// Based on
// https://skia.googlesource.com/skia/+/a063eaeaf1e09e4d6f42e0f44a5723622a46d21c/bench/nanobench.cpp#432.
static void maybe_write_png(BenchmarkTarget* target, std::string outputDir) {
if (target->getBackend() == Benchmark::kNonRendering_Backend) {
return;
}
SkString filename = SkStringPrintf("%s.png", target->getBenchmark()->getUniqueName());
filename = SkOSPath::Join(outputDir.c_str(), filename.c_str());
if (!target->getCanvas() ||
target->getCanvas()->imageInfo().colorType() == kUnknown_SkColorType) {
return;
}
SkBitmap bmp;
bmp.allocPixels(target->getCanvas()->imageInfo());
if (!target->getCanvas()->readPixels(bmp, 0, 0)) {
SkDebugf("Warning: Could not read canvas pixels for benchmark \"%s\".\n",
target->getBenchmark()->getUniqueName());
return;
}
SkFILEWStream stream(filename.c_str());
if (!stream.isValid()) {
SkDebugf("Warning: Could not write file \"%s\".\n", filename.c_str());
return;
}
if (!SkPngEncoder::Encode(&stream, bmp.pixmap(), {})) {
SkDebugf("Warning: Could not encode pixels from benchmark \"%s\" as PNG.\n",
target->getBenchmark()->getUniqueName());
return;
}
if (FLAGS_verbose) {
SkDebugf("PNG for benchmark \"%s\" written to: %s",
target->getBenchmark()->getUniqueName(),
filename.c_str());
}
}
// Non-static because it is used from RasterBenchmarkTarget.cpp.
SkString humanize(double ms) {
if (FLAGS_verbose) return SkStringPrintf("%" PRIu64, (uint64_t)(ms * 1e6));
return HumanizeMs(ms);
}
#define HUMANIZE(ms) humanize(ms).c_str()
static SkString to_string(int n) {
SkString str;
str.appendS32(n);
return str;
}
// Based on
// https://skia.googlesource.com/skia/+/a063eaeaf1e09e4d6f42e0f44a5723622a46d21c/bench/nanobench.cpp#1593.
static void print_benchmark_stats(Stats* stats,
skia_private::TArray<double>* samples,
BenchmarkTarget* target,
std::string surfaceConfig,
int loops) {
if (!FLAGS_autoTuneLoops) {
SkDebugf("%4d/%-4dMB\t%s\t%s ",
sk_tools::getCurrResidentSetSizeMB(),
sk_tools::getMaxResidentSetSizeMB(),
target->getBenchmark()->getUniqueName(),
surfaceConfig.c_str());
SkDebugf("\n");
} else if (FLAGS_quiet) {
const char* mark = " ";
const double stddev_percent = sk_ieee_double_divide(100 * sqrt(stats->var), stats->mean);
if (stddev_percent > 5) mark = "?";
if (stddev_percent > 10) mark = "!";
SkDebugf("%10.2f %s\t%s\t%s\n",
stats->median * 1e3,
mark,
target->getBenchmark()->getUniqueName(),
surfaceConfig.c_str());
} else if (FLAGS_csv) {
const double stddev_percent = sk_ieee_double_divide(100 * sqrt(stats->var), stats->mean);
SkDebugf("%g,%g,%g,%g,%g,%s,%s\n",
stats->min,
stats->median,
stats->mean,
stats->max,
stddev_percent,
surfaceConfig.c_str(),
target->getBenchmark()->getUniqueName());
} else {
const double stddev_percent = sk_ieee_double_divide(100 * sqrt(stats->var), stats->mean);
SkDebugf("%4d/%-4dMB\t%d\t%s\t%s\t%s\t%s\t%.0f%%\t%s\t%s\t%s\n",
sk_tools::getCurrResidentSetSizeMB(),
sk_tools::getMaxResidentSetSizeMB(),
loops,
HUMANIZE(stats->min),
HUMANIZE(stats->median),
HUMANIZE(stats->mean),
HUMANIZE(stats->max),
stddev_percent,
FLAGS_ms ? to_string(samples->size()).c_str() : stats->plot.c_str(),
surfaceConfig.c_str(),
target->getBenchmark()->getUniqueName());
}
target->printStats();
if (FLAGS_verbose) {
SkDebugf("Samples: ");
for (int j = 0; j < samples->size(); j++) {
SkDebugf("%s ", HUMANIZE((*samples)[j]));
}
SkDebugf("%s\n", target->getBenchmark()->getUniqueName());
}
}
// Based on
// https://skia.googlesource.com/skia/+/a063eaeaf1e09e4d6f42e0f44a5723622a46d21c/bench/nanobench.cpp#1337.
int main(int argc, char** argv) {
#ifdef SK_BUILD_FOR_ANDROID
extern bool gSkDebugToStdOut; // If true, sends SkDebugf to stdout as well.
gSkDebugToStdOut = true;
#endif
// Print command-line for debugging purposes.
if (argc < 2) {
SkDebugf("Benchmark runner invoked with no arguments.\n");
} else {
SkDebugf("Benchmark runner invoked with arguments:");
for (int i = 1; i < argc; i++) {
SkDebugf(" %s", argv[i]);
}
SkDebugf("\n");
}
// When running under Bazel (e.g. "bazel test //path/to:test"), we'll store output files in
// $TEST_UNDECLARED_OUTPUTS_DIR unless overridden via the --outputDir flag.
//
// See https://bazel.build/reference/test-encyclopedia#initial-conditions.
std::string testUndeclaredOutputsDir;
if (char* envVar = std::getenv("TEST_UNDECLARED_OUTPUTS_DIR")) {
testUndeclaredOutputsDir = envVar;
}
bool isBazelTest = !testUndeclaredOutputsDir.empty();
// Parse and validate flags.
CommandLineFlags::Parse(argc, argv);
validate_flags(isBazelTest);
// TODO(lovisolo): Define an enum for surface configs and turn this flag into an enum value.
std::string surfaceConfig = FLAGS_surfaceConfig[0];
// Directory where we will write the output JSON file and any PNGs produced by the benchmarks.
std::string outputDir =
FLAGS_outputDir.isEmpty() ? testUndeclaredOutputsDir : FLAGS_outputDir[0];
// Output JSON file.
//
// TODO(lovisolo): Define a constant with the file name, use it here and in flag descriptions.
SkString jsonPath = SkOSPath::Join(outputDir.c_str(), "results.json");
ResultsJSONWriter jsonWriter(jsonPath.c_str());
if (FLAGS_gitHash.size() == 1) {
jsonWriter.addGitHash(FLAGS_gitHash[0]);
} else {
SkDebugf(
"Warning: No --gitHash flag was specified. Perf ingestion ignores JSON files that "
"do not specify a Git hash. This is fine for local debugging, but CI tasks should "
"always set the --gitHash flag.");
}
if (FLAGS_issue.size() == 1 && FLAGS_patchset.size() == 1) {
jsonWriter.addChangelistInfo(FLAGS_issue[0], FLAGS_patchset[0]);
}
// Key.
std::map<std::string, std::string> keyValuePairs = {
// Add a key/value pair that nanobench will never use in order to avoid accidentally
// polluting an existing trace.
{"build_system", "bazel"},
};
for (int i = 1; i < FLAGS_key.size(); i += 2) {
keyValuePairs[FLAGS_key[i - 1]] = FLAGS_key[i];
}
jsonWriter.addKey(keyValuePairs);
// Links.
if (FLAGS_links.size()) {
std::map<std::string, std::string> links;
for (int i = 1; i < FLAGS_links.size(); i += 2) {
links[FLAGS_links[i - 1]] = FLAGS_links[i];
}
jsonWriter.addLinks(links);
}
int runs = 0;
for (auto benchmarkFactory : BenchRegistry::Range()) {
std::unique_ptr<Benchmark> benchmark(benchmarkFactory(nullptr));
benchmark->delayedSetup();
std::unique_ptr<BenchmarkTarget> target =
BenchmarkTarget::FromConfig(surfaceConfig, benchmark.get());
if (benchmark->isSuitableFor(target->getBackend())) {
// Run benchmark and collect samples.
int loops;
skia_private::TArray<double> samples;
skia_private::TArray<SkString> statKeys;
skia_private::TArray<double> statValues;
if (!sample_benchmark(target.get(), &loops, &samples, &statKeys, &statValues)) {
// Sampling failed. A warning has alredy been printed.
pool.drain();
continue;
}
if (FLAGS_writePNGs) {
// Save the bitmap produced by the benchmark to disk, if applicable. Not all
// benchmarks produce bitmaps, e.g. those that use the "nonrendering" config.
maybe_write_png(target.get(), outputDir);
}
// Building stats.plot often shows up in profiles, so skip building it when we're
// not going to print it anyway.
const bool want_plot = !FLAGS_quiet && !FLAGS_ms;
Stats stats(samples, want_plot);
print_benchmark_stats(&stats, &samples, target.get(), surfaceConfig, loops);
ResultsJSONWriter::Result result;
result.key = {
// Based on
// https://skia.googlesource.com/skia/+/a063eaeaf1e09e4d6f42e0f44a5723622a46d21c/bench/nanobench.cpp#1566.
{"name", std::string(benchmark->getName())},
// Replaces the "config" and "extra_config" keys set by nanobench.
{"surface_config", surfaceConfig},
// Based on
// https://skia.googlesource.com/skia/+/a063eaeaf1e09e4d6f42e0f44a5723622a46d21c/bench/nanobench.cpp#1578.
//
// TODO(lovisolo): Determine these dynamically when we add support for GMBench,
// SKPBench, etc.
{"source_type", "bench"},
{"bench_type", "micro"},
// Nanobench adds a "test" key consisting of "<unique name>_<width>_<height>",
// presumably with the goal of making the trace ID unique, see
// https://skia.googlesource.com/skia/+/a063eaeaf1e09e4d6f42e0f44a5723622a46d21c/bench/nanobench.cpp#1456.
//
// However, we can accomplish unique trace IDs by expressing "<width>" and
// "<height>" as their own keys.
//
// Regarding the "<unique name>" part of the "test" key:
//
// - Nanobench sets "<unique name>" to the result of
// Benchmark::getUniqueName():
// https://skia.googlesource.com/skia/+/a063eaeaf1e09e4d6f42e0f44a5723622a46d21c/bench/Benchmark.h#41.
//
// - Benchmark::getUniqueName() returns Benchmark::getName() except for the
// following two cases.
//
// - SKPBench::getUniqueName() returns "<name>_<scale>":
// https://skia.googlesource.com/skia/+/a063eaeaf1e09e4d6f42e0f44a5723622a46d21c/bench/SKPBench.cpp#33.
//
// - SKPAnimationBench returns "<name>_<tag>":
// https://skia.googlesource.com/skia/+/a063eaeaf1e09e4d6f42e0f44a5723622a46d21c/bench/SKPAnimationBench.cpp#18.
//
// Therefore it is important that we add "<scale>" and "<tag>" as their own
// keys when we eventually add support for these kinds of benchmarks.
//
// Based on
// https://skia.googlesource.com/skia/+/a063eaeaf1e09e4d6f42e0f44a5723622a46d21c/bench/nanobench.cpp#1456.
{"width", SkStringPrintf("%d", benchmark->getSize().width()).c_str()},
{"height", SkStringPrintf("%d", benchmark->getSize().height()).c_str()},
};
result.measurements["ms"] = {
// Based on
// https://skia.googlesource.com/skia/+/a063eaeaf1e09e4d6f42e0f44a5723622a46d21c/bench/nanobench.cpp#1571.
{.value = "min", .measurement = stats.min},
{.value = "ratio",
.measurement = sk_ieee_double_divide(stats.median, stats.min)},
};
if (!statKeys.empty()) {
// Based on
// https://skia.googlesource.com/skia/+/a063eaeaf1e09e4d6f42e0f44a5723622a46d21c/bench/nanobench.cpp#1580.
//
// Only SKPBench currently returns valid key/value pairs.
SkASSERT(statKeys.size() == statValues.size());
result.measurements["stats"] = {};
for (int i = 0; i < statKeys.size(); i++) {
result.measurements["stats"].push_back(
{.value = statKeys[i].c_str(), .measurement = statValues[i]});
}
}
jsonWriter.addResult(result);
runs++;
if (runs % FLAGS_flushEvery == 0) {
jsonWriter.flush();
}
pool.drain();
}
}
BenchmarkTarget::printGlobalStats();
SkGraphics::PurgeAllCaches();
// Based on
// https://skia.googlesource.com/skia/+/a063eaeaf1e09e4d6f42e0f44a5723622a46d21c/bench/nanobench.cpp#1668.
jsonWriter.addResult({
.key =
{
{"name", "memory_usage"},
},
.measurements =
{
{"resident_set_size_mb",
{{.value = "max",
.measurement = double(sk_tools::getMaxResidentSetSizeMB())}}},
},
});
SkDebugf("JSON file written to: %s\n", jsonPath.c_str());
SkDebugf("PNGs (if any) written to: %s\n", outputDir.c_str());
return 0;
}