| // 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. |
| late EntryGroup selectedGroup; |
| |
| /// The entries in the selected group. |
| late 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; |
| |
| /// The number of digits in the event stamps that are the same for every |
| /// entry. |
| late 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]. |
| @override |
| 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; |
| var clickHandler = 'clearHighlight()'; |
| var icon = ''; |
| var description = entry.kindName; |
| if (entry is RequestEntry) { |
| var entryId = entry.id; |
| id = 'req$entryId'; |
| clickHandler = 'highlight(\'req$entryId\', \'res$entryId\')'; |
| icon = '→'; |
| description = entry.method; |
| } else if (entry is ResponseEntry) { |
| var entryId = entry.id; |
| var request = log.requestFor(entry); |
| id = 'res$entryId'; |
| clickHandler = 'highlight(\'req$entryId\', \'res$entryId\')'; |
| icon = '←'; |
| if (request != null) { |
| var latency = entry.timeStamp - request.timeStamp; |
| description = |
| '${request.method} <span class="gray">($latency ms)</span>'; |
| } |
| } else if (entry is NotificationEntry) { |
| id = 'e${entry.index}'; |
| var pairedEntry = log.pairedEntry(entry); |
| if (pairedEntry != null) { |
| var pairedId = 'e${pairedEntry.index}'; |
| clickHandler = 'highlight(\'$id\', \'$pairedId\')'; |
| } |
| icon = '←'; |
| description = entry.event; |
| if (entry.isServerStatus) { |
| var analysisStatus = entry.param('analysis'); |
| if (analysisStatus is Map) { |
| if (analysisStatus['isAnalyzing']) { |
| description = '$description <span class="gray">(analyzing)</span>'; |
| } else { |
| var 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 { |
| var duration = _getDuration(pairedEntry, entry); |
| description = |
| '$description <span class="gray">(pub - $duration ms)</span>'; |
| } |
| } |
| } |
| } else if (entry is PluginRequestEntry) { |
| var entryId = entry.id; |
| var pluginId = getPluginId(entry.pluginId); |
| id = 'req$pluginId.$entryId'; |
| clickHandler = |
| 'highlight(\'req$pluginId.$entryId\', \'res$pluginId.$entryId\')'; |
| icon = '→'; |
| description = '${entry.method} (${entry.shortPluginId})'; |
| } else if (entry is PluginResponseEntry) { |
| var entryId = entry.id; |
| var pluginId = getPluginId(entry.pluginId); |
| var request = log.pluginRequestFor(entry); |
| id = 'res$pluginId.$entryId'; |
| clickHandler = |
| 'highlight(\'req$pluginId.$entryId\', \'res$pluginId.$entryId\')'; |
| icon = '←'; |
| if (request != null) { |
| var 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}'; |
| var pairedEntry = log.pairedEntry(entry); |
| if (pairedEntry != null) { |
| var pairedId = 'e${pairedEntry.index}'; |
| clickHandler = 'highlight(\'$id\', \'$pairedId\')'; |
| } |
| icon = '←'; |
| 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())}\')'; |
| var 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) { |
| var length = entries.length; |
| final pageLength = this.pageLength; |
| var 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 (var 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><</b></button>'); |
| } else { |
| sink.write('<button type="button">'); |
| sink.write( |
| '<a href="${WebServer.logPath}?group=${selectedGroup.id}&start=${pageStart - pageLength!}">'); |
| sink.write('<b><</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>></b></button>'); |
| } else { |
| sink.write('<button type="button">'); |
| sink.write( |
| '<a href="${WebServer.logPath}?group=${selectedGroup.id}&start=${pageStart + pageLength!}">'); |
| sink.write('<b>></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 (var i = pageStart; i < pageEnd; i++) { |
| var 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>'); |
| } |
| } |