Skip to content

Add developer guide for vscode-trace-extension#355

Merged
bhufmann merged 7 commits into
eclipse-cdt-cloud:masterfrom
bhufmann:developer-guide
Nov 7, 2025
Merged

Add developer guide for vscode-trace-extension#355
bhufmann merged 7 commits into
eclipse-cdt-cloud:masterfrom
bhufmann:developer-guide

Conversation

@bhufmann

@bhufmann bhufmann commented Oct 2, 2025

Copy link
Copy Markdown
Contributor

What it does

  • doc: Add DEVELOPER-GUIDE.md for 3PP extensions to work with trace-viewer. Remove corresponding section in README.md and add reference to the developer guide.
  • doc: Add a guide to create custom 3PP vscode-extension
    • This custom extension interfaces the vscode-trace-extension.
    • It also explains how to extend the tsp-typescript-client for custom endpoints not part of TSP
  • doc: Add an example vscode webview to the example 3PP vscode extension
  • doc: Update example webview to use vscode-messenger library
  • doc: Add table of contents to developer guide
  • doc: Update to consider bigints using the SerializationUtil

Initial version of guide was generated using Amazon Q. Then it was manually updated to clean-up and update developer guide to fix generated code examples:

  • Generated code by Amazon Q was in many cases incorrect. Update the code examples to have working examples.
  • Add picture of custom view created by code examples
  • Add notes on examples in respect of purpose and limitations

How to test

  • Review documentation
  • Verify that example code is correct

Follow-ups

Review checklist

  • As an author, I have thoroughly tested my changes and carefully followed the instructions in this template

Comment thread DEVELOPER-GUIDE.md Outdated
setHandleTraceResourceType(handleFiles: boolean, handleFolders: boolean): void;
onSignalManagerSignal(event: K extends SignalType, listener: (...args: [...SignalArgs<Signals[K]>]) => void | Promise<void>): void;
offSignalManagerSignal(event: K extends SignalType, listener: (...args: [...SignalArgs<Signals[K]>]) => void | Promise<void>): void;
registerTimeGraphMenuSignal(menuId: string, menuLabel: string): void;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

registerTimeGraphMenuSignal is not available.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was made up by AmazonQ. I'll provide the steps to achieve the registration.

Comment thread DEVELOPER-GUIDE.md Outdated
vscode.window.showInformationMessage(`Trace opened: ${experiment.name}`);
};

const onExperimentClosed = (experiment: any) => {

@Yibhir0 Yibhir0 Oct 8, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method did not work when experiment is closed

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. It might be that this particular signal is not propagated to the vscode-extension and just stays inside the webviews... I need to confirm. For the doc I'll use selection changed signal instead.

Comment thread doc/DEVELOPER-GUIDE.md
}
};

traceAPI.onSignalManagerSignal('EXPERIMENT_OPENED', onExperimentOpened);

@Yibhir0 Yibhir0 Oct 8, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Among the three signals only EXPERIMENT_OPENED that works, I also tried SELECTION_RANGE_UPDATED' and it works. Here is the code I added.

    // Listen for range selcetion
    const onSelcetionRange = (data: any) => {

        outputChannel.appendLine('Selection Range..............');
        outputChannel.appendLine(`Selection data: ${JSON.stringify(data)}`);

        if (data && data.experimentUUID && data.timeRange) {
            const experimentUUID = data.experimentUUID;
            const start = data.timeRange.start;
            const end = data.timeRange.end;

            outputChannel.appendLine(`Selected range - UUID: ${experimentUUID}, Start: ${start}, End: ${end}`);

            // Call your custom analysis with the selected range


            if (!webviewProvider) {
                vscode.window.showErrorMessage('Webview provider not initialized');
                return;
            }
            webviewProvider.updateSelectionRange(data.experimentUUID, data.timeRange);
        };

    }
    
      traceAPI.onSignalManagerSignal('SELECTION_RANGE_UPDATED', onSelcetionRange);
      

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only certain signals are propagated to the extensions of vscode. I'll add a comment.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perfect!

Comment thread doc/DEVELOPER-GUIDE.md
});
}

function addTraceServerContributor() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works but needs custom logic.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the custom logic is what adopters have to implement for their use cases

Comment thread doc/DEVELOPER-GUIDE.md

**Creating Extended TSP Client**

**src/extended-tsp-client.ts**

@Yibhir0 Yibhir0 Oct 8, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this code can be customized for user needs. I decided to use HttpTspClient which calls trace server.

import {
    HttpTspClient,
    GenericResponse,
    TspClientResponse,
    EntryModel,
    Experiment,
    OutputDescriptor,
    Query

} from 'tsp-typescript-client';
import { DataTreeEntry } from "tsp-typescript-client/lib/models/data-tree";


export class ExtendedTsp extends HttpTspClient {
    constructor(baseUrl: string) {
        super(baseUrl);
    }


    // Method to get tree
    async getTree(expUUID: string, outputID: string, parameters: any): Promise<TspClientResponse<GenericResponse<EntryModel<DataTreeEntry>>>> {
        return this.fetchTimeGraphTree(expUUID, outputID, parameters);
        //Add custom code 
    }

    async removeExperiment(expUUID: string): Promise<TspClientResponse<Experiment>> {
        //Add custom code 
        return this.deleteExperiment(expUUID);
    }

    async getOutputIDS(expUUID: string): Promise<TspClientResponse<OutputDescriptor[]>> {
        //Add custom code 
        return this.experimentOutputs(expUUID);
    }

    async getStates(experimentUUID: string, outputId: string, parameters: Query): Promise<any | undefined> {
        //Add custom code 
        return this.fetchTimeGraphStates(experimentUUID, outputId, parameters);
    }

}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion. I'll update the generated code so that the back-end calls to a custom (non-TSP) endpoint is correct. It will return 404 for the default Trace Compass server because it doesn't implement the custom endpoints. Only a custom server with those endpoints would reply 200 with the corresponding data. I think it's ok what we have to demonstrate the procedure to add a custom endpoint.

@Yibhir0 Yibhir0 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me know if you want me to add something or use different approach.

Comment thread DEVELOPER-GUIDE.md Outdated

**src/tsp-service.ts**
```typescript
import { ExtendedTspClient, CustomAnalysisRequest } from './extended-tsp-client';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Further I used this code inside tsp-service.ts

import { ExtendedTsp } from "./extended-tsp-1-client";
import { Query } from 'tsp-typescript-client';


export class TspService {

    private client: ExtendedTsp;

    private count: number;

    private result: any;


    constructor(baseUrl: string = 'http://localhost:8080/tsp/api') {
        this.client = new ExtendedTsp(baseUrl);
        this.count = 0;
        this.result = [];
    }



    async getAvailableOutputIds(experimentUUID: string): Promise<any | undefined> {

        try {
            const response = await this.client.getOutputIDS(experimentUUID);
            if (response.isOk()) {
                const outputs = response.getModel();
                if (outputs) {
                    return outputs
                }

            }

            else {
                console.error('Failed to get OutputID:', response.getStatusMessage());
                return undefined;
            }
            return undefined;
        }
        catch (error) {
            console.error('Error starting custom analysis:', error);
            return undefined;
        }

    }

    private async pollAnalysisStatus(experimentUUID: string, outputId: string, start: bigint, end: bigint, entryId: string): Promise<any | undefined> {

        try {
            const parameters = new Query({
                'requested_times': [start, end],
                'requested_items': [entryId]
            });
            const response = await this.client.getStates(experimentUUID, outputId, parameters);
            if (response.isOk()) {
                const states = response.getModel()?.model.rows;
                if (states) {
                    return states[0].states;

                }
            }
            return undefined;

        }
        catch (error) {
            console.error('Error polling analysis status:', error);
            return undefined;
        }

    }


    async getTreeResult(experimentUUID: string, outputId: string, start: bigint, end: bigint): Promise<any | undefined> {

        try {
            const parameters = new Query({
                'requested_times': [start, end],
                'requested_items': []
            });
            const treeResponse = await this.client.getTree(experimentUUID, outputId, parameters);

            if (treeResponse.isOk()) {
                const entries = treeResponse.getModel()?.model.entries;
                return entries;
            }
            return undefined;
        }

        catch (error) {
            console.error('Error polling analysis status:', error);
            return undefined

        }

    }

    // Gets 10 first states
    async getSomeTimeGraphStates(experimentUUID: string, outputId: string, start: bigint, end: bigint): Promise<{ status: string, results?: any } | undefined> {

        if (this.count == 3) {
            this.count = 0;
            return {
                status: "COMPLETED",
                results: this.result
            };
        }

        try {
            const entries = await this.getTreeResult(experimentUUID, outputId, start, end);


            if (entries) {

                const entry = entries[this.count];

                const state = await this.pollAnalysisStatus(experimentUUID, outputId, entry.start, entry.end, entry.id);
                if (state) {
                    this.result.push(state);
                    this.count++;
                    return {
                        status: "RUNNING",
                        results: this.result
                    }
                }
            }
            return {
                status: "FAILED"
            }

        }
        catch (err) {
            return {
                status: "FAILED"
            }
        }

    }


}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I'll update it accordingly as mentioned earlier.

Comment thread DEVELOPER-GUIDE.md Outdated
// ... rest of activation code ...
}

async function runCustomAnalysis() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the modified version of runCustomAnalysis()

async function runCustomAnalysis() {
    if (!traceAPI || !tspService) return;

    const activeExperiment = traceAPI.getActiveExperiment();
    if (!activeExperiment) {
        vscode.window.showWarningMessage('No active trace for analysis');
        return;
    }

    try {

        const outputId = "org.eclipse.tracecompass.internal.tmf.core.statesystem.provider.StateSystemDataProvider";

        const start = activeExperiment.start;
        const end = activeExperiment.end;
        let result;


        vscode.window.withProgress({
            location: vscode.ProgressLocation.Notification,
            title: 'Running Custom TSP Analysis',
            cancellable: true
        }, async (progress, token) => {

            let completed = false;
            let attempts = 0;
            const maxAttempts = 30;


            while (!completed && attempts < maxAttempts && !token.isCancellationRequested) {
                result = await tspService.getSomeTimeGraphStates(activeExperiment.UUID, outputId, start, end);
                const status = result?.status;

                if (status) {
                    progress.report({
                        increment: (attempts / maxAttempts) * 100,
                        message: `Status: ${status}`
                    });

                    if (status == "FAILED") {
                        vscode.window.showErrorMessage('Analysis failed');
                        break;
                    }

                    else if (status == "COMPLETED") {
                        outputChannel.appendLine(`Analysis status: ${JSON.stringify(status)}`);
                        outputChannel.appendLine(`Number of statess: ${JSON.stringify(result?.results.length)}`);
                        completed = true;
                    }

                }

            }
            if (!completed) {
                await new Promise(resolve => setTimeout(resolve, 1000));
                attempts++;
            }



        });


    } catch (error) {
        console.error('Error running custom analysis:', error);
        vscode.window.showErrorMessage('Failed to run custom analysis');
    }
}

Comment thread DEVELOPER-GUIDE.md Outdated
},
"dependencies": {
"tsp-typescript-client": "^0.4.0",
"@vscode/messenger": "^0.4.5"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vscode/messenger dependency can be removed. I used a built-in messaging functionalities.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was incorrectly added by Amazon Q. I asked to use www.npmjs.com/package/vscode-messenger, but I guess my prompt was not clear enough. I'll update the examples accordingly and reference this dependency.

Comment thread doc/DEVELOPER-GUIDE.md
"dependencies": {
"tsp-typescript-client": "^0.4.0",
"@vscode/messenger": "^0.4.5"
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the update version of webview-provider.ts.

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CustomTraceViewProvider = void 0;
const vscode = require("vscode");

class CustomTraceViewProvider {
    constructor(context, tspService, traceAPI, outputChannel) {
        this.context = context;
        this.tspService = tspService;
        this.traceAPI = traceAPI;
    }
    async createWebview() {
        this.panel = vscode.window.createWebviewPanel('customTraceView', 'Range Display And Analysis', vscode.ViewColumn.Two, {
            enableScripts: true,
            retainContextWhenHidden: true
        });
        this.panel.webview.html = this.getWebviewContent();
        this.setupMessageHandling();
        this.customizeExistingViews();
    }
    setupMessageHandling() {
        if (!this.panel)
            return;
        this.panel.webview.onDidReceiveMessage(async (message) => {
            if (message.command === 'runAnalysis') {
                await this.runAnalysis();
            }
        });
    }
    async runAnalysis() {
        const activeExperiment = this.traceAPI?.getActiveExperiment();
        if (!activeExperiment)
            return;
        const outputId = "org.eclipse.tracecompass.internal.tmf.core.statesystem.provider.StateSystemDataProvider";
        let completed = false;
        this.panel?.webview.postMessage({
            command: 'analysisStatus',
            status: 'RUNNING'
        });
        while (!completed) {
            const result = await this.tspService.getSomeTimeGraphStates(activeExperiment.UUID, outputId, BigInt(activeExperiment.start), BigInt(activeExperiment.end));
            if (result?.status === 'COMPLETED') {
                completed = true;
                this.panel?.webview.postMessage({
                    command: 'analysisStatus',
                    status: 'COMPLETED',
                });
            }
            else if (result?.status === 'FAILED') {
                completed = true;
                this.panel?.webview.postMessage({
                    command: 'analysisStatus',
                    status: 'FAILED'
                });
            }
            await new Promise(resolve => setTimeout(resolve, 1000));
        }
    }
    updateSelectionRange(experimentUUID, timeRange) {
        this.panel?.webview.postMessage({
            command: 'selectionRangeUpdated',
            data: timeRange,
            id: experimentUUID
        });
    }
    customizeExistingViews() {
        const activeWebviews = this.traceAPI.getActiveWebviews();
        const activeWebviewPanels = this.traceAPI.getActiveWebviewPanels();
        activeWebviews.forEach(webview => {
            this.injectRangeHandler(webview.webview);
        });
        Object.values(activeWebviewPanels).forEach(panel => {
            if (panel && panel.webview) {
                this.injectRangeHandler(panel.webview);
            }
        });
    }
    injectRangeHandler(webview) {
        webview.onDidReceiveMessage((message) => {
            if (message.method === 'selectionRangeUpdated') {
                const params = JSON.parse(message.params);
                this.displayRangeData(params.timeRange);
            }
        });
    }
    displayRangeData(timeRange) {
        this.panel?.webview.postMessage({
            command: 'rangeDataUpdated',
            data: timeRange
        });
    }
    getWebviewContent() {
        return `<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Range Display</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 20px; }
        .section { border: 1px solid #ccc; padding: 15px; margin: 10px 0; border-radius: 5px; }
        button { padding: 8px 16px; cursor: pointer; }
    </style>
</head>
<body>
    <div class="section">
        <h3>Analysis</h3>
        <button onclick="runAnalysis()">Run Analysis</button>
        <div id="analysis-status">Ready</div>
    </div>

    <div class="section">
        <h3>Selection Range</h3>
        <div id="selection-info">No selection</div>
    </div>

    <script>
        const vscode = acquireVsCodeApi();

        window.addEventListener('message', event => {
            const message = event.data;
            if (message.command === 'selectionRangeUpdated') {
                document.getElementById('selection-info').innerHTML = 
                    'Start: ' + message.data.start + '<br>End: ' + message.data.end;
            } else if (message.command === 'rangeDataUpdated') {
                document.getElementById('selection-info').innerHTML = 
                    'Start: ' + message.data.start + '<br>End: ' + message.data.end;
            } else if (message.command === 'analysisStatus') {
                document.getElementById('analysis-status').innerHTML = message.status;
            
            }
        });

        window.runAnalysis = function() {
            vscode.postMessage({ command: 'runAnalysis' });
        };
    </script>
</body>
</html>`;
    }
}
exports.CustomTraceViewProvider = CustomTraceViewProvider;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I'll provide an updated version as well.

Comment thread DEVELOPER-GUIDE.md Outdated
// ... existing code ...

// Initialize webview provider
webviewProvider = new CustomTraceViewProvider(context, tspService);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this needs to be initialized at last

 webviewProvider = new CustomTraceViewProvider(context, tspService, traceAPI, outputChannel);
 

Comment thread DEVELOPER-GUIDE.md Outdated
return;
}

await webviewProvider.createWebview(activeExperiment.UUID, activeExperiment.name);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

function openCustomView() {
    if (!webviewProvider) {
        vscode.window.showErrorMessage('Webview provider not initialized');
        return;
    }
     await webviewProvider.webviewProvider.createWebview();
}

@Yibhir0 Yibhir0 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added onSelectionRange handler.

@bhufmann bhufmann force-pushed the developer-guide branch 2 times, most recently from 8b0441c to 6539cc8 Compare October 14, 2025 14:59
@bhufmann bhufmann requested a review from Yibhir0 October 16, 2025 17:38
Remove section in README.md with reference to developer guide.

Generated using Amazon Q

Signed-off-by: Bernd Hufmann <bernd.hufmann@ericsson.com>
- This custom extension interfaces the vscode-trace-extension.
- It also explains how to extend the tsp-typescript-client for custom
endpoints not part of TSP

Updated DEVELOPER-GUIDE.md

Generated using Amazon Q

Signed-off-by: Bernd Hufmann <bernd.hufmann@ericsson.com>
Updated DEVELOPER-GUIDE.md

Generated using Amazon Q

Signed-off-by: Bernd Hufmann <bernd.hufmann@ericsson.com>
Generated using Amazon Q

Signed-off-by: Bernd Hufmann <bernd.hufmann@ericsson.com>
Generated using Amazon Q

Signed-off-by: Bernd Hufmann <bernd.hufmann@ericsson.com>
Generated using Amazon Q

Signed-off-by: Bernd Hufmann <bernd.hufmann@ericsson.com>
- Generated code by Amazon Q was in many case incorrect. Update the
code examples to working examples
- Add picture of custom view created by code examples
- Add notes on examples in respect of purpose and limitations

Signed-off-by: Bernd Hufmann <bernd.hufmann@ericsson.com>

@MatthewKhouzam MatthewKhouzam left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Improvements, thanks!

@bhufmann bhufmann merged commit 9e3bf0a into eclipse-cdt-cloud:master Nov 7, 2025
7 of 9 checks passed
@bhufmann bhufmann deleted the developer-guide branch November 7, 2025 13:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants