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
36 changes: 21 additions & 15 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,53 +52,59 @@
],
"order": 0
},
"tsperf.tracer.saveNameCommand": {
"type": "string",
"default": "",
"description": "Optional shell command used to generate the default save name. The first line of stdout is used; empty output or command failures fall back to \"default\".",
"order": 1
},
"tsperf.tracer.traceCmd": {
"type": "string",
"default": "npx tsc --noEmit --generateTrace ${traceDir}",
"description": "command to generate tsc traces",
"order": 1
"order": 2
},
"tsperf.tracer.typescriptPath": {
"type": "string",
"default": "",
"description": "Path to TypeScript. Must be specified if 'Use TypeScript from' is 'Use tracer TypeScript path setting'",
"order": 2
"order": 3
},
"tsperf.tracer.benchmarkIterations": {
"type": "number",
"default": 3,
"description": "Higher values reduce variance but increase benchmarking time",
"order": 3
"order": 4
},
"tsperf.tracer.restartTsserverOnIteration": {
"type": "boolean",
"default": false,
"description": "Restart tsserver on each iteration to avoid caching influincing measurements",
"order": 4
"order": 5
},
"tsperf.tracer.allIdentifiers": {
"type": "boolean",
"default": false,
"description": "Benchmark all allIdentifiers or only the first of each statement",
"order": 5
"order": 6
},
"tsperf.tracer.enableRealtimeMetrics": {
"type": "boolean",
"default": true,
"description": "Create diagnostics from tsserver",
"order": 6
"order": 7
},
"tsperf.tracer.enableTraceMetrics": {
"type": "boolean",
"default": true,
"description": "Create diagnostics from trace data",
"order": 7
"order": 8
},
"tsperf.tracer.fileBrowserExecutable": {
"type": "string",
"default": "",
"description": "command to open your preferred file browser. Use the ${traceDir} substitution variable",
"order": 8
"order": 9
},
"tsperf.tracer.traceTimeThresholds": {
"type": "object",
Expand All @@ -125,7 +131,7 @@
"error": -1
},
"description": "todo",
"order": 9,
"order": 10,
"markdownDescription": "# Trace Time Thresholds\n\nTrigger diagnostics from trace files when check time in ms exceeds these thresholds.\n\n* -1 will disable diagnostics of that severity\n"
},
"tsperf.tracer.traceTypeThresholds": {
Expand Down Expand Up @@ -153,7 +159,7 @@
"error": -1
},
"description": "todo",
"order": 10,
"order": 11,
"markdownDescription": "# Trace Type Thresholds\n\nTrigger diagnostics from trace files when the number of types created locally in this check call exceeds these thresholds.\n\n* -1 will disable diagnostics of that severity\n"
},
"tsperf.tracer.traceTotalTypeThresholds": {
Expand Down Expand Up @@ -181,14 +187,14 @@
"error": -1
},
"description": "todo",
"order": 11,
"order": 12,
"markdownDescription": "# Trace Total Type Thresholds\n\nTrigger diagnostics from trace files when the number of types created in this check call stack exceeds these thresholds.\n\n* -1 will disable diagnostics of that severity\n"
},
"tsperf.tracer.traceDiagnosticsRelative": {
"type": "boolean",
"default": false,
"description": "Use measurements relative to the average for trace diagnostics",
"order": 12
"order": 13
},
"tsperf.tracer.traceTimeRelativeThresholds": {
"type": "object",
Expand All @@ -215,7 +221,7 @@
"error": -1
},
"description": "todo",
"order": 13
"order": 14
},
"tsperf.tracer.traceTypeRelativeThresholds": {
"type": "object",
Expand All @@ -242,7 +248,7 @@
"error": -1
},
"description": "todo",
"order": 14
"order": 15
},
"tsperf.tracer.traceTotalTypeRelativeThresholds": {
"type": "object",
Expand All @@ -269,7 +275,7 @@
"error": -1
},
"description": "todo",
"order": 15
"order": 16
}
}
},
Expand Down
7 changes: 7 additions & 0 deletions scripts/generate-contributes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,13 @@ const orderedConfigurationProperties: Partial<Record<PropertyConfigKey, Record<s
],
},
},
{
'tsperf.tracer.saveNameCommand': {
type: 'string',
default: '',
description: 'Optional shell command used to generate the default save name. The first line of stdout is used; empty output or command failures fall back to "default".',
},
},
{
'tsperf.tracer.traceCmd': {
type: 'string',
Expand Down
10 changes: 7 additions & 3 deletions src/appState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { type Ref, type ShallowRef, type UnwrapRef, nextTick, watch as plainWatc
import type * as vscode from 'vscode'
import type { TraceData } from '../shared/src/traceData'
import { getTracePanel, isTraceViewAlive, postMessage } from './webview'
import { getProjectName, getWorkspacePath } from './storage'
import { getDefaultSaveName, getProjectName, getWorkspacePath } from './storage'
import { setStatusBarState } from './statusBar'
import { sendTraceDir } from './commands'

Expand Down Expand Up @@ -86,7 +86,7 @@ export async function initAppState(extensionContext: vscode.ExtensionContext) {
}

getSaves(projectPath.value)
saveName.value = 'default'
void openDefaultSaveName()
}, name => postMessage({ message: 'projectOpen', name }))

watchT('savePath', (path) => {
Expand Down Expand Up @@ -138,7 +138,11 @@ export async function initAppState(extensionContext: vscode.ExtensionContext) {

workspacePath.value = getWorkspacePath()
projectName.value = getProjectName()
saveName.value = 'default'
void openDefaultSaveName()
}

async function openDefaultSaveName() {
saveName.value = await getDefaultSaveName()
}

const triggers: Partial<Record<keyof State, { handler: ((arg: any) => void | Promise<void>), remoteHandler: (arg: any) => void }>> = {}
Expand Down
3 changes: 3 additions & 0 deletions src/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const currentConfig = {
benchmarkIterations: 3,
restartTsserverOnIteration: false,
allIdentifiers: false,
saveNameCommand: '',
// eslint-disable-next-line no-template-curly-in-string
traceCmd: 'npx tsc --noEmit --generateTrace ${traceDir}',
traceTimeThresholds: { info: 1, warning: -1, error: -1 },
Expand Down Expand Up @@ -56,6 +57,7 @@ const configValidate = {
benchmarkIterations: isNumber,
restartTsserverOnIteration: isBoolean,
allIdentifiers: isBoolean,
saveNameCommand: isString,
traceCmd: isString,
traceTimeThresholds: isThresholds,
traceTypeThresholds: isThresholds,
Expand All @@ -78,6 +80,7 @@ const configHandlers = {
benchmarkIterations: noop,
restartTsserverOnIteration: noop,
allIdentifiers: noop,
saveNameCommand: noop,
traceCmd: noop,
traceTimeThresholds: noop,
traceTypeThresholds: noop,
Expand Down
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const configKeys = [
'benchmarkIterations',
'restartTsserverOnIteration',
'allIdentifiers',
'saveNameCommand',
'traceCmd',
'traceTimeThresholds',
'traceTypeThresholds',
Expand Down
25 changes: 25 additions & 0 deletions src/saveName.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { isAbsolute, normalize } from 'node:path'

export const DEFAULT_SAVE_NAME = 'default'

export function normalizeSaveName(value: string) {
const trimmed = value.trim()
if (!trimmed)
return DEFAULT_SAVE_NAME

const rawParts = trimmed.split(/[\\/]+/).filter(Boolean)
if (rawParts.includes('..'))
return DEFAULT_SAVE_NAME
if (/^(?:[a-z]:[\\/]|\\\\)/i.test(trimmed))
return DEFAULT_SAVE_NAME

const normalized = normalize(trimmed).replaceAll(/\\/g, '/')
if (isAbsolute(normalized))
return DEFAULT_SAVE_NAME

const parts = normalized.split('/').filter(Boolean)
if (!parts.length || parts.includes('..'))
return DEFAULT_SAVE_NAME

return parts.join('/')
}
25 changes: 25 additions & 0 deletions src/storage.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { exec } from 'node:child_process'
import { readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import { env } from 'node:process'
import { promisify } from 'node:util'
import * as vscode from 'vscode'
import { traceData } from '../shared/src/traceData'
import { getCurrentConfig } from './configuration'
import { log } from './logger'
import { DEFAULT_SAVE_NAME, normalizeSaveName } from './saveName'
import { state } from './appState'

const execAsync = promisify(exec)

export function getProjectName(): string {
if (state.projectName.value)
return state.projectName.value
Expand All @@ -26,6 +31,26 @@ export function getProjectName(): string {
return state.projectName.value
}

export async function getDefaultSaveName(): Promise<string> {
const command = getCurrentConfig().saveNameCommand.trim()
if (!command)
return DEFAULT_SAVE_NAME

try {
const { stdout } = await execAsync(command, {
cwd: state.workspacePath.value || getWorkspacePath(),
timeout: 5000,
windowsHide: true,
})
const [firstLine = ''] = stdout.trim().split(/\r?\n/)
return normalizeSaveName(firstLine)
}
catch (e) {
log(`saveNameCommand failed: ${e}`)
return DEFAULT_SAVE_NAME
}
}

export function getSavePath(): string {
return state.savePath.value = join(state.projectPath.value, state.saveName.value)
}
Expand Down
18 changes: 18 additions & 0 deletions test/save-name.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { describe, expect, it } from 'vitest'
import { DEFAULT_SAVE_NAME, normalizeSaveName } from '../src/saveName'

describe('normalizeSaveName', () => {
it('uses default for empty, absolute, or parent-traversal names', () => {
expect(normalizeSaveName('')).toBe(DEFAULT_SAVE_NAME)
expect(normalizeSaveName(' ')).toBe(DEFAULT_SAVE_NAME)
expect(normalizeSaveName('/tmp/trace')).toBe(DEFAULT_SAVE_NAME)
expect(normalizeSaveName('C:\\trace')).toBe(DEFAULT_SAVE_NAME)
expect(normalizeSaveName('../trace')).toBe(DEFAULT_SAVE_NAME)
expect(normalizeSaveName('feature/../trace')).toBe(DEFAULT_SAVE_NAME)
})

it('allows nested relative save names', () => {
expect(normalizeSaveName(' main ')).toBe('main')
expect(normalizeSaveName('feature/perf-trace')).toBe('feature/perf-trace')
})
})