stackTraceRequest method

  1. @override
Future<void> stackTraceRequest(
  1. Request request,
  2. StackTraceArguments args,
  3. void sendResponse(
    1. StackTraceResponseBody
    )
)

Handles a request from the client for the call stack for args.threadId.

This is usually called after we sent a StoppedEvent to the client notifying it that execution of an isolate has paused and it wants to populate the call stack view.

Clients may fetch the frames in batches and VS Code in particular will send two requests initially - one for the top frame only, and then one for the next 19 frames. For better performance, the first request is satisfied entirely from the threads pauseEvent.topFrame so we do not need to round-trip to the VM Service.

Implementation

@override
Future<void> stackTraceRequest(
  Request request,
  StackTraceArguments args,
  void Function(StackTraceResponseBody) sendResponse,
) async {
  // We prefer to provide frames in small batches. Rather than tell the client
  // how many frames there really are (which can be expensive to compute -
  // especially for web) we just add 20 on to the last frame we actually send,
  // as described in the spec:
  //
  // "Returning monotonically increasing totalFrames values for subsequent
  //  requests can be used to enforce paging in the client."
  const stackFrameBatchSize = 20;

  final threadId = args.threadId;
  final thread = isolateManager.getThread(threadId);
  final topFrame = thread?.pauseEvent?.topFrame;
  final startFrame = args.startFrame ?? 0;
  final numFrames = args.levels ?? 0;
  var totalFrames = 1;

  if (thread == null) {
    if (isolateManager.isInvalidThreadId(threadId)) {
      throw DebugAdapterException('Thread $threadId was not found');
    } else {
      // This condition means the thread ID was valid but the isolate has
      // since exited so rather than displaying an error, just return an empty
      // response because the client will be no longer interested in the
      // response.
      sendResponse(StackTraceResponseBody(
        stackFrames: [],
        totalFrames: 0,
      ));
      return;
    }
  }

  if (!thread.paused) {
    throw DebugAdapterException('Thread $threadId is not paused');
  }

  final stackFrames = <StackFrame>[];
  // If the request is only for the top frame, we may be able to satisfy it
  // from the threads `pauseEvent.topFrame`.
  if (startFrame == 0 && numFrames == 1 && topFrame != null) {
    totalFrames = 1 + stackFrameBatchSize;
    final dapTopFrame = await _converter.convertVmToDapStackFrame(
      thread,
      topFrame,
      isTopFrame: true,
    );
    stackFrames.add(dapTopFrame);
  } else {
    // Otherwise, send the request on to the VM.
    // The VM doesn't support fetching an arbitrary slice of frames, only a
    // maximum limit, so if the client asks for frames 20-30 we must send a
    // request for the first 30 and trim them ourselves.

    // DAP says if numFrames is 0 or missing (which we swap to 0 above) we
    // should return all.
    final limit = numFrames == 0 ? null : startFrame + numFrames;
    final stack = await vmService?.getStack(thread.isolate.id!, limit: limit);
    final frames = stack?.asyncCausalFrames ?? stack?.frames;

    if (stack != null && frames != null) {
      // When the call stack is truncated, we always add [stackFrameBatchSize]
      // to the count, indicating to the client there are more frames and
      // the size of the batch they should request when "loading more".
      //
      // It's ok to send a number that runs past the actual end of the call
      // stack and the client should handle this gracefully:
      //
      // "a client should be prepared to receive less frames than requested,
      //  which is an indication that the end of the stack has been reached."
      totalFrames = (stack.truncated ?? false)
          ? frames.length + stackFrameBatchSize
          : frames.length;

      // Find the first async marker, because some functionality only works
      // up until the first async boundary (e.g. rewind) since we're showing
      // the user async frames which are out-of-sync with the real frames
      // past that point.
      int? firstAsyncMarkerIndex = frames.indexWhere(
        (frame) => frame.kind == vm.FrameKind.kAsyncSuspensionMarker,
      );
      // indexWhere returns -1 if not found, we treat that as no marker (we
      // can rewind for all frames in the stack).
      if (firstAsyncMarkerIndex == -1) {
        firstAsyncMarkerIndex = null;
      }

      // Pre-resolve all URIs in batch so the call below does not trigger
      // many requests to the server.
      final allUris = frames
          .map((frame) => frame.location?.script?.uri)
          .nonNulls
          .map(Uri.parse)
          .toList();
      await thread.resolveUrisToPathsBatch(allUris);

      Future<StackFrame> convert(int index, vm.Frame frame) async {
        return _converter.convertVmToDapStackFrame(
          thread,
          frame,
          firstAsyncMarkerIndex: firstAsyncMarkerIndex,
          isTopFrame: startFrame == 0 && index == 0,
        );
      }

      final frameSubset = frames.sublist(startFrame);
      stackFrames.addAll(await Future.wait(frameSubset.mapIndexed(convert)));
    }
  }

  sendResponse(
    StackTraceResponseBody(
      stackFrames: stackFrames,
      totalFrames: totalFrames,
    ),
  );
}