Display message scheduler information on the diagnostic pages

This adds a new page to the diagnostic pages that displays the current
state of the message scheduler (or rather the messages passing through
the scheduler) as well as the most recent history.

The next steps after this are to
- include this information (and library cycle information) in the
  generated report
- add this (and cycle) information to the analytics we're gathering

I put the new page in its own file, and would like to move existing
pages to separate files as well. Doing so might trigger moving some
files around in the directory structure in order to better organize
the code.

Change-Id: Ic4f0e72ffbadc1e39702f1f8f13850f5bd5814a5
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/431947
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
Reviewed-by: Samuel Rawlins <srawlins@google.com>
diff --git a/pkg/analysis_server/lib/src/analysis_server.dart b/pkg/analysis_server/lib/src/analysis_server.dart
index 3c9bded..5a50788 100644
--- a/pkg/analysis_server/lib/src/analysis_server.dart
+++ b/pkg/analysis_server/lib/src/analysis_server.dart
@@ -27,6 +27,7 @@
     show MessageType;
 import 'package:analysis_server/src/protocol_server.dart' as server;
 import 'package:analysis_server/src/scheduler/message_scheduler.dart';
+import 'package:analysis_server/src/scheduler/scheduler_tracking_listener.dart';
 import 'package:analysis_server/src/server/crash_reporting_attachments.dart';
 import 'package:analysis_server/src/server/diagnostic_server.dart';
 import 'package:analysis_server/src/services/completion/completion_performance.dart';
@@ -296,8 +297,12 @@
          httpClient,
          Platform.environment['PUB_HOSTED_URL'],
        ),
-       messageScheduler = MessageScheduler(listener: messageSchedulerListener) {
-    messageScheduler.setServer(this);
+       messageScheduler = MessageScheduler(
+         listener:
+             messageSchedulerListener ??
+             SchedulerTrackingListener(analyticsManager),
+       ) {
+    messageScheduler.server = this;
     // Set the default URI converter. This uses the resource providers path
     // context (unlike the initialized value) which allows tests to override it.
     uriConverter = ClientUriConverter.noop(baseResourceProvider.pathContext);
diff --git a/pkg/analysis_server/lib/src/scheduler/message_scheduler.dart b/pkg/analysis_server/lib/src/scheduler/message_scheduler.dart
index 08091e7..7024f24 100644
--- a/pkg/analysis_server/lib/src/scheduler/message_scheduler.dart
+++ b/pkg/analysis_server/lib/src/scheduler/message_scheduler.dart
@@ -29,10 +29,10 @@
   static bool allowOverlappingHandlers = true;
 
   /// A listener that can be used to watch the scheduler as it manages messages.
-  final MessageSchedulerListener? _listener;
+  final MessageSchedulerListener? listener;
 
   /// The [AnalysisServer] associated with the scheduler.
-  late final AnalysisServer _server;
+  late final AnalysisServer server;
 
   /// The messages that have been received and are waiting to be handled.
   final ListQueue<ScheduledMessage> _pendingMessages =
@@ -50,13 +50,13 @@
 
   /// Initialize a newly created message scheduler.
   ///
-  /// The caller is expected to set the [_server] immediately after creating the
+  /// The caller is expected to set the [server] immediately after creating the
   /// instance. The only reason the server isn't initialized by the constructor
   /// is because the analysis server and the message scheduler can't be created
   /// atomically and it was decided that it was cleaner for the scheduler to
   /// have the nullable reference to the server rather than the other way
   /// around.
-  MessageScheduler({MessageSchedulerListener? listener}) : _listener = listener;
+  MessageScheduler({required this.listener});
 
   /// Add the [message] to the end of the pending messages queue.
   ///
@@ -81,7 +81,7 @@
   /// - The incoming [legacy.ANALYSIS_REQUEST_UPDATE_CONTENT] message cancels
   ///   any rename files request that is in progress.
   void add(ScheduledMessage message) {
-    _listener?.addPendingMessage(message);
+    listener?.addPendingMessage(message);
     if (message is LegacyMessage) {
       var request = message.request;
       var method = request.method;
@@ -89,10 +89,10 @@
         var id =
             legacy.ServerCancelRequestParams.fromRequest(
               request,
-              clientUriConverter: _server.uriConverter,
+              clientUriConverter: server.uriConverter,
             ).id;
-        _listener?.addActiveMessage(message);
-        (_server as LegacyAnalysisServer).cancelRequest(id);
+        listener?.addActiveMessage(message);
+        (server as LegacyAnalysisServer).cancelRequest(id);
         // The message needs to be added to the queue of pending messages, but
         // it seems like it shouldn't be necessary and that we ought to return
         // at this point. However, doing so causes some tests to timeout.
@@ -107,7 +107,7 @@
                   code: lsp.ErrorCodes.ContentModified.toJson(),
                   reason: 'File content was modified',
                 );
-                _listener?.cancelActiveMessage(activeMessage);
+                listener?.cancelActiveMessage(activeMessage);
               }
             }
           }
@@ -121,8 +121,8 @@
         // Responses don't go on the queue because there might be an active
         // message that can't complete until the response is received. If the
         // response was added to the queue then this process could deadlock.
-        _listener?.addActiveMessage(message);
-        (_server as LspAnalysisServer).handleMessage(msg, null);
+        listener?.addActiveMessage(message);
+        (server as LspAnalysisServer).handleMessage(msg, null);
         return;
       } else if (msg is lsp.NotificationMessage) {
         var method = msg.method;
@@ -133,7 +133,7 @@
           // by removing the request from the queue. It's done this way to allow
           // a response to be sent back to the client saying that the results
           // aren't provided because the request was cancelled.
-          _listener?.addActiveMessage(message);
+          listener?.addActiveMessage(message);
           _processCancellation(msg);
           return;
         } else if (method == lsp.Method.textDocument_didChange) {
@@ -155,7 +155,7 @@
               var message = activeMessage.message as lsp.RequestMessage;
               if (message.method == incomingMsgMethod) {
                 activeMessage.cancellationToken?.cancel(reason: reason);
-                _listener?.cancelActiveMessage(activeMessage);
+                listener?.cancelActiveMessage(activeMessage);
               }
             }
           }
@@ -164,7 +164,7 @@
               var message = pendingMessage.message as lsp.RequestMessage;
               if (message.method == msg.method) {
                 pendingMessage.cancellationToken?.cancel(reason: reason);
-                _listener?.cancelPendingMessage(pendingMessage);
+                listener?.cancelPendingMessage(pendingMessage);
               }
             }
           }
@@ -180,12 +180,12 @@
   /// Dispatch the first message in the queue to be executed.
   void processMessages() async {
     _isProcessing = true;
-    _listener?.startProcessingMessages();
+    listener?.startProcessingMessages();
     try {
       while (_pendingMessages.isNotEmpty) {
         var currentMessage = _pendingMessages.removeFirst();
         _activeMessages.addLast(currentMessage);
-        _listener?.addActiveMessage(currentMessage);
+        listener?.addActiveMessage(currentMessage);
         _completer = Completer<void>();
         unawaited(
           _completer.future.then((_) {
@@ -195,27 +195,27 @@
         switch (currentMessage) {
           case LspMessage():
             var lspMessage = currentMessage.message;
-            (_server as LspAnalysisServer).handleMessage(
+            (server as LspAnalysisServer).handleMessage(
               lspMessage,
               cancellationToken: currentMessage.cancellationToken,
               _completer,
             );
           case LegacyMessage():
             var request = currentMessage.request;
-            (_server as LegacyAnalysisServer).handleRequest(
+            (server as LegacyAnalysisServer).handleRequest(
               request,
               _completer,
               currentMessage.cancellationToken,
             );
           case DtdMessage():
-            _server.dtd!.processMessage(
+            server.dtd!.processMessage(
               currentMessage.message,
               currentMessage.performance,
               currentMessage.responseCompleter,
               _completer,
             );
           case WatcherMessage():
-            _server.contextManager.handleWatchEvent(currentMessage.event);
+            server.contextManager.handleWatchEvent(currentMessage.event);
             // Handling a watch event is a synchronous process, so there's
             // nothing to wait for.
             _completer.complete();
@@ -236,24 +236,17 @@
         // TODO(pq): if not awaited, consider adding a `then` so we can track
         // when the future completes. But note that we may see some flakiness in
         // tests as message handling gets non-deterministically interleaved.
-        _listener?.messageCompleted(currentMessage);
+        listener?.messageCompleted(currentMessage);
       }
     } catch (error, stackTrace) {
-      _server.instrumentationService.logException(
+      server.instrumentationService.logException(
         FatalException('Failed to process message', error, stackTrace),
         null,
-        _server.crashReportingAttachmentsBuilder.forException(error),
+        server.crashReportingAttachmentsBuilder.forException(error),
       );
     }
     _isProcessing = false;
-    _listener?.endProcessingMessages();
-  }
-
-  /// Set the [AnalysisServer].
-  ///
-  /// Throws an exception if the server has already been set.
-  void setServer(AnalysisServer analysisServer) {
-    _server = analysisServer;
+    listener?.endProcessingMessages();
   }
 
   /// Returns the parameters of a cancellation [message].
@@ -269,7 +262,7 @@
           ? cancelJsonHandler.convertParams(paramsJson)
           : null;
     } catch (error, stackTrace) {
-      (_server as LspAnalysisServer).logException(
+      (server as LspAnalysisServer).logException(
         'An error occured while parsing cancel parameters',
         error,
         stackTrace,
@@ -309,7 +302,7 @@
   Map<String, Object?>? _getLspOverLegacyParams(legacy.Request request) {
     var params = legacy.LspHandleParams.fromRequest(
       request,
-      clientUriConverter: _server.uriConverter,
+      clientUriConverter: server.uriConverter,
     );
     return params.lspMessage as Map<String, Object?>;
   }
@@ -352,7 +345,7 @@
         var request = activeMessage.message as lsp.RequestMessage;
         if (request.id == params.id) {
           activeMessage.cancellationToken?.cancel();
-          _listener?.cancelActiveMessage(activeMessage);
+          listener?.cancelActiveMessage(activeMessage);
           return;
         }
       }
@@ -362,7 +355,7 @@
         var request = pendingMessage.message as lsp.RequestMessage;
         if (request.id == params.id) {
           pendingMessage.cancellationToken?.cancel();
-          _listener?.cancelPendingMessage(pendingMessage);
+          listener?.cancelPendingMessage(pendingMessage);
           return;
         }
       }
@@ -410,9 +403,9 @@
             code: lsp.ErrorCodes.ContentModified.toJson(),
           );
           if (isActive) {
-            _listener?.cancelActiveMessage(lspMessage);
+            listener?.cancelActiveMessage(lspMessage);
           } else {
-            _listener?.cancelPendingMessage(lspMessage);
+            listener?.cancelPendingMessage(lspMessage);
           }
         }
       }
@@ -428,9 +421,9 @@
             code: lsp.ErrorCodes.ContentModified.toJson(),
           );
           if (isActive) {
-            _listener?.cancelActiveMessage(lspMessage);
+            listener?.cancelActiveMessage(lspMessage);
           } else {
-            _listener?.cancelPendingMessage(lspMessage);
+            listener?.cancelPendingMessage(lspMessage);
           }
         }
       }
@@ -461,9 +454,13 @@
 
 abstract class MessageSchedulerListener {
   /// Report that the [message] was added to the active message queue.
+  ///
+  /// This implies that the message is no longer on the pending message queue.
   void addActiveMessage(ScheduledMessage message);
 
   /// Report that the [message] was added to the pending message queue.
+  ///
+  /// This is always the first notification for the [message].
   void addPendingMessage(ScheduledMessage message);
 
   /// Report that an active [message] was cancelled.
@@ -476,6 +473,8 @@
   void endProcessingMessages();
 
   /// Report that the [message] has been completed.
+  ///
+  /// This implies that the message was active and wasn't cancelled.
   void messageCompleted(ScheduledMessage message);
 
   /// Report that the loop that processes messages has started to run.
diff --git a/pkg/analysis_server/lib/src/scheduler/scheduled_message.dart b/pkg/analysis_server/lib/src/scheduler/scheduled_message.dart
index e44747b..ec0b6f2 100644
--- a/pkg/analysis_server/lib/src/scheduler/scheduled_message.dart
+++ b/pkg/analysis_server/lib/src/scheduler/scheduled_message.dart
@@ -30,7 +30,7 @@
   });
 
   @override
-  String toString() => message.method.toString();
+  String get id => 'dtd:${message.method}';
 }
 
 /// Represents a message in the Legacy protocol format.
@@ -45,7 +45,7 @@
   LegacyMessage({required this.request, this.cancellationToken});
 
   @override
-  String toString() => request.method;
+  String get id => 'legacy:${request.method}';
 }
 
 /// Represents a message in the LSP protocol format.
@@ -59,18 +59,18 @@
 
   LspMessage({required this.message, this.cancellationToken});
 
-  bool get isRequest => message is lsp.RequestMessage;
-
   @override
-  String toString() {
+  String get id {
     var msg = message;
     return switch (msg) {
-      RequestMessage() => msg.method.toString(),
-      NotificationMessage() => msg.method.toString(),
-      ResponseMessage() => 'ResponseMessage',
-      Message() => 'Message',
+      RequestMessage() => 'lsp:${msg.method}',
+      NotificationMessage() => 'lsp:${msg.method}',
+      ResponseMessage() => 'lsp:ResponseMessage',
+      Message() => 'lsp:Message',
     };
   }
+
+  bool get isRequest => message is lsp.RequestMessage;
 }
 
 /// A message from a client.
@@ -78,7 +78,13 @@
 /// The message can be either a request, a notification, or a response.
 ///
 /// The client can be an IDE, a command-line tool, or DTD.
-sealed class ScheduledMessage {}
+sealed class ScheduledMessage {
+  /// An identifier that identifies this particular kind of message.
+  String get id;
+
+  @override
+  String toString() => id;
+}
 
 /// Represents a message from the file watcher.
 ///
@@ -90,5 +96,5 @@
   WatcherMessage(this.event);
 
   @override
-  String toString() => '${event.type} ${event.path}';
+  String get id => 'watch:${event.type} ${event.path}';
 }
diff --git a/pkg/analysis_server/lib/src/scheduler/scheduler_tracking_listener.dart b/pkg/analysis_server/lib/src/scheduler/scheduler_tracking_listener.dart
new file mode 100644
index 0000000..bfcfc0b
--- /dev/null
+++ b/pkg/analysis_server/lib/src/scheduler/scheduler_tracking_listener.dart
@@ -0,0 +1,182 @@
+// Copyright (c) 2025, 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:convert';
+
+import 'package:analysis_server/src/analytics/analytics_manager.dart';
+import 'package:analysis_server/src/scheduler/message_scheduler.dart';
+import 'package:analysis_server/src/scheduler/scheduled_message.dart';
+
+/// The current time represented as milliseconds since the beginning of the
+/// epoch.
+int get _now => DateTime.now().millisecondsSinceEpoch;
+
+/// Information about a scheduled message's journey through the message
+/// scheduler.
+class MessageData {
+  /// The message for which data is being collected.
+  final ScheduledMessage message;
+
+  /// The number of pending messages already on the queue when the [message] was
+  /// added to the queue.
+  final int pendingMessageCount;
+
+  /// The number of active messages already on the queue when the [message] was
+  /// added to the queue.
+  final int activeMessageCount;
+
+  /// The time at which the [message] was added to the pending queue.
+  final int pendingTime;
+
+  /// The time at which the [message] was added to the active queue.
+  int? activeTime;
+
+  /// Whether the [message] was cancelled.
+  bool wasCancelled = false;
+
+  /// The time at which the [message] was completed.
+  int? completeTime;
+
+  MessageData({
+    required this.message,
+    required this.pendingMessageCount,
+    required this.activeMessageCount,
+  }) : pendingTime = _now;
+}
+
+/// A message scheduler listener that will gather data for reporting purposes.
+///
+/// The data will be included in the reports sent to track down performance
+/// issues, and aggregated data will be reported to the analytics manager.
+class SchedulerTrackingListener extends MessageSchedulerListener {
+  /// The number of lines of data to be kept in the [completedMessageLog].
+  static const int logLength = 100;
+
+  /// The analytics manager through which analytics are to be sent.
+  final AnalyticsManager analyticsManager;
+
+  /// A map from scheduled messages to the data being collected about them.
+  final Map<ScheduledMessage, MessageData> _messageDataMap = {};
+
+  /// The number of messages in the active message queue.
+  int _activeMessageCount = 0;
+
+  /// The number of messages in the pending message queue.
+  int _pendingMessageCount = 0;
+
+  /// The time at which processing last started.
+  int processingStartTime = 0;
+
+  /// The time at which processing last ended, or `-1` if processing hasn't yet
+  /// ended.
+  int processingEndTime = -1;
+
+  /// A list of data about the [logLength] most recently completed messages.
+  List<String> completedMessageLog = [];
+
+  /// Returns a newly created listener that will report to the
+  /// [analyticsManager].
+  SchedulerTrackingListener(this.analyticsManager);
+
+  ({List<MessageData> pending, List<MessageData> active})
+  get pendingAndActiveMessages {
+    var pendingMessages = <MessageData>[];
+    var activeMessages = <MessageData>[];
+    for (var messageData in _messageDataMap.values) {
+      if (messageData.activeTime != null) {
+        activeMessages.add(messageData);
+      } else {
+        pendingMessages.add(messageData);
+      }
+    }
+    return (pending: pendingMessages, active: activeMessages);
+  }
+
+  @override
+  void addActiveMessage(ScheduledMessage message) {
+    var messageData = _messageDataMap[message]!;
+    messageData.activeTime = _now;
+    _pendingMessageCount--;
+    _activeMessageCount++;
+  }
+
+  @override
+  void addPendingMessage(ScheduledMessage message) {
+    _messageDataMap[message] = MessageData(
+      message: message,
+      pendingMessageCount: _pendingMessageCount,
+      activeMessageCount: _activeMessageCount,
+    );
+    _pendingMessageCount++;
+  }
+
+  @override
+  void cancelActiveMessage(ScheduledMessage message) {
+    var messageData = _messageDataMap.remove(message);
+    if (messageData == null) {
+      return;
+    }
+    messageData.completeTime = _now;
+    messageData.wasCancelled = true;
+    _activeMessageCount--;
+    _reportMessageData(messageData);
+  }
+
+  @override
+  void cancelPendingMessage(ScheduledMessage message) {
+    var messageData = _messageDataMap.remove(message)!;
+    messageData.completeTime = _now;
+    messageData.wasCancelled = true;
+    _pendingMessageCount--;
+    _reportMessageData(messageData);
+  }
+
+  /// Report that the loop that processes messages has stopped running.
+  @override
+  void endProcessingMessages() {
+    processingEndTime = _now;
+    // var processingDuration = processingEndTime - processingStartTime;
+    // TODO(brianwilkerson): Record [processingDuration].
+  }
+
+  @override
+  void messageCompleted(ScheduledMessage message) {
+    var messageData = _messageDataMap.remove(message)!;
+    messageData.completeTime = _now;
+    _activeMessageCount--;
+    _reportMessageData(messageData);
+  }
+
+  @override
+  void startProcessingMessages() {
+    processingStartTime = _now;
+    // var idleDuration = processingStartTime - processingEndTime;
+    // TODO(brianwilkerson): Record [idleDuration].
+  }
+
+  /// Reports information about a completed message.
+  void _reportMessageData(MessageData messageData) {
+    var now = _now;
+    var pendingTime = messageData.pendingTime;
+    var activeTime = messageData.activeTime ?? now;
+    var completeTime = messageData.completeTime ?? now;
+
+    var jsonData = {
+      'message': messageData.message.id,
+      'pendingMessageCount': messageData.pendingMessageCount,
+      'activeMessageCount': messageData.activeMessageCount,
+      'pendingDuration': activeTime - pendingTime,
+      'activeDuration': completeTime - activeTime,
+      'wasCancelled': messageData.wasCancelled,
+    };
+
+    if (completedMessageLog.length >= logLength) {
+      completedMessageLog.removeAt(0);
+    }
+    completedMessageLog.add(jsonEncode(jsonData));
+
+    // TODO(brianwilkerson): Report the [messageDataData] to the analytics manager.
+    // analyticsManager;
+  }
+}
diff --git a/pkg/analysis_server/lib/src/status/diagnostics.dart b/pkg/analysis_server/lib/src/status/diagnostics.dart
index fdbbe9f..550ce7a 100644
--- a/pkg/analysis_server/lib/src/status/diagnostics.dart
+++ b/pkg/analysis_server/lib/src/status/diagnostics.dart
@@ -24,6 +24,7 @@
 import 'package:analysis_server/src/socket_server.dart';
 import 'package:analysis_server/src/status/ast_writer.dart';
 import 'package:analysis_server/src/status/element_writer.dart';
+import 'package:analysis_server/src/status/message_scheduler_page.dart';
 import 'package:analysis_server/src/status/pages.dart';
 import 'package:analysis_server/src/utilities/profiling.dart';
 import 'package:analysis_server/src/utilities/stream_string_stink.dart';
@@ -1554,6 +1555,7 @@
     pages.add(ByteStoreTimingPage(this));
     pages.add(CompletionPage(this));
     pages.add(FixesPage(this));
+    pages.add(MessageSchedulerPage(this));
     pages.add(RefactoringsPage(this));
   }
 
diff --git a/pkg/analysis_server/lib/src/status/message_scheduler_page.dart b/pkg/analysis_server/lib/src/status/message_scheduler_page.dart
new file mode 100644
index 0000000..660da67
--- /dev/null
+++ b/pkg/analysis_server/lib/src/status/message_scheduler_page.dart
@@ -0,0 +1,85 @@
+// Copyright (c) 2025, 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:async';
+
+import 'package:analysis_server/src/scheduler/message_scheduler.dart';
+import 'package:analysis_server/src/scheduler/scheduler_tracking_listener.dart';
+import 'package:analysis_server/src/status/diagnostics.dart';
+
+class MessageSchedulerPage extends DiagnosticPageWithNav {
+  MessageSchedulerPage(DiagnosticsSite site)
+    : super(
+        site,
+        'messageScheduler',
+        'Message Scheduler',
+        description: 'The state of the message scheduler.',
+        indentInNav: true,
+      );
+
+  @override
+  Future<void> generateContent(Map<String, String> params) async {
+    var listener = server.messageScheduler.listener;
+
+    h3('Status');
+    buf.writeln(
+      writeOption(
+        'Allows overlapping message handlers:',
+        MessageScheduler.allowOverlappingHandlers,
+      ),
+    );
+    if (listener is! SchedulerTrackingListener) {
+      buf.writeln(writeOption('Tracking listener:', 'none'));
+      return;
+    }
+
+    void writeData(MessageData data, {required bool isActive}) {
+      p(data.message.id);
+      buf.write('<blockquote>');
+      p('Pending messages ahead of this: ${data.pendingMessageCount}');
+      p(
+        'Time spent on pending queue: ${(data.activeTime ?? DateTime.now().millisecondsSinceEpoch) - data.pendingTime}',
+      );
+      if (isActive) {
+        p('Active messages ahead of this: ${data.activeMessageCount}');
+        p(
+          'Time spent running: ${DateTime.now().millisecondsSinceEpoch - data.activeTime!}',
+        );
+      }
+      buf.write('</blockquote>');
+    }
+
+    var (:pending, :active) = listener.pendingAndActiveMessages;
+
+    h3('Pending messages');
+    if (pending.isEmpty) {
+      p('none');
+    } else {
+      pending.sort(
+        (first, second) => first.pendingTime.compareTo(second.pendingTime),
+      );
+      for (var data in pending) {
+        writeData(data, isActive: false);
+      }
+    }
+
+    h3('Active messages');
+    if (active.isEmpty) {
+      p('none');
+    } else {
+      active.sort(
+        (first, second) => first.activeTime!.compareTo(second.activeTime!),
+      );
+      for (var data in active) {
+        writeData(data, isActive: true);
+      }
+    }
+
+    var lines = listener.completedMessageLog;
+    if (lines.isNotEmpty) {
+      h3('Completed messages');
+      p(lines.join('\n'), style: 'white-space: pre');
+    }
+  }
+}
diff --git a/pkg/analysis_server/lib/src/status/pages.dart b/pkg/analysis_server/lib/src/status/pages.dart
index 0045724..651cc4d 100644
--- a/pkg/analysis_server/lib/src/status/pages.dart
+++ b/pkg/analysis_server/lib/src/status/pages.dart
@@ -60,9 +60,7 @@
 
   Future<String> generate(Map<String, String> params) async {
     buf.clear();
-    // TODO(brianwilkerson): Determine if await is necessary, if so, change the
-    // return type of [generatePage] to `Future<void>`.
-    await (generatePage(params) as dynamic);
+    await generatePage(params);
     return buf.toString();
   }
 
diff --git a/pkg/analysis_server/test/integration/server/message_scheduler_test.dart b/pkg/analysis_server/test/integration/server/message_scheduler_test.dart
index 3bbe215..d4d9966 100644
--- a/pkg/analysis_server/test/integration/server/message_scheduler_test.dart
+++ b/pkg/analysis_server/test/integration/server/message_scheduler_test.dart
@@ -60,10 +60,10 @@
     await setRoots(included: [workspaceRootPath], excluded: []);
     await waitForTasksFinished();
     _assertLogContents(testView!, r'''
-Incoming LegacyMessage: analysis.setAnalysisRoots
+Incoming LegacyMessage: legacy:analysis.setAnalysisRoots
 Entering process messages loop
-  Start LegacyMessage: analysis.setAnalysisRoots
-  Complete LegacyMessage: analysis.setAnalysisRoots
+  Start LegacyMessage: legacy:analysis.setAnalysisRoots
+  Complete LegacyMessage: legacy:analysis.setAnalysisRoots
 Exit process messages loop
 ''');
   }
@@ -80,13 +80,13 @@
     await Future.wait(futures);
     await waitForTasksFinished();
     _assertLogContents(testView!, r'''
-Incoming LegacyMessage: analysis.setAnalysisRoots
+Incoming LegacyMessage: legacy:analysis.setAnalysisRoots
 Entering process messages loop
-  Start LegacyMessage: analysis.setAnalysisRoots
-Incoming LegacyMessage: execution.createContext
-  Complete LegacyMessage: analysis.setAnalysisRoots
-  Start LegacyMessage: execution.createContext
-  Complete LegacyMessage: execution.createContext
+  Start LegacyMessage: legacy:analysis.setAnalysisRoots
+Incoming LegacyMessage: legacy:execution.createContext
+  Complete LegacyMessage: legacy:analysis.setAnalysisRoots
+  Start LegacyMessage: legacy:execution.createContext
+  Complete LegacyMessage: legacy:execution.createContext
 Exit process messages loop
 ''');
   }
@@ -147,34 +147,34 @@
     }
 
     _assertLogContents(testView!, r'''
-Incoming RequestMessage: initialize
+Incoming RequestMessage: lsp:initialize
 Entering process messages loop
-  Start LspMessage: initialize
-  Complete LspMessage: initialize
+  Start LspMessage: lsp:initialize
+  Complete LspMessage: lsp:initialize
 Exit process messages loop
-Incoming NotificationMessage: initialized
+Incoming NotificationMessage: lsp:initialized
 Entering process messages loop
-  Start LspMessage: initialized
-  Complete LspMessage: initialized
+  Start LspMessage: lsp:initialized
+  Complete LspMessage: lsp:initialized
 Exit process messages loop
-Incoming NotificationMessage: textDocument/didOpen
+Incoming NotificationMessage: lsp:textDocument/didOpen
 Entering process messages loop
-  Start LspMessage: textDocument/didOpen
-  Complete LspMessage: textDocument/didOpen
+  Start LspMessage: lsp:textDocument/didOpen
+  Complete LspMessage: lsp:textDocument/didOpen
 Exit process messages loop
-Incoming RequestMessage: textDocument/codeAction
+Incoming RequestMessage: lsp:textDocument/codeAction
 Entering process messages loop
-  Start LspMessage: textDocument/codeAction
-  Complete LspMessage: textDocument/codeAction
+  Start LspMessage: lsp:textDocument/codeAction
+  Complete LspMessage: lsp:textDocument/codeAction
 Exit process messages loop
-Incoming RequestMessage: workspace/executeCommand
+Incoming RequestMessage: lsp:workspace/executeCommand
 Entering process messages loop
-  Start LspMessage: workspace/executeCommand
-Incoming NotificationMessage: textDocument/didChange
-Canceled in progress request workspace/executeCommand
-  Complete LspMessage: workspace/executeCommand
-  Start LspMessage: textDocument/didChange
-  Complete LspMessage: textDocument/didChange
+  Start LspMessage: lsp:workspace/executeCommand
+Incoming NotificationMessage: lsp:textDocument/didChange
+Canceled in progress request lsp:workspace/executeCommand
+  Complete LspMessage: lsp:workspace/executeCommand
+  Start LspMessage: lsp:textDocument/didChange
+  Complete LspMessage: lsp:textDocument/didChange
 Exit process messages loop
 ''');
   }
@@ -207,34 +207,34 @@
     await pumpEventQueue(times: 5000);
 
     _assertLogContents(testView!, r'''
-Incoming RequestMessage: initialize
+Incoming RequestMessage: lsp:initialize
 Entering process messages loop
-  Start LspMessage: initialize
-  Complete LspMessage: initialize
+  Start LspMessage: lsp:initialize
+  Complete LspMessage: lsp:initialize
 Exit process messages loop
-Incoming NotificationMessage: initialized
+Incoming NotificationMessage: lsp:initialized
 Entering process messages loop
-  Start LspMessage: initialized
-  Complete LspMessage: initialized
+  Start LspMessage: lsp:initialized
+  Complete LspMessage: lsp:initialized
 Exit process messages loop
-Incoming NotificationMessage: textDocument/didOpen
+Incoming NotificationMessage: lsp:textDocument/didOpen
 Entering process messages loop
-  Start LspMessage: textDocument/didOpen
-  Complete LspMessage: textDocument/didOpen
+  Start LspMessage: lsp:textDocument/didOpen
+  Complete LspMessage: lsp:textDocument/didOpen
 Exit process messages loop
-Incoming RequestMessage: textDocument/completion
+Incoming RequestMessage: lsp:textDocument/completion
 Entering process messages loop
-  Start LspMessage: textDocument/completion
-Incoming RequestMessage: textDocument/completion
-Canceled in progress request textDocument/completion
-Incoming RequestMessage: textDocument/completion
-Canceled in progress request textDocument/completion
-Canceled request on queue textDocument/completion
-  Complete LspMessage: textDocument/completion
-  Start LspMessage: textDocument/completion
-  Complete LspMessage: textDocument/completion
-  Start LspMessage: textDocument/completion
-  Complete LspMessage: textDocument/completion
+  Start LspMessage: lsp:textDocument/completion
+Incoming RequestMessage: lsp:textDocument/completion
+Canceled in progress request lsp:textDocument/completion
+Incoming RequestMessage: lsp:textDocument/completion
+Canceled in progress request lsp:textDocument/completion
+Canceled request on queue lsp:textDocument/completion
+  Complete LspMessage: lsp:textDocument/completion
+  Start LspMessage: lsp:textDocument/completion
+  Complete LspMessage: lsp:textDocument/completion
+  Start LspMessage: lsp:textDocument/completion
+  Complete LspMessage: lsp:textDocument/completion
 Exit process messages loop
 ''');
   }
@@ -244,15 +244,15 @@
     await initialAnalysis;
     await pumpEventQueue(times: 5000);
     _assertLogContents(testView!, r'''
-Incoming RequestMessage: initialize
+Incoming RequestMessage: lsp:initialize
 Entering process messages loop
-  Start LspMessage: initialize
-  Complete LspMessage: initialize
+  Start LspMessage: lsp:initialize
+  Complete LspMessage: lsp:initialize
 Exit process messages loop
-Incoming NotificationMessage: initialized
+Incoming NotificationMessage: lsp:initialized
 Entering process messages loop
-  Start LspMessage: initialized
-  Complete LspMessage: initialized
+  Start LspMessage: lsp:initialized
+  Complete LspMessage: lsp:initialized
 Exit process messages loop
 ''');
   }
@@ -274,23 +274,23 @@
     await pumpEventQueue(times: 5000);
 
     _assertLogContents(testView!, r'''
-Incoming RequestMessage: initialize
+Incoming RequestMessage: lsp:initialize
 Entering process messages loop
-  Start LspMessage: initialize
-  Complete LspMessage: initialize
+  Start LspMessage: lsp:initialize
+  Complete LspMessage: lsp:initialize
 Exit process messages loop
-Incoming NotificationMessage: initialized
+Incoming NotificationMessage: lsp:initialized
 Entering process messages loop
-  Start LspMessage: initialized
-  Complete LspMessage: initialized
+  Start LspMessage: lsp:initialized
+  Complete LspMessage: lsp:initialized
 Exit process messages loop
-Incoming RequestMessage: textDocument/documentSymbol
+Incoming RequestMessage: lsp:textDocument/documentSymbol
 Entering process messages loop
-  Start LspMessage: textDocument/documentSymbol
-Incoming RequestMessage: textDocument/documentLink
-  Complete LspMessage: textDocument/documentSymbol
-  Start LspMessage: textDocument/documentLink
-  Complete LspMessage: textDocument/documentLink
+  Start LspMessage: lsp:textDocument/documentSymbol
+Incoming RequestMessage: lsp:textDocument/documentLink
+  Complete LspMessage: lsp:textDocument/documentSymbol
+  Start LspMessage: lsp:textDocument/documentLink
+  Complete LspMessage: lsp:textDocument/documentLink
 Exit process messages loop
 ''');
   }
@@ -325,26 +325,26 @@
     await pumpEventQueue(times: 5000);
 
     _assertLogContents(testView!, r'''
-Incoming RequestMessage: initialize
+Incoming RequestMessage: lsp:initialize
 Entering process messages loop
-  Start LspMessage: initialize
-  Complete LspMessage: initialize
+  Start LspMessage: lsp:initialize
+  Complete LspMessage: lsp:initialize
 Exit process messages loop
-Incoming NotificationMessage: initialized
+Incoming NotificationMessage: lsp:initialized
 Entering process messages loop
-  Start LspMessage: initialized
-  Complete LspMessage: initialized
+  Start LspMessage: lsp:initialized
+  Complete LspMessage: lsp:initialized
 Exit process messages loop
-Incoming RequestMessage: textDocument/codeAction
+Incoming RequestMessage: lsp:textDocument/codeAction
 Entering process messages loop
-  Start LspMessage: textDocument/codeAction
-  Complete LspMessage: textDocument/codeAction
+  Start LspMessage: lsp:textDocument/codeAction
+  Complete LspMessage: lsp:textDocument/codeAction
 Exit process messages loop
-Incoming RequestMessage: workspace/executeCommand
+Incoming RequestMessage: lsp:workspace/executeCommand
 Entering process messages loop
-  Start LspMessage: workspace/executeCommand
+  Start LspMessage: lsp:workspace/executeCommand
 Incoming ResponseMessage: ResponseMessage
-  Complete LspMessage: workspace/executeCommand
+  Complete LspMessage: lsp:workspace/executeCommand
 Exit process messages loop
 ''');
   }