| // 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. |
| |
| import 'dart:async' show EventSink; |
| import 'dart:collection'; |
| import 'dart:convert'; |
| |
| import '../../compiler.dart'; |
| import '../common.dart'; |
| import '../compiler.dart' show Compiler; |
| import '../util/util.dart'; |
| |
| // Helper methods for statistics. |
| |
| /// Current stats collector. Use [enableStatsOutput] to enable recording of |
| /// stats. |
| Stats get stats { |
| enableDebugMode(); |
| if (_stats == null) { |
| _stats = const Stats(); |
| } |
| return _stats; |
| } |
| |
| Stats _stats; |
| |
| /// Enable recording of stats. Use [Stats.dumpStats] to output the record stats. |
| /// |
| /// Pass the [outputProvider] of [Compiler] to generate stats into a separate |
| /// file using [name] and [extension] for the filename. If omitted, stats are |
| /// printed on standard out. |
| /// |
| /// If [xml] is `true`, stats output is formatted as XML with a default |
| /// extension of 'xml', otherwise the output is indented text with a default |
| /// extension of 'log'. |
| void enableStatsOutput( |
| {CompilerOutputProvider outputProvider, |
| bool xml: true, |
| String name: 'stats', |
| String extension, |
| int examples: 10}) { |
| if (_stats != null) { |
| throw new StateError('Stats have already been initialized.'); |
| } |
| enableDebugMode(); |
| |
| StatsOutput output; |
| if (outputProvider != null) { |
| if (extension == null) { |
| extension = xml ? 'xml' : 'log'; |
| } |
| output = new SinkOutput(outputProvider(name, extension)); |
| } else { |
| output = const DebugOutput(); |
| } |
| StatsPrinter printer; |
| if (xml) { |
| printer = new XMLPrinter(output: output, examples: examples); |
| } else { |
| printer = new ConsolePrinter(output: output, examples: examples); |
| } |
| _stats = new ActiveStats(printer); |
| } |
| |
| /// 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]) {} |
| |
| /// Records the current stack trace under the key [id]. Only every |
| /// [sampleFrequency] call with the same id is recorded, and if omitted |
| /// [stackTraceSampleFrequency] is used. |
| void recordTrace(id, {int sampleFrequency}) {} |
| |
| /// The default sample frequency used for recording stack traces. |
| int get stackTraceSampleFrequency => 0; |
| |
| /// Set the default sample frequency used for recording stack traces. |
| void set stackTraceSampleFrequency(int value) {} |
| |
| /// 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); |
| } |
| |
| /// Output to an [EventSink]. Used to output to a file through the |
| /// [CompilerOutputProvider]. |
| class SinkOutput implements StatsOutput { |
| EventSink<String> sink; |
| |
| SinkOutput(this.sink); |
| |
| void println(String text) { |
| sink.add(text); |
| sink.add('\n'); |
| } |
| } |
| |
| /// 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 with Indentation { |
| final int examples; |
| final StatsOutput output; |
| |
| BasePrinter({this.output: const DebugOutput(), this.examples: 10}) { |
| indentationUnit = " "; |
| } |
| } |
| |
| /// [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()); |
| indentMore(); |
| } |
| |
| void close(String id) { |
| if (extraLevel > 0) return; |
| |
| indentLess(); |
| } |
| |
| 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(); |
| sb.write(indentation); |
| sb.write('<$id'); |
| data.forEach((key, value) { |
| if (value != null) { |
| sb.write(' $key="${escape.convert('$value')}"'); |
| } |
| }); |
| sb.write('>'); |
| output.println(sb.toString()); |
| indentMore(); |
| } |
| |
| void close(String id) { |
| indentLess(); |
| output.println('${indentation}</$id>'); |
| } |
| |
| void beginExtra() { |
| open('extra'); |
| } |
| |
| void endExtra() { |
| close('extra'); |
| } |
| } |
| |
| /// A node in a stack trace tree used to store and organize stack traces by |
| /// common prefixes. |
| class _StackTraceNode implements Comparable<_StackTraceNode> { |
| int count; |
| List<StackTraceLine> commonPrefix; |
| List<_StackTraceNode> subtraces; |
| |
| _StackTraceNode(this.commonPrefix, this.count, this.subtraces); |
| |
| _StackTraceNode.root() : this([], 0, []); |
| |
| _StackTraceNode.leaf(StackTraceLines stackTrace) |
| : this(stackTrace.lines, 1, const []); |
| |
| _StackTraceNode.node(List<StackTraceLine> commonPrefix, _StackTraceNode first, |
| _StackTraceNode second) |
| : this(commonPrefix, first.count + second.count, [first, second]); |
| |
| void add(StackTraceLines stackTrace) { |
| count++; |
| if (!stackTrace.lines.isEmpty) { |
| addSubtrace(stackTrace); |
| } |
| } |
| |
| void addSubtrace(StackTraceLines stackTrace) { |
| List<StackTraceLine> lines = stackTrace.lines; |
| for (_StackTraceNode subtrace in subtraces) { |
| int commonPrefixLength = |
| longestCommonPrefixLength(subtrace.commonPrefix, lines); |
| if (commonPrefixLength > 0) { |
| stackTrace = stackTrace.subtrace(commonPrefixLength); |
| if (commonPrefixLength == subtrace.commonPrefix.length) { |
| subtrace.add(stackTrace); |
| } else { |
| subtrace.commonPrefix = |
| subtrace.commonPrefix.sublist(commonPrefixLength); |
| subtraces.remove(subtrace); |
| subtraces.add(new _StackTraceNode.node( |
| lines.sublist(0, commonPrefixLength), |
| subtrace, |
| new _StackTraceNode.leaf(stackTrace))); |
| } |
| return; |
| } |
| } |
| subtraces.add(new _StackTraceNode.leaf(stackTrace)); |
| } |
| |
| void dumpTraces(StatsPrinter printer) { |
| printer.open('trace', {'count': count, 'line': commonPrefix.first}); |
| if (commonPrefix.length > 1) { |
| for (StackTraceLine line in commonPrefix.skip(1)) { |
| printer.child('trace', {'line': line}); |
| } |
| } |
| dumpSubtraces(printer); |
| printer.close('trace'); |
| } |
| |
| void dumpSubtraces(StatsPrinter printer) { |
| if (!subtraces.isEmpty) { |
| subtraces.sort(); |
| for (_StackTraceNode step in subtraces) { |
| step.dumpTraces(printer); |
| } |
| } |
| } |
| |
| int compareTo(_StackTraceNode other) { |
| // Sorts in decreasing count order. |
| return other.count - count; |
| } |
| |
| void printOn(StringBuffer sb, String indentation) { |
| String countText = '$indentation$count '; |
| sb.write(countText); |
| sb.write('\n'); |
| indentation = ''.padLeft(countText.length, ' '); |
| if (commonPrefix != null) { |
| int index = 0; |
| for (StackTraceLine line in commonPrefix) { |
| sb.write(indentation); |
| if (index > 1) { |
| sb.write('...\n'); |
| break; |
| } |
| sb.write(line); |
| sb.write('\n'); |
| index++; |
| } |
| } |
| subtraces.sort(); |
| for (_StackTraceNode subtrace in subtraces) { |
| subtrace.printOn(sb, indentation); |
| } |
| } |
| |
| String toString() { |
| StringBuffer sb = new StringBuffer(); |
| printOn(sb, ''); |
| return sb.toString(); |
| } |
| } |
| |
| class _StackTraceTree extends _StackTraceNode { |
| final id; |
| int totalCount = 0; |
| final int sampleFrequency; |
| |
| _StackTraceTree(this.id, this.sampleFrequency) : super.root(); |
| |
| void dumpTraces(StatsPrinter printer) { |
| printer.open('trace', { |
| 'id': id, |
| 'totalCount': totalCount, |
| 'sampleFrequency': sampleFrequency |
| }); |
| dumpSubtraces(printer); |
| printer.close('trace'); |
| } |
| |
| void sample() { |
| if (totalCount++ % sampleFrequency == 0) { |
| add(stackTrace(offset: 3)); |
| } |
| } |
| } |
| |
| /// 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>>{}; |
| Map<dynamic, _StackTraceTree> traceMap = {}; |
| int stackTraceSampleFrequency = 1; |
| |
| 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<dynamic, List> map = frequencyMaps.putIfAbsent(id, () => {}); |
| map.putIfAbsent(value, () => []); |
| map[value].add(example); |
| } |
| |
| void recordFrequencies(id, Map<dynamic, Iterable> frequencyMap) { |
| Map<dynamic, 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; |
| } |
| |
| void recordTrace(key, {int sampleFrequency}) { |
| if (sampleFrequency == null) { |
| sampleFrequency = stackTraceSampleFrequency; |
| } |
| traceMap |
| .putIfAbsent(key, () => new _StackTraceTree(key, sampleFrequency)) |
| .sample(); |
| } |
| |
| 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(); |
| dumpTraces(); |
| 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 dumpTraces() { |
| printer.group('traces', () { |
| traceMap.keys.forEach(dumpTrace); |
| }); |
| } |
| |
| void dumpTrace(key) { |
| _StackTraceTree tree = traceMap[key]; |
| tree.dumpTraces(printer); |
| } |
| |
| 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<String, dynamic> 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<V, Set<K>> inverseMap<K, V>(Map<K, V> map, |
| {bool equals(V key1, V key2), |
| int hashCode(V key), |
| bool isValidKey(V potentialKey)}) { |
| Map<V, Set<K>> result = new LinkedHashMap<V, Set<K>>( |
| 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<K, V> trySortMap<K, V>(Map<K, V> map) { |
| Iterable<K> iterable = map.keys.where((K k) => k != null); |
| if (iterable.isEmpty) return map; |
| var key = iterable.first; |
| if (key is Comparable<K>) { |
| 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<K, V> sortMap<K, V>(Map<K, V> map, [int compare(K a, K b)]) { |
| List<K> keys = map.keys.toList(); |
| keys.sort(compare); |
| Map<K, V> sortedMap = new Map<K, V>(); |
| keys.forEach((K k) => sortedMap[k] = map[k]); |
| return sortedMap; |
| } |