blob: 7591a5d19e7a1d101263b0a836e61781b0166c61 [file] [log] [blame]
// Copyright (c) 2014, 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.
part of dart2js.helpers;
// Helper methods for statistics.
/// Current stats collector. Use [ActiveStats] for active collection of data.
final Stats stats = new ActiveStats(new ConsolePrinter());
/// Interface for gathering and display of statistical information.
/// This class serves as the noop collector.
class Stats {
const Stats();
/// Registers [key], [value] pair in the map [id]. If [fromExisting] is
/// non-null and [key] already exists, the value associated with [key] will
/// be the return value of [fromExisting] when called with the existing value.
///
/// The recorded information is not dumped automatically.
void recordMap(id, key, value, {fromExisting(value)}) {}
/// Returns the map [id] recorded with [recordMap].
Map getMap(id) => const {};
/// Registers [element] as an element of the list [id]. If provided, [data]
/// provides additional data for [element].
///
/// The recorded information is dumped automatically on call to [dumpStats].
///
/// Example:
/// Calling [recordElement] like this:
/// recordElement('foo', 'a', data: 'first-a-data');
/// recordElement('foo', 'a', data: 'second-a-data');
/// recordElement('foo', 'b');
/// recordElement('bar', 'a', data: 'third-a-data');
/// recordElement('bar', 'c');
/// will result in a dump like this:
/// foo: 2
/// value=a data=second-a-data
/// b
/// bar: 2
/// value=a data=third-a-data
/// c
///
void recordElement(id, element, {data}) {}
/// Returns the list [id] recorded with [recordElement].
Iterable getList(String id) => const [];
/// Registers [value] as an occurrence of [id]. If passed, [example] provides
/// an example data of the occurrence of [value].
///
/// The recorded information is dumped automatically on call to [dumpStats].
///
/// Example:
/// Calling [recordFrequency] like this:
/// recordFrequency('foo', 'a', 'first-a-data');
/// recordFrequency('foo', 'a', 'second-a-data');
/// recordFrequency('bar', 'b', 'first-b-data');
/// recordFrequency('foo', 'c');
/// recordFrequency('bar', 'b');
/// will result in a dump like this:
/// foo:
/// a: 2
/// first-a-data
/// second-a-data
/// c: 1
/// bar:
/// b: 2
/// first-b-data
///
void recordFrequency(id, value, [example]) {}
/// For each key/value pair in [map] the elements in the value are registered
/// as examples of occurrences of the key in [id].
void recordFrequencies(id, Map<dynamic, Iterable> map) {}
/// Returns the examples given for the occurrence of [value] for [id].
Iterable recordedFrequencies(id, value) => const [];
/// Increases the counter [id] by 1. If provided, [example] is used as an
/// example of the count and [data] provides additional information for
/// [example].
///
/// The recorded information is dumped automatically on call to [dumpStats].
///
/// Example:
/// Calling [recordCounter] like this:
/// recordCounter('foo', 'a');
/// recordCounter('foo', 'a');
/// recordCounter('foo', 'b');
/// recordCounter('bar', 'c', 'first-c-data');
/// recordCounter('bar', 'c', 'second-c-data');
/// recordCounter('bar', 'd');
/// recordCounter('bar', 'd');
/// recordCounter('baz');
/// recordCounter('baz');
/// will result in a dump like this:
/// foo: 3
/// count=2 example=a
/// count=1 example=b
/// bar: 4
/// count=2 examples=2
/// c:
/// first-c-data
/// second-c-data
/// d
/// baz: 2
///
void recordCounter(id, [example, data]) {}
/// Dumps the stats for the recorded frequencies, sets, and counters. If
/// provided [beforeClose] is called before closing the dump output. This
/// can be used to include correlations on the collected data through
/// [dumpCorrelation].
void dumpStats({void beforeClose()}) {}
/// Prints the correlation between the elements of [a] and [b].
///
/// Three sets are output using [idA] and [idB] as labels for the elements
/// [a] and [b]:
///
/// 'idA && idB' lists the elements both in [a] and [b],
/// '!idA && idB' lists the elements not in [a] but in [b], and
/// 'idA && !idB' lists the elements in [a] but not in [b].
///
/// If [dataA] and/or [dataB] are provided, additional information on the
/// elements are looked up in [dataA] or [dataB] using [dataA] as the primary
/// source.
void dumpCorrelation(idA, Iterable a, idB, Iterable b,
{Map dataA, Map dataB}) {}
}
/// Interface for printing output data.
///
/// This class serves as the disabled output.
class StatsOutput {
const StatsOutput();
/// Print [text] as on a separate line.
void println(String text) {}
}
/// Output to the [debugPrint] method.
class DebugOutput implements StatsOutput {
const DebugOutput();
void println(String text) => debugPrint(text);
}
/// Interface for printing stats collected in [Stats].
abstract class StatsPrinter {
/// The number of examples printer. If `null` all examples are printed.
int get examples => 0;
/// Start a group [id].
void start(String id) {}
/// Create a group [id] with content created by [createGroupContent].
void group(String id, void createGroupContent()) {
start(id);
createGroupContent();
end(id);
}
/// End a group [id].
void end(String id) {}
/// Start a stat entry for [id] with additional [data].
void open(String id,
[Map<String, dynamic> data = const <String, dynamic>{}]) {}
/// Create a stat entry for [id] with additional [data] and content created by
/// [createChildContent].
void child(String id,
[Map<String, dynamic> data = const <String, dynamic>{},
void createChildContent()]) {
open(id, data);
if (createChildContent != null) createChildContent();
close(id);
}
/// End a stat entry for [id].
void close(String id) {}
/// Starts a group of additional information.
void beginExtra() {}
/// Starts a group of additional information.
void endExtra() {}
}
/// Abstract base class for [ConsolePrinter] and [XMLPrinter].
abstract class BasePrinter extends StatsPrinter {
final int examples;
final StatsOutput output;
int indentationLevel = 0;
BasePrinter({this.output: const DebugOutput(),
this.examples: 10});
String get indentation {
StringBuffer sb = new StringBuffer();
for (int i = 0 ; i < indentationLevel ; i++) {
sb.write(' ');
}
return sb.toString();
}
}
/// [StatsPrinter] that displays stats in console lines.
class ConsolePrinter extends BasePrinter {
int extraLevel = 0;
ConsolePrinter({StatsOutput output: const DebugOutput(),
int examples: 10})
: super(output: output, examples: examples);
void open(String id,
[Map<String, dynamic> data = const <String, dynamic>{}]) {
if (extraLevel > 0) return;
StringBuffer sb = new StringBuffer();
sb.write(indentation);
String space = '';
if (data['title'] != null) {
sb.write('${data['title']}:');
space = ' ';
data.remove('title');
} else if (data['name'] != null) {
sb.write('${data['name']}');
space = ' ';
data.remove('name');
}
Iterable nonNullValues = data.values.where((v) => v != null);
if (nonNullValues.length == 1) {
sb.write('$space${nonNullValues.first}');
} else {
data.forEach((key, value) {
sb.write('$space$key=$value');
space = ' ';
});
}
output.println(sb.toString());
indentationLevel++;
}
void close(String id) {
if (extraLevel > 0) return;
indentationLevel--;
}
void beginExtra() {
if (extraLevel == 0) output.println('$indentation...');
extraLevel++;
}
void endExtra() {
extraLevel--;
}
}
/// [StatsPrinter] that displays stats in XML format.
class XMLPrinter extends BasePrinter {
static const HtmlEscape escape = const HtmlEscape();
bool opened = false;
XMLPrinter({output: const DebugOutput(),
int examples: 10})
: super(output: output, examples: examples);
void start(String id) {
if (!opened) {
output.println('<?xml version="1.0" encoding="UTF-8"?>');
opened = true;
}
open(id);
}
void end(String id) {
close(id);
}
void open(String id,
[Map<String, dynamic> data = const <String, dynamic>{}]) {
StringBuffer sb = new StringBuffer();
for (int i = 0 ; i < indentationLevel ; i++) {
sb.write(' ');
}
sb.write('<$id');
data.forEach((key, value) {
if (value != null) {
sb.write(' $key="${escape.convert('$value')}"');
}
});
sb.write('>');
output.println(sb.toString());
indentationLevel++;
}
void close(String id) {
indentationLevel--;
output.println('${indentation}</$id>');
}
void beginExtra() {
open('extra');
}
void endExtra() {
close('extra');
}
}
/// Actual implementation of [Stats].
class ActiveStats implements Stats {
final StatsPrinter printer;
Map<dynamic, Map> maps = {};
Map<dynamic, Map<dynamic, List>> frequencyMaps = {};
Map<dynamic, Map> setsMap = {};
Map<dynamic, Map<dynamic, List>> countersMap =
<dynamic, Map<dynamic, List>>{};
ActiveStats(StatsPrinter this.printer);
void recordMap(id, key, value, {fromExisting(value)}) {
Map map = maps.putIfAbsent(id, () => {});
if (fromExisting != null && map.containsKey(key)) {
map[key] = fromExisting(map[key]);
} else {
map[key] = value;
}
}
Map getMap(key) {
return maps[key];
}
void recordFrequency(id, value, [example]) {
Map<int, List> map = frequencyMaps.putIfAbsent(id, () => {});
map.putIfAbsent(value, () => []);
map[value].add(example);
}
void recordFrequencies(id, Map<dynamic, Iterable> frequencyMap) {
Map<int, List> map = frequencyMaps.putIfAbsent(id, () => {});
frequencyMap.forEach((value, examples) {
map.putIfAbsent(value, () => []);
map[value].addAll(examples);
});
}
Iterable recordedFrequencies(id, value) {
Map<dynamic, List> map = frequencyMaps[id];
if (map == null) return const [];
List list = map[value];
if (list == null) return const [];
return list;
}
void recordCounter(id, [reason, example]) {
Map<dynamic, List> map = countersMap.putIfAbsent(id, () => {});
map.putIfAbsent(reason, () => []).add(example);
}
void recordElement(key, element, {data}) {
setsMap.putIfAbsent(key, () => new Map())[element] = data;
}
Iterable getList(String key) {
Map map = setsMap[key];
if (map == null) return const [];
return map.keys;
}
void dumpStats({void beforeClose()}) {
printer.start('stats');
dumpFrequencies();
dumpSets();
dumpCounters();
if (beforeClose != null) {
beforeClose();
}
printer.end('stats');
}
void dumpSets() {
printer.group('sets', () {
setsMap.forEach((k, set) {
dumpIterable('examples', '$k', set.keys,
limit: printer.examples, dataMap: set);
});
});
}
void dumpFrequencies() {
printer.group('frequencies', () {
frequencyMaps.forEach((key, Map<dynamic, List> map) {
printer.child('frequency', {'title': '$key'}, () {
dumpFrequency(map);
});
});
});
}
void dumpFrequency(Map<dynamic, Iterable> map) {
Map sortedMap = trySortMap(map);
sortedMap.forEach((k, list) {
dumpIterable('examples', '$k', list, limit: printer.examples);
});
}
void dumpCounters() {
printer.group('counters', () {
countersMap.keys.forEach(dumpCounter);
});
}
void dumpCounter(id) {
Map<dynamic, List> map = countersMap[id];
bool hasData(example) {
if (map == null) return false;
List list = map[example];
if (list == null) return false;
return list.any((data) => data != null);
}
int count = 0;
Map<dynamic, int> frequencyMap = {};
map.forEach((var category, List examples) {
if (category != null) {
frequencyMap.putIfAbsent(category, () => 0);
frequencyMap[category] += examples.length;
}
count += examples.length;
});
Map<int, Set> result = sortMap(inverseMap(frequencyMap), (a, b) => b - a);
int examplesLimit = null;
if (printer.examples != null && result.length >= printer.examples) {
examplesLimit = 0;
}
int counter = 0;
bool hasMore = false;
printer.open('counter', {'title': '$id', 'count': count});
result.forEach((int count, Set examples) {
if (counter == printer.examples) {
printer.beginExtra();
hasMore = true;
}
if (examples.length == 1 &&
(examplesLimit == 0 || !hasData(examples.first))) {
printer.child('examples', {'count': count, 'example': examples.first});
} else {
printer.child('examples',
{'count': count, 'examples': examples.length},
() {
examples.forEach((example) {
dumpIterable(
'examples', '$example', map[example],
limit: examplesLimit,
includeCount: false);
});
});
}
counter++;
});
if (hasMore) {
printer.endExtra();
}
printer.close('counter');
}
void dumpCorrelation(keyA, Iterable a, keyB, Iterable b,
{Map dataA, Map dataB}) {
printer.child('correlations', {'title': '$keyA vs $keyB'}, () {
List aAndB = a.where((e) => e != null && b.contains(e)).toList();
List aAndNotB = a.where((e) => e != null && !b.contains(e)).toList();
List notAandB = b.where((e) => e != null && !a.contains(e)).toList();
dumpIterable('correlation', '$keyA && $keyB', aAndB, dataMap: dataA,
limit: printer.examples);
dumpIterable('correlation', '$keyA && !$keyB', aAndNotB, dataMap: dataA,
limit: printer.examples);
dumpIterable('correlation', '!$keyA && $keyB', notAandB, dataMap: dataB,
limit: printer.examples);
});
}
void dumpIterable(String tag, String title, Iterable iterable,
{int limit, Map dataMap, bool includeCount: true}) {
if (limit == 0) return;
Map childData = {};
Iterable nonNullIterable = iterable.where((e) => e != null);
if (nonNullIterable.isEmpty && !includeCount) {
childData['name'] = title;
} else {
childData['title'] = title;
}
if (includeCount) {
childData['count'] = iterable.length;
}
printer.child(tag, childData, () {
bool hasMore = false;
int counter = 0;
nonNullIterable.forEach((element) {
if (counter == limit) {
printer.beginExtra();
hasMore = true;
}
var data = dataMap != null ? dataMap[element] : null;
if (data != null) {
printer.child('example', {'value': element, 'data': data});
} else {
printer.child('example', {'value': element});
}
counter++;
});
if (hasMore) {
printer.endExtra();
}
});
}
}
/// Returns a map that is an inversion of [map], where the keys are the values
/// of [map] and the values are the set of keys in [map] that share values.
///
/// If [equals] and [hashCode] are provided, these are used to determine
/// equality among the values of [map].
///
/// If [isValidKey] is provided, this is used to determine with a value of [map]
/// is a potential key of the inversion map.
Map<dynamic, Set> inverseMap(Map map,
{bool equals(key1, key2),
int hashCode(key),
bool isValidKey(potentialKey)}) {
Map<dynamic, Set> result = new LinkedHashMap<dynamic, Set>(
equals: equals, hashCode: hashCode, isValidKey: isValidKey);
map.forEach((k, v) {
if (isValidKey == null || isValidKey(v)) {
result.putIfAbsent(v, () => new Set()).add(k);
}
});
return result;
}
/// Return a new map heuristically sorted by the keys of [map]. If the first
/// key of [map] is [Comparable], the keys are sorted using [sortMap] under
/// the assumption that all keys are [Comparable].
/// Otherwise, the keys are sorted as string using their `toString`
/// representation.
Map trySortMap(Map map) {
Iterable iterable = map.keys.where((k) => k != null);
if (iterable.isEmpty) return map;
var key = iterable.first;
if (key is Comparable) {
return sortMap(map);
}
return sortMap(map, (a, b) => '$a'.compareTo('$b'));
}
/// Returns a new map in which the keys of [map] are sorted using [compare].
/// If [compare] is null, the keys must be [Comparable].
Map sortMap(Map map, [int compare(a,b)]) {
List keys = map.keys.toList();
keys.sort(compare);
Map sortedMap = new Map();
keys.forEach((k) => sortedMap[k] = map[k]);
return sortedMap;
}