├── .gitignore ├── CHANGELOG.md ├── screenshot-inline.png ├── screenshot-compiled.png ├── tsconfig.json ├── examples ├── basic.js ├── package.json └── deoptimize.js ├── src ├── util.ts ├── annotate.ts ├── extension.ts ├── instrument.ts ├── editors.ts ├── commands.ts ├── decompile.ts └── analyze.ts ├── LICENSE.md ├── README.md └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | out -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | No changes so far -------------------------------------------------------------------------------- /screenshot-inline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jonasdoubleyou/vs-v8-insights/HEAD/screenshot-inline.png -------------------------------------------------------------------------------- /screenshot-compiled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jonasdoubleyou/vs-v8-insights/HEAD/screenshot-compiled.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2020", 5 | "outDir": "out", 6 | "lib": [ 7 | "ES2020" 8 | ], 9 | "sourceMap": true, 10 | "rootDir": "src", 11 | "strict": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/basic.js: -------------------------------------------------------------------------------- 1 | 2 | function cold() { 3 | 4 | } 5 | cold(); 6 | 7 | function hot() { 8 | let sum = 0; 9 | for(let i = 0; i < 100; i++) { 10 | sum += i; 11 | } 12 | return sum; 13 | } 14 | 15 | let sumSum = 0; 16 | for(let n = 0; n < 1000; n++) 17 | sumSum += hot(); 18 | 19 | console.log("sumSum", sumSum); 20 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "examples", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "example.js", 6 | "scripts": { 7 | "basic-example": "node basic.js", 8 | "deoptimize-example": "node deoptimize.js" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "type": "module" 13 | } 14 | -------------------------------------------------------------------------------- /examples/deoptimize.js: -------------------------------------------------------------------------------- 1 | var b = false; 2 | 3 | function change_o() { 4 | // as long as b is false, this won't be reassigned ... 5 | if (b) o = { y : 1, x : 0}; 6 | } 7 | 8 | var o = { x : 1 }; 9 | 10 | function f() { 11 | change_o(); 12 | // ... and thus o.x can be compiled down 13 | return o.x; 14 | } 15 | 16 | // f and change_o are compiled somewhen 17 | %OptimizeFunctionOnNextCall(f); 18 | f() 19 | 20 | // a precondition changes 21 | b = true; 22 | 23 | // during execution of change_o o is reassigned and changes its shape, accessing o.x will now need a different offset and thus f (which is currently on the stack) becomes invalid 24 | f(); -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | 2 | export async function* byLine(stream: AsyncIterable): AsyncIterable { 3 | let acc = ""; 4 | for await(const chunk of stream) { 5 | const lines = (acc + chunk).split("\n"); 6 | acc = lines.pop() ?? ""; 7 | for(const line of lines) 8 | yield line; 9 | } 10 | } 11 | 12 | export function formatTime(time?: number) { 13 | if (time === undefined) return `?s`; 14 | 15 | const micro = time % 1000; 16 | let result = `${micro}μs`; 17 | 18 | if (time > 1000) { 19 | const milli = (time / 1000) % 1000; 20 | result = `${milli}ms ` + result; 21 | } 22 | 23 | if (time > 1000 * 1000) { 24 | const seconds = Math.floor(time / 1000 / 1000); 25 | result = `${seconds}s ` + result; 26 | } 27 | 28 | return result; 29 | } 30 | 31 | export function findLast(array: T[], predicate: (it: T) => boolean): T | null { 32 | for (let i = array.length - 1; i >= 0; i-= 1) { 33 | const value = array[i]; 34 | if (predicate(value)) return value; 35 | } 36 | 37 | return null; 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Jonas Wilms 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/annotate.ts: -------------------------------------------------------------------------------- 1 | import { window, OverviewRulerLane, TextDocument, CodeLens } from "vscode"; 2 | import { FileInsights, getInsights } from "./analyze"; 3 | import { formatTime } from "./util"; 4 | 5 | 6 | export function getLenses(document: TextDocument): CodeLens[] { 7 | const lenses: CodeLens[] = []; 8 | const insights = getInsights(document.uri.toString().replace("file://", "")); 9 | if (!insights) return []; 10 | 11 | for (const functionInsight of insights.functions) { 12 | let summary = `${functionInsight.isCompiled ? `compiled in ${formatTime(functionInsight.compileTime)}` : `interpreted`}`; 13 | 14 | if (functionInsight.wasDeoptimized) 15 | summary += ` - was deoptimized`; 16 | 17 | lenses.push({ 18 | range: functionInsight.nameLocation, 19 | isResolved: false, 20 | 21 | command: { 22 | command: "v8-insights.compilation-history", 23 | title: summary, 24 | arguments: [insights, functionInsight] 25 | } 26 | }); 27 | 28 | if (functionInsight.isCompiled) lenses.push({ 29 | range: functionInsight.nameLocation, 30 | isResolved: false, 31 | 32 | command: { 33 | command: "v8-insights.show-compiled-code", 34 | title: "compiled code", 35 | arguments: [functionInsight] 36 | } 37 | }); 38 | } 39 | return lenses; 40 | } -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import { ExtensionContext, commands, workspace, languages, TextDocument } from "vscode"; 2 | import { analysisDone } from "./analyze"; 3 | import { getLenses } from "./annotate"; 4 | import { runNPMCommand, runFileCommand, analyzeCommand, cleanupCommand, showCompiledCommand, showHistoryCommand, renameLabelCommand } from "./commands"; 5 | import { compiledEditorProvider, historyEditorProvider } from "./editors"; 6 | 7 | 8 | export function activate(context: ExtensionContext) { 9 | const commandDisposals = [ 10 | /* These commands can be run through CTRL+SHIFT+P or editor buttons */ 11 | commands.registerCommand('v8-insights.run-npm', runNPMCommand), 12 | commands.registerCommand("v8-insights.run-file", runFileCommand), 13 | commands.registerCommand('v8-insights.analyze', analyzeCommand), 14 | commands.registerCommand('v8-insights.cleanup', cleanupCommand), 15 | 16 | /* These commands can be run from the code lens on functions */ 17 | commands.registerCommand("v8-insights.show-compiled-code", showCompiledCommand), 18 | commands.registerCommand("v8-insights.compilation-history", showHistoryCommand), 19 | 20 | /* These commands can be run from the compiled editor menu */ 21 | commands.registerCommand("v8-insights.set-label", renameLabelCommand) 22 | ]; 23 | 24 | 25 | workspace.registerTextDocumentContentProvider("v8-compiled", compiledEditorProvider); 26 | workspace.registerTextDocumentContentProvider("v8-history", historyEditorProvider); 27 | 28 | /* The main functionality: A Code lens which shows compilation information inside JS code */ 29 | languages.registerCodeLensProvider("javascript", { 30 | onDidChangeCodeLenses: analysisDone.event, 31 | provideCodeLenses(document: TextDocument) { 32 | return getLenses(document); 33 | } 34 | }); 35 | 36 | context.subscriptions.push(...commandDisposals); 37 | } 38 | 39 | export function deactivate() {} 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VSCode V8 Insights 2 | 3 | **This only works reliably with NodeJS v14!** 4 | 5 | Sometimes when benchmarking JavaScript I ask myself _How can this be that fast?_ ... 6 | 7 | Although V8 has rich debug logging, finding the right logs is really a challenge. 8 | Thus I wrote this plugin which - slightly inspired by the 'deoptigate' plugin - indexes the V8 logs and displays them next to the JavaScript code. 9 | 10 | ![Inline Annotations from V8's TurboFan](./screenshot-inline.png) 11 | 12 | **Instrument -** To use this extension with NodeJS, `node` needs to be run using the correct flags to turn on debug logging. 13 | To launch an existing npm command listed in package.json with the correct commands, 14 | run CTRL + SHIFT + P _V8 Insights Run NPM command with instrumentation_. 15 | Alternatively open a self-containing JavaScript file (or the entrypoint of a module) and right-click _V8 Insights Run File with instrumentation_. 16 | Afterwards V8 will write a lot of logs inside the .vs-insights directory. 17 | 18 | **Analyze -** Once the application got warm or finished running, the logs can be analyzed and grouped by function. 19 | To trigger this either right-click _V8 Insights Analyze and Start_ or run the same from the command palette. 20 | 21 | **Inspect -** When opening a JavaScript file, functions will be annotated with whether they were optimized or deoptimized, 22 | also the compile time is shown. One can navigate to a function history, detailing all the events of the function, 23 | or also view the compiled code in Assembly. The Assembly Editor automatically inserts labels for jump targets, which can be renamed through right-click. 24 | It also has some basic syntax highlighting, though installing another extension for x86 Assembly syntax support is recommended. 25 | 26 | ![Editing Compiled Assembly](./screenshot-compiled.png) 27 | 28 | **Cleanup -** To delete the .vs-insights logs, run the command _V8 Insights Cleanup_ from the command palette 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "v8-insights", 3 | "displayName": "v8-insights", 4 | "description": "Analyze V8's optimizations from within VS Code", 5 | "version": "0.0.2", 6 | "repository": { 7 | "url": "https://github.com/Jonasdoubleyou/vs-v8-insights" 8 | }, 9 | "engines": { 10 | "vscode": "^1.64.0" 11 | }, 12 | "categories": [ 13 | "Other" 14 | ], 15 | "publisher": "JonasWilms", 16 | "activationEvents": [ 17 | "onCommand:v8-insights.run-file", 18 | "onCommand:v8-insights.run-npm", 19 | "onCommand:v8-insights.analyze", 20 | "onCommand:v8-insights.cleanup" 21 | ], 22 | "main": "./out/extension.js", 23 | "contributes": { 24 | "commands": [ 25 | { 26 | "command": "v8-insights.run-file", 27 | "title": "V8 Insights Run File with instrumentation" 28 | }, 29 | { 30 | "command": "v8-insights.run-npm", 31 | "title": "V8 Insights Run NPM command with instrumentation" 32 | }, 33 | { 34 | "command": "v8-insights.analyze", 35 | "title": "V8 Insights Analyze and Start" 36 | }, 37 | { 38 | "command": "v8-insights.cleanup", 39 | "title": "V8 Insights Cleanup" 40 | }, 41 | { 42 | "command": "v8-insights.set-label", 43 | "title": "Set Label inside Compiled Code" 44 | } 45 | ], 46 | "menus": { 47 | "editor/context": [ 48 | { 49 | "when": "resourceLangId == javascript", 50 | "command": "v8-insights.run-file", 51 | "group": "navigation@1" 52 | }, 53 | { 54 | "when": "resourceLangId == javascript", 55 | "command": "v8-insights.analyze", 56 | "group": "navigation@2" 57 | }, 58 | { 59 | "when": "resourceScheme == v8-compiled", 60 | "command": "v8-insights.set-label", 61 | "group": "navigation@1" 62 | } 63 | ] 64 | } 65 | }, 66 | "scripts": { 67 | "vscode:prepublish": "npm run compile", 68 | "compile": "tsc -p ./" 69 | }, 70 | "devDependencies": { 71 | "@types/vscode": "^1.64.0", 72 | "@types/glob": "^7.2.0", 73 | "@types/node": "14.x", 74 | "glob": "^7.2.0", 75 | "typescript": "^4.5.4" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/instrument.ts: -------------------------------------------------------------------------------- 1 | import { Uri, workspace } from "vscode"; 2 | 3 | /* By passing some flags into NodeJS (which passes them on to V8), V8 logs everything we need to analyze what's going on 4 | intg the .v8-insights folder */ 5 | const INSTRUMENTATION_ARGS = [ 6 | // "--trace-ignition-codegen", - in the future we might want to inspect IR, however this is logged to sterr ... 7 | "--no-logfile-per-isolate", 8 | "--allow-natives-syntax", // optional, though usually useful when inspecting V8 9 | "--trace-ic", // traces inline caching 10 | 11 | "--print-opt-code", 12 | "--redirect-code-traces", // opt-code is logged to a separate file 13 | "--redirect-code-traces-to=.v8-insights/opt-code", 14 | 15 | "--logfile=.v8-insights/log" 16 | ]; 17 | 18 | interface ScriptInfo { 19 | name: string; 20 | packagePath: Uri; 21 | folderPath: Uri; 22 | command: string; 23 | } 24 | 25 | export async function cleanupInstrumentationFolder() { 26 | for (const folder of workspace.workspaceFolders ?? []) { 27 | try { 28 | const insightsFolder = Uri.joinPath(folder.uri, "./.v8-insights"); 29 | await workspace.fs.delete(insightsFolder, { recursive: true }); 30 | 31 | } catch(error) {} 32 | } 33 | } 34 | 35 | /* Returns all 'node ...' commands in all package.jsons in all workspaces root folders */ 36 | export async function getScripts(): Promise { 37 | const scripts: ScriptInfo[] = []; 38 | 39 | for (const folder of workspace.workspaceFolders ?? []) { 40 | try { 41 | const packagePath = Uri.joinPath(folder.uri, "./package.json") 42 | const packageDeclaration = JSON.parse((await workspace.fs.readFile(packagePath)).toString()); 43 | for(const [name, command] of Object.entries(packageDeclaration.scripts as Record)) { 44 | if (!command.startsWith("node")) continue; 45 | scripts.push({ 46 | name, 47 | packagePath, 48 | folderPath: folder.uri, 49 | command 50 | }); 51 | } 52 | 53 | } catch(error) { 54 | console.log(`Failed to inspect package.json of Workspace(${folder.name}), skipping`, error); 55 | } 56 | } 57 | 58 | return scripts; 59 | } 60 | 61 | /* Takes a NodeJS command like 'node --some-arg file.js' and adds all necessary flags for V8 insights */ 62 | export function instrumentCommand(command: string) { 63 | const commandArgs = command.split(" "); 64 | commandArgs.shift(); // "node" 65 | const commandFile = commandArgs.pop(); 66 | 67 | commandArgs.push(...INSTRUMENTATION_ARGS); 68 | 69 | const enrichedCommand = `node ${commandArgs.join(" ")} ${commandFile}`; 70 | console.log(`Command '${command}' enriched as '${enrichedCommand}'`); 71 | 72 | return enrichedCommand; 73 | } -------------------------------------------------------------------------------- /src/editors.ts: -------------------------------------------------------------------------------- 1 | import { CancellationToken, EventEmitter, Range, TextDocument, TextDocumentContentProvider, Uri, ViewColumn, window, workspace } from "vscode"; 2 | import { CompileEndEvent, FileInsights, FunctionInsights, getInsights, getOptimizedCode } from "./analyze"; 3 | import { decompile } from "./decompile"; 4 | import { findLast } from "./util"; 5 | 6 | const reloadCompiled = new EventEmitter(); 7 | 8 | export async function openCompiledEditor(functionInsights: FunctionInsights) { 9 | const lastCompiled = findLast(functionInsights.events, it => it.name === "compile-end") as CompileEndEvent; 10 | if (!lastCompiled) { 11 | window.showErrorMessage(`Failed to find last compiled code for function ${functionInsights.name}`); 12 | return; 13 | } 14 | 15 | const uri = Uri.parse(`v8-compiled:${lastCompiled.memory.start}.asm`); 16 | const document = await workspace.openTextDocument(uri); 17 | const editor = await window.showTextDocument(document, ViewColumn.Beside); 18 | editor.setDecorations(highlightRed, findWords(document, /ret/g)); 19 | editor.setDecorations(highlightOrange, findWords(document, /j[a-z]+/g)); 20 | editor.setDecorations(dishighlight, findWords(document, /nop/g)); 21 | } 22 | 23 | export function reloadCompiledEditor(editor: Uri) { 24 | reloadCompiled.fire(editor); 25 | } 26 | 27 | export const compiledEditorProvider: TextDocumentContentProvider = { 28 | onDidChange: reloadCompiled.event, 29 | async provideTextDocumentContent(uri: Uri, token: CancellationToken): Promise { 30 | const memoryLocation = uri.path.split(".")[0]; 31 | const optimized = (await getOptimizedCode(memoryLocation)); 32 | const readable = decompile(optimized, uri); 33 | return readable; 34 | } 35 | 36 | 37 | } 38 | 39 | const highlightRed = window.createTextEditorDecorationType({ 40 | color: "white", 41 | backgroundColor: "red", 42 | fontWeight: "lighter" 43 | }); 44 | 45 | 46 | const highlightOrange = window.createTextEditorDecorationType({ 47 | color: "orange", 48 | fontWeight: "bold" 49 | }); 50 | 51 | const dishighlight = window.createTextEditorDecorationType({ 52 | color: "grey" 53 | }); 54 | 55 | 56 | 57 | export function findWords(document: TextDocument, regexp: RegExp): Range[] { 58 | const result: Range[] = []; 59 | const text = document.getText(); 60 | 61 | const expr = new RegExp(regexp); 62 | let match: RegExpMatchArray | null; 63 | while((match = expr.exec(text)) !== null) { 64 | console.log("findText", expr.lastIndex); 65 | result.push(new Range(document.positionAt(expr.lastIndex - match[0].length), document.positionAt(expr.lastIndex))); 66 | } 67 | return result; 68 | } 69 | 70 | export async function openHistoryEditor(fileInsights: FileInsights, functionInsights: FunctionInsights) { 71 | const uri = Uri.parse(`v8-history:${fileInsights.filename}:${functionInsights.nameLocation.start.line}:${functionInsights.nameLocation.start.character}`); 72 | await workspace.openTextDocument(uri).then(doc => window.showTextDocument(doc, ViewColumn.Beside)); 73 | } 74 | 75 | export const historyEditorProvider = { 76 | async provideTextDocumentContent(uri: Uri, token: CancellationToken): Promise { 77 | const [filename, line, column] = uri.path.split(":"); 78 | const insights = getInsights(filename); 79 | if (!insights) return `Unknown file ${filename}`; 80 | 81 | const functionInsight = insights.functions.find(it => it.nameLocation.start.line === +line); 82 | if (!functionInsight) return `Unknown function at ${uri.path}`; 83 | 84 | return JSON.stringify(functionInsight.events, null, 2); 85 | } 86 | } -------------------------------------------------------------------------------- /src/commands.ts: -------------------------------------------------------------------------------- 1 | import { window, Uri, workspace, commands } from "vscode"; 2 | import { analyze, FunctionInsights, FileInsights } from "./analyze"; 3 | import { getLabel, getLabelName } from "./decompile"; 4 | import { openCompiledEditor, openHistoryEditor, reloadCompiledEditor } from "./editors"; 5 | import { getScripts, instrumentCommand, cleanupInstrumentationFolder } from "./instrument"; 6 | 7 | async function askForAnalyze() { 8 | const pick = await window.showInformationMessage( 9 | "After some time the logs can be analyzed", 10 | "Analyze now" 11 | ); 12 | 13 | if (pick) commands.executeCommand("v8-insights.analyze"); 14 | } 15 | 16 | async function prepareInsightsFolder(folder: Uri) { 17 | const insightsFolder = Uri.joinPath(folder, "./.v8-insights"); 18 | await workspace.fs.createDirectory(insightsFolder); 19 | console.log(`Prepared insights folder at '${insightsFolder.toString()}'`); 20 | } 21 | 22 | export async function runNPMCommand() { 23 | const scripts = await getScripts(); 24 | 25 | if (!scripts.length) { 26 | window.showErrorMessage("Failed to find a package.json with 'node' scripts"); 27 | return; 28 | } 29 | 30 | const commandName = await window.showQuickPick(scripts.map(it => it.name), { 31 | title: "Package Command to run" 32 | }); 33 | 34 | const commandToRun = scripts.find(it => it.name === commandName); 35 | if (!commandToRun) return; 36 | 37 | await prepareInsightsFolder(commandToRun.folderPath); 38 | 39 | const instrumentedCommand = instrumentCommand(commandToRun.command); 40 | 41 | const runner = window.createTerminal(`${commandToRun.name} - V8 Insights`); 42 | runner.show(true); 43 | runner.sendText(instrumentedCommand); 44 | 45 | await askForAnalyze(); 46 | } 47 | 48 | export async function runFileCommand() { 49 | let file = window.activeTextEditor?.document.uri.toString().replace("file://", ""); 50 | if (!file) { 51 | window.showErrorMessage(`Please open a JavaScript file to run this command`); 52 | return; 53 | } 54 | 55 | await prepareInsightsFolder(workspace.getWorkspaceFolder(window.activeTextEditor!.document.uri)!.uri); 56 | 57 | const runner = window.createTerminal(`${file.split("/").pop()} - V8 Insights`); 58 | runner.show(true); 59 | runner.sendText(instrumentCommand(`node ${file}`)); 60 | 61 | await askForAnalyze(); 62 | } 63 | 64 | 65 | export async function analyzeCommand() { 66 | const editor = window.activeTextEditor; 67 | if (!editor) return; 68 | 69 | const currentFile = editor.document.uri; 70 | if (!currentFile) return; 71 | 72 | window.showInformationMessage(`Indexing logs in .v8-insights`); 73 | 74 | await analyze(); 75 | 76 | window.showInformationMessage(`Indexing done`); 77 | } 78 | 79 | export async function showCompiledCommand(functionInsights?: FunctionInsights) { 80 | if (!functionInsights) { 81 | window.showErrorMessage("Compiled code can only be shown for a specific function"); 82 | return; 83 | } 84 | 85 | await openCompiledEditor(functionInsights); 86 | } 87 | 88 | export async function showHistoryCommand(fileInsights?: FileInsights, functionInsights?: FunctionInsights) { 89 | if (!fileInsights || !functionInsights) { 90 | window.showErrorMessage("The History can only be shown for a specific function"); 91 | return; 92 | } 93 | 94 | await openHistoryEditor(fileInsights, functionInsights); 95 | } 96 | 97 | export async function cleanupCommand() { 98 | await cleanupInstrumentationFolder(); 99 | window.showInformationMessage(`Successfully cleaned up the v8 insight traces`); 100 | } 101 | 102 | export async function renameLabelCommand() { 103 | const location = window.activeTextEditor?.selection.active.line; 104 | if (!location) { 105 | window.showErrorMessage("Unknown file location"); 106 | return; 107 | } 108 | 109 | const label = getLabel(location); 110 | if (!label) { 111 | window.showErrorMessage("Please select a line with a label"); 112 | return; 113 | } 114 | 115 | const newLabel = await window.showInputBox({ 116 | ignoreFocusOut: true, 117 | title: `new name for ${getLabelName(label)}` 118 | }); 119 | 120 | if (!newLabel) return; 121 | 122 | label.label = newLabel; 123 | reloadCompiledEditor(label.editor); 124 | } -------------------------------------------------------------------------------- /src/decompile.ts: -------------------------------------------------------------------------------- 1 | import { Uri } from "vscode"; 2 | 3 | interface Label { 4 | id: number; 5 | // the editor line where this label appears 6 | line?: number; 7 | // the number of jumps going here 8 | jumpCount: number; 9 | // the editor lines where this label is used as a jump target 10 | referencedAt: number[]; 11 | // a user set label 12 | label?: string; 13 | // the editor this label is used in 14 | editor: Uri; 15 | // naive label type deduction: 16 | // whether the label is reached through a backward jump 17 | backward: boolean; 18 | // whether the label is reached through a forward jump 19 | forward: boolean; 20 | } 21 | 22 | const labelForLocation = new Map(); 23 | 24 | export function getLabel(line: number) { 25 | for (const label of labelForLocation.values()) { 26 | if (label.line === line || label.referencedAt.includes(line)) { 27 | return label; 28 | } 29 | } 30 | 31 | return null; 32 | } 33 | 34 | export function getLabelName(label: Label) { 35 | if (label.label) 36 | return label.label; 37 | // user did not set a label, let's deduce something 38 | if (label.jumpCount === 1 && label.backward) 39 | return `loop${label.id}`; 40 | if (label.backward && !label.forward) 41 | return `reentry${label.id}`; 42 | if (label.forward && !label.backward) 43 | return `skip${label.id}`; 44 | return `label${label.id}`; 45 | } 46 | 47 | 48 | export function decompile(lines: string[], editor: Uri): string { 49 | console.log("labels", JSON.stringify(labelForLocation.entries(), null, 2)); 50 | const result: string[] = []; 51 | let labelCount = 0; 52 | 53 | const destructLine = (line: string) => { 54 | const location = line.slice(0, line.indexOf(" ")); 55 | const instructionBegin = line.indexOf(" ", 30); 56 | let instruction = line.slice(instructionBegin).trimStart(); 57 | 58 | // Drop REX.W prefixes as they're useless according to https://stackoverflow.com/questions/36788685/meaning-of-rex-w-prefix-before-amd64-jmp-ff25 59 | if (instruction.startsWith("REX.W")) 60 | instruction = instruction.slice(6); 61 | 62 | return { location, instruction }; 63 | }; 64 | 65 | const { location: startLocation } = destructLine(lines[0]); 66 | const { location: endLocation } = destructLine(lines[lines.length - 2]); 67 | 68 | console.log(`Code Range ${startLocation} - ${endLocation}`); 69 | 70 | for(const [index, line] of lines.entries()) { 71 | const { location, instruction } = destructLine(line); 72 | 73 | if (instruction.startsWith("j")) { 74 | let [jumpType, target] = instruction.split(" "); 75 | // ignore jumps from registers 76 | if (!target.startsWith("0x")) continue; 77 | 78 | if (!labelForLocation.has(target)) { 79 | console.log("setting label for", target); 80 | 81 | labelForLocation.set(target, { referencedAt: [], editor, backward: false, forward: false, id: labelCount++, jumpCount: 0 }); 82 | } 83 | 84 | const label = labelForLocation.get(target)!; 85 | label.backward ||= target < location; 86 | label.forward ||= target > location; 87 | label.jumpCount += 1; 88 | } 89 | } 90 | 91 | for(const line of lines) { 92 | const { location, instruction } = destructLine(line); 93 | console.log("location", location); 94 | const label = labelForLocation.get(location); 95 | if (label) { 96 | // Instead of taking the position in 'lines', the position in 'result' is taken, 97 | // as injecting labels increases the number of lines 98 | label.line = result.length; 99 | console.log("got label") 100 | result.push(`${getLabelName(label)}:`); 101 | } 102 | 103 | if (instruction.startsWith("j")) { 104 | const [jumpType, target] = instruction.split(" "); 105 | // ignore jumps from registers 106 | if (!target.startsWith("0x")) { 107 | result.push(` ${jumpType} ${target}`); 108 | } else if(target > endLocation || target < startLocation) { 109 | result.push(` ${jumpType} ${target} (outside)`); 110 | } else { 111 | const targetLabel = labelForLocation.get(target)!; 112 | targetLabel.referencedAt.push(result.length); 113 | result.push(` ${jumpType} ${getLabelName(targetLabel)}`); 114 | } 115 | 116 | } else { 117 | result.push(` ${instruction}`); 118 | } 119 | } 120 | 121 | return result.join("\n"); 122 | } -------------------------------------------------------------------------------- /src/analyze.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter, Location, Position, Range, Uri, window, workspace } from "vscode"; 2 | import * as fs from "fs"; 3 | import { byLine, findLast } from "./util"; 4 | 5 | export interface FileInsights { 6 | filename: string; 7 | functions: FunctionInsights[]; 8 | } 9 | 10 | export interface FunctionInsights { 11 | name: string; 12 | nameLocation: Range; 13 | events: FunctionEvent[]; 14 | isCompiled: boolean; 15 | wasDeoptimized: boolean; 16 | compileTime?: number; 17 | } 18 | 19 | interface MemoryArea { start: string /* as 0x... */, size: number } 20 | 21 | export interface ParseEvent { 22 | name: "parse"; 23 | memory: MemoryArea; 24 | } 25 | 26 | export interface CompileStartEvent { 27 | name: "compile-start"; 28 | } 29 | 30 | export interface CompileEndEvent { 31 | name: "compile-end"; 32 | memory: MemoryArea; 33 | } 34 | 35 | export interface DeoptimizeEvent { 36 | name: "deoptimize"; 37 | reason: string; 38 | from: string; 39 | } 40 | 41 | type FunctionEvent = ({ at: number }) & (ParseEvent | CompileStartEvent | CompileEndEvent | DeoptimizeEvent); 42 | 43 | function getInsightsFolder() { 44 | const currentFile = window.activeTextEditor?.document.uri; 45 | if (!currentFile) { 46 | window.showErrorMessage("Failed to determine current file"); 47 | throw new Error(`Open a file`); 48 | } 49 | 50 | const currentWorkspace = workspace.getWorkspaceFolder(currentFile); 51 | if (!currentWorkspace) { 52 | window.showErrorMessage("Failed to determine current workspace"); 53 | throw new Error(`No workspace found for file '${currentFile.toString()}`); 54 | } 55 | 56 | return Uri.joinPath(currentWorkspace!.uri, "./.v8-insights"); 57 | } 58 | 59 | function readTrace(name: string) { 60 | const filePath = Uri.joinPath(getInsightsFolder(), name).toString().replace("file://", ""); 61 | console.log("creating read stream", filePath); 62 | return fs.createReadStream(filePath); 63 | } 64 | 65 | function parseFullLocation(path: string, unnamed: boolean) { 66 | let name: string = "unknown", fullLocation: string; 67 | 68 | if (unnamed) { 69 | fullLocation = path.slice(1, -1); 70 | } else { 71 | ([name, fullLocation] = path.split(" ")); 72 | } 73 | 74 | const [filename, line, column] = fullLocation.replace("file://", "").split(":"); 75 | return { name, filename, line: +line - 1, column: +column, fullLocation }; 76 | } 77 | 78 | 79 | const insights: Map = new Map(); 80 | 81 | export const analysisDone = new EventEmitter(); 82 | 83 | export async function analyze(): Promise { 84 | insights.clear(); 85 | codeCache.clear(); 86 | 87 | const functionsByLocation = new Map(); 88 | const functionsByCompiledLocation = new Map(); 89 | 90 | function createFunctionInsights(source: string, unnamedSource: boolean): FunctionInsights { 91 | const { name, filename, line, column, fullLocation } = parseFullLocation(source, unnamedSource); 92 | 93 | let functionInsight = functionsByLocation.get(fullLocation); 94 | 95 | if (!functionInsight) { 96 | functionInsight = { 97 | name, 98 | events: [], 99 | nameLocation: new Range( 100 | new Position(line, column), 101 | new Position(line, column + name.length) 102 | ), 103 | isCompiled: false, 104 | wasDeoptimized: false 105 | } 106 | 107 | 108 | let fileInsights = insights.get(filename); 109 | if (!fileInsights) { 110 | fileInsights = { filename, functions: [] }; 111 | insights.set(filename, fileInsights); 112 | console.log(`Registered insights for file '${filename}'`); 113 | } 114 | 115 | fileInsights.functions.push(functionInsight); 116 | functionsByLocation.set(fullLocation, functionInsight); 117 | console.log(`Registered insights for function '${filename}' -> '${name}'`); 118 | } 119 | 120 | return functionInsight; 121 | } 122 | 123 | for await(const logLine of byLine(readTrace("./log"))) { 124 | if (logLine.startsWith("code-creation,Script,11")) { 125 | // code-creation,Script,11,129974,0x250f4074d83e,81, file:///home/jonas/projects/vs-v8-insights/examples/deoptimize.js:1:1,0x250f4074d5f0,~ 126 | const [,,,timestamp,memoryStart,memorySize,source] = logLine.split(","); 127 | const insights = createFunctionInsights(source, false); 128 | 129 | insights.events.push({ 130 | name: "parse", 131 | at: +timestamp, 132 | memory: { start: memoryStart, size: +memorySize } 133 | }); 134 | } else if(logLine.startsWith("code-creation,LazyCompile,11")) { 135 | // code-creation,LazyCompile,11,130193,0x250f4074dcb6,16,f file:///home/jonas/projects/vs-v8-insights/examples/deoptimize.js:10:11,0x250f4074d740,~ 136 | const [,,,timestamp,memoryStart,memorySize,source] = logLine.split(","); 137 | const insights = createFunctionInsights(source, false); 138 | 139 | insights.events.push({ 140 | name: "compile-start", 141 | at: +timestamp 142 | }); 143 | } else if (logLine.startsWith("code-creation,LazyCompile,0")) { 144 | // code-creation,LazyCompile,0,131818,0xadc3bf43380,161,f file:///home/jonas/projects/vs-v8-insights/examples/deoptimize.js:10:11,0x250f4074d740,* 145 | const [,,,timestamp,memoryStart,memorySize,source] = logLine.split(","); 146 | const insights = createFunctionInsights(source, false); 147 | 148 | 149 | const compileStart = findLast(insights.events, it => it.name === "compile-start"); 150 | if (compileStart) { 151 | insights.compileTime = +timestamp - compileStart.at; 152 | } 153 | 154 | insights.isCompiled = true; 155 | 156 | insights.events.push({ 157 | name: "compile-end", 158 | at: +timestamp, 159 | memory: { start: memoryStart, size: +memorySize } 160 | }); 161 | 162 | functionsByCompiledLocation.set(memoryStart, insights); 163 | } else if(logLine.startsWith("code-deopt")) { 164 | // code-deopt,131848,288,0xadc3bf43380,-1,165,soft,,Insufficient type feedback for call 165 | 166 | const [, timestamp, size, code, inliningId, scriptOffset, bailoutType, source, reason] = logLine.split(","); 167 | const insights = functionsByCompiledLocation.get(code); 168 | if (!insights) continue; 169 | 170 | insights.isCompiled = false; 171 | insights.wasDeoptimized = true; 172 | 173 | insights.events.push({ 174 | name: "deoptimize", 175 | at: +timestamp, 176 | reason, 177 | from: source.slice(1, -1) 178 | }); 179 | } 180 | } 181 | 182 | analysisDone.fire(); 183 | } 184 | 185 | export function getInsights(file: string) { 186 | console.log(`Getting insights for file '${file}'`); 187 | return insights.get(file); 188 | } 189 | 190 | const codeCache = new Map(); 191 | 192 | export async function getOptimizedCode(memoryLocation: string) { 193 | if (codeCache.has(memoryLocation)) { 194 | console.log(`Read optimized code for ${memoryLocation} from cache`); 195 | return codeCache.get(memoryLocation)!; 196 | } 197 | 198 | const result: string[] = []; 199 | console.log(`Reading optimized code from ${memoryLocation}`); 200 | 201 | let scan = false; 202 | for await(const optLine of byLine(readTrace("./opt-code"))) { 203 | if (!scan) { 204 | if (!optLine.startsWith(memoryLocation)) continue; 205 | scan = true; 206 | continue; 207 | } 208 | if (scan) { 209 | if (optLine === "") scan = false; 210 | 211 | } 212 | 213 | result.push(optLine) 214 | } 215 | 216 | codeCache.set(memoryLocation, result); 217 | return result; 218 | } --------------------------------------------------------------------------------