Skip to content

Commit

Permalink
[trace-view] Start debugging in disassembly (#20963)
Browse files Browse the repository at this point in the history
## Description 

This PR adds the ability to start trace debugging a unit test with an
assembly file (rather than a source file) in active editor


## Test plan 

Tested manually that one can start debugging with a disassembly file
opened
  • Loading branch information
awelc authored Jan 29, 2025
1 parent 1ce2726 commit 37743e3
Show file tree
Hide file tree
Showing 3 changed files with 200 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -268,15 +268,16 @@ export class Runtime extends EventEmitter {
/**
* Start a trace viewing session and set up the initial state of the runtime.
*
* @param source path to the Move source file whose traces are to be viewed.
* @param openedFilePath path to the Move source file (or disassembled bytecode file)
* whose traces are to be viewed.
* @param traceInfo trace selected for viewing.
* @throws Error with a descriptive error message if starting runtime has failed.
*
*/
public async start(source: string, traceInfo: string, stopOnEntry: boolean): Promise<void> {
const pkgRoot = await findPkgRoot(source);
public async start(openedFilePath: string, traceInfo: string, stopOnEntry: boolean): Promise<void> {
const pkgRoot = await findPkgRoot(openedFilePath);
if (!pkgRoot) {
throw new Error(`Cannot find package root for file: ${source}`);
throw new Error(`Cannot find package root for file: ${openedFilePath}`);
}
const manifest_path = path.join(pkgRoot, 'Move.toml');

Expand All @@ -287,6 +288,14 @@ export class Runtime extends EventEmitter {
throw Error(`Cannot find package name in manifest file: ${manifest_path}`);
}

const openedFileExt = path.extname(openedFilePath);
if (openedFileExt !== MOVE_FILE_EXT
&& openedFileExt !== BCODE_FILE_EXT
&& openedFileExt !== JSON_FILE_EXT) {
throw new Error(`File extension: ${openedFileExt} is not supported by trace debugger`);
}
const showDisassembly = openedFileExt === BCODE_FILE_EXT;

// create file maps for all files in the `sources` directory, including both package source
// files and source files for dependencies
hashToFileMap(path.join(pkgRoot, 'build', pkg_name, 'sources'), this.filesMap, MOVE_FILE_EXT);
Expand Down Expand Up @@ -335,6 +344,7 @@ export class Runtime extends EventEmitter {
currentEvent.optimizedSrcLines,
currentEvent.optimizedBcodeLines
);
newFrame.showDisassembly = showDisassembly;
this.frameStack = {
frames: [newFrame],
globals: new Map<number, RuntimeValueType>()
Expand Down
15 changes: 13 additions & 2 deletions external-crates/move/crates/move-analyzer/trace-debug/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"publisher": "mysten",
"icon": "images/move.png",
"license": "Apache-2.0",
"version": "0.0.4",
"version": "0.0.5",
"preview": true,
"repository": {
"url": "https://github.com/MystenLabs/sui.git",
Expand Down Expand Up @@ -55,6 +55,16 @@
"extensions": [
".mvb"
]
},
{
"id": "mtrace",
"aliases": [
"mtrace"
],
"extensions": [
".json",
".JSON"
]
}
],
"breakpoints": [{ "language": "move" }, { "language": "mvb" }],
Expand All @@ -69,7 +79,8 @@
],
"languages": [
"move",
"mvb"
"mvb",
"mtrace"
],
"configurationAttributes": {
"launch": {
Expand Down
208 changes: 173 additions & 35 deletions external-crates/move/crates/move-analyzer/trace-debug/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ const LOG_LEVEL = 'log';
*/
const DEBUGGER_TYPE = 'move-debug';

const MOVE_FILE_EXT = ".move";
const BCODE_FILE_EXT = ".mvb";


/**
* Provider of on-hover information during debug session.
*/
Expand All @@ -36,6 +40,15 @@ class MoveEvaluatableExpressionProvider {
}
}

/**
* Information about a traced function.
*/
interface TracedFunctionInfo {
pkgAddr: number;
module: string;
function: string;
}

/**
* Called when the extension is activated.
*/
Expand Down Expand Up @@ -75,10 +88,6 @@ export function activate(context: vscode.ExtensionContext) {
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.optimizedLines;
Expand Down Expand Up @@ -171,7 +180,9 @@ class MoveConfigurationProvider implements vscode.DebugConfigurationProvider {
// if launch.json is missing or empty
if (!config.type && !config.request && !config.name) {
const editor = vscode.window.activeTextEditor;
if (editor && editor.document.languageId === 'move') {
if (editor && (editor.document.languageId === 'move'
|| editor.document.languageId === 'mvb'
|| editor.document.languageId === 'mtrace')) {

try {
let traceInfo = await findTraceInfo(editor);
Expand Down Expand Up @@ -206,7 +217,7 @@ class MoveConfigurationProvider implements vscode.DebugConfigurationProvider {
* Finds the trace information for the current active editor.
*
* @param editor active text editor.
* @returns trace information of the form `<package>::<module>::<function>`.
* @returns trace information of the form `<package>::<module>::<function_name>`.
* @throws Error with a descriptive error message if the trace information cannot be found.
*/
async function findTraceInfo(editor: vscode.TextEditor): Promise<string> {
Expand All @@ -215,14 +226,34 @@ async function findTraceInfo(editor: vscode.TextEditor): Promise<string> {
throw new Error(`Cannot find package root for file '${editor.document.uri.fsPath}'`);
}

const pkgModules = findModules(editor.document.getText());
if (pkgModules.length === 0) {
throw new Error(`Cannot find any modules in file '${editor.document.uri.fsPath}'`);
let tracedFunctions: string[] = [];
if (path.extname(editor.document.uri.fsPath) === MOVE_FILE_EXT) {
const pkgModules = findSrcModules(editor.document.getText());
if (pkgModules.length === 0) {
throw new Error(`Cannot find any modules in file '${editor.document.uri.fsPath}'`);
}
tracedFunctions = findTracedFunctionsFromPath(pkgRoot, pkgModules);
} else if (path.extname(editor.document.uri.fsPath) === BCODE_FILE_EXT) {
const modulePattern = /\bmodule\s+\d+\.\w+\b/g;
const moduleSequences = editor.document.getText().match(modulePattern);
if (!moduleSequences || moduleSequences.length === 0) {
throw new Error(`Cannot find module declaration in disassembly file '${editor.document.uri.fsPath}'`);
}
// there should be only one module declaration in a disassembly file
const [pkgAddrStr, module] = moduleSequences[0].substring('module'.length).trim().split('.');
const pkgAddr = parseInt(pkgAddrStr);
if (isNaN(pkgAddr)) {
throw new Error(`Cannot parse package address from '${pkgAddrStr}' in disassembly file '${editor.document.uri.fsPath}'`);
}
tracedFunctions = findTracedFunctionsFromTrace(pkgRoot, pkgAddr, module);
} else {
// this is a JSON (hopefully) trace as this function is only called if
// the active file is either a .move, .mvb, or .json file
const fpath = editor.document.uri.fsPath;
const tracedFunctionInfo = getTracedFunctionInfo(fpath);
tracedFunctions = [constructTraceInfo(fpath, tracedFunctionInfo)];
}

const tracedFunctions = findTracedFunctions(pkgRoot, pkgModules);

if (tracedFunctions.length === 0) {
if (!tracedFunctions || tracedFunctions.length === 0) {
throw new Error(`No traced functions found for package at '${pkgRoot}'`);
}

Expand All @@ -233,7 +264,6 @@ async function findTraceInfo(editor: vscode.TextEditor): Promise<string> {
if (!fun) {
throw new Error(`No function to be trace-debugged selected from\n` + tracedFunctions.join('\n'));
}

return fun;
}

Expand Down Expand Up @@ -268,15 +298,15 @@ async function findPkgRoot(active_file_path: string): Promise<string | undefined
}

/**
* Finds modules by searching the content of the file to look for
* Finds modules by searching the content of a source file to look for
* module declarations of the form `module <package>::<module>`.
* We cannot rely on the directory structure to find modules because
* trace info is generated based on module names in the source files.
*
* @param file_content content of the file.
* @returns modules in the file content of the form `<package>::<module>`.
*/
function findModules(file_content: string): string[] {
function findSrcModules(file_content: string): string[] {
const modulePattern = /\bmodule\s+\w+::\w+\b/g;
const moduleSequences = file_content.match(modulePattern);
return moduleSequences
Expand All @@ -285,28 +315,19 @@ function findModules(file_content: string): string[] {
}

/**
* Find all functions that have a corresponding trace file.
* Find all functions that have a corresponding trace file by looking at
* the trace file names that have the following format and extracting all
* function names that match:
* `<package>__<module>__<function_name>.json`.
*
* @param pkgRoot root directory of the package.
* @param pkgModules modules in the package of the form `<package>::<module>`.
* @returns list of functions of the form `<package>::<module>::<function>`.
* @throws Error (containing a descriptive message) if no trace files are found for the package.
* @returns list of functions of the form `<package>::<module>::<function_name>`.
* @throws Error (containing a descriptive message) if no traced functions are found for the package.
*/
function findTracedFunctions(pkgRoot: string, pkgModules: string[]): string[] {
function findTracedFunctionsFromPath(pkgRoot: string, pkgModules: string[]): string[] {

function getFiles(tracesDir: string): string[] {
try {
return fs.readdirSync(tracesDir);
} catch (err) {
throw new Error(`Error accessing 'traces' directory for package at '${pkgRoot}'`);
}
}
const tracesDir = path.join(pkgRoot, 'traces');

const filePaths = getFiles(tracesDir);
if (filePaths.length === 0) {
throw new Error(`No trace files for package at ${pkgRoot}`);
}
const filePaths = getTraceFiles(pkgRoot);
const result: [string, string[]][] = [];

pkgModules.forEach((module) => {
Expand All @@ -327,11 +348,128 @@ function findTracedFunctions(pkgRoot: string, pkgModules: string[]): string[] {
}).flat();
}

/**
* Find all functions that have a corresponding trace file by looking at
* the content of the trace file and its name (`<package>__<module>__<function_name>.json`).
* We need to match the package address, module name, and function name in the trace
* file itself as this is the only place where we can find the (potentially matching)
* package address (module name and function name could be extracted from the trace
* file name).
*
* @param pkgRoot root directory of the package.
* @param pkgAddr package address.
* @param module module name.
* @returns list of functions of the form `<package>::<module>::<function_name>`.
* @throws Error (containing a descriptive message) if no traced functions are found for the package.
*/
function findTracedFunctionsFromTrace(pkgRoot: string, pkgAddr: number, module: string): string[] {
const filePaths = getTraceFiles(pkgRoot);
const result: string[] = [];
for (const p of filePaths) {
const tracePath = path.join(pkgRoot, 'traces', p);
const tracedFunctionInfo = getTracedFunctionInfo(tracePath);
if (tracedFunctionInfo.pkgAddr === pkgAddr && tracedFunctionInfo.module === module) {
result.push(constructTraceInfo(tracePath, tracedFunctionInfo));
}
}
return result;
}

/**
* Retrieves traced function info from the trace file.
*
* @param tracePath path to the trace file.
* @returns traced function info containing package address, module, and function itself.
*/
function getTracedFunctionInfo(tracePath: string): TracedFunctionInfo {
let traceContent = undefined;
try {
traceContent = fs.readFileSync(tracePath, 'utf-8');
} catch {
throw new Error(`Error reading trace file '${tracePath}'`);
}

const trace = JSON.parse(traceContent);
if (!trace) {
throw new Error(`Error parsing trace file '${tracePath}'`);
}
if (trace.events.length === 0) {
throw new Error(`Empty trace file '${tracePath}'`);
}
const frame = trace.events[0]?.OpenFrame?.frame;
const pkgAddrStrInTrace = frame?.module?.address;
if (!pkgAddrStrInTrace) {
throw new Error(`No package address for the initial frame in trace file '${tracePath}'`);
}
const pkgAddrInTrace = parseInt(pkgAddrStrInTrace);
if (isNaN(pkgAddrInTrace)) {
throw new Error('Cannot parse package address '
+ pkgAddrStrInTrace
+ ' for the initial frame in trace file '
+ tracePath);
}
const moduleInTrace = frame?.module?.name;
if (!moduleInTrace) {
throw new Error(`No module name for the initial frame in trace file '${tracePath}'`);
}
const functionInTrace = frame?.function_name;
if (!functionInTrace) {
throw new Error(`No function name for the initial frame in trace file '${tracePath}'`);
}
return {
pkgAddr: pkgAddrInTrace,
module: moduleInTrace,
function: functionInTrace
};
}

/**
* Given trace file path and traced function, constructs a string of the form
* `<package>::<module>::<function_name>`, taking package from the trace file name
* (module name and function are the same in the file name and in the trace itself).
*
* @param tracePath path to the trace file.
* @param tracedFunctionInfo traced function info.
* @returns string of the form `<package>::<module>::<function_name>`.
*/
function constructTraceInfo(tracePath: string, tracedFunctionInfo: TracedFunctionInfo): string {
const tracedFileBaseName = path.basename(tracePath, path.extname(tracePath));
const fileBaseNameSuffix = '__' + tracedFunctionInfo.module + '__' + tracedFunctionInfo.function;
if (!tracedFileBaseName.endsWith(fileBaseNameSuffix)) {
throw new Error('Trace file name (' + tracedFileBaseName + ')'
+ 'does not end with expected suffix (' + fileBaseNameSuffix + ')'
+ ' obtained from concateneting module and entry function found in the trace');
}
const pkgName = tracedFileBaseName.substring(0, tracedFileBaseName.length - fileBaseNameSuffix.length);
return pkgName + '::' + tracedFunctionInfo.module + '::' + tracedFunctionInfo.function;
}

/**
* Return list of trace files for a given package.
*
* @param pkgRoot root directory of the package.
* @returns list of trace files for the package.
* @throws Error (containing a descriptive message) if no trace files are found for the package.
*/
function getTraceFiles(pkgRoot: string): string[] {
const tracesDir = path.join(pkgRoot, 'traces');
let filePaths = [];
try {
filePaths = fs.readdirSync(tracesDir);
} catch (err) {
throw new Error(`Error accessing 'traces' directory for package at '${pkgRoot}'`);
}
if (filePaths.length === 0) {
throw new Error(`No trace files for package at ${pkgRoot}`);
}
return filePaths;
}

/**
* Prompts the user to select a function to debug from a list of traced functions.
*
* @param tracedFunctions list of traced functions of the form `<package>::<module>::<function>`.
* @returns single function to debug of the form `<package>::<module>::<function>`.
* @param tracedFunctions list of traced functions of the form `<package>::<module>::<function_name>`.
* @returns single function to debug of the form `<package>::<module>::<function_name>`.
*/
async function pickFunctionToDebug(tracedFunctions: string[]): Promise<string | undefined> {
const selectedFunction = await vscode.window.showQuickPick(tracedFunctions.map(pkgFun => {
Expand Down

0 comments on commit 37743e3

Please sign in to comment.