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
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,15 @@ dist-ssr
.yarn/install-state.gz
.vite/

# Extension build output
extension/out
extension/node_modules

# Editor directories and files
.vscode/*
!.vscode/extensions.json
!.vscode/launch.json
!.vscode/tasks.json
.idea
.DS_Store
*.suo
Expand Down
17 changes: 17 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Run Extension",
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}/extension"
],
"outFiles": [
"${workspaceFolder}/extension/out/**/*.js"
],
"preLaunchTask": "Build Extension"
}
]
}
34 changes: 34 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Build Webapp",
"type": "shell",
"command": "yarn build",
"group": "build",
"problemMatcher": []
},
{
"label": "Build Extension",
"type": "shell",
"command": "npm run build",
"options": {
"cwd": "${workspaceFolder}/extension"
},
"group": "build",
"dependsOn": "Build Webapp",
"problemMatcher": ["$tsc"]
},
{
"label": "Watch Extension",
"type": "shell",
"command": "npm run watch",
"options": {
"cwd": "${workspaceFolder}/extension"
},
"isBackground": true,
"group": "build",
"problemMatcher": ["$tsc-watch"]
}
]
}
33 changes: 33 additions & 0 deletions extension/esbuild.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// @ts-check
const esbuild = require('esbuild');

const watch = process.argv.includes('--watch');

/** @type {import('esbuild').BuildOptions} */
const buildOptions = {
entryPoints: ['src/extension.ts'],
bundle: true,
outfile: 'out/extension.js',
external: ['vscode'],
format: 'cjs',
platform: 'node',
target: 'es2020',
sourcemap: true,
minify: false,
};

async function main() {
if (watch) {
const ctx = await esbuild.context(buildOptions);
await ctx.watch();
console.log('Watching for changes...');
} else {
await esbuild.build(buildOptions);
console.log('Build complete.');
}
}

main().catch((e) => {
console.error(e);
process.exit(1);
});
48 changes: 48 additions & 0 deletions extension/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"name": "zephelin-trace-viewer",
"displayName": "Zephelin Trace Viewer",
"description": "Visualize Zephyr RTOS trace files inside VS Code",
"version": "0.0.1",
"publisher": "adi",
"license": "Apache-2.0",
"engines": {
"vscode": "^1.85.0"
},
"categories": [
"Visualization"
],
"activationEvents": [],
"main": "./out/extension.js",
"contributes": {
"customEditors": [
{
"viewType": "zephelinTraceViewer",
"displayName": "Zephelin Trace Viewer",
"selector": [
{
"filenamePattern": "*.tef"
}
],
"priority": "default"
},
{
"viewType": "zephelinTraceViewer",
"displayName": "Zephelin Trace Viewer",
"selector": [
{
"filenamePattern": "*.json"
}
],
"priority": "option"
}
]
},
"scripts": {
"build": "node esbuild.js",
"watch": "node esbuild.js --watch"
},
"devDependencies": {
"@types/vscode": "^1.85.0",
"esbuild": "^0.24.0"
}
}
194 changes: 194 additions & 0 deletions extension/src/extension.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
/*
* Copyright (c) 2026 Analog Devices, Inc.
*
* SPDX-License-Identifier: Apache-2.0
*/

import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';
import * as crypto from 'crypto';

export function activate(context: vscode.ExtensionContext): void {
const provider = new TraceEditorProvider(context);

context.subscriptions.push(
vscode.window.registerCustomEditorProvider(
TraceEditorProvider.viewType,
provider,
{
webviewOptions: {
retainContextWhenHidden: true,
},
}
)
);
}

export function deactivate(): void {
// Nothing to dispose
}

class TraceEditorProvider implements vscode.CustomTextEditorProvider {
static readonly viewType = 'zephelinTraceViewer';

constructor(private readonly context: vscode.ExtensionContext) { }

async resolveCustomTextEditor(
document: vscode.TextDocument,
webviewPanel: vscode.WebviewPanel,
_token: vscode.CancellationToken
): Promise<void> {
const distPath = this.getDistPath();

webviewPanel.webview.options = {
enableScripts: true,
localResourceRoots: [vscode.Uri.file(distPath)],
};

webviewPanel.webview.html = this.getWebviewHtml(
webviewPanel.webview,
document,
distPath
);

const updateWebview = () => {
const text = document.getText();
const base64 = Buffer.from(text).toString('base64');
webviewPanel.webview.postMessage({ command: 'reloadTrace', data: base64 });
};

let debounceTimer: ReturnType<typeof setTimeout> | undefined;
const changeSubscription = vscode.workspace.onDidChangeTextDocument(e => {
if (e.document.uri.toString() !== document.uri.toString()) {
return;
}
if (debounceTimer) {
clearTimeout(debounceTimer);
}
debounceTimer = setTimeout(updateWebview, 300);
});

webviewPanel.onDidDispose(() => changeSubscription.dispose());
this.context.subscriptions.push(changeSubscription);
}

/**
* Resolves the path to the built webapp dist/ folder.
* Looks for it at `../dist` relative to the extension directory.
*/
private getDistPath(): string {
const extensionRoot = this.context.extensionPath;
return path.join(extensionRoot, '..', 'dist');
}

/**
* Generates the webview HTML content.
*
* Reads the built index.html from dist/, rewrites asset paths
* to webview URIs, injects the trace data as base64 into
* window.initialTraces, and adds a CSP meta tag with a nonce.
*/
private getWebviewHtml(
webview: vscode.Webview,
document: vscode.TextDocument,
distPath: string
): string {
const nonce = this.getNonce();
const distUri = vscode.Uri.file(distPath);

// Read the built index.html
const indexHtmlPath = path.join(distPath, 'index.html');
let html: string;
try {
html = fs.readFileSync(indexHtmlPath, 'utf-8');
} catch {
return this.getErrorHtml(
'Webapp not built',
'Could not find dist/index.html. Run <code>yarn build</code> in the project root first.'
);
}

// Base64-encode the document content for injection
const traceContent = document.getText();
const base64Content = Buffer.from(traceContent).toString('base64');

// Rewrite relative asset paths (href="./..." and src="./...") to webview URIs
html = html.replace(
/(href|src)="\.\/([^"]+)"/g,
(_match, attr, relativePath) => {
const assetUri = webview.asWebviewUri(
vscode.Uri.joinPath(distUri, relativePath)
);
return `${attr}="${assetUri}"`;
}
);

// Add nonce to all <script> tags
html = html.replace(
/<script(\s)/g,
`<script nonce="${nonce}"$1`
);

// Inject CSP meta tag and the initialTraces script into <head>
const cspMeta = `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'nonce-${nonce}' ${webview.cspSource} 'wasm-unsafe-eval'; style-src ${webview.cspSource} 'unsafe-inline' https://fonts.googleapis.com; img-src ${webview.cspSource} data:; font-src ${webview.cspSource} https://fonts.gstatic.com; connect-src ${webview.cspSource};">`;
const traceScript = `<script nonce="${nonce}">window.initialTraces = "${base64Content}";</script>`;

html = html.replace(
'</head>',
`${cspMeta}\n${traceScript}\n</head>`
);

return html;
}

/**
* Returns a fallback error page if the webapp build is missing.
*/
private getErrorHtml(title: string, message: string): string {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Error</title>
<style>
body {
font-family: var(--vscode-font-family);
color: var(--vscode-foreground);
background: var(--vscode-editor-background);
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
}
.error-container {
text-align: center;
max-width: 500px;
padding: 2rem;
}
h1 { color: var(--vscode-errorForeground); }
code {
background: var(--vscode-textCodeBlock-background);
padding: 2px 6px;
border-radius: 3px;
}
</style>
</head>
<body>
<div class="error-container">
<h1>${title}</h1>
<p>${message}</p>
</div>
</body>
</html>`;
}

/**
* Generates a random nonce string for CSP.
*/
private getNonce(): string {
return crypto.randomBytes(16).toString('hex');
}
}
19 changes: 19 additions & 0 deletions extension/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./out",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "out"]
}
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
"dev": "vite",
"build": "vite build --base=./",
"build-singlefile": "vite build --base=./ --mode=single-file",
"preview": "vite preview"
"preview": "vite preview",
"build:ext": "cd extension && npm run build",
"build:all": "yarn build && yarn build:ext"
},
"dependencies": {
"@bokeh/bokehjs": "^3.7.3",
Expand Down
1 change: 1 addition & 0 deletions public/advanced.tef

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,15 @@ if (tracesBaked) {
.catch(e => console.error(e));
}

window.addEventListener('message', (event) => {
const message = event.data;
if (message?.command === 'reloadTrace' && typeof message.data === 'string') {
useSpeedscopeLoader(false)
.loadProfile(() => importProfilesFromBase64('tracefile', message.data))
.catch(e => console.error(e));
}
});

// Configure Speedscope - only once before application starts
configureSpeedscope();

Expand Down
Loading