blob: 907f16c2d28274fe9ad74f9688cc17082730dc97 [file] [log] [blame]
// 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].
*/
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) {
int 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 (LogEntry entry in entries) {
String kind = entry.kind;
increment(entryCounts, kind);
if (entry is ResponseEntry) {
if (entry.result('error') != null) {
errorCount++;
}
} else if (entry is RequestEntry) {
String method = entry.method;
int latency = entry.timeStamp - entry.clientRequestTime;
latencyData.putIfAbsent(method, () => new List<int>()).add(latency);
if (method == 'completion.getSuggestions') {
ResponseEntry response = log.responseFor(entry);
if (response != null) {
String id = response.result('id');
if (id != null) {
List<NotificationEntry> events = log.completionEventsWithId(id);
if (events != null && events.length > 0) {
completionResponseTimes
.add(events[0].timeStamp - entry.timeStamp);
}
}
}
}
} else if (entry is PluginResponseEntry) {
if (entry.result('error') != null) {
int count = pluginErrorCount[entry.pluginId] ?? 0;
pluginErrorCount[entry.pluginId] = count + 1;
}
} else if (entry is PluginRequestEntry) {
PluginResponseEntry response = log.pluginResponseFor(entry);
int responseTime = response.timeStamp - entry.timeStamp;
var pluginData = pluginResponseData.putIfAbsent(
entry.pluginId, () => <String, List<int>>{});
pluginData
.putIfAbsent(entry.method, () => new List<int>())
.add(responseTime);
}
}
}
void _writeLeftColumn(StringSink sink) {
List<String> filePaths = log.logFilePaths;
List<LogEntry> entries = log.logEntries;
DateTime startDate = entries[0].toTime;
DateTime endDate = entries[entries.length - 1].toTime;
Duration duration = endDate.difference(startDate);
List<String> 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> ');
bool needsSeparator = false;
for (String 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 (String 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>');
List<String> methodNames = latencyData.keys.toList()..sort();
for (String method in methodNames) {
List<int> 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>');
List<String> methodNames = responseData.keys.toList()..sort();
for (String method in methodNames) {
List<int> 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>');
});
}
}
}