|  | // 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:io"; | 
|  |  | 
|  | import "../test/simple_stats.dart"; | 
|  |  | 
|  | void usage([String? extraMessage]) { | 
|  | print("Usage:"); | 
|  | print("On Linux via bash you can do something like"); | 
|  | print("dart pkg/front_end/tool/stat_on_dash_v.dart \ " | 
|  | "   now_run_{1..10}.data then_run_{1..10}.data"); | 
|  | if (extraMessage != null) { | 
|  | print(""); | 
|  | print("Notice:"); | 
|  | print(extraMessage); | 
|  | } | 
|  | exit(1); | 
|  | } | 
|  |  | 
|  | void main(List<String> args) { | 
|  | if (args.length < 4) { | 
|  | usage("Requires more input."); | 
|  | } | 
|  | // Maps from "part" (or "category" or whatever) => | 
|  | // (map from file group => list of runtimes) | 
|  | Map<String, Map<String, List<int>>> data = {}; | 
|  | Set<String> allGroups = {}; | 
|  | for (String file in args) { | 
|  | File f = new File(file); | 
|  | if (!f.existsSync()) usage("$file doesn't exist."); | 
|  | String groupId = replaceNumbers(file); | 
|  | allGroups.add(groupId); | 
|  | String fileContent = f.readAsStringSync(); | 
|  | List<String> fileLines = fileContent.split("\n"); | 
|  | Set<String> partsSeen = {}; | 
|  | for (String line in fileLines) { | 
|  | if (!isTimePrependedLine(line)) continue; | 
|  | String trimmedLine = line.substring(16).trim(); | 
|  | String part = replaceNumbers(trimmedLine); | 
|  | if (!partsSeen.add(part)) { | 
|  | int seen = 2; | 
|  | while (true) { | 
|  | String newPartName = "$part ($seen)"; | 
|  | if (partsSeen.add(newPartName)) { | 
|  | part = newPartName; | 
|  | break; | 
|  | } | 
|  | seen++; | 
|  | } | 
|  | } | 
|  | int microSeconds = findMs(trimmedLine, inMs: true); | 
|  | Map<String, List<int>> groupToTime = data[part] ??= {}; | 
|  | List<int> times = groupToTime[groupId] ??= []; | 
|  | times.add(microSeconds); | 
|  | } | 
|  | } | 
|  |  | 
|  | if (allGroups.length < 2) { | 
|  | assert(allGroups.length == 1); | 
|  | usage("Found only 1 group. At least two are required."); | 
|  | } | 
|  |  | 
|  | Map<String, double> combinedChange = {}; | 
|  |  | 
|  | bool printedAnything = false; | 
|  | for (String part in data.keys) { | 
|  | Map<String, List<int>> partData = data[part]!; | 
|  | List<int>? prevRuntimes; | 
|  | String? prevGroup; | 
|  | bool printed = false; | 
|  | for (String group in allGroups) { | 
|  | List<int>? runtimes = partData[group]; | 
|  | if (runtimes == null) { | 
|  | // Fake it to be a small list of 0s. | 
|  | runtimes = new List<int>.filled(5, 0); | 
|  | if (!printed) { | 
|  | printed = true; | 
|  | print("$part:"); | 
|  | } | 
|  | print("Notice: faking data for $group"); | 
|  | } | 
|  | if (prevRuntimes != null) { | 
|  | TTestResult result = SimpleTTestStat.ttest(runtimes, prevRuntimes); | 
|  | if (result.significant) { | 
|  | if (!printed) { | 
|  | printed = true; | 
|  | print("$part:"); | 
|  | } | 
|  | print("$prevGroup => $group: $result"); | 
|  | print("$group: $runtimes"); | 
|  | print("$prevGroup: $prevRuntimes"); | 
|  | combinedChange["$prevGroup => $group"] ??= 0; | 
|  | double leastConfidentChange; | 
|  | if (result.diff < 0) { | 
|  | leastConfidentChange = result.diff + result.confidence; | 
|  | } else { | 
|  | leastConfidentChange = result.diff - result.confidence; | 
|  | } | 
|  |  | 
|  | combinedChange["$prevGroup => $group"] = | 
|  | combinedChange["$prevGroup => $group"]! + leastConfidentChange; | 
|  | } | 
|  | } | 
|  | prevRuntimes = runtimes; | 
|  | prevGroup = group; | 
|  | } | 
|  | if (printed) { | 
|  | print("---"); | 
|  | printedAnything = true; | 
|  | } | 
|  | } | 
|  | if (printedAnything) { | 
|  | for (String part in combinedChange.keys) { | 
|  | print("Combined least change for $part: " | 
|  | "${combinedChange[part]!.toStringAsFixed(2)} ms."); | 
|  | } | 
|  | } else { | 
|  | print("Nothing significant found."); | 
|  | } | 
|  | } | 
|  |  | 
|  | /// Returns ms or µs or throws if ms not found. | 
|  | int findMs(String s, {bool inMs = true}) { | 
|  | // Find " in " followed by numbers possibly followed by (a dot and more | 
|  | // numbers) followed by "ms"; e.g. " in 42.3ms" | 
|  |  | 
|  | // This is O(n^2) but it doesn't matter. | 
|  | for (int i = 0; i < s.length; i++) { | 
|  | int j = 0; | 
|  | if (s.codeUnitAt(i + j++) != $SPACE) continue; | 
|  | if (s.codeUnitAt(i + j++) != $i) continue; | 
|  | if (s.codeUnitAt(i + j++) != $n) continue; | 
|  | if (s.codeUnitAt(i + j++) != $SPACE) continue; | 
|  | int numberStartsAt = i + j; | 
|  | if (!isNumber(s.codeUnitAt(i + j++))) continue; | 
|  | while (isNumber(s.codeUnitAt(i + j))) { | 
|  | j++; | 
|  | } | 
|  | // We've seen " is 0+" => we should now either have "ms" or a dot, | 
|  | // more numbers followed by "ms". | 
|  | if (s.codeUnitAt(i + j) == $m) { | 
|  | j++; | 
|  | if (s.codeUnitAt(i + j++) != $s) continue; | 
|  | // Seen " is 0+ms" => We're done. | 
|  | int ms = int.parse(s.substring(numberStartsAt, i + j - 2)); | 
|  | if (inMs) return ms; | 
|  | return ms * 1000; | 
|  | } else if (s.codeUnitAt(i + j) == $PERIOD) { | 
|  | int dotAt = i + j; | 
|  | j++; | 
|  | if (!isNumber(s.codeUnitAt(i + j++))) continue; | 
|  | while (isNumber(s.codeUnitAt(i + j))) { | 
|  | j++; | 
|  | } | 
|  | if (s.codeUnitAt(i + j++) != $m) continue; | 
|  | if (s.codeUnitAt(i + j++) != $s) continue; | 
|  | // Seen " is 0+.0+ms" => We're done. | 
|  | // int.parse(s.substring(numberStartsAt, i + j - 2)); | 
|  | int ms = int.parse(s.substring(numberStartsAt, dotAt)); | 
|  | if (inMs) return ms; | 
|  | int fraction = int.parse(s.substring(dotAt + 1, i + j - 2)); | 
|  | while (fraction < 100) { | 
|  | fraction *= 10; | 
|  | } | 
|  | while (fraction >= 1000) { | 
|  | fraction ~/= 10; | 
|  | } | 
|  | return ms * 1000 + fraction; | 
|  | } else { | 
|  | continue; | 
|  | } | 
|  | } | 
|  | usage("Didn't find any ms data in line '$s'."); | 
|  | throw "usage should exit"; | 
|  | } | 
|  |  | 
|  | const int $SPACE = 32; | 
|  | const int $PERIOD = 46; | 
|  | const int $0 = 48; | 
|  | const int $9 = 57; | 
|  | const int $COLON = 58; | 
|  | const int $_ = 95; | 
|  | const int $i = 105; | 
|  | const int $m = 109; | 
|  | const int $n = 110; | 
|  | const int $s = 115; | 
|  |  | 
|  | /// Check that format is like '0:00:00.000000: '. | 
|  | bool isTimePrependedLine(String s) { | 
|  | if (s.length < 15) return false; | 
|  | int index = 0; | 
|  | if (!isNumber(s.codeUnitAt(index++))) return false; | 
|  | if (s.codeUnitAt(index++) != $COLON) return false; | 
|  | if (!isNumber(s.codeUnitAt(index++))) return false; | 
|  | if (!isNumber(s.codeUnitAt(index++))) return false; | 
|  | if (s.codeUnitAt(index++) != $COLON) return false; | 
|  | if (!isNumber(s.codeUnitAt(index++))) return false; | 
|  | if (!isNumber(s.codeUnitAt(index++))) return false; | 
|  | if (s.codeUnitAt(index++) != $PERIOD) return false; | 
|  | for (int i = 0; i < 6; i++) { | 
|  | if (!isNumber(s.codeUnitAt(index++))) return false; | 
|  | } | 
|  | if (s.codeUnitAt(index++) != $COLON) return false; | 
|  | return true; | 
|  | } | 
|  |  | 
|  | bool isNumber(int codeUnit) { | 
|  | return codeUnit >= $0 && codeUnit <= $9; | 
|  | } | 
|  |  | 
|  | String replaceNumbers(String s) { | 
|  | StringBuffer sb = new StringBuffer(); | 
|  | bool lastWasNumber = false; | 
|  | for (int i = 0; i < s.length; i++) { | 
|  | int codeUnit = s.codeUnitAt(i); | 
|  | if (isNumber(codeUnit)) { | 
|  | if (!lastWasNumber) { | 
|  | // Ignore number; replace with '_'. | 
|  | sb.writeCharCode($_); | 
|  | lastWasNumber = true; | 
|  | } | 
|  | } else { | 
|  | sb.writeCharCode(codeUnit); | 
|  | lastWasNumber = false; | 
|  | } | 
|  | } | 
|  | return sb.toString(); | 
|  | } |