Skip to content

Commit 9ee4414

Browse files
DanTupCommit Queue
authored andcommitted
[analysis_server] Add a diagnostic page to stream the analysis PerformanceLog
This adds a new page to the analysis server diagnostics that allow you to start/stop streaming the analyzer `PerformanceLog` to the client. This can be useful to capture a slice of this traffic (for example while typing in a file where performance feels bad) without having to enable the on-disk log which can grow very large and hard to match up with specific points in time. Change-Id: I6bf2b891481191929593beaef813c02df5d3a0c2 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/403420 Reviewed-by: Konstantin Shcheglov <[email protected]> Commit-Queue: Brian Wilkerson <[email protected]> Reviewed-by: Brian Wilkerson <[email protected]> Reviewed-by: Phil Quitslund <[email protected]>
1 parent fe6f87f commit 9ee4414

File tree

7 files changed

+280
-39
lines changed

7 files changed

+280
-39
lines changed

pkg/analysis_server/lib/src/analysis_server.dart

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ import 'package:analysis_server/src/services/user_prompts/dart_fix_prompt_manage
4545
import 'package:analysis_server/src/services/user_prompts/survey_manager.dart';
4646
import 'package:analysis_server/src/services/user_prompts/user_prompts.dart';
4747
import 'package:analysis_server/src/utilities/file_string_sink.dart';
48-
import 'package:analysis_server/src/utilities/null_string_sink.dart';
4948
import 'package:analysis_server/src/utilities/process.dart';
5049
import 'package:analysis_server/src/utilities/request_statistics.dart';
5150
import 'package:analysis_server/src/utilities/tee_string_sink.dart';
@@ -204,6 +203,9 @@ abstract class AnalysisServer {
204203

205204
final RequestStatisticsHelper? requestStatistics;
206205

206+
final PerformanceLog<TeeStringSink> analysisPerformanceLogger =
207+
PerformanceLog<TeeStringSink>(TeeStringSink());
208+
207209
/// Manages prompts telling the user about "dart fix".
208210
late final DartFixPromptManager _dartFixPrompt;
209211

@@ -338,21 +340,21 @@ abstract class AnalysisServer {
338340
}
339341

340342
var logName = options.newAnalysisDriverLog;
341-
StringSink sink = NullStringSink();
342343
if (logName != null) {
343344
if (logName == 'stdout') {
344-
sink = io.stdout;
345+
analysisPerformanceLogger.sink.addSink(io.stdout);
345346
} else if (logName.startsWith('file:')) {
346347
var path = logName.substring('file:'.length);
347-
sink = FileStringSink(path);
348+
analysisPerformanceLogger.sink.addSink(FileStringSink(path));
348349
}
349350
}
350351

351352
var requestStatistics = this.requestStatistics;
352353
if (requestStatistics != null) {
353-
sink = TeeStringSink(sink, requestStatistics.perfLoggerStringSink);
354+
analysisPerformanceLogger.sink.addSink(
355+
requestStatistics.perfLoggerStringSink,
356+
);
354357
}
355-
var analysisPerformanceLogger = PerformanceLog(sink);
356358

357359
byteStore = createByteStore(resourceProvider);
358360
fileContentCache = FileContentCache(resourceProvider);

pkg/analysis_server/lib/src/server/http_server.dart

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ abstract class AbstractHttpHandler {
1515

1616
/// Handle a POST request received by the HTTP server.
1717
void handlePostRequest(HttpRequest request);
18+
19+
/// Handle a request to upgrade to a WebSocket.
20+
void handleWebSocketRequest(HttpRequest request);
1821
}
1922

2023
/// Instances of the class [HttpServer] implement a simple HTTP server. The
@@ -93,22 +96,16 @@ class HttpAnalysisServer {
9396

9497
/// Attach a listener to a newly created HTTP server.
9598
void _handleServer(HttpServer httpServer) {
96-
httpServer.listen((HttpRequest request) {
97-
var updateValues = request.headers[HttpHeaders.upgradeHeader];
98-
if (request.method == 'GET') {
99+
httpServer.listen((HttpRequest request) async {
100+
if (WebSocketTransformer.isUpgradeRequest(request) &&
101+
// For WebSockets, verify we're same origin (since the browser would
102+
// not).
103+
request.headers.value('origin') == request.requestedUri.origin) {
104+
_httpHandler.handleWebSocketRequest(request);
105+
} else if (request.method == 'GET') {
99106
_httpHandler.handleGetRequest(request);
100107
} else if (request.method == 'POST') {
101108
_httpHandler.handlePostRequest(request);
102-
} else if (updateValues != null && updateValues.contains('websocket')) {
103-
// We no longer support serving analysis server communications over
104-
// WebSocket connections.
105-
var response = request.response;
106-
response.statusCode = HttpStatus.notFound;
107-
response.headers.contentType = ContentType.text;
108-
response.write(
109-
'WebSocket connections not supported (${request.uri.path}).',
110-
);
111-
unawaited(response.close());
112109
} else {
113110
_returnUnknownRequest(request);
114111
}

pkg/analysis_server/lib/src/status/diagnostics.dart

Lines changed: 159 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5+
import 'dart:async';
56
import 'dart:convert' show JsonEncoder;
67
import 'dart:developer' as developer;
78
import 'dart:io';
@@ -22,6 +23,7 @@ import 'package:analysis_server/src/status/ast_writer.dart';
2223
import 'package:analysis_server/src/status/element_writer.dart';
2324
import 'package:analysis_server/src/status/pages.dart';
2425
import 'package:analysis_server/src/utilities/profiling.dart';
26+
import 'package:analysis_server/src/utilities/stream_string_stink.dart';
2527
import 'package:analyzer/dart/analysis/context_root.dart';
2628
import 'package:analyzer/dart/analysis/results.dart';
2729
import 'package:analyzer/src/context/source.dart';
@@ -173,7 +175,6 @@ _CollectedOptionsData _collectOptionsData(AnalysisDriver driver) {
173175
}
174176

175177
class AnalysisDriverTimingsPage extends DiagnosticPageWithNav
176-
with PerformanceChartMixin
177178
implements PostablePage {
178179
static const _resetFormId = 'reset-driver-timers';
179180

@@ -217,6 +218,47 @@ class AnalysisDriverTimingsPage extends DiagnosticPageWithNav
217218
}
218219
}
219220

221+
class AnalysisPerformanceLogPage extends WebSocketLoggingPage {
222+
AnalysisPerformanceLogPage(DiagnosticsSite site)
223+
: super(
224+
site,
225+
'analysis-performance-log',
226+
'Analysis Performance Log',
227+
description: 'Realtime logging from the Analysis Performance Log',
228+
);
229+
230+
@override
231+
Future<void> generateContent(Map<String, String> params) async {
232+
_writeWebSocketLogPanel();
233+
}
234+
235+
@override
236+
Future<void> handleWebSocket(WebSocket socket) async {
237+
var logger = server.analysisPerformanceLogger;
238+
239+
// We were able to attach our temporary sink. Forward all data over the
240+
// WebSocket and wait for it to close (this is done by the user clicking
241+
// the Stop button or navigating away from the page).
242+
var controller = StreamController<String>();
243+
var sink = StreamStringSink(controller.sink);
244+
try {
245+
unawaited(socket.addStream(controller.stream));
246+
logger.sink.addSink(sink);
247+
248+
// Wait for the socket to be closed so we can remove the secondary sink.
249+
var completer = Completer<void>();
250+
socket.listen(
251+
null,
252+
onDone: completer.complete,
253+
onError: completer.complete,
254+
);
255+
await completer.future;
256+
} finally {
257+
logger.sink.removeSink(sink);
258+
}
259+
}
260+
}
261+
220262
class AnalyticsPage extends DiagnosticPageWithNav {
221263
AnalyticsPage(DiagnosticsSite site)
222264
: super(
@@ -1280,6 +1322,7 @@ class DiagnosticsSite extends Site implements AbstractHttpHandler {
12801322
pages.add(TimingPage(this));
12811323
pages.add(ByteStoreTimingPage(this));
12821324
pages.add(AnalysisDriverTimingsPage(this));
1325+
pages.add(AnalysisPerformanceLogPage(this));
12831326

12841327
var profiler = ProcessProfiler.getProfilerForPlatform();
12851328
if (profiler != null) {
@@ -1982,6 +2025,121 @@ class TimingPage extends DiagnosticPageWithNav with PerformanceChartMixin {
19822025
}
19832026
}
19842027

2028+
/// A base class for pages that provide real-time logging over a WebSocket.
2029+
abstract class WebSocketLoggingPage extends DiagnosticPageWithNav
2030+
implements WebSocketPage {
2031+
WebSocketLoggingPage(super.site, super.id, super.title, {super.description});
2032+
2033+
void button(String text, {String? id, String classes = '', String? onClick}) {
2034+
var attributes = {
2035+
'type': 'button',
2036+
if (id != null) 'id': id,
2037+
'class': 'btn $classes'.trim(),
2038+
if (onClick != null) 'onclick': onClick,
2039+
'value': text,
2040+
};
2041+
2042+
tag('input', attributes: attributes);
2043+
}
2044+
2045+
/// Writes an HTML tag for [tagName] with the given [attributes].
2046+
///
2047+
/// If [gen] is supplied, it is executed to write child content to [buf].
2048+
void tag(
2049+
String tagName, {
2050+
Map<String, String>? attributes,
2051+
void Function()? gen,
2052+
}) {
2053+
buf.write('<$tagName');
2054+
if (attributes != null) {
2055+
for (var MapEntry(:key, :value) in attributes.entries) {
2056+
buf.write(' $key="${escape(value)}"');
2057+
}
2058+
}
2059+
buf.write('>');
2060+
gen?.call();
2061+
buf.writeln('</$tagName>');
2062+
}
2063+
2064+
/// Writes Start/Stop/Clear buttons and associated scripts to connect and
2065+
/// disconnect a websocket back to this page, along with a panel to show
2066+
/// any output received from the server over the WebSocket.
2067+
void _writeWebSocketLogPanel() {
2068+
// Add buttons to start/stop logging. Using "position: sticky" so they're
2069+
// always visible even when scrolled.
2070+
tag(
2071+
'div',
2072+
attributes: {
2073+
'style':
2074+
'position: sticky; top: 10px; text-align: right; margin-bottom: 20px;',
2075+
},
2076+
gen: () {
2077+
button(
2078+
'Start Logging',
2079+
id: 'btnStartLog',
2080+
classes: 'btn-danger',
2081+
onClick: 'startLogging()',
2082+
);
2083+
button(
2084+
'Stop Logging',
2085+
id: 'btnStopLog',
2086+
classes: 'btn-danger',
2087+
onClick: 'stopLogging()',
2088+
);
2089+
button('Clear', onClick: 'clearLog()');
2090+
},
2091+
);
2092+
2093+
// Write the log container.
2094+
pre(() {
2095+
tag('code', attributes: {'id': 'logContent'});
2096+
});
2097+
2098+
// Write the scripts to connect/disconnect the websocket and display the
2099+
// data.
2100+
buf.write('''
2101+
<script>
2102+
let logContent = document.getElementById('logContent');
2103+
let btnEnable = document.getElementById('btnEnable');
2104+
let btnDisable = document.getElementById('btnDisable');
2105+
let socket;
2106+
2107+
function clearLog(data) {
2108+
logContent.textContent = '';
2109+
}
2110+
2111+
function append(data) {
2112+
logContent.appendChild(document.createTextNode(data));
2113+
}
2114+
2115+
function startLogging() {
2116+
append("Connecting...\\n");
2117+
socket = new WebSocket("${this.path}");
2118+
socket.addEventListener("open", (event) => {
2119+
append("Connected!\\n");
2120+
});
2121+
socket.addEventListener("close", (event) => {
2122+
append("Disconnected!\\n");
2123+
stopLogging();
2124+
});
2125+
socket.addEventListener("message", (event) => {
2126+
append(event.data);
2127+
});
2128+
btnEnable.disabled = true;
2129+
btnDisable.disabled = false;
2130+
}
2131+
2132+
function stopLogging() {
2133+
socket?.close(1000, 'User closed');
2134+
socket = undefined;
2135+
btnEnable.disabled = false;
2136+
btnDisable.disabled = true;
2137+
}
2138+
</script>
2139+
''');
2140+
}
2141+
}
2142+
19852143
class _CollectedOptionsData {
19862144
final Set<String> lints = <String>{};
19872145
final Set<String> plugins = <String>{};

pkg/analysis_server/lib/src/status/pages.dart

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,24 @@ abstract class Site {
246246
});
247247
}
248248

249+
Future<void> handleWebSocketRequest(HttpRequest request) async {
250+
var path = request.uri.path;
251+
252+
await _tryHandleRequest(request, (response, queryParameters) async {
253+
var page = _getPage(path);
254+
if (page == null) {
255+
await respond(request, createUnknownPage(path), HttpStatus.notFound);
256+
return;
257+
} else if (page is WebSocketPage) {
258+
var webSocket = await WebSocketTransformer.upgrade(request);
259+
await (page as WebSocketPage).handleWebSocket(webSocket);
260+
await webSocket.done;
261+
} else {
262+
throw 'Method not supported';
263+
}
264+
});
265+
}
266+
249267
Future<void> respond(
250268
HttpRequest request,
251269
Page page, [
@@ -316,11 +334,22 @@ abstract class Site {
316334
);
317335
} catch (e, st) {
318336
var response = request.response;
319-
response.statusCode = HttpStatus.internalServerError;
320-
response.headers.contentType = ContentType.text;
321-
response.write('$e\n\n$st');
337+
try {
338+
response.statusCode = HttpStatus.internalServerError;
339+
response.headers.contentType = ContentType.text;
340+
response.write('$e\n\n$st');
341+
} catch (_) {
342+
// We may fail to send the above if the request that errored had
343+
// already caused the HTTP headers to be flushed (this can happen
344+
// after a WebSocket upgrade, for example).
345+
}
322346
unawaited(response.close());
323347
}
324348
}
325349
}
326350
}
351+
352+
abstract interface class WebSocketPage {
353+
/// Handles a WebSocket connection to the page.
354+
Future<void> handleWebSocket(WebSocket socket);
355+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:async';
6+
7+
/// A [StringSink] that writes into a StreamSink<String>.
8+
class StreamStringSink implements StringSink {
9+
final StreamSink<String> _sink;
10+
11+
StreamStringSink(this._sink);
12+
13+
@override
14+
void write(Object? obj) {
15+
_sink.add('$obj');
16+
}
17+
18+
@override
19+
void writeAll(Iterable<dynamic> objects, [String separator = '']) {
20+
_sink.add(objects.join(separator));
21+
}
22+
23+
@override
24+
void writeCharCode(int charCode) {
25+
_sink.add(String.fromCharCode(charCode));
26+
}
27+
28+
@override
29+
void writeln([Object? obj = '']) {
30+
_sink.add('$obj\n');
31+
}
32+
}

0 commit comments

Comments
 (0)