blob: bb9b9c9e93a4c1c722ca12cb4b30f8be64fdcd47 [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 'dart:math' as math;
import '../log/log.dart';
import '../server.dart';
import 'page_writer.dart';
/**
* A page writer that will produce the page containing access to the full
* content of the log.
*/
class LogPage extends PageWriter {
/**
* The instrumentation log to be written.
*/
InstrumentationLog log;
/**
* The id of the entry groups to be displayed.
*/
EntryGroup selectedGroup;
/**
* The entries in the selected group.
*/
List<LogEntry> entries;
/**
* The index of the first entry to be written.
*/
int pageStart = 0;
/**
* The number of entries to be written, or `null` if all of the entries should
* be written.
*/
int pageLength = null;
/**
* The number of digits in the event stamps that are the same for every entry.
*/
int prefixLength;
/**
* A table mapping the ids of plugins to an index for the plugin.
*/
Map<String, int> pluginIdMap = <String, int>{};
/**
* Initialize a newly created writer to write the content of the given
* [instrumentationLog].
*/
LogPage(this.log);
/**
* Return the encoding for the given [pluginId] that is used to build anchors.
*/
int getPluginId(String pluginId) {
return pluginIdMap.putIfAbsent(pluginId, () => pluginIdMap.length);
}
@override
void writeBody(StringSink sink) {
entries = log.entriesInGroup(selectedGroup);
prefixLength = computePrefixLength(entries);
writeMenu(sink);
writeTwoColumns(
sink, 'leftColumn', _writeLeftColumn, 'rightColumn', _writeRightColumn);
}
@override
void writeScripts(StringSink sink) {
super.writeScripts(sink);
sink.writeln(r'''
var highlightedRows = [];
function clearHighlight() {
for (i = 0; i < highlightedRows.length; i++) {
setFontWeight(highlightedRows[i], "normal");
}
}
function highlight(requestId, responseId) {
clearHighlight();
setFontWeight(requestId, "bold");
setFontWeight(responseId, "bold");
highlightedRows = [requestId, responseId];
}
function setFontWeight(id, weight) {
var element = document.getElementById(id);
if (element != null) {
element.style.fontWeight = weight;
}
}
function setDetails(detailsContent) {
var element = document.getElementById("details");
if (element != null) {
element.innerHTML = detailsContent;
}
}
function selectEntryGroup(pageStart) {
var element = document.getElementById("entryGroup");
var url = "/log?group=" + element.value;
window.location.assign(url);
}
''');
}
/**
* 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 number of milliseconds elapsed between the [startEntry] and the
* [endEntry], or a question .
*/
String _getDuration(LogEntry startEntry, LogEntry endEntry) {
if (startEntry != null && endEntry != null) {
return (endEntry.timeStamp - startEntry.timeStamp).toString();
}
return '?';
}
/**
* Write the given log [entry] to the given [sink].
*/
void _writeEntry(StringSink sink, LogEntry entry) {
String id = null;
String clickHandler = 'clearHighlight()';
String icon = '';
String description = entry.kindName;
if (entry is RequestEntry) {
String entryId = entry.id;
id = 'req$entryId';
clickHandler = 'highlight(\'req$entryId\', \'res$entryId\')';
icon = '&rarr;';
description = entry.method;
} else if (entry is ResponseEntry) {
String entryId = entry.id;
RequestEntry request = log.requestFor(entry);
id = 'res$entryId';
clickHandler = 'highlight(\'req$entryId\', \'res$entryId\')';
icon = '&larr;';
if (request != null) {
int latency = entry.timeStamp - request.timeStamp;
description =
'${request.method} <span class="gray">($latency ms)</span>';
}
} else if (entry is NotificationEntry) {
id = 'e${entry.index}';
LogEntry pairedEntry = log.pairedEntry(entry);
if (pairedEntry != null) {
String pairedId = 'e${pairedEntry.index}';
clickHandler = 'highlight(\'$id\', \'$pairedId\')';
}
icon = '&larr;';
description = entry.event;
if (entry.isServerStatus) {
var analysisStatus = entry.param('analysis');
if (analysisStatus is Map) {
if (analysisStatus['isAnalyzing']) {
description =
'$description <span class="gray">(analysis)</span> (<a href="${WebServer.taskPath}?analysisStart=${entry.index}">tasks</a>)';
} else {
String duration = _getDuration(pairedEntry, entry);
description =
'$description <span class="gray">(analysis - $duration ms)</span>';
}
}
var pubStatus = entry.param('pub');
if (pubStatus is Map) {
if (pubStatus['isListingPackageDirs']) {
description = '$description <span class="gray">(pub)</span>';
} else {
String duration = _getDuration(pairedEntry, entry);
description =
'$description <span class="gray">(pub - $duration ms)</span>';
}
}
}
} else if (entry is PluginRequestEntry) {
String entryId = entry.id;
int pluginId = getPluginId(entry.pluginId);
id = 'req$pluginId.$entryId';
clickHandler =
'highlight(\'req$pluginId.$entryId\', \'res$pluginId.$entryId\')';
icon = '&rarr;';
description = '${entry.method} (${entry.shortPluginId})';
} else if (entry is PluginResponseEntry) {
String entryId = entry.id;
int pluginId = getPluginId(entry.pluginId);
PluginRequestEntry request = log.pluginRequestFor(entry);
id = 'res$pluginId.$entryId';
clickHandler =
'highlight(\'req$pluginId.$entryId\', \'res$pluginId.$entryId\')';
icon = '&larr;';
if (request != null) {
int latency = entry.timeStamp - request.timeStamp;
description =
'${request.method} <span class="gray">($latency ms)</span> (${entry.shortPluginId})';
}
} else if (entry is PluginNotificationEntry) {
id = 'e${entry.index}';
LogEntry pairedEntry = log.pairedEntry(entry);
if (pairedEntry != null) {
String pairedId = 'e${pairedEntry.index}';
clickHandler = 'highlight(\'$id\', \'$pairedId\')';
}
icon = '&larr;';
description = '${entry.event} (${entry.shortPluginId})';
} else if (entry is TaskEntry) {
description = entry.description;
} else if (entry is ErrorEntry) {
description = '<span class="error">$description</span>';
} else if (entry is PluginErrorEntry) {
description =
'<span class="error">$description</span> (${entry.shortPluginId})';
} else if (entry is ExceptionEntry) {
description = '<span class="error">$description</span>';
} else if (entry is MalformedLogEntry) {
description = '<span class="error">$description</span>';
}
id = id == null ? '' : 'id="$id" ';
clickHandler = '$clickHandler; setDetails(\'${escape(entry.details())}\')';
String timeStamp = entry.timeStamp.toString();
if (prefixLength > 0) {
timeStamp = timeStamp.substring(prefixLength);
}
sink.writeln('<tr ${id}onclick="$clickHandler">');
sink.writeln('<td>$icon</td>');
sink.writeln('<td>');
sink.writeln(timeStamp);
sink.writeln('</td>');
sink.writeln('<td style="white-space:nowrap;">');
sink.writeln(description);
sink.writeln('</td>');
sink.writeln('</tr>');
}
/**
* Write the entries in the instrumentation log to the given [sink].
*/
void _writeLeftColumn(StringSink sink) {
int length = entries.length;
int pageEnd =
pageLength == null ? length : math.min(pageStart + pageLength, length);
//
// Write the header of the column.
//
sink.writeln('<div class="columnHeader">');
sink.writeln('<div style="float: left">');
sink.writeln('<select id="entryGroup" onchange="selectEntryGroup()">');
for (EntryGroup group in EntryGroup.groups) {
sink.write('<option value="');
sink.write(group.id);
sink.write('"');
if (group == selectedGroup) {
sink.write(' selected');
}
sink.write('>');
sink.write(group.name);
sink.writeln('</option>');
}
sink.writeln('</select>');
if (length == 0) {
sink.writeln('No matching events');
} else {
sink.writeln('Events $pageStart - ${pageEnd - 1} of $length');
}
sink.writeln('</div>');
sink.writeln('<div style="float: right">');
if (pageStart == 0) {
sink.writeln('<button type="button" disabled><b>&lt;</b></button>');
} else {
sink.write('<button type="button">');
sink.write(
'<a href="${WebServer.logPath}?group=${selectedGroup.id}&start=${pageStart - pageLength}">');
sink.write('<b>&lt;</b>');
sink.writeln('</a></button>');
}
// TODO(brianwilkerson) Add a text field for selecting the start index.
if (pageEnd == length) {
sink.writeln('<button type="button" disabled><b>&gt;</b></button>');
} else {
sink.write('<button type="button">');
sink.write(
'<a href="${WebServer.logPath}?group=${selectedGroup.id}&start=${pageStart + pageLength}">');
sink.write('<b>&gt;</b>');
sink.writeln('</a></button>');
}
sink.writeln('</div>');
sink.writeln('</div>');
//
// Write the main body of the column.
//
sink.writeln('<table class="fullWidth">');
sink.writeln('<tr>');
sink.writeln('<th class="narrow"></th>');
sink.writeln('<th>Time</th>');
sink.writeln('<th>Kind</th>');
sink.writeln('</tr>');
for (int i = pageStart; i < pageEnd; i++) {
LogEntry entry = entries[i];
_writeEntry(sink, entry);
}
sink.writeln('</table>');
}
/**
* Write a placeholder to the given [sink] where the details of a selected
* entry can be displayed.
*/
void _writeRightColumn(StringSink sink) {
//
// Write the header of the column.
//
sink.writeln('<div class="columnHeader">');
sink.writeln('<p><b>Entry Details</b></p>');
sink.writeln('</div>');
sink.writeln('<div id="details"></div>');
}
}