// 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; ArithmeticMeanComputer(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 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 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 _buckets = {}; int _totalCount = 0; Counter(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 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 map) { for (var entry in (map['buckets'] as Map).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(''); } } /// Return a map used to represent this counter in a JSON structure. Map toJson() { return {'buckets': _buckets, 'totalCount': _totalCount}; } } class DistributionComputer { /// The buckets in which values are counted: [0..9], [10..19], ... [100..]. List 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 map) { buckets = map['buckets'] as List; } /// Return a map used to represent this computer in a JSON structure. Map 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; MeanReciprocalRankComputer(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 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 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 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 map) { valueLimit = map['valueLimit'] as int; _counts = Uint32List.fromList((map['counts'] as List).cast()); valueCount = map['valueCount'] as int; aboveValueMaxCount = map['aboveValueMaxCount'] as int; aboveValueMaxSamples = (map['aboveValueMaxSamples'] as List).cast(); 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 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 Place(this._numerator, this._denominator) : assert(_numerator > 0), assert(_denominator >= _numerator); /// Return an instance extracted from the decoded JSON [map]. factory Place.fromJson(Map map) { return Place(map['numerator'] as int, map['denominator'] as int); } const Place.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 toJson() { return {'numerator': _numerator, 'denominator': _denominator}; } }