Skip to content
Draft
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
72 changes: 70 additions & 2 deletions src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { addTraceFile, getWorkspacePath, openTerminal, openTraceDirectoryExterna
import { addTraceDiagnostics, clearTaceDiagnostics } from './traceDiagnostics'
import { setStatusBarState } from './statusBar'
import { afterWatches, projectPath, saveName, state, traceFiles, traceRunning } from './appState'
import { TRACE_RUN_METRICS_FILE, buildTraceRunMetrics, discoverTraceJsonFiles, summarizeTraceParse, writeTraceRunMetrics } from './traceRunMetrics'

const readdir = promisify(readdirC)

Expand Down Expand Up @@ -148,21 +149,50 @@ async function runTrace(args?: unknown[]) {
setStatusBarState('traceError', false)

log(`shell: ${process.env.SHELL}`)
const startedAtMs = Date.now()
const startedAt = new Date(startedAtMs).toISOString()
const cmdProcess = spawn(fullCmd, [], { cwd: newProjectPath, shell: process.env.SHELL })

let err = ''
let out = ''
cmdProcess.stderr.on('data', data => err += data.toString())

cmdProcess.stdout.on('data', data => log(data.toString()))
cmdProcess.stdout.on('data', (data) => {
const chunk = data.toString()
out += chunk
log(chunk)
})

let metricsWritten = false
async function writeMetricsOnce(exitCode: number | null) {
if (metricsWritten)
return

cmdProcess.on('error', (error) => {
metricsWritten = true
await writeRunMetrics({
command: fullCmd,
cwd: newProjectPath,
traceDir,
startedAt,
startedAtMs,
exitCode,
stdout: out,
stderr: err,
})
}

cmdProcess.on('error', async (error) => {
traceRunning.value = false
setStatusBarState('traceError', true)
vscode.window.showErrorMessage(error.message)
await writeMetricsOnce(null)
})

cmdProcess.on('exit', async (code) => {
log('---- trace stderr -----')
log(err)
traceRunning.value = false
await writeMetricsOnce(code)
if (code) {
setStatusBarState('traceError', true)
vscode.window.showErrorMessage('error running trace')
Expand All @@ -174,6 +204,41 @@ async function runTrace(args?: unknown[]) {
})
}

async function writeRunMetrics(input: {
command: string
cwd: string
traceDir: string
startedAt: string
startedAtMs: number
exitCode: number | null
stdout: string
stderr: string
}) {
try {
const traceJsonFiles = existsSync(input.traceDir) ? await discoverTraceJsonFiles(input.traceDir) : []
const parseSummary = existsSync(input.traceDir)
? await summarizeTraceParse(input.traceDir, traceJsonFiles)
: { status: 'missing' as const, topLevelWarning: 'Trace directory was not created.' }

await writeTraceRunMetrics(input.traceDir, buildTraceRunMetrics({
command: input.command,
cwd: input.cwd,
traceDir: input.traceDir,
startedAt: input.startedAt,
endedAt: new Date().toISOString(),
wallTimeMs: Date.now() - input.startedAtMs,
exitCode: input.exitCode,
stdout: input.stdout,
stderr: input.stderr,
traceJsonFiles,
parseSummary,
}))
}
catch (error) {
log(`error writing trace metrics: ${error instanceof Error ? error.message : `${error}`}`)
}
}

export async function sendTraceDir(traceDir: string) {
try {
if (!existsSync(traceDir)) {
Expand All @@ -182,6 +247,9 @@ export async function sendTraceDir(traceDir: string) {

const fileNames = await readdir(traceDir)
for (const fileName of fileNames) {
if (fileName === TRACE_RUN_METRICS_FILE)
continue

sendTrace(traceDir, fileName)
}
}
Expand Down
182 changes: 182 additions & 0 deletions src/extendedDiagnostics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
export type ExtendedDiagnosticUnit = 'count' | 'kb' | 'ms'

export interface ExtendedDiagnosticMetric {
value: number
unit: ExtendedDiagnosticUnit
raw: string
}

export interface ExtendedDiagnosticsSummary {
files?: number
lines?: {
total?: number
library?: number
definitions?: number
typescript?: number
javascript?: number
json?: number
other?: number
}
identifiers?: number
symbols?: number
types?: number
instantiations?: number
memoryUsedKb?: number
parseTimeMs?: number
bindTimeMs?: number
checkTimeMs?: number
emitTimeMs?: number
totalTimeMs?: number
metrics: Record<string, ExtendedDiagnosticMetric>
}

const metricKeysByLabel = new Map<string, string>([
['files', 'files'],
['lines', 'lines'],
['lines of library', 'linesOfLibrary'],
['lines of definitions', 'linesOfDefinitions'],
['lines of typescript', 'linesOfTypeScript'],
['lines of javascript', 'linesOfJavaScript'],
['lines of json', 'linesOfJson'],
['lines of other', 'linesOfOther'],
['nodes', 'nodes'],
['identifiers', 'identifiers'],
['symbols', 'symbols'],
['types', 'types'],
['instantiations', 'instantiations'],
['memory used', 'memoryUsed'],
['assignability cache size', 'assignabilityCacheSize'],
['identity cache size', 'identityCacheSize'],
['subtype cache size', 'subtypeCacheSize'],
['strict subtype cache size', 'strictSubtypeCacheSize'],
['i/o read time', 'ioReadTime'],
['i/o write time', 'ioWriteTime'],
['parse time', 'parseTime'],
['resolvemodule time', 'resolveModuleTime'],
['resolvetypereference time', 'resolveTypeReferenceTime'],
['resolvelibrary time', 'resolveLibraryTime'],
['program time', 'programTime'],
['bind time', 'bindTime'],
['check time', 'checkTime'],
['transformtime time', 'transformTime'],
['commenttime time', 'commentTime'],
['printtime time', 'printTime'],
['emit time', 'emitTime'],
['total time', 'totalTime'],
])

const valuePattern = /^([+-]?(?:\d{1,3}(?:,\d{3})+|\d+)(?:\.\d+)?)\s*([a-z]+)?$/i

export function parseExtendedDiagnostics(text: string): ExtendedDiagnosticsSummary | undefined {
const metrics: Record<string, ExtendedDiagnosticMetric> = {}

for (const line of text.split(/\r?\n/)) {
const separatorIndex = line.indexOf(':')
if (separatorIndex === -1)
continue

const label = line.slice(0, separatorIndex).trim().toLowerCase()
const key = metricKeysByLabel.get(label)
if (!key)
continue

const metric = parseMetricValue(line.slice(separatorIndex + 1))
if (!metric)
continue

metrics[key] = metric
}

if (Object.keys(metrics).length === 0)
return undefined

const summary: ExtendedDiagnosticsSummary = { metrics }
setSummaryNumber(summary, 'files', metrics.files)
setSummaryNumber(summary, 'identifiers', metrics.identifiers)
setSummaryNumber(summary, 'symbols', metrics.symbols)
setSummaryNumber(summary, 'types', metrics.types)
setSummaryNumber(summary, 'instantiations', metrics.instantiations)
setSummaryNumber(summary, 'memoryUsedKb', metrics.memoryUsed)
setSummaryNumber(summary, 'parseTimeMs', metrics.parseTime)
setSummaryNumber(summary, 'bindTimeMs', metrics.bindTime)
setSummaryNumber(summary, 'checkTimeMs', metrics.checkTime)
setSummaryNumber(summary, 'emitTimeMs', metrics.emitTime)
setSummaryNumber(summary, 'totalTimeMs', metrics.totalTime)

const lines = {
total: metrics.lines?.value,
library: metrics.linesOfLibrary?.value,
definitions: metrics.linesOfDefinitions?.value,
typescript: metrics.linesOfTypeScript?.value,
javascript: metrics.linesOfJavaScript?.value,
json: metrics.linesOfJson?.value,
other: metrics.linesOfOther?.value,
}
if (Object.values(lines).some(value => value !== undefined))
summary.lines = lines

return summary
}

function parseMetricValue(rawValue: string): ExtendedDiagnosticMetric | undefined {
const raw = rawValue.trim()
const match = valuePattern.exec(raw)
if (!match)
return undefined

const value = Number(match[1].replace(/,/g, ''))
if (!Number.isFinite(value))
return undefined

const rawUnit = match[2]?.toLowerCase()
if (rawUnit === 's') {
return {
value: normalizeNumber(value * 1000),
unit: 'ms',
raw,
}
}

if (rawUnit === 'ms') {
return {
value: normalizeNumber(value),
unit: 'ms',
raw,
}
}

if (rawUnit === 'k' || rawUnit === 'kb' || rawUnit === 'kib') {
return {
value: normalizeNumber(value),
unit: 'kb',
raw,
}
}

if (rawUnit === 'm' || rawUnit === 'mb' || rawUnit === 'mib') {
return {
value: normalizeNumber(value * 1024),
unit: 'kb',
raw,
}
}

return {
value: normalizeNumber(value),
unit: 'count',
raw,
}
}

function normalizeNumber(value: number) {
return Number(value.toFixed(3))
}

function setSummaryNumber(
summary: ExtendedDiagnosticsSummary,
key: Exclude<keyof ExtendedDiagnosticsSummary, 'lines' | 'metrics'>,
metric: ExtendedDiagnosticMetric | undefined,
) {
if (metric)
summary[key] = metric.value
}
Loading