summaryrefslogtreecommitdiff
path: root/media/libjxl/src/tools/benchmark/benchmark_stats.cc
diff options
context:
space:
mode:
Diffstat (limited to 'media/libjxl/src/tools/benchmark/benchmark_stats.cc')
-rw-r--r--media/libjxl/src/tools/benchmark/benchmark_stats.cc369
1 files changed, 369 insertions, 0 deletions
diff --git a/media/libjxl/src/tools/benchmark/benchmark_stats.cc b/media/libjxl/src/tools/benchmark/benchmark_stats.cc
new file mode 100644
index 0000000000..ef5932aa9d
--- /dev/null
+++ b/media/libjxl/src/tools/benchmark/benchmark_stats.cc
@@ -0,0 +1,369 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include "tools/benchmark/benchmark_stats.h"
+
+#include <stdarg.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <string.h>
+
+#include <algorithm>
+#include <cmath>
+
+#include "lib/jxl/base/printf_macros.h"
+#include "lib/jxl/base/status.h"
+#include "tools/benchmark/benchmark_args.h"
+
+namespace jxl {
+namespace {
+
+// Computes longest codec name from Args()->codec, for table alignment.
+uint32_t ComputeLargestCodecName() {
+ std::vector<std::string> methods = SplitString(Args()->codec, ',');
+ size_t max = strlen("Aggregate:"); // Include final row's name
+ for (const auto& method : methods) {
+ max = std::max(max, method.size());
+ }
+ return max;
+}
+
+// The benchmark result is a table of heterogeneous data, the column type
+// specifies its data type. The type affects how it is printed as well as how
+// aggregate values are computed.
+enum ColumnType {
+ // Formatted string
+ TYPE_STRING,
+ // Positive size, prints 0 as "---"
+ TYPE_SIZE,
+ // Floating point value (double precision) which is interpreted as
+ // "not applicable" if <= 0, must be strictly positive to be valid but can be
+ // set to 0 or negative to be printed as "---", for example for a speed that
+ // is not measured.
+ TYPE_POSITIVE_FLOAT,
+ // Counts of some event
+ TYPE_COUNT,
+};
+
+struct ColumnDescriptor {
+ // Column name
+ std::string label;
+ // Total width to render the values of this column. If t his is a floating
+ // point value, make sure this is large enough to contain a space and the
+ // point, plus precision digits after the point, plus the max amount of
+ // integer digits you expect in front of the point.
+ uint32_t width;
+ // Amount of digits after the point, or 0 if not a floating point value.
+ uint32_t precision;
+ ColumnType type;
+ bool more; // Whether to print only if more_columns is enabled
+};
+
+static const ColumnDescriptor ExtraMetricDescriptor() {
+ ColumnDescriptor d{{"DO NOT USE"}, 12, 4, TYPE_POSITIVE_FLOAT, false};
+ return d;
+}
+
+// To add or change a column to the benchmark ASCII table output, add/change
+// an entry here with table header line 1, table header line 2, width of the
+// column, precision after the point in case of floating point, and the
+// data type. Then add/change the corresponding formula or formatting in
+// the function ComputeColumns.
+std::vector<ColumnDescriptor> GetColumnDescriptors(size_t num_extra_metrics) {
+ // clang-format off
+ std::vector<ColumnDescriptor> result = {
+ {{"Encoding"}, ComputeLargestCodecName() + 1, 0, TYPE_STRING, false},
+ {{"kPixels"}, 10, 0, TYPE_SIZE, false},
+ {{"Bytes"}, 9, 0, TYPE_SIZE, false},
+ {{"BPP"}, 13, 7, TYPE_POSITIVE_FLOAT, false},
+ {{"E MP/s"}, 8, 3, TYPE_POSITIVE_FLOAT, false},
+ {{"D MP/s"}, 8, 3, TYPE_POSITIVE_FLOAT, false},
+ {{"Max norm"}, 13, 8, TYPE_POSITIVE_FLOAT, false},
+ {{"pnorm"}, 13, 8, TYPE_POSITIVE_FLOAT, false},
+ {{"PSNR"}, 7, 2, TYPE_POSITIVE_FLOAT, true},
+ {{"QABPP"}, 8, 3, TYPE_POSITIVE_FLOAT, true},
+ {{"SmallB"}, 8, 4, TYPE_POSITIVE_FLOAT, true},
+ {{"DCT4x8"}, 8, 4, TYPE_POSITIVE_FLOAT, true},
+ {{"AFV"}, 8, 4, TYPE_POSITIVE_FLOAT, true},
+ {{"DCT8x8"}, 8, 4, TYPE_POSITIVE_FLOAT, true},
+ {{"8x16"}, 8, 4, TYPE_POSITIVE_FLOAT, true},
+ {{"8x32"}, 8, 4, TYPE_POSITIVE_FLOAT, true},
+ {{"16"}, 8, 4, TYPE_POSITIVE_FLOAT, true},
+ {{"16x32"}, 8, 4, TYPE_POSITIVE_FLOAT, true},
+ {{"32"}, 8, 4, TYPE_POSITIVE_FLOAT, true},
+ {{"32x64"}, 8, 4, TYPE_POSITIVE_FLOAT, true},
+ {{"64"}, 8, 4, TYPE_POSITIVE_FLOAT, true},
+ {{"BPP*pnorm"}, 16, 12, TYPE_POSITIVE_FLOAT, false},
+ {{"Bugs"}, 7, 5, TYPE_COUNT, false},
+ };
+ // clang-format on
+
+ for (size_t i = 0; i < num_extra_metrics; i++) {
+ result.push_back(ExtraMetricDescriptor());
+ }
+
+ return result;
+}
+
+// Computes throughput [megapixels/s] as reported in the report table
+static double ComputeSpeed(size_t pixels, double time_s) {
+ if (time_s == 0.0) return 0;
+ return pixels * 1E-6 / time_s;
+}
+
+static std::string FormatFloat(const ColumnDescriptor& label, double value) {
+ std::string result =
+ StringPrintf("%*.*f", label.width - 1, label.precision, value);
+
+ // Reduce precision if the value is too wide for the column. However, keep
+ // at least one digit to the right of the point, and especially the integer
+ // digits.
+ if (result.size() >= label.width) {
+ size_t point = result.rfind('.');
+ if (point != std::string::npos) {
+ int end = std::max<int>(point + 2, label.width - 1);
+ result = result.substr(0, end);
+ }
+ }
+ return result;
+}
+
+} // namespace
+
+std::string StringPrintf(const char* format, ...) {
+ char buf[2000];
+ va_list args;
+ va_start(args, format);
+ vsnprintf(buf, sizeof(buf), format, args);
+ va_end(args);
+ return std::string(buf);
+}
+
+void BenchmarkStats::Assimilate(const BenchmarkStats& victim) {
+ total_input_files += victim.total_input_files;
+ total_input_pixels += victim.total_input_pixels;
+ total_compressed_size += victim.total_compressed_size;
+ total_adj_compressed_size += victim.total_adj_compressed_size;
+ total_time_encode += victim.total_time_encode;
+ total_time_decode += victim.total_time_decode;
+ max_distance = std::max(max_distance, victim.max_distance);
+ distance_p_norm += victim.distance_p_norm;
+ distance_2 += victim.distance_2;
+ distances.insert(distances.end(), victim.distances.begin(),
+ victim.distances.end());
+ total_errors += victim.total_errors;
+ jxl_stats.Assimilate(victim.jxl_stats);
+ if (extra_metrics.size() < victim.extra_metrics.size()) {
+ extra_metrics.resize(victim.extra_metrics.size());
+ }
+ for (size_t i = 0; i < victim.extra_metrics.size(); i++) {
+ extra_metrics[i] += victim.extra_metrics[i];
+ }
+}
+
+void BenchmarkStats::PrintMoreStats() const {
+ if (Args()->print_more_stats) {
+ jxl_stats.Print();
+ }
+ if (Args()->print_distance_percentiles) {
+ std::vector<float> sorted = distances;
+ std::sort(sorted.begin(), sorted.end());
+ int p50idx = 0.5 * distances.size();
+ int p90idx = 0.9 * distances.size();
+ printf("50th/90th percentile distance: %.8f %.8f\n", sorted[p50idx],
+ sorted[p90idx]);
+ }
+}
+
+std::vector<ColumnValue> BenchmarkStats::ComputeColumns(
+ const std::string& codec_desc, size_t corpus_size) const {
+ JXL_CHECK(total_input_files == corpus_size);
+ const double comp_bpp = total_compressed_size * 8.0 / total_input_pixels;
+ const double adj_comp_bpp =
+ total_adj_compressed_size * 8.0 / total_input_pixels;
+ // Note: this is not affected by alpha nor bit depth.
+ const double compression_speed =
+ ComputeSpeed(total_input_pixels, total_time_encode);
+ const double decompression_speed =
+ ComputeSpeed(total_input_pixels, total_time_decode);
+ // Already weighted, no need to divide by #channels.
+ const double rmse = std::sqrt(distance_2 / total_input_pixels);
+ const double psnr = total_compressed_size == 0 ? 0.0
+ : (distance_2 == 0) ? 99.99
+ : (20 * std::log10(1 / rmse));
+ const double p_norm = distance_p_norm / total_input_pixels;
+ const double bpp_p_norm = p_norm * comp_bpp;
+
+ std::vector<ColumnValue> values(
+ GetColumnDescriptors(extra_metrics.size()).size());
+
+ values[0].s = codec_desc;
+ values[1].i = total_input_pixels / 1000;
+ values[2].i = total_compressed_size;
+ values[3].f = comp_bpp;
+ values[4].f = compression_speed;
+ values[5].f = decompression_speed;
+ values[6].f = static_cast<double>(max_distance);
+ values[7].f = p_norm;
+ values[8].f = psnr;
+ values[9].f = adj_comp_bpp;
+ // The DCT2, DCT4, AFV and DCT4X8 are applied to an 8x8 block by having 4x4
+ // DCT2X2s, 2x2 DCT4x4s/AFVs, or 2x1 DCT4X8s, filling the whole 8x8 blocks.
+ // Thus we need to multiply the block count by 8.0 * 8.0 pixels for these
+ // transforms.
+ values[10].f = 100.f * jxl_stats.aux_out.num_small_blocks * 8.0 * 8.0 /
+ total_input_pixels;
+ values[11].f = 100.f * jxl_stats.aux_out.num_dct4x8_blocks * 8.0 * 8.0 /
+ total_input_pixels;
+ values[12].f =
+ 100.f * jxl_stats.aux_out.num_afv_blocks * 8.0 * 8.0 / total_input_pixels;
+ values[13].f = 100.f * jxl_stats.aux_out.num_dct8_blocks * 8.0 * 8.0 /
+ total_input_pixels;
+ values[14].f = 100.f * jxl_stats.aux_out.num_dct8x16_blocks * 8.0 * 16.0 /
+ total_input_pixels;
+ values[15].f = 100.f * jxl_stats.aux_out.num_dct8x32_blocks * 8.0 * 32.0 /
+ total_input_pixels;
+ values[16].f = 100.f * jxl_stats.aux_out.num_dct16_blocks * 16.0 * 16.0 /
+ total_input_pixels;
+ values[17].f = 100.f * jxl_stats.aux_out.num_dct16x32_blocks * 16.0 * 32.0 /
+ total_input_pixels;
+ values[18].f = 100.f * jxl_stats.aux_out.num_dct32_blocks * 32.0 * 32.0 /
+ total_input_pixels;
+ values[19].f = 100.f * jxl_stats.aux_out.num_dct32x64_blocks * 32.0 * 64.0 /
+ total_input_pixels;
+ values[20].f = 100.f * jxl_stats.aux_out.num_dct64_blocks * 64.0 * 64.0 /
+ total_input_pixels;
+ values[21].f = bpp_p_norm;
+ values[22].i = total_errors;
+ for (size_t i = 0; i < extra_metrics.size(); i++) {
+ values[23 + i].f = extra_metrics[i] / total_input_files;
+ }
+ return values;
+}
+
+static std::string PrintFormattedEntries(
+ size_t num_extra_metrics, const std::vector<ColumnValue>& values) {
+ const auto& descriptors = GetColumnDescriptors(num_extra_metrics);
+
+ std::string out;
+ for (size_t i = 0; i < descriptors.size(); i++) {
+ if (!Args()->more_columns && descriptors[i].more) continue;
+ std::string value;
+ if (descriptors[i].type == TYPE_STRING) {
+ value = values[i].s;
+ } else if (descriptors[i].type == TYPE_SIZE) {
+ value = values[i].i ? StringPrintf("%" PRIdS, values[i].i) : "---";
+ } else if (descriptors[i].type == TYPE_POSITIVE_FLOAT) {
+ value = FormatFloat(descriptors[i], values[i].f);
+ value = FormatFloat(descriptors[i], values[i].f);
+ } else if (descriptors[i].type == TYPE_COUNT) {
+ value = StringPrintf("%" PRIdS, values[i].i);
+ }
+
+ int numspaces = descriptors[i].width - value.size();
+ if (numspaces < 1) {
+ numspaces = 1;
+ }
+ // All except the first one are right-aligned, the first one is the name,
+ // others are numbers with digits matching from the right.
+ if (i == 0) out += value.c_str();
+ out += std::string(numspaces, ' ');
+ if (i != 0) out += value.c_str();
+ }
+ return out + "\n";
+}
+
+std::string BenchmarkStats::PrintLine(const std::string& codec_desc,
+ size_t corpus_size) const {
+ std::vector<ColumnValue> values = ComputeColumns(codec_desc, corpus_size);
+ return PrintFormattedEntries(extra_metrics.size(), values);
+}
+
+std::string PrintHeader(const std::vector<std::string>& extra_metrics_names) {
+ std::string out;
+ // Extra metrics are handled separately.
+ const auto& descriptors = GetColumnDescriptors(0);
+ for (size_t i = 0; i < descriptors.size(); i++) {
+ if (!Args()->more_columns && descriptors[i].more) continue;
+ const std::string& label = descriptors[i].label;
+ int numspaces = descriptors[i].width - label.size();
+ // All except the first one are right-aligned.
+ if (i == 0) out += label.c_str();
+ out += std::string(numspaces, ' ');
+ if (i != 0) out += label.c_str();
+ }
+ for (const std::string& em : extra_metrics_names) {
+ int numspaces = ExtraMetricDescriptor().width - em.size();
+ JXL_CHECK(numspaces >= 1);
+ out += std::string(numspaces, ' ');
+ out += em;
+ }
+ out += '\n';
+ for (const auto& descriptor : descriptors) {
+ if (!Args()->more_columns && descriptor.more) continue;
+ out += std::string(descriptor.width, '-');
+ }
+ out += std::string(ExtraMetricDescriptor().width * extra_metrics_names.size(),
+ '-');
+ return out + "\n";
+}
+
+std::string PrintAggregate(
+ size_t num_extra_metrics,
+ const std::vector<std::vector<ColumnValue>>& aggregate) {
+ const auto& descriptors = GetColumnDescriptors(num_extra_metrics);
+
+ for (size_t i = 0; i < aggregate.size(); i++) {
+ // Check when statistics has wrong amount of column entries
+ JXL_CHECK(aggregate[i].size() == descriptors.size());
+ }
+
+ std::vector<ColumnValue> result(descriptors.size());
+
+ // Statistics for the aggregate row are combined together with different
+ // formulas than Assimilate uses for combining the statistics of files.
+ for (size_t i = 0; i < descriptors.size(); i++) {
+ if (descriptors[i].type == TYPE_STRING) {
+ // "---" for the Iters column since this does not have meaning for
+ // the aggregate stats.
+ result[i].s = i == 0 ? "Aggregate:" : "---";
+ continue;
+ }
+ if (descriptors[i].type == TYPE_COUNT) {
+ size_t sum = 0;
+ for (size_t j = 0; j < aggregate.size(); j++) {
+ sum += aggregate[j][i].i;
+ }
+ result[i].i = sum;
+ continue;
+ }
+
+ ColumnType type = descriptors[i].type;
+
+ double logsum = 0;
+ size_t numvalid = 0;
+ for (size_t j = 0; j < aggregate.size(); j++) {
+ double value =
+ (type == TYPE_SIZE) ? aggregate[j][i].i : aggregate[j][i].f;
+ if (value > 0) {
+ numvalid++;
+ logsum += std::log2(value);
+ }
+ }
+ double geomean = numvalid ? std::exp2(logsum / numvalid) : 0.0;
+
+ if (type == TYPE_SIZE || type == TYPE_COUNT) {
+ result[i].i = static_cast<size_t>(geomean + 0.5);
+ } else if (type == TYPE_POSITIVE_FLOAT) {
+ result[i].f = geomean;
+ } else {
+ JXL_ABORT("unknown entry type");
+ }
+ }
+
+ return PrintFormattedEntries(num_extra_metrics, result);
+}
+
+} // namespace jxl