Add median, p90, and p95 metrics to completion_metrics
This adds a section to the output like this:
```
### Percentile metrics
p50 p90 p95 count > 2s
ms per completion 4 6 7 0
```
Bug: https://github.com/dart-lang/sdk/issues/48788
Change-Id: I868580324b3bc83605aa6466ffd5799625d1a9f3
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/240941
Commit-Queue: Samuel Rawlins <srawlins@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
diff --git a/pkg/analysis_server/test/tool/completion_metrics/metrics_util_test.dart b/pkg/analysis_server/test/tool/completion_metrics/metrics_util_test.dart
index 639b3eb..9702d0e 100644
--- a/pkg/analysis_server/test/tool/completion_metrics/metrics_util_test.dart
+++ b/pkg/analysis_server/test/tool/completion_metrics/metrics_util_test.dart
@@ -161,6 +161,57 @@
});
});
+ group('PercentileComputer', () {
+ test('empty', () {
+ var computer = PercentileComputer('empty', valueLimit: 2000);
+ expect(computer.median, equals(0));
+ expect(computer.valueCount, equals(0));
+ });
+
+ test('clear', () {
+ var computer = PercentileComputer('name', valueLimit: 2000);
+ computer.addValue(4);
+ computer.addValue(5);
+ computer.addValue(6);
+ computer.addValue(3000);
+
+ expect(computer.median, equals(5));
+ expect(computer.valueCount, equals(3));
+ expect(computer.aboveValueMaxCount, equals(1));
+ computer.clear();
+
+ expect(computer.median, equals(0));
+ expect(computer.valueCount, equals(0));
+ expect(computer.aboveValueMaxCount, equals(0));
+ expect(computer.aboveValueMaxSamples, isEmpty);
+ });
+
+ test('percentiles', () {
+ var computer = PercentileComputer('name', valueLimit: 2000);
+ for (var i = 0; i < 100; i++) {
+ computer.addValue(i);
+ }
+
+ expect(computer.median, equals(50));
+ expect(computer.p90, equals(90));
+ expect(computer.p95, equals(95));
+ });
+
+ test('values above maxValue', () {
+ var computer = PercentileComputer('name', valueLimit: 2000);
+
+ computer.addValue(1);
+ computer.addValue(2);
+ computer.addValue(3);
+ computer.addValue(2500);
+ computer.addValue(3000);
+
+ expect(computer.median, equals(2));
+ expect(computer.aboveValueMaxCount, equals(2));
+ expect(computer.aboveValueMaxSamples, equals([2500, 3000]));
+ });
+ });
+
group('Place', () {
test('none', () {
var place = Place.none();
diff --git a/pkg/analysis_server/tool/code_completion/completion_metrics.dart b/pkg/analysis_server/tool/code_completion/completion_metrics.dart
index fcc4650..ed88f0c 100644
--- a/pkg/analysis_server/tool/code_completion/completion_metrics.dart
+++ b/pkg/analysis_server/tool/code_completion/completion_metrics.dart
@@ -66,10 +66,13 @@
///
/// This approach has several drawbacks:
///
-/// - The AST is always complete and correct, and that's rarely the case for
-/// real completion requests. Usually the tree is incomplete and often has a
-/// completely different structure because of the way recovery works. We
-/// currently have no way of measuring completions under realistic conditions.
+/// - The options for creating an "in-progress" file are limited. In the default
+/// 'overlay' mode, the AST is always complete and correct, rarely the case
+/// for real completion requests. The other 'overlay' modes generate
+/// incomplete ASTs with error recovery nodes, but neither of these quite
+/// properly emulate the act of editing the middle of a file, perhaps the
+/// middle of an expression, or the middle of an argument list. We currently
+/// have no way of measuring completions under realistic conditions.
///
/// - We can't measure completions for several keywords because the presence of
/// the keyword in the AST causes it to not be suggested.
@@ -77,7 +80,7 @@
/// - The time it takes to compute the suggestions doesn't include the time
/// required to finish analyzing the file if the analysis hasn't been
/// completed before suggestions are requested. While the times are accurate
-/// (within the accuracy of the `Stopwatch` class) they are the minimum
+/// (within the accuracy of the [Stopwatch] class) they are the minimum
/// possible time. This doesn't give us a measure of how completion will
/// perform in production, but does give us an optimistic approximation.
///
@@ -193,7 +196,7 @@
..addFlag(CompletionMetricsOptions.PRINT_SHADOWED_COMPLETION_DETAILS,
defaultsTo: false,
help: 'Print detailed information every time a completion request '
- 'produces a suggestions whose name matches the expected suggestion '
+ 'produces a suggestion whose name matches the expected suggestion '
'but that is referencing a different element',
negatable: false)
..addFlag(CompletionMetricsOptions.PRINT_SLOWEST_RESULTS,
@@ -332,6 +335,11 @@
final ArithmeticMeanComputer meanCompletionMS =
ArithmeticMeanComputer('ms per completion');
+ /// A percentile computer for the ms per completion request, using 2.000
+ /// seconds as the max value to use in percentile calculations.
+ final PercentileComputer percentileCompletionMS =
+ PercentileComputer('ms per completion', valueLimit: 2000);
+
final DistributionComputer distributionCompletionMS = DistributionComputer();
final MeanReciprocalRankComputer mrrComputer =
@@ -398,6 +406,8 @@
.fromJson(map['completionElementKindCounter'] as Map<String, dynamic>);
metrics.meanCompletionMS
.fromJson(map['meanCompletionMS'] as Map<String, dynamic>);
+ metrics.percentileCompletionMS
+ .fromJson(map['percentileMS'] as Map<String, dynamic>);
metrics.distributionCompletionMS
.fromJson(map['distributionCompletionMS'] as Map<String, dynamic>);
metrics.mrrComputer.fromJson(map['mrrComputer'] as Map<String, dynamic>);
@@ -453,6 +463,7 @@
completionKindCounter.addData(metrics.completionKindCounter);
completionElementKindCounter.addData(metrics.completionElementKindCounter);
meanCompletionMS.addData(metrics.meanCompletionMS);
+ percentileCompletionMS.addData(metrics.percentileCompletionMS);
distributionCompletionMS.addData(metrics.distributionCompletionMS);
mrrComputer.addData(metrics.mrrComputer);
successfulMrrComputer.addData(metrics.successfulMrrComputer);
@@ -545,6 +556,7 @@
'completionKindCounter': completionKindCounter.toJson(),
'completionElementKindCounter': completionElementKindCounter.toJson(),
'meanCompletionMS': meanCompletionMS.toJson(),
+ 'percentileCompletionMS': percentileCompletionMS.toJson(),
'distributionCompletionMS': distributionCompletionMS.toJson(),
'mrrComputer': mrrComputer.toJson(),
'successfulMrrComputer': successfulMrrComputer.toJson(),
@@ -617,6 +629,7 @@
/// Record this elapsed ms count for the average ms count.
void _recordTime(CompletionResult result) {
meanCompletionMS.addValue(result.elapsedMS);
+ percentileCompletionMS.addValue(result.elapsedMS);
distributionCompletionMS.addValue(result.elapsedMS);
}
@@ -815,7 +828,7 @@
void printComparisonOfCompletionCounts() {
String toString(int count, int totalCount) {
- return '$count (${printPercentage(count / totalCount, 2)})';
+ return '$count (${(count / totalCount).asPercentage(2)})';
}
var counters = targetMetrics.map((metrics) => metrics.completionCounter);
@@ -1093,25 +1106,45 @@
}
void printOtherMetrics(CompletionMetrics metrics) {
- List<String> toRow(ArithmeticMeanComputer computer) {
- var min = computer.min;
- var mean = computer.mean.toStringAsFixed(6);
- var max = computer.max;
- return [computer.name, '$min, $mean, $max'];
+ List<String> meanComputingRow(ArithmeticMeanComputer computer) {
+ return [
+ computer.name,
+ computer.min!.toStringAsFixed(3),
+ computer.mean.toStringAsFixed(3),
+ computer.max!.toStringAsFixed(3),
+ ];
}
var table = [
- toRow(metrics.meanCompletionMS),
- toRow(metrics.charsBeforeTop),
- toRow(metrics.charsBeforeTopFive),
- toRow(metrics.insertionLengthTheoretical),
+ ['', 'min', 'mean', 'max'],
+ meanComputingRow(metrics.meanCompletionMS),
+ meanComputingRow(metrics.charsBeforeTop),
+ meanComputingRow(metrics.charsBeforeTopFive),
+ meanComputingRow(metrics.insertionLengthTheoretical),
];
rightJustifyColumns(table, range(1, table[0].length));
printHeading(2, 'Other metrics');
printTable(table);
+ var percentileTable = [
+ ['', 'p50', 'p90', 'p95', 'count > 2s', 'max'],
+ [
+ metrics.percentileCompletionMS.name,
+ metrics.percentileCompletionMS.median.toString(),
+ metrics.percentileCompletionMS.p90.toString(),
+ metrics.percentileCompletionMS.p95.toString(),
+ metrics.percentileCompletionMS.aboveValueMaxCount.toString(),
+ metrics.percentileCompletionMS.maxValue.toString(),
+ ],
+ ];
+ rightJustifyColumns(percentileTable, range(1, percentileTable[1].length));
+
+ printHeading(3, 'Percentile metrics');
+ printTable(percentileTable);
+
var distribution = metrics.distributionCompletionMS.displayString();
+ printHeading(3, 'Completion ms distribution');
print('${metrics.name}: $distribution');
print('');
}
@@ -2083,6 +2116,12 @@
}
}
+extension on num {
+ String asPercentage([int fractionDigits = 1]) =>
+ '${(this * 100).toStringAsFixed(fractionDigits)}%'
+ .padLeft(4 + fractionDigits);
+}
+
extension AvailableSuggestionsExtension on protocol.AvailableSuggestion {
// TODO(jwren) I am not sure if we want CompletionSuggestionKind.INVOCATION in
// call cases here, to iterate I need to figure out why this algorithm is
diff --git a/pkg/analysis_server/tool/code_completion/metrics_util.dart b/pkg/analysis_server/tool/code_completion/metrics_util.dart
index 673f1bb..8899842 100644
--- a/pkg/analysis_server/tool/code_completion/metrics_util.dart
+++ b/pkg/analysis_server/tool/code_completion/metrics_util.dart
@@ -3,6 +3,7 @@
// BSD-style license that can be found in the LICENSE file.
import 'dart:math' as math;
+import 'dart:typed_data';
import 'package:analysis_server/src/status/pages.dart';
@@ -295,6 +296,136 @@
}
}
+/// A computer for calculating percentile-based metrics on a data set.
+///
+/// Specifically this tracks p50 (the median), p90, and p95.
+///
+/// See https://en.wikipedia.org/wiki/Percentile.
+class PercentileComputer {
+ final String name;
+
+ /// The value limit allowed by this computer.
+ ///
+ /// The computer can calculate percentile values for all data that range
+ /// between 0 and [valueLimit], exclusive.
+ int valueLimit;
+
+ /// An array of counts; the value at each index _i_ is the number of
+ /// occurrences of value _i_.
+ ///
+ /// Any values larger than [valueLimit] are not counted here.
+ Uint32List _counts;
+
+ /// The number of values which are less than [valueLimit].
+ int valueCount = 0;
+
+ /// The number of values greater than [valueLimit].
+ int aboveValueMaxCount = 0;
+
+ List<int> aboveValueMaxSamples = [];
+
+ int maxValue = 0;
+
+ PercentileComputer(this.name, {required this.valueLimit})
+ : _counts = Uint32List(valueLimit);
+
+ /// Calculates the median (p50) value.
+ int get median => kthPercentile(50);
+
+ /// Calculates the p90 value; the value at the 90th percentile of the data.
+ int get p90 => kthPercentile(90);
+
+ /// Calculates the p95 value; the value at the 95th percentile of the data.
+ int get p95 => kthPercentile(95);
+
+ /// Add the data from the given [computer] to this computer.
+ void addData(PercentileComputer computer) {
+ if (computer.valueLimit != valueLimit) {
+ throw UnsupportedError(
+ 'Cannot combine two PercentileComputers with different valueLimit '
+ 'values');
+ }
+ for (var i = 0; i < _counts.length; i++) {
+ _counts[i] += computer._counts[i];
+ }
+ valueCount += computer.valueCount;
+ aboveValueMaxCount += computer.aboveValueMaxCount;
+ for (var val in computer.aboveValueMaxSamples) {
+ if (aboveValueMaxSamples.length < 10) {
+ aboveValueMaxSamples.add(val);
+ }
+ }
+ maxValue = math.max(maxValue, computer.maxValue);
+ }
+
+ void addValue(int val) {
+ if (val > valueLimit) {
+ aboveValueMaxCount++;
+ if (aboveValueMaxSamples.length < 10) {
+ aboveValueMaxSamples.add(val);
+ }
+ } else {
+ _counts[val]++;
+ valueCount++;
+ }
+ maxValue = math.max(maxValue, val);
+ }
+
+ void clear() {
+ _counts = Uint32List(0);
+ valueCount = 0;
+ aboveValueMaxCount = 0;
+ aboveValueMaxSamples = [];
+ maxValue = 0;
+ }
+
+ /// Set the state of this computer to the state recorded in the decoded JSON
+ /// [map].
+ void fromJson(Map<String, dynamic> map) {
+ valueLimit = map['valueLimit'] as int;
+ _counts = Uint32List.fromList((map['counts'] as List<dynamic>).cast<int>());
+ valueCount = map['valueCount'] as int;
+ aboveValueMaxCount = map['aboveValueMaxCount'] as int;
+ aboveValueMaxSamples =
+ (map['aboveValueMaxSamples'] as List<dynamic>).cast<int>();
+ maxValue = map['maxValue'] as int;
+ }
+
+ /// Calculates the value at the _k_th percentile of the data.
+ int kthPercentile(int percentile) {
+ if (valueCount == 0) {
+ return 0;
+ }
+ // Linear walk through the data takes O([maxValue]) time. If this is too
+ // slow, a binary search can be implemented.
+ var targetIndex = valueCount * percentile / 100;
+ // The number of values represented by walking the counts.
+ var accumulation = 0;
+ for (var i = 0; i < _counts.length; i++) {
+ accumulation += _counts[i];
+ if (accumulation > targetIndex) {
+ // We've now accounted for [targetIndex] values, which includes the
+ // median value.
+ return i;
+ }
+ }
+ // The median value is in the very highest expected possible value.
+ return valueLimit;
+ }
+
+ /// Return a map used to represent this computer in a JSON structure.
+ Map<String, dynamic> toJson() {
+ return {
+ 'counts': _counts,
+ 'valueLimit': valueLimit,
+ 'valueCount': valueCount,
+ 'aboveValueMaxCount': aboveValueMaxCount,
+ 'aboveValueMaxSamples': aboveValueMaxSamples,
+ 'maxValue': maxValue,
+ };
+ }
+}
+
/// An immutable class to represent the placement in some list, for example '2nd
/// place out of 5'.
class Place {