Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions media/panel.html
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,10 @@ <h2>Memscope</h2>
</button>
</span>
</div>
<label> <input type="checkbox" id="live" checked /> Live Preview </label>
<label><input type="checkbox" id="live" checked />Live Preview</label>
<label><input type="checkbox" id="pause" />Pause on draw</label>
<div class="button-container">
<button onclick="fetchImage()">Draw</button>
<button onclick="requestDraw()">Draw</button>
<button onclick="copyToClipboard()">Copy</button>
<button onclick="saveImage('png')">Save PNG</button>
<button onclick="saveImage('jpg')">Save JPG</button>
Expand Down
13 changes: 12 additions & 1 deletion media/panel.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,9 @@ document.querySelectorAll('.datatype').forEach(button => {
selectedDatatype = button.getAttribute('data-type');
selectedTypeSize = parseInt(button.getAttribute('data-size'), 10);

if (document.getElementById('live').checked)
if (document.getElementById('live').checked) {
fetchImage();
}
});
});

Expand All @@ -134,6 +135,16 @@ function fetchImage() {
});
}

function requestDraw() {
const pause = document.getElementById("pause").checked;
if (pause) {
vscode.postMessage({
command: 'pause',
});
}
fetchImage();
}

function clear() {
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
Expand Down
121 changes: 8 additions & 113 deletions src/memScopePanel.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import * as vscode from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
import { handleFetch, handlePause } from './utils';

export class MemScopePanel {
public static currentPanel: MemScopePanel | undefined;
private readonly panel: vscode.WebviewPanel;
private readonly extensionUri: vscode.Uri;
private disposables: vscode.Disposable[] = [];
private currentRequestId: number = 0;
private abortController: AbortController | null = null;

public static createOrShow(context: vscode.ExtensionContext) {
const column = vscode.ViewColumn.Beside;
Expand Down Expand Up @@ -47,117 +46,13 @@ export class MemScopePanel {
}

private async handleMessage(msg: any) {
if (msg.command === 'fetch') {
// Cancel previous request
if (this.abortController) {
this.abortController.abort();
}

this.abortController = new AbortController();
const signal = this.abortController.signal;
const isCancelled = () => signal.aborted;

try {
const session = vscode.debug.activeDebugSession;
if (!session) {
throw new Error("No active debug session!");
}

if (isCancelled()) {
return;
}

const threads = await session.customRequest('threads');
const threadId = threads.threads[0].id;

if (isCancelled()) {
return;
}

const stack = await session.customRequest('stackTrace', {
threadId,
startFrame: 0,
levels: 1
});
const frameId = stack.stackFrames[0].id;

if (isCancelled()) {
return;
}

const getValue = async (expr: string) => {
const res = await session.customRequest('evaluate', {
expression: expr,
frameId,
context: 'watch'
});
return res.result;
};

const getPtr = async (expr: string) => {
const res = await session.customRequest('evaluate', {
expression: expr,
frameId,
context: 'watch'
});
return res.memoryReference;
};

const width = parseInt(await getValue(msg.widthExpr));
const height = parseInt(await getValue(msg.heightExpr));
const channels = parseInt(await getValue(msg.channels));
const datatype = msg.datatype || 'uint8';
const typeSize = msg.typeSize || 1;
const count = width * height * channels * typeSize;

if (isNaN(count) || count <= 0) {
throw new Error("Invalid image dimensions");
}

if (isCancelled()) {
return;
}

const rowsPerChunk = 100;
const bytesPerRow = width * channels * typeSize;
const maxChunkSize = bytesPerRow * rowsPerChunk;

for (let i = 0; i < count; i += maxChunkSize) {
const currentReadSize = Math.min(maxChunkSize, count - i);

const elementOffset = i / typeSize;
const pointerStr = (await getPtr(`${msg.pointerExpr} + ${elementOffset}`) || '0');

if (isCancelled()) { return; }

const memory = await session.customRequest('readMemory', {
memoryReference: pointerStr,
count: currentReadSize
});

const uint8Array = new Uint8Array(Buffer.from(memory.data, 'base64'));

this.panel.webview.postMessage({
command: 'render',
memory: uint8Array.buffer,
width,
height,
channels,
datatype,
typeSize,
// StartRow tells the webview where to begin drawing this block
startRow: Math.floor(i / bytesPerRow),
totalChunks: Math.ceil(count / maxChunkSize),
requestID: msg.requestID
});
}

} catch (err: any) {
if (!signal.aborted) {
this.panel.webview.postMessage({ command: 'error', message: err.message || String(err), requestID: msg.requestID });
}
}
this.abortController = null;
switch (msg.command) {
case 'fetch':
handleFetch(msg, this.panel);
break;
case 'pause':
handlePause(msg, this.panel);
break;
}
}

Expand Down
120 changes: 120 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import vscode from 'vscode';

let abortController: AbortController | null = null;

export async function handleFetch(msg: any, panel: vscode.WebviewPanel) {
if (abortController) {
abortController.abort();
}

abortController = new AbortController();
const signal = abortController.signal;
const isCancelled = () => signal.aborted;

try {
const session = vscode.debug.activeDebugSession;
if (!session) {
throw new Error("No active debug session!");
}

if (isCancelled()) {
return;
}

const threads = await session.customRequest('threads');
const threadId = threads.threads[0].id;

if (isCancelled()) {
return;
}

const stack = await session.customRequest('stackTrace', {
threadId,
startFrame: 0,
levels: 1
});
const frameId = stack.stackFrames[0].id;

if (isCancelled()) {
return;
}

const getValue = async (expr: string) => {
const res = await session.customRequest('evaluate', {
expression: expr,
frameId,
context: 'watch'
});
return res.result;
};

const getPtr = async (expr: string) => {
const res = await session.customRequest('evaluate', {
expression: expr,
frameId,
context: 'watch'
});
return res.memoryReference;
};

const width = parseInt(await getValue(msg.widthExpr));
const height = parseInt(await getValue(msg.heightExpr));
const channels = parseInt(await getValue(msg.channels));
const datatype = msg.datatype || 'uint8';
const typeSize = msg.typeSize || 1;
const count = width * height * channels * typeSize;

if (isNaN(count) || count <= 0) {
throw new Error("Invalid image dimensions");
}

if (isCancelled()) {
return;
}

const rowsPerChunk = 100;
const bytesPerRow = width * channels * typeSize;
const maxChunkSize = bytesPerRow * rowsPerChunk;

for (let i = 0; i < count; i += maxChunkSize) {
const currentReadSize = Math.min(maxChunkSize, count - i);

const elementOffset = i / typeSize;
const pointerStr = (await getPtr(`${msg.pointerExpr} + ${elementOffset}`) || '0');

if (isCancelled()) { return; }

const memory = await session.customRequest('readMemory', {
memoryReference: pointerStr,
count: currentReadSize
});

const uint8Array = new Uint8Array(Buffer.from(memory.data, 'base64'));

panel.webview.postMessage({
command: 'render',
memory: uint8Array.buffer,
width,
height,
channels,
datatype,
typeSize,
// StartRow tells the webview where to begin drawing this block
startRow: Math.floor(i / bytesPerRow),
totalChunks: Math.ceil(count / maxChunkSize),
requestID: msg.requestID
});
}

} catch (err: any) {
if (!signal.aborted) {
panel.webview.postMessage({ command: 'error', message: err.message || String(err), requestID: msg.requestID });
}
}
abortController = null;
}

export async function handlePause(msg: any, panel: vscode.WebviewPanel) {
const session = vscode.debug.activeDebugSession;
await session?.customRequest('pause');
}