Skip to content

Commit

Permalink
[trace-view] Added support for marking optimized away code lines (#19306
Browse files Browse the repository at this point in the history
)

## Description 

In Move, there isn't necessarily 1:1 correspondence between source code
and bytecode due to compiler optimizations. In the following code
example (admittedly somewhat artificial for presentation purposes),
there will be no entry for `constant` variable in the bytecode as it
will be optimized away via constant propagation:
```
    fun hello(param: u64): vector<u64> {
        let mut res = param;
        let constant = 42; // optimized away

        if (constant >= 42) { // optimized away and turned into straight line code
            res = 42;
        };

        vector::singleton(res)
    }
```

This PR implements a heuristic that will mark source code lines
optimized away by the compiler as having grey background. We do this by
analyzing source maps for a given file and marking lines that are
present in the source map but not in the source file (with some
exceptions, notably empty lines, lines starting with `const`, and lines
that only contain right brace `}`).

At this point, the "optimized away" lines include comments (we can
finesse it in the future if need be, but it does in some sense reflect
the spirit of this visualization)

## Test plan 

Tested manually to see if grey lines appear and changes throughout
debugging session when going to different files, and that they are reset
once debug session is finished.
  • Loading branch information
awelc authored Sep 11, 2024
1 parent 601ff73 commit 7fd291a
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,25 @@ import {
} from '@vscode/debugadapter';
import { DebugProtocol } from '@vscode/debugprotocol';
import * as path from 'path';
import { Runtime, RuntimeEvents } from './runtime';
import { Runtime, RuntimeEvents, IRuntimeStack } from './runtime';

const enum LogLevel {
Log = 'log',
Verbose = 'verbose',
None = 'none'
}

/**
* Customized stack trace response that includes additional data.
*/
interface CustomizedStackTraceResponse extends DebugProtocol.StackTraceResponse {
body: {
stackFrames: StackFrame[];
totalFrames?: number;
optimized_lines: number[];
};
}

/**
* Converts a log level string to a Logger.LogLevel.
*
Expand Down Expand Up @@ -163,15 +174,19 @@ export class MoveDebugSession extends LoggingDebugSession {
this.sendResponse(response);
}

protected stackTraceRequest(response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments): void {
protected stackTraceRequest(response: CustomizedStackTraceResponse, args: DebugProtocol.StackTraceArguments): void {
try {
const runtimeStack = this.runtime.stack();
const stack_height = runtimeStack.frames.length;
response.body = {
stackFrames: runtimeStack.frames.map(frame => {
const fileName = path.basename(frame.file);
return new StackFrame(frame.id, frame.name, new Source(fileName, frame.file), frame.line);
}).reverse(),
totalFrames: runtimeStack.frames.length
totalFrames: stack_height,
optimized_lines: stack_height > 0
? runtimeStack.frames[stack_height - 1].sourceMap.optimized_lines
: []
};
} catch (err) {
response.success = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ interface IRuntimeStackFrame {
* Describes the runtime stack during trace viewing session
* (oldest frame is at the bottom of the stack at index 0).
*/
interface IRuntimeStack {
export interface IRuntimeStack {
frames: IRuntimeStackFrame[];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,22 @@ interface ISrcDefinitionLocation {
end: number;
}

interface ISrcStructSourceMapEntry {
definition_location: ISrcDefinitionLocation;
type_parameters: [string, ISrcDefinitionLocation][];
fields: ISrcDefinitionLocation[];
}

interface ISrcEnumSourceMapEntry {
definition_location: ISrcDefinitionLocation;
type_parameters: [string, ISrcDefinitionLocation][];
variants: [[string, ISrcDefinitionLocation], ISrcDefinitionLocation[]][];
}

interface ISrcFunctionMapEntry {
definition_location: ISrcDefinitionLocation;
type_parameters: any[];
parameters: any[];
type_parameters: [string, ISrcDefinitionLocation][];
parameters: [string, ISrcDefinitionLocation][];
locals: [string, ISrcDefinitionLocation][];
nops: Record<string, any>;
code_map: Record<string, ISrcDefinitionLocation>;
Expand All @@ -26,10 +38,10 @@ interface ISrcFunctionMapEntry {
interface ISrcRootObject {
definition_location: ISrcDefinitionLocation;
module_name: string[];
struct_map: Record<string, any>;
enum_map: Record<string, any>;
struct_map: Record<string, ISrcStructSourceMapEntry>;
enum_map: Record<string, ISrcEnumSourceMapEntry>;
function_map: Record<string, ISrcFunctionMapEntry>;
constant_map: Record<string, any>;
constant_map: Record<string, string>;
}

// Runtime data types.
Expand All @@ -47,7 +59,7 @@ interface ILoc {
*/
interface ISourceMapFunction {
// Locations indexed with PC values.
pcLocs: ILoc[]
pcLocs: ILoc[],
}

/**
Expand All @@ -68,7 +80,9 @@ export interface IFileInfo {
export interface ISourceMap {
fileHash: string
modInfo: ModuleInfo,
functions: Map<string, ISourceMapFunction>
functions: Map<string, ISourceMapFunction>,
// Lines that are not present in the source map.
optimized_lines: number[]
}

export function readAllSourceMaps(directory: string, filesMap: Map<string, IFileInfo>): Map<string, ISourceMap> {
Expand Down Expand Up @@ -116,7 +130,10 @@ function readSourceMap(sourceMapPath: string, filesMap: Map<string, IFileInfo>):
+ " when processing source map at: "
+ sourceMapPath);
}
for (const funEntry of Object.values(sourceMapJSON.function_map)) {
const allSourceMapLines = new Set<number>();
prePopulateSourceMapLines(sourceMapJSON, fileInfo, allSourceMapLines);
const functionMap = sourceMapJSON.function_map;
for (const funEntry of Object.values(functionMap)) {
let nameStart = funEntry.definition_location.start;
let nameEnd = funEntry.definition_location.end;
const funName = fileInfo.content.slice(nameStart, nameEnd);
Expand All @@ -131,6 +148,9 @@ function readSourceMap(sourceMapPath: string, filesMap: Map<string, IFileInfo>):
for (const [pc, defLocation] of Object.entries(funEntry.code_map)) {
const currentPC = parseInt(pc);
const currentLoc = byteOffsetToLineColumn(fileInfo, defLocation.start);
allSourceMapLines.add(currentLoc.line);
// add the end line to the set as well even if we don't need it for pcLocs
allSourceMapLines.add(byteOffsetToLineColumn(fileInfo, defLocation.end).line);
for (let i = prevPC + 1; i < currentPC; i++) {
pcLocs.push(prevLoc);
}
Expand All @@ -140,9 +160,91 @@ function readSourceMap(sourceMapPath: string, filesMap: Map<string, IFileInfo>):
}
functions.set(funName, { pcLocs });
}
return { fileHash, modInfo, functions };
let optimized_lines: number[] = [];
for (let i = 0; i < fileInfo.lines.length; i++) {
if (!allSourceMapLines.has(i + 1)) { // allSourceMapLines is 1-based
optimized_lines.push(i); // result must be 0-based
}
}

return { fileHash, modInfo, functions, optimized_lines };
}

/**
* Pre-populates the set of source file lines that are present in the source map
* with lines corresponding to the definitions of module, structs, enums, and functions
* (excluding location of instructions in the function body which are handled elsewhere).
* Constants do not have location information in the source map and must be handled separately.
*
* @param sourceMapJSON
* @param fileInfo
* @param sourceMapLines
*/
function prePopulateSourceMapLines(
sourceMapJSON: ISrcRootObject,
fileInfo: IFileInfo,
sourceMapLines: Set<number>
) {
addLinesForLocation(sourceMapJSON.definition_location, fileInfo, sourceMapLines);
const structMap = sourceMapJSON.struct_map;
for (const structEntry of Object.values(structMap)) {
addLinesForLocation(structEntry.definition_location, fileInfo, sourceMapLines);
for (const typeParam of structEntry.type_parameters) {
addLinesForLocation(typeParam[1], fileInfo, sourceMapLines);
}
for (const fieldDef of structEntry.fields) {
addLinesForLocation(fieldDef, fileInfo, sourceMapLines);
}
}

const enumMap = sourceMapJSON.enum_map;
for (const enumEntry of Object.values(enumMap)) {
addLinesForLocation(enumEntry.definition_location, fileInfo, sourceMapLines);
for (const typeParam of enumEntry.type_parameters) {
addLinesForLocation(typeParam[1], fileInfo, sourceMapLines);
}
for (const variant of enumEntry.variants) {
addLinesForLocation(variant[0][1], fileInfo, sourceMapLines);
for (const fieldDef of variant[1]) {
addLinesForLocation(fieldDef, fileInfo, sourceMapLines);
}
}
}

const functionMap = sourceMapJSON.function_map;
for (const funEntry of Object.values(functionMap)) {
addLinesForLocation(funEntry.definition_location, fileInfo, sourceMapLines);
for (const typeParam of funEntry.type_parameters) {
addLinesForLocation(typeParam[1], fileInfo, sourceMapLines);
}
for (const param of funEntry.parameters) {
addLinesForLocation(param[1], fileInfo, sourceMapLines);
}
for (const local of funEntry.locals) {
addLinesForLocation(local[1], fileInfo, sourceMapLines);
}
}
}

/**
* Adds source file lines for the given location to the set.
*
* @param loc location in the source file.
* @param fileInfo source file information.
* @param sourceMapLines set of source file lines.
*/
function addLinesForLocation(
loc: ISrcDefinitionLocation,
fileInfo: IFileInfo,
sourceMapLines: Set<number>
) {
const startLine = byteOffsetToLineColumn(fileInfo, loc.start).line;
sourceMapLines.add(startLine);
const endLine = byteOffsetToLineColumn(fileInfo, loc.end).line;
sourceMapLines.add(endLine);
}


/**
* Computes source file location (line/colum) from the byte offset
* (assumes that lines and columns are 1-based).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
import * as fs from 'fs';
import * as vscode from 'vscode';
import * as path from 'path';
import { WorkspaceFolder, DebugConfiguration, ProviderResult, CancellationToken, DebugSession } from 'vscode';
import { StackFrame } from '@vscode/debugadapter';
import { WorkspaceFolder, DebugConfiguration, CancellationToken } from 'vscode';

/**
* Log level for the debug adapter.
Expand Down Expand Up @@ -35,6 +36,66 @@ export function activate(context: vscode.ExtensionContext) {
})
);

let previousSourcePath: string | undefined;
const decorationType = vscode.window.createTextEditorDecorationType({
color: 'grey',
backgroundColor: 'rgba(220, 220, 220, 0.5)' // grey with 50% opacity
});
context.subscriptions.push(
vscode.debug.onDidChangeActiveStackItem(async stackItem => {
if (stackItem instanceof vscode.DebugStackFrame) {
const session = vscode.debug.activeDebugSession;
if (session) {
// Request the stack frame details from the debug adapter
const stackTraceResponse = await session.customRequest('stackTrace', {
threadId: stackItem.threadId,
startFrame: stackItem.frameId,
levels: 1
});

const stackFrame: StackFrame = stackTraceResponse.stackFrames[0];
if (stackFrame && stackFrame.source && stackFrame.source.path !== previousSourcePath) {
previousSourcePath = stackFrame.source.path;
const source = stackFrame.source;
const line = stackFrame.line;
console.log(`Frame details: ${source?.name} at line ${line}`);

const editor = vscode.window.activeTextEditor;
if (editor) {
const optimized_lines = stackTraceResponse.optimized_lines;
const document = editor.document;
let decorationsArray: vscode.DecorationOptions[] = [];

optimized_lines.forEach((lineNumber: number) => {
const line = document.lineAt(lineNumber);
const lineLength = line.text.length;
const lineText = line.text.trim();
if (lineText.length !== 0 // ignore empty lines
&& !lineText.startsWith("const") // ignore constant declarations (not in the source map)
&& !lineText.startsWith("}")) { // ignore closing braces with nothing else on the same line
const decoration = {
range: new vscode.Range(lineNumber, 0, lineNumber, lineLength),
};
decorationsArray.push(decoration);
}
});

editor.setDecorations(decorationType, decorationsArray);
}

}
}
}
}),
vscode.debug.onDidTerminateDebugSession(() => {
// reset all decorations when the debug session is terminated
// to avoid showing lines for code that was optimized away
const editor = vscode.window.activeTextEditor;
if (editor) {
editor.setDecorations(decorationType, []);
}
})
);
}

/**
Expand Down

0 comments on commit 7fd291a

Please sign in to comment.