| // Copyright (c) 2020, 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 'dart:typed_data'; | |
| import 'package:analysis_server/src/status/pages.dart'; | |
| import 'output_utilities.dart'; | |
| /// https://en.wikipedia.org/wiki/Average#Arithmetic_mean | |
| class ArithmeticMeanComputer { | |
| final String name; | |
| int sum = 0; | |
| int count = 0; | |
| int? min; | |
| int? max; | |
| new(this.name); | |
| double get mean => sum / count; | |
| /// Add the data from the given [computer] to this computer. | |
| void addData(ArithmeticMeanComputer computer) { | |
| sum += computer.sum; | |
| count += computer.count; | |
| min = _min(min, computer.min); | |
| max = _max(max, computer.max); | |
| } | |
| void addValue(int val) { | |
| sum += val; | |
| count++; | |
| min = _min(min, val); | |
| max = _max(max, val); | |
| } | |
| void clear() { | |
| sum = 0; | |
| count = 0; | |
| } | |
| /// Set the state of this computer to the state recorded in the decoded JSON | |
| /// [map]. | |
| void fromJson(Map<String, dynamic> map) { | |
| sum = map['sum'] as int; | |
| count = map['count'] as int; | |
| min = map['min'] as int?; | |
| max = map['max'] as int?; | |
| } | |
| /// Return a map used to represent this computer in a JSON structure. | |
| Map<String, dynamic> toJson() { | |
| return { | |
| 'sum': sum, | |
| 'count': count, | |
| if (min != null) 'min': min, | |
| if (max != null) 'max': max, | |
| }; | |
| } | |
| int? _max(int? first, int? second) { | |
| if (first == null) { | |
| return second; | |
| } else if (second == null) { | |
| return first; | |
| } else { | |
| return math.max(first, second); | |
| } | |
| } | |
| int? _min(int? first, int? second) { | |
| if (first == null) { | |
| return second; | |
| } else if (second == null) { | |
| return first; | |
| } else { | |
| return math.min(first, second); | |
| } | |
| } | |
| } | |
| /// A simple counter class. A [String] name is passed to name the counter. Each | |
| /// time something is counted, a non-null, non-empty [String] key is passed to | |
| /// [count] to increment the amount from zero. [printCounterValues] is provided | |
| /// to have a [String] summary of the generated counts, example: | |
| /// | |
| /// ``` | |
| /// Counts for 'counter example': | |
| /// [bucket-1] 60 (60.0%) | |
| /// [bucket-2] 25 (25.0%) | |
| /// [bucket-3] 5 (5.0%) | |
| /// [bucket-4] 10 (10.0%) | |
| /// ``` | |
| class Counter { | |
| final String name; | |
| final Map<String, int> _buckets = {}; | |
| int _totalCount = 0; | |
| new(this.name); | |
| /// Return a copy of all the current count data, this getter copies and | |
| /// returns the data to ensure that the data is only modified with the public | |
| /// accessors in this class. | |
| Map<String, int> get map => Map.from(_buckets); | |
| int get totalCount => _totalCount; | |
| /// Add the data from the given [counter] to this counter. | |
| void addData(Counter counter) { | |
| for (var entry in counter._buckets.entries) { | |
| var bucket = entry.key; | |
| _buckets[bucket] = (_buckets[bucket] ?? 0) + entry.value; | |
| } | |
| _totalCount += counter._totalCount; | |
| } | |
| void clear() { | |
| _buckets.clear(); | |
| _totalCount = 0; | |
| } | |
| void count(String id, [int countNumber = 1]) { | |
| assert(id.isNotEmpty && 1 <= countNumber); | |
| _buckets.update( | |
| id, | |
| (value) => value + countNumber, | |
| ifAbsent: () => countNumber, | |
| ); | |
| _totalCount += countNumber; | |
| } | |
| /// Set the state of this counter to the state recorded in the decoded JSON | |
| /// [map]. | |
| void fromJson(Map<String, dynamic> map) { | |
| for (var entry in (map['buckets'] as Map<String, dynamic>).entries) { | |
| _buckets[entry.key] = entry.value as int; | |
| } | |
| _totalCount = map['totalCount'] as int; | |
| } | |
| int getCountOf(String id) => _buckets[id] ?? 0; | |
| void printCounterValues() { | |
| if (_totalCount > 0) { | |
| var table = [ | |
| ['', 'count', 'percent'], | |
| ]; | |
| var entries = _buckets.entries.toList(); | |
| entries.sort((first, second) => second.value - first.value); | |
| for (var entry in entries) { | |
| var id = entry.key; | |
| var count = entry.value; | |
| table.add([ | |
| id, | |
| count.toString(), | |
| printPercentage(count / _totalCount, 2), | |
| ]); | |
| } | |
| printTable(table); | |
| } else { | |
| print('<no counts>'); | |
| } | |
| } | |
| /// Return a map used to represent this counter in a JSON structure. | |
| Map<String, dynamic> toJson() { | |
| return {'buckets': _buckets, 'totalCount': _totalCount}; | |
| } | |
| } | |
| class DistributionComputer { | |
| /// The buckets in which values are counted: [0..9], [10..19], ... [100..]. | |
| List<int> buckets = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; | |
| /// Add the data from the given [computer] to this computer. | |
| void addData(DistributionComputer computer) { | |
| for (var i = 0; i < buckets.length; i++) { | |
| buckets[i] += computer.buckets[i]; | |
| } | |
| } | |
| /// Add a millisecond value to the list of buckets. | |
| void addValue(int value) { | |
| var bucket = math.min(value ~/ 10, buckets.length - 1); | |
| buckets[bucket]++; | |
| } | |
| /// Return a textual representation of the distribution. | |
| String displayString() { | |
| var buffer = StringBuffer(); | |
| for (var i = 0; i < buckets.length; i++) { | |
| if (i > 0) { | |
| buffer.write(' '); | |
| } | |
| buffer.write('['); | |
| buffer.write(i * 10); | |
| buffer.write('] '); | |
| buffer.write(buckets[i]); | |
| } | |
| return buffer.toString(); | |
| } | |
| /// Set the state of this computer to the state recorded in the decoded JSON | |
| /// [map]. | |
| void fromJson(Map<String, dynamic> map) { | |
| buckets = map['buckets'] as List<int>; | |
| } | |
| /// Return a map used to represent this computer in a JSON structure. | |
| Map<String, dynamic> toJson() { | |
| return {'buckets': buckets}; | |
| } | |
| } | |
| /// A computer for the mean reciprocal rank. The MRR as well as the MRR only | |
| /// if the item was in the top 5 in the list see [_maxRank], is computed. | |
| /// https://en.wikipedia.org/wiki/Mean_reciprocal_rank. | |
| class MeanReciprocalRankComputer { | |
| static const int _maxRank = 5; | |
| final String name; | |
| double _sum = 0; | |
| double _sum_5 = 0; | |
| int _count = 0; | |
| new(this.name); | |
| int get count => _count; | |
| double get mrr { | |
| if (count == 0) { | |
| return 0; | |
| } | |
| return _sum / count; | |
| } | |
| double get mrr_5 { | |
| if (count == 0) { | |
| return 0; | |
| } | |
| return _sum_5 / count; | |
| } | |
| /// Add the data from the given [computer] to this computer. | |
| void addData(MeanReciprocalRankComputer computer) { | |
| _sum += computer._sum; | |
| _sum_5 += computer._sum_5; | |
| _count += computer._count; | |
| } | |
| void addRank(int rank) { | |
| if (rank != 0) { | |
| _sum += 1 / rank; | |
| if (rank <= _maxRank) { | |
| _sum_5 += 1 / rank; | |
| } | |
| } | |
| _count++; | |
| } | |
| void clear() { | |
| _sum = 0; | |
| _sum_5 = 0; | |
| _count = 0; | |
| } | |
| /// Set the state of this computer to the state recorded in the decoded JSON | |
| /// [map]. | |
| void fromJson(Map<String, dynamic> map) { | |
| _sum = map['sum'] as double; | |
| _sum_5 = map['sum_5'] as double; | |
| _count = map['count'] as int; | |
| } | |
| void printMean() { | |
| print('Mean Reciprocal Rank \'$name\' (total = $count)'); | |
| print( | |
| 'mrr = ${mrr.toStringAsFixed(6)} ' | |
| '(inverse = ${(1 / mrr).toStringAsFixed(3)})', | |
| ); | |
| print( | |
| 'mrr_5 = ${mrr_5.toStringAsFixed(6)} ' | |
| '(inverse = ${(1 / mrr_5).toStringAsFixed(3)})', | |
| ); | |
| } | |
| /// Return a map used to represent this computer in a JSON structure. | |
| Map<String, dynamic> toJson() { | |
| return {'sum': _sum, 'sum_5': _sum_5, 'count': _count}; | |
| } | |
| } | |
| /// 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; | |
| new(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 { | |
| /// A 1-indexed place in a list | |
| final int _numerator; | |
| /// The total number of possible places. | |
| final int _denominator; | |
| const new(this._numerator, this._denominator) | |
| : assert(_numerator > 0), | |
| assert(_denominator >= _numerator); | |
| /// Return an instance extracted from the decoded JSON [map]. | |
| factory fromJson(Map<String, dynamic> map) { | |
| return Place(map['numerator'] as int, map['denominator'] as int); | |
| } | |
| const new none() : _numerator = 0, _denominator = 0; | |
| int get denominator => _denominator; | |
| @override | |
| int get hashCode => Object.hash(_numerator, _denominator); | |
| int get numerator => _numerator; | |
| int get rank => _numerator; | |
| @override | |
| bool operator ==(Object other) => | |
| other is Place && | |
| _numerator == other._numerator && | |
| _denominator == other._denominator; | |
| /// Return a map used to represent this place in a JSON structure. | |
| Map<String, dynamic> toJson() { | |
| return {'numerator': _numerator, 'denominator': _denominator}; | |
| } | |
| } |