| // Copyright (c) 2016, 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 '../log/log.dart'; |
| import 'page_writer.dart'; |
| |
| /// A page writer that will produce the page containing statistics about an |
| /// instrumentation log. |
| class StatsPage extends PageWriter { |
| /// The instrumentation log to be written. |
| final InstrumentationLog log; |
| |
| /// A table mapping the kinds of entries in the log to the number of each |
| /// kind. |
| final Map<String, int> entryCounts = <String, int>{}; |
| |
| /// The number of responses that returned an error. |
| int errorCount = 0; |
| |
| /// The number of responses from each plugin that returned an error. |
| Map<String, int> pluginErrorCount = <String, int>{}; |
| |
| /// A table mapping request method names to a list of the latencies associated |
| /// with those requests, where the latency is defined to be the time between |
| /// when the request was sent by the client and when the server started |
| /// processing the request. |
| final Map<String, List<int>> latencyData = <String, List<int>>{}; |
| |
| /// A table mapping request method names to a list of the latencies associated |
| /// with those requests, where the latency is defined to be the time between |
| /// when the request was sent by the server and when the plugin sent a |
| /// response. |
| final Map<String, Map<String, List<int>>> pluginResponseData = |
| <String, Map<String, List<int>>>{}; |
| |
| /// A list of the number of milliseconds between a completion request and the |
| /// first event for that request. |
| final List<int> completionResponseTimes = <int>[]; |
| |
| /// Initialize a newly created page writer to write information about the |
| /// given instrumentation [log]. |
| StatsPage(this.log) { |
| _processEntries(log.logEntries); |
| } |
| |
| @override |
| void writeBody(StringSink sink) { |
| writeMenu(sink); |
| writeTwoColumns( |
| sink, 'leftColumn', _writeLeftColumn, 'rightColumn', _writeRightColumn); |
| } |
| |
| /// Write the content of the style sheet (without the 'script' tag) for the |
| /// page to the given [sink]. |
| @override |
| void writeStyleSheet(StringSink sink) { |
| super.writeStyleSheet(sink); |
| writeTwoColumnStyles(sink, 'leftColumn', 'rightColumn'); |
| } |
| |
| /// Return the mean of the values in the given list of [values]. |
| int _mean(List<int> values) { |
| var sum = values.fold(0, (int sum, int latency) => sum + latency); |
| return sum ~/ values.length; |
| } |
| |
| /// Return a table mapping the kinds of the given [entries] to the number of |
| /// each kind. |
| void _processEntries(List<LogEntry> entries) { |
| void increment<K>(Map<K, int> map, K key) { |
| map[key] = (map[key] ?? 0) + 1; |
| } |
| |
| for (var entry in entries) { |
| var kind = entry.kind; |
| increment(entryCounts, kind); |
| if (entry is ResponseEntry) { |
| if (entry.result('error') != null) { |
| errorCount++; |
| } |
| } else if (entry is RequestEntry) { |
| var method = entry.method; |
| var latency = entry.timeStamp - entry.clientRequestTime; |
| latencyData.putIfAbsent(method, () => <int>[]).add(latency); |
| if (method == 'completion.getSuggestions') { |
| var response = log.responseFor(entry); |
| if (response != null) { |
| var id = response.result('id'); |
| if (id is String) { |
| var events = log.completionEventsWithId(id); |
| if (events != null && events.isNotEmpty) { |
| completionResponseTimes |
| .add(events[0].timeStamp - entry.timeStamp); |
| } |
| } |
| } |
| } |
| } else if (entry is PluginResponseEntry) { |
| if (entry.result('error') != null) { |
| var count = pluginErrorCount[entry.pluginId] ?? 0; |
| pluginErrorCount[entry.pluginId] = count + 1; |
| } |
| } else if (entry is PluginRequestEntry) { |
| var response = log.pluginResponseFor(entry)!; |
| var responseTime = response.timeStamp - entry.timeStamp; |
| var pluginData = pluginResponseData.putIfAbsent( |
| entry.pluginId, () => <String, List<int>>{}); |
| pluginData.putIfAbsent(entry.method, () => <int>[]).add(responseTime); |
| } |
| } |
| } |
| |
| void _writeLeftColumn(StringSink sink) { |
| var filePaths = log.logFilePaths; |
| var entries = log.logEntries; |
| var startDate = entries[0].toTime; |
| var endDate = entries[entries.length - 1].toTime; |
| var duration = endDate.difference(startDate); |
| var entryKinds = entryCounts.keys.toList()..sort(); |
| |
| sink.writeln('<h3>General</h3>'); |
| sink.writeln('<p>'); |
| if (filePaths.length == 1) { |
| sink.write('<span class="label">Log file:</span> '); |
| sink.write(filePaths[0]); |
| } else { |
| sink.write('<span class="label">Log files:</span> '); |
| var needsSeparator = false; |
| for (var path in filePaths) { |
| if (needsSeparator) { |
| sink.write(', '); |
| } else { |
| needsSeparator = true; |
| } |
| sink.write(path); |
| } |
| } |
| sink.writeln('<br>'); |
| sink.write('<span class="label">Start time:</span> '); |
| writeDate(sink, startDate); |
| sink.writeln('<br>'); |
| sink.write('<span class="label">End time:</span> '); |
| writeDate(sink, endDate); |
| sink.writeln('<br>'); |
| sink.write('<span class="label">Duration:</span> '); |
| sink.write(duration.toString()); |
| sink.writeln('</p>'); |
| |
| sink.writeln('<h3>Entries</h3>'); |
| sink.write('<p>'); |
| sink.write('<span class="label">Number of entries:</span> '); |
| sink.write(entries.length); |
| sink.writeln('</p>'); |
| sink.write('<p>'); |
| sink.write('<span class="label">Error count:</span> '); |
| sink.write(errorCount); |
| sink.writeln('</p>'); |
| pluginErrorCount.forEach((String pluginId, int count) { |
| sink.write('<p>'); |
| sink.write('<span class="label">Errors from $pluginId:</span> '); |
| sink.write(count); |
| sink.writeln('</p>'); |
| }); |
| sink.writeln('<table>'); |
| sink.writeln('<tr><th>count</th><th>kind</th></tr>'); |
| for (var kind in entryKinds) { |
| sink.write('<tr><td class="int">'); |
| sink.write(entryCounts[kind]); |
| sink.write('</td><td>'); |
| sink.write(kind); |
| sink.writeln('</td></tr>'); |
| } |
| sink.write('<tr><td class="int">'); |
| sink.write(entries.length); |
| sink.writeln('</td><td>Total</td></tr>'); |
| sink.writeln('</table>'); |
| } |
| |
| void _writeRightColumn(StringSink sink) { |
| completionResponseTimes.sort(); |
| |
| sink.writeln('<h3>Latency</h3>'); |
| sink.write('<p>'); |
| sink.write('<span class="label">Latency by method</span>'); |
| sink.writeln('</p>'); |
| sink.writeln('<table>'); |
| sink.writeln( |
| '<tr><th>min</th><th>mean</th><th>max</th><th>method</th></tr>'); |
| var methodNames = latencyData.keys.toList()..sort(); |
| for (var method in methodNames) { |
| var latencies = latencyData[method]!..sort(); |
| // TODO(brianwilkerson) Add a spark-line distribution graph. |
| sink.write('<tr><td class="int">'); |
| sink.write(latencies[0]); |
| sink.write('</td><td class="int">'); |
| sink.write(_mean(latencies)); |
| sink.write('</td><td class="int">'); |
| sink.write(latencies[latencies.length - 1]); |
| sink.write('</td><td>'); |
| sink.write(method); |
| sink.writeln('</td></tr>'); |
| } |
| sink.writeln('</table>'); |
| |
| sink.writeln('<h3>Completion</h3>'); |
| sink.write('<p>'); |
| sink.write('<span class="label">Time to first notification:</span> '); |
| sink.write(completionResponseTimes[0]); |
| sink.write(', '); |
| sink.write(_mean(completionResponseTimes)); |
| sink.write(', '); |
| sink.write(completionResponseTimes[completionResponseTimes.length - 1]); |
| sink.writeln('</p>'); |
| |
| if (pluginResponseData.isNotEmpty) { |
| sink.writeln('<h3>Plugin response times</h3>'); |
| pluginResponseData |
| .forEach((String pluginId, Map<String, List<int>> responseData) { |
| sink.write('<p>'); |
| sink.write(pluginId); |
| sink.writeln('</p>'); |
| sink.writeln('<table>'); |
| var methodNames = responseData.keys.toList()..sort(); |
| for (var method in methodNames) { |
| var responseTimes = responseData[method]!..sort(); |
| // TODO(brianwilkerson) Add a spark-line distribution graph. |
| sink.write('<tr><td class="int">'); |
| sink.write(responseTimes[0]); |
| sink.write('</td><td class="int">'); |
| sink.write(_mean(responseTimes)); |
| sink.write('</td><td class="int">'); |
| sink.write(responseTimes[responseTimes.length - 1]); |
| sink.write('</td><td>'); |
| sink.write(method); |
| sink.writeln('</td></tr>'); |
| } |
| sink.writeln('</table>'); |
| }); |
| } |
| } |
| } |