ba98ca1be1
This CL adds an initial framework for testing performance of programming patterns. Included is a test of the pattern used to choose implementation based on a runtime type, using either an if-then-else sequence, dynamic dispatch on the runtime object, or visitor pattern. The test framework runs the patterns on a range of inputs using a predefined set of scenarios and outputs a table of the measurements which can be copied into Google Sheets to visualize the results. Further work is to use statistics to quantify the results, and expand the framework to be used to detect asymptotic performance of algorithms used in the compiler itself. Change-Id: Ic99588d13adead309715957e1bc0aa6def46c9dc Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/226104 Reviewed-by: Jens Johansen <jensj@google.com>
234 lines
6.7 KiB
Dart
234 lines
6.7 KiB
Dart
// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
|
|
// for details. All rights reserved. Use of this source code is governed by a
|
|
// BSD-style license that can be found in the LICENSE file.
|
|
|
|
import 'dart:math' as math;
|
|
import '../../test/simple_stats.dart';
|
|
|
|
/// Key used for a strategy in a test setup.
|
|
class Strategy {
|
|
/// The short name of the strategy used in printouts.
|
|
final String name;
|
|
|
|
/// A full description of the strategy.
|
|
final String description;
|
|
|
|
const Strategy(this.name, this.description);
|
|
|
|
@override
|
|
int get hashCode => name.hashCode;
|
|
|
|
@override
|
|
bool operator ==(Object other) {
|
|
if (identical(this, other)) return true;
|
|
return other is Strategy && name == other.name;
|
|
}
|
|
|
|
@override
|
|
String toString() => 'Strategy($name)';
|
|
}
|
|
|
|
/// Key used for a scenario used when running a test.
|
|
class Scenario {
|
|
/// The short name of the scenario used in printouts.
|
|
final String name;
|
|
|
|
/// A full description of the scenario.
|
|
final String description;
|
|
|
|
const Scenario(this.name, this.description);
|
|
|
|
@override
|
|
int get hashCode => name.hashCode;
|
|
|
|
@override
|
|
bool operator ==(Object other) {
|
|
if (identical(this, other)) return true;
|
|
return other is Scenario && name == other.name;
|
|
}
|
|
|
|
@override
|
|
String toString() => 'Strategy($name)';
|
|
}
|
|
|
|
/// The values that constitutes the input size values of the test.
|
|
class XAxis {
|
|
final List<num> values;
|
|
|
|
XAxis(this.values);
|
|
}
|
|
|
|
/// Key for a strategy/scenario pair.
|
|
class SeriesKey {
|
|
final Strategy strategy;
|
|
final Scenario scenario;
|
|
|
|
const SeriesKey(this.strategy, this.scenario);
|
|
|
|
@override
|
|
int get hashCode => strategy.hashCode * 13 + scenario.hashCode * 17;
|
|
|
|
@override
|
|
bool operator ==(Object other) {
|
|
if (identical(this, other)) return true;
|
|
return other is SeriesKey &&
|
|
strategy == other.strategy &&
|
|
scenario == other.scenario;
|
|
}
|
|
|
|
@override
|
|
String toString() => 'SeriesKey($strategy,$scenario)';
|
|
}
|
|
|
|
/// Data collected for running one strategy using one scenario.
|
|
class Series {
|
|
/// The key that identifiers this series.
|
|
final SeriesKey key;
|
|
|
|
/// The x-axis values corresponding the [values].
|
|
final XAxis xAxis;
|
|
|
|
/// The measured values. The indices of the outer list corresponds to the
|
|
/// indices of the [xAxis]. The size of the inner list is the number
|
|
/// measurements performed for that x-value.
|
|
final List<List<num>> values;
|
|
|
|
Series(this.key, this.xAxis, this.values);
|
|
|
|
/// Returns a new [Series] where measurements have been removed using
|
|
/// [filter].
|
|
///
|
|
/// This can for instance be used to remove outliers from the measurements.
|
|
Series filter(List<num> filter(List<num> list)) {
|
|
List<List<num>> filteredValues = [];
|
|
for (List<num> list in values) {
|
|
filteredValues.add(filter(list));
|
|
}
|
|
return new Series(key, xAxis, filteredValues);
|
|
}
|
|
}
|
|
|
|
/// A full set of series collected for a set of strategies and scenarios.
|
|
class SeriesSet {
|
|
/// The shared x-axis of all series in [seriesList].
|
|
final XAxis xAxis;
|
|
|
|
/// All collected [Series].
|
|
final List<Series> seriesList;
|
|
|
|
SeriesSet(this.xAxis, this.seriesList);
|
|
|
|
/// Returns a new [SeriesSet] where measurements have been removed using
|
|
/// [filter].
|
|
///
|
|
/// This can for instance be used to remove outliers from the measurements.
|
|
SeriesSet filter(List<num> filter(List<num> list)) {
|
|
List<Series> filteredSeries = [];
|
|
for (Series series in this.seriesList) {
|
|
filteredSeries.add(series.filter(filter));
|
|
}
|
|
return new SeriesSet(xAxis, filteredSeries);
|
|
}
|
|
|
|
/// Returns a tab-based table of the averages of all measurements for a given
|
|
/// [scenario].
|
|
String getAveragedSpreadByScenario(Scenario scenario) {
|
|
Map<Series, List<num>> stats = {};
|
|
for (Series series in this.seriesList) {
|
|
if (series.key.scenario != scenario) continue;
|
|
stats[series] = series.values
|
|
.map((data) => SimpleTTestStat.average(data))
|
|
.toList(growable: false);
|
|
}
|
|
StringBuffer sb = new StringBuffer();
|
|
sb.write(scenario.name);
|
|
for (Series series in stats.keys) {
|
|
sb.write('\t${series.key.strategy.name}');
|
|
}
|
|
sb.writeln();
|
|
for (int index = 0; index < xAxis.values.length; index++) {
|
|
sb.write(xAxis.values[index]);
|
|
for (List<num> stat in stats.values) {
|
|
sb.write('\t');
|
|
sb.write(stat[index]);
|
|
}
|
|
sb.writeln();
|
|
}
|
|
return sb.toString();
|
|
}
|
|
|
|
/// Returns a tab-based table of all measurements for a given [scenario].
|
|
String getFullSpreadByScenario(Scenario scenario) {
|
|
List<Series> seriesList = [];
|
|
for (Series series in this.seriesList) {
|
|
if (series.key.scenario != scenario) continue;
|
|
seriesList.add(series);
|
|
}
|
|
StringBuffer sb = new StringBuffer();
|
|
sb.write(scenario.name);
|
|
for (Series series in seriesList) {
|
|
int columns = series.values.map((l) => l.length).reduce(math.max);
|
|
sb.write('\t${series.key.strategy.name}');
|
|
for (int i = 0; i < columns; i++) {
|
|
sb.write('\t');
|
|
}
|
|
}
|
|
sb.writeln();
|
|
for (int index = 0; index < xAxis.values.length; index++) {
|
|
sb.write(xAxis.values[index]);
|
|
for (Series series in seriesList) {
|
|
List<num> values = series.values[index];
|
|
for (int i = 0; i < values.length; i++) {
|
|
sb.write('\t${values[i]}');
|
|
}
|
|
}
|
|
sb.writeln();
|
|
}
|
|
return sb.toString();
|
|
}
|
|
}
|
|
|
|
/// Registry used to collect data during measurement.
|
|
class Registry {
|
|
final Set<num> _xAxisSet = {};
|
|
final Map<SeriesKey, Map<num, List<num>>> _seriesMap = {};
|
|
|
|
/// Registers the measurement of [y] for the given [x] value under the
|
|
/// strategy/scenario defined by [key].
|
|
void registerData(SeriesKey key, num x, num y) {
|
|
_xAxisSet.add(x);
|
|
((_seriesMap[key] ??= {})[x] ??= []).add(y);
|
|
}
|
|
|
|
/// Generates [SeriesSet] for all collected measurements.
|
|
SeriesSet generateSeriesSet() {
|
|
XAxis xAxis = new XAxis(_xAxisSet.toList(growable: false)..sort());
|
|
List<Series> series = [];
|
|
for (MapEntry<SeriesKey, Map<num, List<num>>> entry in _seriesMap.entries) {
|
|
SeriesKey key = entry.key;
|
|
Map<num, List<num>> valuesMap = entry.value;
|
|
List<List<num>> values = [];
|
|
for (num x in xAxis.values) {
|
|
values.add(valuesMap[x] ?? []);
|
|
}
|
|
series.add(new Series(key, xAxis, values));
|
|
}
|
|
_xAxisSet.clear();
|
|
_seriesMap.clear();
|
|
return new SeriesSet(xAxis, series);
|
|
}
|
|
}
|
|
|
|
/// Filter function that removes the max [removeMaxCount] values from a list.
|
|
///
|
|
/// This can be used in [Series.filter] and [SeriesSet.filter] to remove
|
|
/// outliers from a set of measurements.
|
|
List<num> removeMax(List<num> list, int removeMaxCount) {
|
|
if (removeMaxCount == 0) return list;
|
|
List<num> copy = list.toList()..sort();
|
|
for (int i = 0; i < removeMaxCount; i++) {
|
|
copy.removeLast();
|
|
}
|
|
return copy;
|
|
}
|