Skip to content

Commit 42396ee

Browse files
[web_benchmarks] Add support for analyzing benchmark results (flutter#5630)
This PR adds a new top level library `analysis.dart` that exposes methods: * `computeDelta` * `computeAverage` These were already added to DevTools in flutter/devtools#6918 and flutter/devtools#6920, respectively, but I think it makes more sense to upstream this logic into `package:web_benchmarks` so that other users of this package can take advantage of it. As part of this PR, I have updated the README to give an example of how to use the analysis features.
1 parent 8163731 commit 42396ee

File tree

8 files changed

+566
-12
lines changed

8 files changed

+566
-12
lines changed

packages/web_benchmarks/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 1.1.0
2+
3+
* Adds `computeAverage` and `computeDelta` methods to support analysis of benchmark results.
4+
15
## 1.0.1
26

37
* Adds `parse` constructors for the `BenchmarkResults` and `BenchmarkScore` classes.

packages/web_benchmarks/README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,45 @@ app's code and assets. Additionally, the server communicates with the browser to
1313
extract the performance traces.
1414

1515
[1]: https://github.com/flutter/packages/blob/master/packages/web_benchmarks/testing/web_benchmarks_test.dart
16+
17+
# Analyzing benchmark results
18+
19+
After running web benchmarks, you may want to analyze the results or compare
20+
with the results from other benchmark runs. The `web_benchmarks` package
21+
supports the following analysis operations:
22+
23+
* compute the delta between two benchmark results
24+
* compute the average of a set of benchmark results
25+
26+
<?code-excerpt "example/analyze_example.dart (analyze)"?>
27+
```dart
28+
import 'dart:convert';
29+
import 'dart:io';
30+
31+
import 'package:web_benchmarks/analysis.dart';
32+
33+
void main() {
34+
final BenchmarkResults baselineResults =
35+
_benchmarkResultsFromFile('/path/to/benchmark_baseline.json');
36+
final BenchmarkResults testResults1 =
37+
_benchmarkResultsFromFile('/path/to/benchmark_test_1.json');
38+
final BenchmarkResults testResults2 =
39+
_benchmarkResultsFromFile('/path/to/benchmark_test_2.json');
40+
41+
// Compute the delta between [baselineResults] and [testResults1].
42+
final BenchmarkResults delta = computeDelta(baselineResults, testResults1);
43+
stdout.writeln(delta.toJson());
44+
45+
// Compute the average of [testResults] and [testResults2].
46+
final BenchmarkResults average =
47+
computeAverage(<BenchmarkResults>[testResults1, testResults2]);
48+
stdout.writeln(average.toJson());
49+
}
50+
51+
BenchmarkResults _benchmarkResultsFromFile(String path) {
52+
final File file = File.fromUri(Uri.parse(path));
53+
final Map<String, Object?> fileContentAsJson =
54+
jsonDecode(file.readAsStringSync()) as Map<String, Object?>;
55+
return BenchmarkResults.parse(fileContentAsJson);
56+
}
57+
```
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
// #docregion analyze
6+
import 'dart:convert';
7+
import 'dart:io';
8+
9+
import 'package:web_benchmarks/analysis.dart';
10+
11+
void main() {
12+
final BenchmarkResults baselineResults =
13+
_benchmarkResultsFromFile('/path/to/benchmark_baseline.json');
14+
final BenchmarkResults testResults1 =
15+
_benchmarkResultsFromFile('/path/to/benchmark_test_1.json');
16+
final BenchmarkResults testResults2 =
17+
_benchmarkResultsFromFile('/path/to/benchmark_test_2.json');
18+
19+
// Compute the delta between [baselineResults] and [testResults1].
20+
final BenchmarkResults delta = computeDelta(baselineResults, testResults1);
21+
stdout.writeln(delta.toJson());
22+
23+
// Compute the average of [testResults] and [testResults2].
24+
final BenchmarkResults average =
25+
computeAverage(<BenchmarkResults>[testResults1, testResults2]);
26+
stdout.writeln(average.toJson());
27+
}
28+
29+
BenchmarkResults _benchmarkResultsFromFile(String path) {
30+
final File file = File.fromUri(Uri.parse(path));
31+
final Map<String, Object?> fileContentAsJson =
32+
jsonDecode(file.readAsStringSync()) as Map<String, Object?>;
33+
return BenchmarkResults.parse(fileContentAsJson);
34+
}
35+
// #enddocregion analyze
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:collection/collection.dart';
6+
import 'server.dart';
7+
8+
export 'src/benchmark_result.dart';
9+
10+
/// Returns the average of the benchmark results in [results].
11+
///
12+
/// Each element in [results] is expected to have identical benchmark names and
13+
/// metrics; otherwise, an [Exception] will be thrown.
14+
BenchmarkResults computeAverage(List<BenchmarkResults> results) {
15+
if (results.isEmpty) {
16+
throw ArgumentError('Cannot take average of empty list.');
17+
}
18+
19+
final BenchmarkResults totalSum = results.reduce(
20+
(BenchmarkResults sum, BenchmarkResults next) => sum._sumWith(next),
21+
);
22+
23+
final BenchmarkResults average = totalSum;
24+
for (final String benchmark in totalSum.scores.keys) {
25+
final List<BenchmarkScore> scoresForBenchmark = totalSum.scores[benchmark]!;
26+
for (int i = 0; i < scoresForBenchmark.length; i++) {
27+
final BenchmarkScore score = scoresForBenchmark[i];
28+
final double averageValue = score.value / results.length;
29+
average.scores[benchmark]![i] =
30+
BenchmarkScore(metric: score.metric, value: averageValue);
31+
}
32+
}
33+
return average;
34+
}
35+
36+
/// Computes the delta for each matching metric in [test] and [baseline], and
37+
/// returns a new [BenchmarkResults] object where each [BenchmarkScore] contains
38+
/// a [delta] value.
39+
BenchmarkResults computeDelta(
40+
BenchmarkResults baseline,
41+
BenchmarkResults test,
42+
) {
43+
final Map<String, List<BenchmarkScore>> delta =
44+
<String, List<BenchmarkScore>>{};
45+
for (final String benchmarkName in test.scores.keys) {
46+
final List<BenchmarkScore> testScores = test.scores[benchmarkName]!;
47+
final List<BenchmarkScore>? baselineScores = baseline.scores[benchmarkName];
48+
delta[benchmarkName] = testScores.map<BenchmarkScore>(
49+
(BenchmarkScore testScore) {
50+
final BenchmarkScore? baselineScore = baselineScores?.firstWhereOrNull(
51+
(BenchmarkScore s) => s.metric == testScore.metric);
52+
return testScore._copyWith(
53+
delta: baselineScore == null
54+
? null
55+
: (testScore.value - baselineScore.value).toDouble(),
56+
);
57+
},
58+
).toList();
59+
}
60+
return BenchmarkResults(delta);
61+
}
62+
63+
extension _AnalysisExtension on BenchmarkResults {
64+
/// Sums this [BenchmarkResults] instance with [other] by adding the values
65+
/// of each matching benchmark score.
66+
///
67+
/// Returns a [BenchmarkResults] object with the summed values.
68+
///
69+
/// When [throwExceptionOnMismatch] is true (default), the set of benchmark
70+
/// names and metric names in [other] are expected to be identical to those in
71+
/// [scores], or else an [Exception] will be thrown.
72+
BenchmarkResults _sumWith(
73+
BenchmarkResults other, {
74+
bool throwExceptionOnMismatch = true,
75+
}) {
76+
final Map<String, List<BenchmarkScore>> sum =
77+
<String, List<BenchmarkScore>>{};
78+
for (final String benchmark in scores.keys) {
79+
// Look up this benchmark in [other].
80+
final List<BenchmarkScore>? matchingBenchmark = other.scores[benchmark];
81+
if (matchingBenchmark == null) {
82+
if (throwExceptionOnMismatch) {
83+
throw Exception(
84+
'Cannot sum benchmarks because [other] is missing an entry for '
85+
'benchmark "$benchmark".',
86+
);
87+
}
88+
continue;
89+
}
90+
91+
final List<BenchmarkScore> scoresForBenchmark = scores[benchmark]!;
92+
sum[benchmark] =
93+
scoresForBenchmark.map<BenchmarkScore>((BenchmarkScore score) {
94+
// Look up this score in the [matchingBenchmark] from [other].
95+
final BenchmarkScore? matchingScore = matchingBenchmark
96+
.firstWhereOrNull((BenchmarkScore s) => s.metric == score.metric);
97+
if (matchingScore == null && throwExceptionOnMismatch) {
98+
throw Exception(
99+
'Cannot sum benchmarks because benchmark "$benchmark" is missing '
100+
'a score for metric ${score.metric}.',
101+
);
102+
}
103+
return score._copyWith(
104+
value: matchingScore == null
105+
? score.value
106+
: score.value + matchingScore.value,
107+
);
108+
}).toList();
109+
}
110+
return BenchmarkResults(sum);
111+
}
112+
}
113+
114+
extension _CopyExtension on BenchmarkScore {
115+
BenchmarkScore _copyWith({String? metric, num? value, num? delta}) =>
116+
BenchmarkScore(
117+
metric: metric ?? this.metric,
118+
value: value ?? this.value,
119+
delta: delta ?? this.delta,
120+
);
121+
}

packages/web_benchmarks/lib/src/benchmark_result.dart

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,26 @@ class BenchmarkScore {
1010
BenchmarkScore({
1111
required this.metric,
1212
required this.value,
13+
this.delta,
1314
});
1415

1516
/// Deserializes a JSON object to create a [BenchmarkScore] object.
1617
factory BenchmarkScore.parse(Map<String, Object?> json) {
17-
final String metric = json[_metricKey]! as String;
18-
final double value = (json[_valueKey]! as num).toDouble();
19-
return BenchmarkScore(metric: metric, value: value);
18+
final String metric = json[metricKey]! as String;
19+
final double value = (json[valueKey]! as num).toDouble();
20+
final num? delta = json[deltaKey] as num?;
21+
return BenchmarkScore(metric: metric, value: value, delta: delta);
2022
}
2123

22-
static const String _metricKey = 'metric';
23-
static const String _valueKey = 'value';
24+
/// The key for the value [metric] in the [BenchmarkScore] JSON
25+
/// representation.
26+
static const String metricKey = 'metric';
27+
28+
/// The key for the value [value] in the [BenchmarkScore] JSON representation.
29+
static const String valueKey = 'value';
30+
31+
/// The key for the value [delta] in the [BenchmarkScore] JSON representation.
32+
static const String deltaKey = 'delta';
2433

2534
/// The name of the metric that this score is categorized under.
2635
///
@@ -31,11 +40,18 @@ class BenchmarkScore {
3140
/// The result of measuring a particular metric in this benchmark run.
3241
final num value;
3342

43+
/// Optional delta value describing the difference between this metric's score
44+
/// and the score of a matching metric from another [BenchmarkResults].
45+
///
46+
/// This value may be assigned by the [computeDelta] analysis method.
47+
final num? delta;
48+
3449
/// Serializes the benchmark metric to a JSON object.
3550
Map<String, Object?> toJson() {
3651
return <String, Object?>{
37-
_metricKey: metric,
38-
_valueKey: value,
52+
metricKey: metric,
53+
valueKey: value,
54+
if (delta != null) deltaKey: delta,
3955
};
4056
}
4157
}
@@ -53,7 +69,7 @@ class BenchmarkResults {
5369
final List<BenchmarkScore> scores = (json[key]! as List<Object?>)
5470
.cast<Map<String, Object?>>()
5571
.map(BenchmarkScore.parse)
56-
.toList();
72+
.toList(growable: false);
5773
results[key] = scores;
5874
}
5975
return BenchmarkResults(results);

packages/web_benchmarks/pubspec.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ name: web_benchmarks
22
description: A benchmark harness for performance-testing Flutter apps in Chrome.
33
repository: https://github.com/flutter/packages/tree/main/packages/web_benchmarks
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+web_benchmarks%22
5-
version: 1.0.1
5+
version: 1.1.0
66

77
environment:
88
sdk: ">=3.2.0 <4.0.0"
99
flutter: ">=3.16.0"
1010

1111
dependencies:
12+
collection: ^1.18.0
1213
flutter:
1314
sdk: flutter
1415
flutter_test:

0 commit comments

Comments
 (0)