Skip to content

Commit

Permalink
Add Breakpoint label frame to optimize debug stepping performance
Browse files Browse the repository at this point in the history
  • Loading branch information
JustinGrote committed Nov 19, 2024
1 parent fec1f3a commit f052fa5
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 134 deletions.
1 change: 1 addition & 0 deletions src/PowerShellEditorServices/Server/PsesDebugServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ public async Task StartAsync()
response.SupportsHitConditionalBreakpoints = true;
response.SupportsLogPoints = true;
response.SupportsSetVariable = true;
response.SupportsDelayedStackTraceLoading = true;

return Task.CompletedTask;
});
Expand Down
66 changes: 8 additions & 58 deletions src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Management.Automation;
using System.Management.Automation.Language;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
Expand Down Expand Up @@ -50,7 +48,6 @@ internal class DebugService
private VariableContainerDetails scriptScopeVariables;
private VariableContainerDetails localScopeVariables;
private StackFrameDetails[] stackFrameDetails;
private readonly PropertyInfo invocationTypeScriptPositionProperty;

private readonly SemaphoreSlim debugInfoHandle = AsyncUtils.CreateSimpleLockingSemaphore();
#endregion
Expand All @@ -75,6 +72,12 @@ internal class DebugService
/// </summary>
public DebuggerStoppedEventArgs CurrentDebuggerStoppedEventArgs { get; private set; }

/// <summary>
/// Returns a task that completes when script frames and variables have completed population
/// </summary>
public Task StackFramesAndVariablesFetched { get; private set; }


/// <summary>
/// Tracks whether we are running <c>Debug-Runspace</c> in an out-of-process runspace.
/// </summary>
Expand Down Expand Up @@ -111,12 +114,6 @@ public DebugService(
_debugContext.DebuggerResuming += OnDebuggerResuming;
_debugContext.BreakpointUpdated += OnBreakpointUpdated;
_remoteFileManager = remoteFileManager;

invocationTypeScriptPositionProperty =
typeof(InvocationInfo)
.GetProperty(
"ScriptPosition",
BindingFlags.NonPublic | BindingFlags.Instance);
}

#endregion
Expand Down Expand Up @@ -981,8 +978,8 @@ await _executionService.ExecutePSCommandAsync<PSObject>(
}
}

// Get call stack and variables.
await FetchStackFramesAndVariablesAsync(noScriptName ? localScriptPath : null).ConfigureAwait(false);
// Begin call stack and variables fetch. We don't need to block here.
StackFramesAndVariablesFetched = FetchStackFramesAndVariablesAsync(noScriptName ? localScriptPath : null);

// If this is a remote connection and the debugger stopped at a line
// in a script file, get the file contents
Expand All @@ -996,53 +993,6 @@ await _remoteFileManager.FetchRemoteFileAsync(
_psesHost.CurrentRunspace).ConfigureAwait(false);
}

if (stackFrameDetails.Length > 0)
{
// Augment the top stack frame with details from the stop event
if (invocationTypeScriptPositionProperty.GetValue(e.InvocationInfo) is IScriptExtent scriptExtent)
{
StackFrameDetails targetFrame = stackFrameDetails[0];

// Certain context changes (like stepping into the default value expression
// of a parameter) do not create a call stack frame. In order to represent
// this context change we create a fake call stack frame.
if (!string.IsNullOrEmpty(scriptExtent.File)
&& !PathUtils.IsPathEqual(scriptExtent.File, targetFrame.ScriptPath))
{
await debugInfoHandle.WaitAsync().ConfigureAwait(false);
try
{
targetFrame = new StackFrameDetails
{
ScriptPath = scriptExtent.File,
// Just use the last frame's variables since we don't have a
// good way to get real values.
AutoVariables = targetFrame.AutoVariables,
CommandVariables = targetFrame.CommandVariables,
// Ideally we'd get a real value here but since there's no real
// call stack frame for this, we'd need to replicate a lot of
// engine code.
FunctionName = "<ScriptBlock>",
};

StackFrameDetails[] newFrames = new StackFrameDetails[stackFrameDetails.Length + 1];
newFrames[0] = targetFrame;
stackFrameDetails.CopyTo(newFrames, 1);
stackFrameDetails = newFrames;
}
finally
{
debugInfoHandle.Release();
}
}

targetFrame.StartLineNumber = scriptExtent.StartLineNumber;
targetFrame.EndLineNumber = scriptExtent.EndLineNumber;
targetFrame.StartColumnNumber = scriptExtent.StartColumnNumber;
targetFrame.EndColumnNumber = scriptExtent.EndColumnNumber;
}
}

CurrentDebuggerStoppedEventArgs =
new DebuggerStoppedEventArgs(
e,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,17 @@ internal class ScopesHandler : IScopesHandler

public Task<ScopesResponse> Handle(ScopesArguments request, CancellationToken cancellationToken)
{
VariableScope[] variableScopes =
_debugService.GetVariableScopes(
(int)request.FrameId);
//We have an artificial breakpoint label, so just copy the stacktrace from the first stack entry for this.
int frameId = request.FrameId == 0 ? 0 : (int)request.FrameId - 1;

VariableScope[] variableScopes = _debugService.GetVariableScopes(frameId);

return Task.FromResult(new ScopesResponse
{
Scopes = new Container<Scope>(variableScopes
.Select(LspDebugUtils.CreateScope))
Scopes = new Container<Scope>(
variableScopes
.Select(LspDebugUtils.CreateScope)
)
});
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,64 +1,132 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#nullable enable

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using System.Management.Automation;
using Microsoft.PowerShell.EditorServices.Services;
using Microsoft.PowerShell.EditorServices.Services.DebugAdapter;
using Microsoft.PowerShell.EditorServices.Utility;
using OmniSharp.Extensions.DebugAdapter.Protocol.Models;
using OmniSharp.Extensions.DebugAdapter.Protocol.Requests;
using Microsoft.PowerShell.EditorServices.Services.DebugAdapter;
using System.Linq;

namespace Microsoft.PowerShell.EditorServices.Handlers;

namespace Microsoft.PowerShell.EditorServices.Handlers
internal class StackTraceHandler(DebugService debugService) : IStackTraceHandler
{
internal class StackTraceHandler : IStackTraceHandler
/// <summary>
/// Because we don't know the size of the stacktrace beforehand, we will tell the client that there are more frames available, this is effectively a paging size, as the client should request this many frames after the first one.
/// </summary>
private const int INITIAL_PAGE_SIZE = 20;

public async Task<StackTraceResponse> Handle(StackTraceArguments request, CancellationToken cancellationToken)
{
private readonly DebugService _debugService;
if (!debugService.IsDebuggerStopped)
{
throw new NotSupportedException("Stacktrace was requested while we are not stopped at a breakpoint.");
}

public StackTraceHandler(DebugService debugService) => _debugService = debugService;
// Adapting to int to let us use LINQ, realistically if you have a stacktrace larger than this that the client is requesting, you have bigger problems...
int skip = Convert.ToInt32(request.StartFrame ?? 0);
int take = Convert.ToInt32(request.Levels ?? 0);

public async Task<StackTraceResponse> Handle(StackTraceArguments request, CancellationToken cancellationToken)
{
StackFrameDetails[] stackFrameDetails = await _debugService.GetStackFramesAsync(cancellationToken).ConfigureAwait(false);
// We generate a label for the breakpoint and can return that immediately if the client is supporting DelayedStackTraceLoading.
InvocationInfo invocationInfo = debugService.CurrentDebuggerStoppedEventArgs?.OriginalEvent?.InvocationInfo
?? throw new NotSupportedException("InvocationInfo was not available on CurrentDebuggerStoppedEvent args. This is a bug.");

// Handle a rare race condition where the adapter requests stack frames before they've
// begun building.
if (stackFrameDetails is null)
{
return new StackTraceResponse
{
StackFrames = Array.Empty<StackFrame>(),
TotalFrames = 0
};
}

List<StackFrame> newStackFrames = new();

long startFrameIndex = request.StartFrame ?? 0;
long maxFrameCount = stackFrameDetails.Length;

// If the number of requested levels == 0 (or null), that means get all stack frames
// after the specified startFrame index. Otherwise get all the stack frames.
long requestedFrameCount = request.Levels ?? 0;
if (requestedFrameCount > 0)
{
maxFrameCount = Math.Min(maxFrameCount, startFrameIndex + requestedFrameCount);
}
StackFrame breakpointLabel = CreateBreakpointLabel(invocationInfo);

for (long i = startFrameIndex; i < maxFrameCount; i++)
if (skip == 0 && take == 1) // This indicates the client is doing an initial fetch, so we want to return quickly to unblock the UI and wait on the remaining stack frames for the subsequent requests.
{
return new StackTraceResponse()
{
// Create the new StackFrame object with an ID that can
// be referenced back to the current list of stack frames
newStackFrames.Add(LspDebugUtils.CreateStackFrame(stackFrameDetails[i], id: i));
}
StackFrames = new StackFrame[] { breakpointLabel },
TotalFrames = INITIAL_PAGE_SIZE //Indicate to the client that there are more frames available
};
}

// Wait until the stack frames and variables have been fetched.
await debugService.StackFramesAndVariablesFetched.ConfigureAwait(false);

StackFrameDetails[] stackFrameDetails = await debugService.GetStackFramesAsync(cancellationToken)
.ConfigureAwait(false);

// Handle a rare race condition where the adapter requests stack frames before they've
// begun building.
if (stackFrameDetails is null)
{
return new StackTraceResponse
{
StackFrames = newStackFrames,
TotalFrames = newStackFrames.Count
StackFrames = Array.Empty<StackFrame>(),
TotalFrames = 0
};
}

List<StackFrame> newStackFrames = new();
if (skip == 0)
{
newStackFrames.Add(breakpointLabel);
}

newStackFrames.AddRange(
stackFrameDetails
.Skip(skip != 0 ? skip - 1 : skip)
.Take(take != 0 ? take - 1 : take)
.Select((frame, index) => CreateStackFrame(frame, index + 1))
);

return new StackTraceResponse
{
StackFrames = newStackFrames,
TotalFrames = newStackFrames.Count
};
}

public static StackFrame CreateStackFrame(StackFrameDetails stackFrame, long id)
{
SourcePresentationHint sourcePresentationHint =
stackFrame.IsExternalCode ? SourcePresentationHint.Deemphasize : SourcePresentationHint.Normal;

// When debugging an interactive session, the ScriptPath is <No File> which is not a valid source file.
// We need to make sure the user can't open the file associated with this stack frame.
// It will generate a VSCode error in this case.
Source? source = null;
if (!stackFrame.ScriptPath.Contains("<"))
{
source = new Source
{
Path = stackFrame.ScriptPath,
PresentationHint = sourcePresentationHint
};
}

return new StackFrame
{
Id = id,
Name = (source is not null) ? stackFrame.FunctionName : "Interactive Session",
Line = (source is not null) ? stackFrame.StartLineNumber : 0,
EndLine = stackFrame.EndLineNumber,
Column = (source is not null) ? stackFrame.StartColumnNumber : 0,
EndColumn = stackFrame.EndColumnNumber,
Source = source
};
}

public static StackFrame CreateBreakpointLabel(InvocationInfo invocationInfo, int id = 0) => new()
{
Name = "<Breakpoint>",
Id = id,
Source = new()
{
Path = invocationInfo.ScriptName
},
Line = invocationInfo.ScriptLineNumber,
Column = invocationInfo.OffsetInLine,
PresentationHint = StackFramePresentationHint.Label
};

}

32 changes: 0 additions & 32 deletions src/PowerShellEditorServices/Utility/LspDebugUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,38 +56,6 @@ public static Breakpoint CreateBreakpoint(
};
}

public static StackFrame CreateStackFrame(
StackFrameDetails stackFrame,
long id)
{
SourcePresentationHint sourcePresentationHint =
stackFrame.IsExternalCode ? SourcePresentationHint.Deemphasize : SourcePresentationHint.Normal;

// When debugging an interactive session, the ScriptPath is <No File> which is not a valid source file.
// We need to make sure the user can't open the file associated with this stack frame.
// It will generate a VSCode error in this case.
Source source = null;
if (!stackFrame.ScriptPath.Contains("<"))
{
source = new Source
{
Path = stackFrame.ScriptPath,
PresentationHint = sourcePresentationHint
};
}

return new StackFrame
{
Id = id,
Name = (source != null) ? stackFrame.FunctionName : "Interactive Session",
Line = (source != null) ? stackFrame.StartLineNumber : 0,
EndLine = stackFrame.EndLineNumber,
Column = (source != null) ? stackFrame.StartColumnNumber : 0,
EndColumn = stackFrame.EndColumnNumber,
Source = source
};
}

public static Scope CreateScope(VariableScope scope)
{
return new Scope
Expand Down

0 comments on commit f052fa5

Please sign in to comment.