From 5ef2278ed67773924600445c5a2b96f597e90f13 Mon Sep 17 00:00:00 2001 From: Will Yang Date: Wed, 5 Nov 2025 17:17:20 -0600 Subject: [PATCH 1/4] wip - working prototype --- .../components/utils/time-range-data-map.ts | 28 +-- .../trace-explorer-time-range-data-widget.tsx | 41 ++-- vscode-trace-extension/package.json | 5 + .../src/csv-download/csv-download.ts | 218 ++++++++++++++++++ vscode-trace-extension/src/extension.ts | 7 + ...plorer-time-range-data-webview-provider.ts | 25 +- 6 files changed, 282 insertions(+), 42 deletions(-) create mode 100644 vscode-trace-extension/src/csv-download/csv-download.ts diff --git a/local-libs/traceviewer-libs/react-components/src/components/utils/time-range-data-map.ts b/local-libs/traceviewer-libs/react-components/src/components/utils/time-range-data-map.ts index 9f7311e4..a675637d 100644 --- a/local-libs/traceviewer-libs/react-components/src/components/utils/time-range-data-map.ts +++ b/local-libs/traceviewer-libs/react-components/src/components/utils/time-range-data-map.ts @@ -3,13 +3,13 @@ import { ExperimentTimeRangeData } from '../../trace-explorer/trace-explorer-tim import { Experiment } from 'tsp-typescript-client/lib/models/experiment'; export class TimeRangeDataMap { - private _experimentDataMap: Map; - private _activeData?: ExperimentTimeRangeData; + private static _experimentDataMap: Map = new Map(); + private static _activeData?: ExperimentTimeRangeData; constructor() { - this._experimentDataMap = new Map(); + // Static class pattern: no instance initialization required } - public updateViewRange = (payload: TimeRangeUpdatePayload): void => { + public static updateViewRange = (payload: TimeRangeUpdatePayload): void => { const { experimentUUID: UUID, timeRange } = payload; const update = { @@ -20,7 +20,7 @@ export class TimeRangeDataMap { this.updateExperimentTimeRangeData(update); }; - public updateSelectionRange = (payload: TimeRangeUpdatePayload): void => { + public static updateSelectionRange = (payload: TimeRangeUpdatePayload): void => { const { experimentUUID: UUID, timeRange } = payload; const update = { @@ -31,7 +31,7 @@ export class TimeRangeDataMap { this.updateExperimentTimeRangeData(update); }; - public updateAbsoluteRange = (experiment: Experiment): void => { + public static updateAbsoluteRange = (experiment: Experiment): void => { if (!experiment) { return; } @@ -54,7 +54,7 @@ export class TimeRangeDataMap { * you only input the data to change and the existing values persist. * @param data Partial data of Experiment Time Range Data. */ - private updateExperimentTimeRangeData = (data: ExperimentTimeRangeData): void => { + private static updateExperimentTimeRangeData = (data: ExperimentTimeRangeData): void => { const map = this._experimentDataMap; const id = data.UUID; const existingData = map.get(id) || {}; @@ -70,32 +70,32 @@ export class TimeRangeDataMap { } }; - public setActiveExperiment(data?: ExperimentTimeRangeData): void { + public static setActiveExperiment(data?: ExperimentTimeRangeData): void { this._activeData = data; } - public delete = (experiment: Experiment | string): void => { + public static delete = (experiment: Experiment | string): void => { const id = typeof experiment === 'string' ? experiment : experiment.UUID; this._experimentDataMap.delete(id); }; - public get(UUID: string): ExperimentTimeRangeData | undefined { + public static get(UUID: string): ExperimentTimeRangeData | undefined { return this._experimentDataMap.get(UUID); } - public set(data: ExperimentTimeRangeData): void { + public static set(data: ExperimentTimeRangeData): void { this._experimentDataMap.set(data.UUID, data); } - public clear(): void { + public static clear(): void { this._experimentDataMap.clear(); } - get activeData(): ExperimentTimeRangeData | undefined { + public static get activeData(): ExperimentTimeRangeData | undefined { return this._activeData; } - get experimentDataMap(): Map { + public static get experimentDataMap(): Map { return this._experimentDataMap; } } diff --git a/local-libs/traceviewer-libs/react-components/src/trace-explorer/trace-explorer-time-range-data-widget.tsx b/local-libs/traceviewer-libs/react-components/src/trace-explorer/trace-explorer-time-range-data-widget.tsx index 7f37efb1..56b05220 100644 --- a/local-libs/traceviewer-libs/react-components/src/trace-explorer/trace-explorer-time-range-data-widget.tsx +++ b/local-libs/traceviewer-libs/react-components/src/trace-explorer/trace-explorer-time-range-data-widget.tsx @@ -33,11 +33,8 @@ export class ReactTimeRangeDataWidget extends React.Component< ReactTimeRangeDataWidgetProps, ReactTimeRangeDataWidgetState > { - private experimentDataMap: TimeRangeDataMap; - constructor(props: ReactTimeRangeDataWidgetProps) { super(props); - this.experimentDataMap = new TimeRangeDataMap(); this.state = { inputting: false, userInputSelectionStartIsValid: true, @@ -65,17 +62,17 @@ export class ReactTimeRangeDataWidget extends React.Component< }; private onViewRangeUpdated = (payload: TimeRangeUpdatePayload): void => { - this.experimentDataMap.updateViewRange(payload); + TimeRangeDataMap.updateViewRange(payload); this.renderIfActive(); }; private onSelectionRangeUpdated = (payload: TimeRangeUpdatePayload): void => { - this.experimentDataMap.updateSelectionRange(payload); + TimeRangeDataMap.updateSelectionRange(payload); this.renderIfActive(); }; private onAbsoluteRangeUpdate = (experiment: Experiment): void => { - this.experimentDataMap.updateAbsoluteRange(experiment); + TimeRangeDataMap.updateAbsoluteRange(experiment); this.renderIfActive(); }; @@ -85,36 +82,44 @@ export class ReactTimeRangeDataWidget extends React.Component< }; private onExperimentSelected = (experiment: Experiment | undefined): void => { - let newActiveData; + let newActiveData: ExperimentTimeRangeData | undefined; if (experiment) { - // TODO - consider changing this logic? + // Update absolute range first so the entry exists/merges this.onAbsoluteRangeUpdate(experiment); - newActiveData = this.experimentDataMap.get(experiment.UUID); + newActiveData = TimeRangeDataMap.get(experiment.UUID); } this.setActiveExperiment(newActiveData); }; private onExperimentClosed = (experiment: Experiment | string): void => { - this.experimentDataMap.delete(experiment); + const id = typeof experiment === 'string' ? experiment : experiment.UUID; + TimeRangeDataMap.delete(id); + // If we just closed the active one, clear local state, too + if (this.state.activeData?.UUID === id) { + this.setActiveExperiment(undefined); + } }; private setActiveExperiment = (timeData?: ExperimentTimeRangeData): void => { - this.experimentDataMap.setActiveExperiment(timeData); - this.setState({ activeData: timeData ? this.experimentDataMap.get(timeData.UUID) : undefined }); + TimeRangeDataMap.setActiveExperiment(timeData); + this.setState({ + activeData: timeData ? TimeRangeDataMap.get(timeData.UUID) : undefined + }); }; private renderIfActive(): void { - const { state, experimentDataMap } = this; - if (state.activeData?.UUID === experimentDataMap.activeData?.UUID) { - const activeData = state.activeData ? experimentDataMap.get(state.activeData.UUID) : undefined; - this.setState({ activeData }); + const { state } = this; + const globalActive = TimeRangeDataMap.activeData; + if (state.activeData?.UUID === globalActive?.UUID) { + const fresh = state.activeData ? TimeRangeDataMap.get(state.activeData.UUID) : undefined; + this.setState({ activeData: fresh }); } } public restoreData = (mapArray: Array, activeData: ExperimentTimeRangeData): void => { - this.experimentDataMap.clear(); + TimeRangeDataMap.clear(); for (const experimentData of mapArray) { - this.experimentDataMap.set(experimentData); + TimeRangeDataMap.set(experimentData); } this.setActiveExperiment(activeData); }; diff --git a/vscode-trace-extension/package.json b/vscode-trace-extension/package.json index 0963e188..6bee232f 100644 --- a/vscode-trace-extension/package.json +++ b/vscode-trace-extension/package.json @@ -114,6 +114,10 @@ "title": "Trace Viewer Keyboard and Mouse Shortcuts", "icon": "$(info)" }, + { + "command": "traceViewer.exportCsvFromSelectionRange", + "title": "Export CSV From Selection Range" + }, { "command": "serverStatus.started", "title": "Trace Server: started" @@ -334,6 +338,7 @@ "terser": "4.8.1", "traceviewer-base": "0.10.0", "traceviewer-react-components": "0.10.0", + "tsp-typescript-client": "^0.9.0", "vscode-messenger": "^0.5.0", "vscode-trace-common": "0.7.0" }, diff --git a/vscode-trace-extension/src/csv-download/csv-download.ts b/vscode-trace-extension/src/csv-download/csv-download.ts new file mode 100644 index 00000000..24aab095 --- /dev/null +++ b/vscode-trace-extension/src/csv-download/csv-download.ts @@ -0,0 +1,218 @@ +import { TimeRangeDataMap } from "traceviewer-react-components/src/components/utils/time-range-data-map"; +import * as vscode from 'vscode' +import * as fs from 'fs'; +import * as path from 'path'; +import { getTspClient } from "../utils/backend-tsp-client-provider"; +import { QueryHelper } from "tsp-typescript-client"; + +export const exportCSV = async (outputID?: string) => { + + // Helper FUnctions + const REQUEST_SIZE = 1000; + const WRITE_HIGH_WATERMARK = 1 << 20; // 1 MiB + + function csvEscape(value: unknown): string { + // Always quote to keep logic simple; double any embedded quotes + const s = (value ?? '').toString().replace(/"/g, '""'); + return `"${s}"`; + } + + function createBatchWriter(filePath: string) { + const stream = fs.createWriteStream(filePath, { flags: 'w', highWaterMark: WRITE_HIGH_WATERMARK }); + + const write = (data: string | Buffer) => + new Promise((resolve, reject) => { + const ok = stream.write(data); + if (ok) return resolve(); + stream.once('drain', resolve); + stream.once('error', reject); + }); + + const end = () => + new Promise((resolve, reject) => { + stream.end(); + stream.once('finish', resolve); + stream.once('error', reject); + }); + + return { write, end }; + } + const { activeData } = TimeRangeDataMap; + if (!activeData) { + vscode.window.showErrorMessage('No trace is open or selected.'); + return; + } + + const { absoluteRange, selectionRange } = activeData; + if (!absoluteRange || !selectionRange) { + vscode.window.showErrorMessage('No selection range found.'); + return; + } + + const uri = await vscode.window.showSaveDialog({ + title: 'Save CSV File', + defaultUri: vscode.Uri.file( + path.join(vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? '', 'data.csv') + ), + filters: { 'CSV Files': ['csv'] }, + saveLabel: 'Save CSV', + }); + if (!uri) { + vscode.window.showInformationMessage('Save cancelled.'); + return; + } + + const tsp = getTspClient(); + outputID = outputID || + 'org.eclipse.tracecompass.internal.provisional.tmf.core.model.events.TmfEventTableDataProvider'; + + await vscode.window.withProgress( + { location: vscode.ProgressLocation.Notification, title: 'Exporting CSV…', cancellable: true }, + async (progress, token) => { + const measureStart = Date.now(); + + try { + // 1) Resolve headers + progress.report({ message: 'Fetching column headers…', increment: 5 }); + const headersResp = await tsp.fetchTableColumns(activeData.UUID, outputID, QueryHelper.query()); + const headersModel = headersResp.getModel()?.model; + if (!headersModel) { + vscode.window.showErrorMessage('Failed to fetch table columns.'); + return; + } + const headerNames: string[] = headersModel.map((c: { name: string }) => c.name); + + // 2) Compute absolute timestamps for selection + const START_TIME = BigInt(absoluteRange.start) + BigInt(selectionRange.start); + const END_TIME = BigInt(absoluteRange.start) + BigInt(selectionRange.end); + + // 3) Resolve start/end indices by time (like your POC) + progress.report({ message: 'Resolving table indices…', increment: 10 }); + + const indexBody = (requestedTime: bigint) => ({ + requested_times: [requestedTime], + requested_table_count: 1, + }); + + const t1Req = await tsp.fetchTableLines( + activeData.UUID, + outputID, + QueryHelper.query(indexBody(START_TIME)) + ); + const t2Req = await tsp.fetchTableLines( + activeData.UUID, + outputID, + QueryHelper.query(indexBody(END_TIME)) + ); + + const startIndex: number | undefined = t1Req.getModel()?.model?.lowIndex; + const endIndex: number | undefined = t2Req.getModel()?.model?.lowIndex; + + if (startIndex === undefined || endIndex === undefined) { + vscode.window.showErrorMessage('Failed to locate table indices for the selected range.'); + return; + } + + const totalRows = Math.max(0, endIndex - startIndex); + if (totalRows === 0) { + // Still write header-only CSV to honor user’s save request + const writer = createBatchWriter(uri.fsPath); + await writer.write(headerNames.map(csvEscape).join(',') + '\n'); + await writer.end(); + vscode.window.showInformationMessage(`No rows in selection. Wrote headers to ${uri.fsPath}`); + return; + } + + // 4) Prepare writer and write header + const writer = createBatchWriter(uri.fsPath); + await writer.write(headerNames.map(csvEscape).join(',') + '\n'); + + // 5) Batching + pipelined fetching (like the POC) + let rowsLeft = totalRows; + let nextIndex = startIndex; + let nextCount = Math.min(REQUEST_SIZE, rowsLeft); + + const makeLinesBody = (index: number, count: number) => ({ + requested_table_column_ids: headerNames.map((_, i) => i), + requested_table_count: count, + requested_table_index: index, + table_search_expressions: {}, + }); + + // Prime first request + let ongoing = tsp.fetchTableLines(activeData.UUID, outputID, QueryHelper.query(makeLinesBody(nextIndex, nextCount))); + + const writeLines = async (lines: any[]) => { + const lineStrings: string[] = []; + for (const line of lines) { + const cells = (line.cells ?? []).map((cell: { content: unknown }) => csvEscape(cell?.content)); + lineStrings.push(cells.join(',')); + } + if (lineStrings.length > 0) { + await writer.write(lineStrings.join('\n') + '\n'); + } + }; + + let processed = 0; + progress.report({ message: `Exporting ${totalRows.toLocaleString()} rows…`, increment: 10 }); + + while (rowsLeft > 0 && !token.isCancellationRequested) { + // Advance iterators for the *next* request before awaiting current + nextIndex += nextCount; + rowsLeft -= nextCount; + nextCount = Math.min(REQUEST_SIZE, rowsLeft); + + // Await current, kick off next + const currentModel = await ongoing; + const currentLines = currentModel.getModel()?.model?.lines ?? []; + + // Start next request (or a resolved empty promise when done) + ongoing = + nextCount > 0 + ? tsp.fetchTableLines( + activeData.UUID, + outputID, + QueryHelper.query(makeLinesBody(nextIndex, nextCount)) + ) + : Promise.resolve({ getModel: () => ({ model: { lines: [] } }) } as any); + + await writeLines(currentLines); + processed += currentLines.length; + + // Progress update (cap increments to avoid >100%) + const pct = 10 + Math.min(80, Math.floor((processed / totalRows) * 80)); + progress.report({ + message: `Exported ${processed.toLocaleString()} / ${totalRows.toLocaleString()}…`, + increment: pct - (progress as any)._lastPct + }); + (progress as any)._lastPct = pct; + + } + + if (token.isCancellationRequested) { + await writer.end(); + vscode.window.showWarningMessage('CSV export cancelled. Partial file may be present.'); + return; + } + + // Flush the final pending request + const finalModel = await ongoing; + const finalLines = finalModel.getModel()?.model?.lines ?? []; + if (finalLines.length) { + await writeLines(finalLines); + processed += finalLines.length; + } + + await writer.end(); + + const elapsedMs = Date.now() - measureStart; + vscode.window.showInformationMessage( + `CSV export complete: ${processed.toLocaleString()} rows → ${uri.fsPath} in ${(elapsedMs / 1000).toFixed(1)}s (batch=${REQUEST_SIZE}).` + ); + } catch (err: any) { + console.error(err); + vscode.window.showErrorMessage(`CSV export failed: ${err?.message ?? err}`); + } + } + ); +} \ No newline at end of file diff --git a/vscode-trace-extension/src/extension.ts b/vscode-trace-extension/src/extension.ts index 9e53b3c2..a9a7e10f 100644 --- a/vscode-trace-extension/src/extension.ts +++ b/vscode-trace-extension/src/extension.ts @@ -31,6 +31,7 @@ import { VSCODE_MESSAGES } from 'vscode-trace-common/lib/messages/vscode-message import { TraceViewerPanel } from './trace-viewer-panel/trace-viewer-webview-panel'; import { TraceServerManager } from './utils/trace-server-manager'; import { ResourceType, TraceExplorerResourceTypeHandler } from './utils/trace-explorer-resource-type-handler'; +import { exportCSV } from './csv-download/csv-download'; export let traceLogger: TraceExtensionLogger; export const traceExtensionWebviewManager: TraceExtensionWebviewManager = new TraceExtensionWebviewManager(); @@ -125,6 +126,12 @@ export async function activate(context: vscode.ExtensionContext): Promise { + exportCSV(outputID); + }) + ); + context.subscriptions.push( vscode.commands.registerCommand('traceViewer.customization.saveConfig', async () => { await jsonConfigEditor.saveDocumentIfValid(); diff --git a/vscode-trace-extension/src/trace-explorer/time-range/trace-explorer-time-range-data-webview-provider.ts b/vscode-trace-extension/src/trace-explorer/time-range/trace-explorer-time-range-data-webview-provider.ts index 5813f4b1..1429b5d4 100644 --- a/vscode-trace-extension/src/trace-explorer/time-range/trace-explorer-time-range-data-webview-provider.ts +++ b/vscode-trace-extension/src/trace-explorer/time-range/trace-explorer-time-range-data-webview-provider.ts @@ -27,7 +27,6 @@ export class TraceExplorerTimeRangeDataProvider extends AbstractTraceExplorerPro vscode.Uri.joinPath(this._extensionUri, 'lib', 'codicons') ] }; - private _experimentDataMap = new TimeRangeDataMap(); // VSCODE message handlers private _onVscodeRequestSelectionRangeChange = (data: any): void => { @@ -57,8 +56,8 @@ export class TraceExplorerTimeRangeDataProvider extends AbstractTraceExplorerPro _webviewView.onDidChangeVisibility(() => { if (this._view?.visible) { const data = { - mapArray: Array.from(this._experimentDataMap.experimentDataMap.values()), - activeData: this._experimentDataMap.activeData + mapArray: Array.from(TimeRangeDataMap.experimentDataMap.values()), + activeData: TimeRangeDataMap.activeData }; this._messenger.sendNotification(restoreView, this._webviewParticipant, JSONBigUtils.stringify(data)); } @@ -85,7 +84,7 @@ export class TraceExplorerTimeRangeDataProvider extends AbstractTraceExplorerPro private onViewRangeUpdated = (update: TimeRangeUpdatePayload) => { this._messenger.sendNotification(viewRangeUpdated, this._webviewParticipant, JSONBigUtils.stringify(update)); - this._experimentDataMap.updateViewRange(update); + TimeRangeDataMap.updateViewRange(update); }; private onSelectionRangeUpdated = (update: TimeRangeUpdatePayload) => { @@ -94,32 +93,38 @@ export class TraceExplorerTimeRangeDataProvider extends AbstractTraceExplorerPro this._webviewParticipant, JSONBigUtils.stringify(update) ); - this._experimentDataMap.updateSelectionRange(update); + TimeRangeDataMap.updateSelectionRange(update); }; private onExperimentSelected = (experiment: Experiment | undefined) => { const data = { wrapper: experiment ? JSONBigUtils.stringify(experiment) : undefined }; this._messenger.sendNotification(experimentSelected, this._webviewParticipant, data); + if (experiment) { - this._experimentDataMap.updateAbsoluteRange(experiment); + // Update absolute range first so the entry exists/merges + TimeRangeDataMap.updateAbsoluteRange(experiment); + // Ensure activeData is an ExperimentTimeRangeData, not Experiment + const entry = TimeRangeDataMap.get(experiment.UUID); + TimeRangeDataMap.setActiveExperiment(entry); + } else { + TimeRangeDataMap.setActiveExperiment(undefined); } - this._experimentDataMap.setActiveExperiment(experiment); }; private onExperimentUpdated = (experiment: Experiment) => { const data = { wrapper: JSONBigUtils.stringify(experiment) }; this._messenger.sendNotification(experimentUpdated, this._webviewParticipant, data); - this._experimentDataMap.updateAbsoluteRange(experiment); + TimeRangeDataMap.updateAbsoluteRange(experiment); }; private onExperimentClosed = (experiment: Experiment) => { const data = { wrapper: JSONBigUtils.stringify(experiment) }; this._messenger.sendNotification(experimentClosed, this._webviewParticipant, data); - this._experimentDataMap.delete(experiment); + TimeRangeDataMap.delete(experiment); }; private onExperimentTabClosed = (experimentUUID: string) => { this._messenger.sendNotification(traceViewerTabClosed, this._webviewParticipant, experimentUUID); - this._experimentDataMap.delete(experimentUUID); + TimeRangeDataMap.delete(experimentUUID); }; } From cc23722afe24bdc727b434e44ef7226862a50e52 Mon Sep 17 00:00:00 2001 From: Will Yang Date: Wed, 5 Nov 2025 18:53:21 -0600 Subject: [PATCH 2/4] wip - pick a table output --- .../src/csv-download/csv-download.ts | 34 ++++++++++++++++--- vscode-trace-extension/src/extension.ts | 6 +++- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/vscode-trace-extension/src/csv-download/csv-download.ts b/vscode-trace-extension/src/csv-download/csv-download.ts index 24aab095..2394b9ab 100644 --- a/vscode-trace-extension/src/csv-download/csv-download.ts +++ b/vscode-trace-extension/src/csv-download/csv-download.ts @@ -1,11 +1,11 @@ -import { TimeRangeDataMap } from "traceviewer-react-components/src/components/utils/time-range-data-map"; +import { TimeRangeDataMap } from "traceviewer-react-components/lib/components/utils/time-range-data-map"; import * as vscode from 'vscode' import * as fs from 'fs'; import * as path from 'path'; import { getTspClient } from "../utils/backend-tsp-client-provider"; import { QueryHelper } from "tsp-typescript-client"; -export const exportCSV = async (outputID?: string) => { +export const exportCSV = async (outputID: string = 'org.eclipse.tracecompass.internal.provisional.tmf.core.model.events.TmfEventTableDataProvider') => { // Helper FUnctions const REQUEST_SIZE = 1000; @@ -63,8 +63,6 @@ export const exportCSV = async (outputID?: string) => { } const tsp = getTspClient(); - outputID = outputID || - 'org.eclipse.tracecompass.internal.provisional.tmf.core.model.events.TmfEventTableDataProvider'; await vscode.window.withProgress( { location: vscode.ProgressLocation.Notification, title: 'Exporting CSV…', cancellable: true }, @@ -74,6 +72,7 @@ export const exportCSV = async (outputID?: string) => { try { // 1) Resolve headers progress.report({ message: 'Fetching column headers…', increment: 5 }); + // todo fix const headersResp = await tsp.fetchTableColumns(activeData.UUID, outputID, QueryHelper.query()); const headersModel = headersResp.getModel()?.model; if (!headersModel) { @@ -215,4 +214,31 @@ export const exportCSV = async (outputID?: string) => { } } ); +} + +export const queryForOutputType = async () => { + const tsp = getTspClient(); + const { activeData } = TimeRangeDataMap; + if (!activeData) { + console.log('no active data'); + return; + } + const outputsResponse = await tsp.experimentOutputs(activeData?.UUID); + console.dir(outputsResponse); + const outputDescriptors = outputsResponse.getModel(); + if (!outputDescriptors || outputDescriptors.length <= 0) { + console.log('no outputs'); + return; + } + const tables = outputDescriptors?.filter(output => output.type === 'TABLE'); + const items = tables.map(descriptor => ({ label: descriptor.name, id: descriptor.id })); + const selection = await vscode.window.showQuickPick(items, { + title: 'Select a table output', + placeHolder: 'pick one', + matchOnDescription: true, + canPickMany: false, + }) + + return selection?.id; + } \ No newline at end of file diff --git a/vscode-trace-extension/src/extension.ts b/vscode-trace-extension/src/extension.ts index a9a7e10f..92ea038a 100644 --- a/vscode-trace-extension/src/extension.ts +++ b/vscode-trace-extension/src/extension.ts @@ -31,7 +31,8 @@ import { VSCODE_MESSAGES } from 'vscode-trace-common/lib/messages/vscode-message import { TraceViewerPanel } from './trace-viewer-panel/trace-viewer-webview-panel'; import { TraceServerManager } from './utils/trace-server-manager'; import { ResourceType, TraceExplorerResourceTypeHandler } from './utils/trace-explorer-resource-type-handler'; -import { exportCSV } from './csv-download/csv-download'; +import { exportCSV, queryForOutputType } from './csv-download/csv-download'; + export let traceLogger: TraceExtensionLogger; export const traceExtensionWebviewManager: TraceExtensionWebviewManager = new TraceExtensionWebviewManager(); @@ -128,6 +129,9 @@ export async function activate(context: vscode.ExtensionContext): Promise { + if (!outputID) { + outputID = await queryForOutputType(); + } exportCSV(outputID); }) ); From 783335db0277dc57227c1906af4a784873406bd6 Mon Sep 17 00:00:00 2001 From: Will Yang Date: Mon, 10 Nov 2025 11:35:43 -0600 Subject: [PATCH 3/4] fixed normalize bug + other stuff --- .../src/csv-download/csv-download.ts | 18 +++++++++++------- vscode-trace-extension/src/extension.ts | 4 +++- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/vscode-trace-extension/src/csv-download/csv-download.ts b/vscode-trace-extension/src/csv-download/csv-download.ts index 2394b9ab..d3f8bcc4 100644 --- a/vscode-trace-extension/src/csv-download/csv-download.ts +++ b/vscode-trace-extension/src/csv-download/csv-download.ts @@ -5,7 +5,7 @@ import * as path from 'path'; import { getTspClient } from "../utils/backend-tsp-client-provider"; import { QueryHelper } from "tsp-typescript-client"; -export const exportCSV = async (outputID: string = 'org.eclipse.tracecompass.internal.provisional.tmf.core.model.events.TmfEventTableDataProvider') => { +export const exportCSV = async (outputID: string) => { // Helper FUnctions const REQUEST_SIZE = 1000; @@ -71,7 +71,7 @@ export const exportCSV = async (outputID: string = 'org.eclipse.tracecompass.int try { // 1) Resolve headers - progress.report({ message: 'Fetching column headers…', increment: 5 }); + progress.report({ message: 'Fetching column headers…', increment: 2 }); // todo fix const headersResp = await tsp.fetchTableColumns(activeData.UUID, outputID, QueryHelper.query()); const headersModel = headersResp.getModel()?.model; @@ -82,11 +82,15 @@ export const exportCSV = async (outputID: string = 'org.eclipse.tracecompass.int const headerNames: string[] = headersModel.map((c: { name: string }) => c.name); // 2) Compute absolute timestamps for selection - const START_TIME = BigInt(absoluteRange.start) + BigInt(selectionRange.start); - const END_TIME = BigInt(absoluteRange.start) + BigInt(selectionRange.end); + const t1 = BigInt(absoluteRange.start) + BigInt(selectionRange.start); + const t2 = BigInt(absoluteRange.start) + BigInt(selectionRange.end); + + // Normalize + const START_TIME = t1 < t2 ? t1 : t2; + const END_TIME = t1 < t2 ? t2 : t1; // 3) Resolve start/end indices by time (like your POC) - progress.report({ message: 'Resolving table indices…', increment: 10 }); + progress.report({ message: 'Resolving table indices…', increment: 2 }); const indexBody = (requestedTime: bigint) => ({ requested_times: [requestedTime], @@ -153,7 +157,7 @@ export const exportCSV = async (outputID: string = 'org.eclipse.tracecompass.int }; let processed = 0; - progress.report({ message: `Exporting ${totalRows.toLocaleString()} rows…`, increment: 10 }); + progress.report({ message: `Exporting ${totalRows.toLocaleString()} rows…`, increment: 2 }); while (rowsLeft > 0 && !token.isCancellationRequested) { // Advance iterators for the *next* request before awaiting current @@ -241,4 +245,4 @@ export const queryForOutputType = async () => { return selection?.id; -} \ No newline at end of file +} diff --git a/vscode-trace-extension/src/extension.ts b/vscode-trace-extension/src/extension.ts index 92ea038a..8916ff79 100644 --- a/vscode-trace-extension/src/extension.ts +++ b/vscode-trace-extension/src/extension.ts @@ -132,7 +132,9 @@ export async function activate(context: vscode.ExtensionContext): Promise Date: Mon, 10 Nov 2025 13:24:23 -0600 Subject: [PATCH 4/4] lint --- .../components/utils/time-range-data-map.ts | 5 +- .../src/csv-download/csv-download.ts | 46 ++++++++++--------- vscode-trace-extension/src/extension.ts | 1 - 3 files changed, 28 insertions(+), 24 deletions(-) diff --git a/local-libs/traceviewer-libs/react-components/src/components/utils/time-range-data-map.ts b/local-libs/traceviewer-libs/react-components/src/components/utils/time-range-data-map.ts index a675637d..caa21b26 100644 --- a/local-libs/traceviewer-libs/react-components/src/components/utils/time-range-data-map.ts +++ b/local-libs/traceviewer-libs/react-components/src/components/utils/time-range-data-map.ts @@ -3,7 +3,10 @@ import { ExperimentTimeRangeData } from '../../trace-explorer/trace-explorer-tim import { Experiment } from 'tsp-typescript-client/lib/models/experiment'; export class TimeRangeDataMap { - private static _experimentDataMap: Map = new Map(); + private static _experimentDataMap: Map = new Map< + string, + ExperimentTimeRangeData + >(); private static _activeData?: ExperimentTimeRangeData; constructor() { // Static class pattern: no instance initialization required diff --git a/vscode-trace-extension/src/csv-download/csv-download.ts b/vscode-trace-extension/src/csv-download/csv-download.ts index d3f8bcc4..8cb749c4 100644 --- a/vscode-trace-extension/src/csv-download/csv-download.ts +++ b/vscode-trace-extension/src/csv-download/csv-download.ts @@ -1,12 +1,14 @@ -import { TimeRangeDataMap } from "traceviewer-react-components/lib/components/utils/time-range-data-map"; -import * as vscode from 'vscode' +import { TimeRangeDataMap } from 'traceviewer-react-components/lib/components/utils/time-range-data-map'; +import * as vscode from 'vscode'; import * as fs from 'fs'; import * as path from 'path'; -import { getTspClient } from "../utils/backend-tsp-client-provider"; -import { QueryHelper } from "tsp-typescript-client"; +import { getTspClient } from '../utils/backend-tsp-client-provider'; +import { QueryHelper } from 'tsp-typescript-client'; + +/* eslint-disable @typescript-eslint/no-explicit-any */ + export const exportCSV = async (outputID: string) => { - // Helper FUnctions const REQUEST_SIZE = 1000; const WRITE_HIGH_WATERMARK = 1 << 20; // 1 MiB @@ -51,11 +53,9 @@ export const exportCSV = async (outputID: string) => { const uri = await vscode.window.showSaveDialog({ title: 'Save CSV File', - defaultUri: vscode.Uri.file( - path.join(vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? '', 'data.csv') - ), + defaultUri: vscode.Uri.file(path.join(vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? '', 'data.csv')), filters: { 'CSV Files': ['csv'] }, - saveLabel: 'Save CSV', + saveLabel: 'Save CSV' }); if (!uri) { vscode.window.showInformationMessage('Save cancelled.'); @@ -94,7 +94,7 @@ export const exportCSV = async (outputID: string) => { const indexBody = (requestedTime: bigint) => ({ requested_times: [requestedTime], - requested_table_count: 1, + requested_table_count: 1 }); const t1Req = await tsp.fetchTableLines( @@ -139,11 +139,15 @@ export const exportCSV = async (outputID: string) => { requested_table_column_ids: headerNames.map((_, i) => i), requested_table_count: count, requested_table_index: index, - table_search_expressions: {}, + table_search_expressions: {} }); // Prime first request - let ongoing = tsp.fetchTableLines(activeData.UUID, outputID, QueryHelper.query(makeLinesBody(nextIndex, nextCount))); + let ongoing = tsp.fetchTableLines( + activeData.UUID, + outputID, + QueryHelper.query(makeLinesBody(nextIndex, nextCount)) + ); const writeLines = async (lines: any[]) => { const lineStrings: string[] = []; @@ -173,10 +177,10 @@ export const exportCSV = async (outputID: string) => { ongoing = nextCount > 0 ? tsp.fetchTableLines( - activeData.UUID, - outputID, - QueryHelper.query(makeLinesBody(nextIndex, nextCount)) - ) + activeData.UUID, + outputID, + QueryHelper.query(makeLinesBody(nextIndex, nextCount)) + ) : Promise.resolve({ getModel: () => ({ model: { lines: [] } }) } as any); await writeLines(currentLines); @@ -189,7 +193,6 @@ export const exportCSV = async (outputID: string) => { increment: pct - (progress as any)._lastPct }); (progress as any)._lastPct = pct; - } if (token.isCancellationRequested) { @@ -218,7 +221,7 @@ export const exportCSV = async (outputID: string) => { } } ); -} +}; export const queryForOutputType = async () => { const tsp = getTspClient(); @@ -240,9 +243,8 @@ export const queryForOutputType = async () => { title: 'Select a table output', placeHolder: 'pick one', matchOnDescription: true, - canPickMany: false, - }) + canPickMany: false + }); return selection?.id; - -} +}; diff --git a/vscode-trace-extension/src/extension.ts b/vscode-trace-extension/src/extension.ts index 8916ff79..b93956a9 100644 --- a/vscode-trace-extension/src/extension.ts +++ b/vscode-trace-extension/src/extension.ts @@ -33,7 +33,6 @@ import { TraceServerManager } from './utils/trace-server-manager'; import { ResourceType, TraceExplorerResourceTypeHandler } from './utils/trace-explorer-resource-type-handler'; import { exportCSV, queryForOutputType } from './csv-download/csv-download'; - export let traceLogger: TraceExtensionLogger; export const traceExtensionWebviewManager: TraceExtensionWebviewManager = new TraceExtensionWebviewManager(); export const traceServerManager: TraceServerManager = new TraceServerManager();