diff --git a/shared/src/traceData.ts b/shared/src/traceData.ts index 4516821..6795042 100644 --- a/shared/src/traceData.ts +++ b/shared/src/traceData.ts @@ -6,7 +6,7 @@ export const typeLine = z.object({ intrinsicName: z.string().optional(), recursionId: z.number().optional(), flags: z.array(z.string()).optional(), - ts: z.number(), + ts: z.number().optional(), dur: z.number().optional(), display: z.string().optional(), }) @@ -39,3 +39,29 @@ export type DataLine = TraceLine | TypeLine export type TraceData = z.infer export const traceData = z.array(typeLine.or(traceLine)) + +export interface TraceDataSummary { + totalTypes: number + timestampedTypes: number + untimestampedTypes: number + parseWarning?: string +} + +export function hasTypeTimestamp(line: TypeLine): line is TypeLine & { ts: number } { + return typeof line.ts === 'number' +} + +export function getTraceDataSummary(data: TraceData): TraceDataSummary { + const typeLines = data.filter((line): line is TypeLine => 'id' in line) + const timestampedTypes = typeLines.filter(hasTypeTimestamp).length + const untimestampedTypes = typeLines.length - timestampedTypes + + return { + totalTypes: typeLines.length, + timestampedTypes, + untimestampedTypes, + parseWarning: untimestampedTypes > 0 + ? `${untimestampedTypes} type entr${untimestampedTypes === 1 ? 'y is' : 'ies are'} missing timestamps and cannot be attributed to trace spans.` + : undefined, + } +} diff --git a/src/traceTree.ts b/src/traceTree.ts index 0618d33..636518b 100644 --- a/src/traceTree.ts +++ b/src/traceTree.ts @@ -1,6 +1,6 @@ import { isAbsolute, join, relative } from 'node:path' import type { FileStat } from '../shared/src/messages' -import type { TraceData, TraceLine, TypeLine } from '../shared/src/traceData' +import { type TraceData, type TraceLine, type TypeLine, hasTypeTimestamp } from '../shared/src/traceData' import { getWorkspacePath } from './storage' import { postMessage } from './webview' import { traceFiles } from './appState' @@ -27,6 +27,10 @@ function getRoot(): Tree { } let treeIndexes: Tree[] = [] +function hasTraceTimestamp(line: TraceData[number]): line is TraceLine | TypeLine & { ts: number } { + return 'cat' in line || ('id' in line && hasTypeTimestamp(line)) +} + export function toTree(traceData: TraceData, workspacePath: string): Tree { const tree: Tree = { ...getRoot() } let endTs = Number.MAX_SAFE_INTEGER @@ -38,7 +42,7 @@ export function toTree(traceData: TraceData, workspacePath: string): Tree { treeIndexes = [tree] - const data = traceData.filter(x => 'id' in x || ('cat' in x)).sort((a, b) => a.ts - b.ts) + const data = traceData.filter(hasTraceTimestamp).sort((a, b) => a.ts - b.ts) // const data = traceData.filter(x => 'id' in x || ('cat' in x && x.cat?.startsWith('check'))).sort((a, b) => a.ts - b.ts) for (const line of data) { if ('args' in line && line.args?.path && isAbsolute(line.args?.path)) diff --git a/test/traceData.test.ts b/test/traceData.test.ts new file mode 100644 index 0000000..1f3bbbd --- /dev/null +++ b/test/traceData.test.ts @@ -0,0 +1,69 @@ +import { describe, expect, it } from 'vitest' +import { getTraceDataSummary, traceData } from '../shared/src/traceData' + +describe('traceData', () => { + it('parses trace events', () => { + const result = traceData.safeParse([ + { + pid: 1, + tid: 1, + ph: 'X', + cat: 'check', + ts: 10, + name: 'checkSourceFile', + dur: 20, + args: { path: '/workspace/src/index.ts', pos: 1, end: 2 }, + }, + ]) + + expect(result.success).toBe(true) + }) + + it('parses timestamped types', () => { + const result = traceData.safeParse([ + { + id: 1, + intrinsicName: 'string', + recursionId: 1, + flags: ['String'], + ts: 12, + dur: 1, + display: 'string', + }, + ]) + + expect(result.success).toBe(true) + expect(result.success && getTraceDataSummary(result.data)).toEqual({ + totalTypes: 1, + timestampedTypes: 1, + untimestampedTypes: 0, + parseWarning: undefined, + }) + }) + + it('parses untimestamped stock TypeScript types', () => { + const result = traceData.safeParse([ + { + id: 1, + intrinsicName: 'string', + recursionId: 1, + flags: ['String'], + display: 'string', + }, + { + id: 2, + recursionId: 2, + flags: ['Object'], + display: '{ value: string }', + }, + ]) + + expect(result.success).toBe(true) + expect(result.success && getTraceDataSummary(result.data)).toEqual({ + totalTypes: 2, + timestampedTypes: 0, + untimestampedTypes: 2, + parseWarning: '2 type entries are missing timestamps and cannot be attributed to trace spans.', + }) + }) +}) diff --git a/test/traceTree.test.ts b/test/traceTree.test.ts new file mode 100644 index 0000000..0472820 --- /dev/null +++ b/test/traceTree.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, it, vi } from 'vitest' +import { toTree } from '../src/traceTree' +import type { TraceData } from '../shared/src/traceData' + +vi.mock('../src/storage', () => ({ + getWorkspacePath: vi.fn(() => '/workspace'), +})) + +vi.mock('../src/webview', () => ({ + postMessage: vi.fn(), +})) + +vi.mock('../src/appState', () => ({ + traceFiles: { value: {} }, +})) + +vi.mock('vscode', () => ({ + workspace: { + workspaceFolders: [{ uri: { fsPath: '/workspace' } }], + getConfiguration: vi.fn(() => ({ + get: vi.fn(), + })), + onDidChangeConfiguration: vi.fn(), + }, + window: { + showErrorMessage: vi.fn(), + createOutputChannel: vi.fn(() => ({ + appendLine: vi.fn(), + show: vi.fn(), + })), + }, + env: { + appRoot: '/Applications/Visual Studio Code.app', + }, +})) + +describe('traceTree', () => { + it('does not crash or attribute untimestamped types', () => { + const data: TraceData = [ + { + pid: 1, + tid: 1, + ph: 'X', + cat: 'check', + ts: 10, + name: 'checkSourceFile', + dur: 20, + args: { path: '/workspace/src/index.ts', pos: 1, end: 2 }, + }, + { + id: 1, + intrinsicName: 'string', + recursionId: 1, + flags: ['String'], + display: 'string', + }, + ] + + const tree = toTree(data, '/workspace') + + expect(tree.children).toHaveLength(1) + expect(tree.children[0].types).toHaveLength(0) + expect(tree.children[0].typeCnt).toBe(0) + }) +})