Skip to content

Commit 9391c20

Browse files
feat: add profiling utilities for performance analysis (#16198)
## Summary Adds lightweight profiling utilities for measuring execution time of sync and async functions. This is a developer tool for debugging performance issues during development — wrap any function with `timeSync` or `timeAsync` to measure its duration, then call `printProfileResults` to see a formatted table of timings. Not intended for production use or to be imported by default, just available when needed for performance investigations. These utilities were used to generate results for: - #16134 - #16129
1 parent e8502fa commit 9391c20

File tree

2 files changed

+137
-0
lines changed

2 files changed

+137
-0
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { describe, expect, it } from 'vitest'
2+
3+
import {
4+
clearProfiles,
5+
getProfileResults,
6+
printProfileResults,
7+
timeAsync,
8+
timeSync,
9+
} from './profiling.js'
10+
11+
describe('Profiling Utilities', () => {
12+
it('should time synchronous functions', () => {
13+
clearProfiles()
14+
15+
const result = timeSync('syncOperation', () => {
16+
let sum = 0
17+
for (let i = 0; i < 1000; i++) {
18+
sum += i
19+
}
20+
return sum
21+
})
22+
23+
expect(result).toBe(499500)
24+
25+
const results = getProfileResults()
26+
expect(results).toHaveLength(1)
27+
expect(results[0]?.name).toBe('syncOperation')
28+
expect(results[0]?.calls).toBe(1)
29+
expect(results[0]?.duration).toBeGreaterThanOrEqual(0)
30+
})
31+
32+
it('should time async functions', async () => {
33+
clearProfiles()
34+
35+
const result = await timeAsync('asyncOperation', async () => {
36+
await new Promise((resolve) => setTimeout(resolve, 10))
37+
return 'done'
38+
})
39+
40+
expect(result).toBe('done')
41+
42+
const results = getProfileResults()
43+
expect(results).toHaveLength(1)
44+
expect(results[0]?.name).toBe('asyncOperation')
45+
expect(results[0]?.calls).toBe(1)
46+
expect(results[0]?.duration).toBeGreaterThanOrEqual(9)
47+
})
48+
49+
it('should accumulate multiple calls', () => {
50+
clearProfiles()
51+
52+
for (let i = 0; i < 5; i++) {
53+
timeSync('repeatedOperation', () => i * 2)
54+
}
55+
56+
const results = getProfileResults()
57+
expect(results).toHaveLength(1)
58+
expect(results[0]?.calls).toBe(5)
59+
})
60+
61+
it('should print profile results', () => {
62+
clearProfiles()
63+
64+
timeSync('operation1', () => 'a')
65+
timeSync('operation2', () => 'b')
66+
67+
// Just ensure it doesn't throw
68+
printProfileResults()
69+
70+
const results = getProfileResults()
71+
expect(results).toHaveLength(2)
72+
})
73+
})
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
type ProfileEntry = {
2+
calls: number
3+
duration: number
4+
name: string
5+
}
6+
7+
const profiles = new Map<string, ProfileEntry>()
8+
const callStack: string[] = []
9+
10+
export const timeSync = <T>(name: string, fn: () => T): T => {
11+
callStack.push(name)
12+
const start = performance.now()
13+
try {
14+
return fn()
15+
} finally {
16+
const duration = performance.now() - start
17+
callStack.pop()
18+
const entry = profiles.get(name) || { name, calls: 0, duration: 0 }
19+
entry.duration += duration
20+
entry.calls++
21+
profiles.set(name, entry)
22+
}
23+
}
24+
25+
export const timeAsync = async <T>(name: string, fn: () => Promise<T>): Promise<T> => {
26+
callStack.push(name)
27+
const start = performance.now()
28+
try {
29+
return await fn()
30+
} finally {
31+
const duration = performance.now() - start
32+
callStack.pop()
33+
const entry = profiles.get(name) || { name, calls: 0, duration: 0 }
34+
entry.duration += duration
35+
entry.calls++
36+
profiles.set(name, entry)
37+
}
38+
}
39+
40+
export const clearProfiles = () => {
41+
profiles.clear()
42+
}
43+
44+
export const printProfileResults = () => {
45+
const entries = Array.from(profiles.values()).sort((a, b) => b.duration - a.duration)
46+
47+
console.log('\n=== Profile Results ===')
48+
console.log('Name'.padEnd(50) + 'Duration (ms)'.padStart(15) + 'Calls'.padStart(10))
49+
console.log('-'.repeat(75))
50+
51+
for (const entry of entries) {
52+
console.log(
53+
entry.name.padEnd(50) +
54+
entry.duration.toFixed(2).padStart(15) +
55+
String(entry.calls).padStart(10),
56+
)
57+
}
58+
59+
console.log('='.repeat(75) + '\n')
60+
}
61+
62+
export const getProfileResults = () => {
63+
return Array.from(profiles.values()).sort((a, b) => b.duration - a.duration)
64+
}

0 commit comments

Comments
 (0)