Skip to content

Commit df529b5

Browse files
committed
line profiler experiments
1 parent d7186b2 commit df529b5

7 files changed

Lines changed: 2160 additions & 0 deletions

File tree

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
# Node.js Line Profiler Experiment Results
2+
3+
## Executive Summary
4+
5+
**Recommendation: Use custom `process.hrtime.bigint()` instrumentation for line-level profiling in Codeflash.**
6+
7+
Despite the significant overhead (2000-7500%), the custom instrumentation approach:
8+
1. Correctly identifies hot spots with 100% accuracy
9+
2. Provides precise per-line timing data
10+
3. Works reliably with V8's JIT (after ~1000 iteration warmup)
11+
4. Can leverage existing tree-sitter infrastructure
12+
13+
---
14+
15+
## Approaches Tested
16+
17+
### 1. V8 Inspector Sampling Profiler
18+
19+
**How it works:** Uses V8's built-in CPU profiler via the inspector protocol. Samples the call stack at regular intervals.
20+
21+
**Results:**
22+
- Total samples: 6,028
23+
- Correctly identified `reverseString` as hottest (61.76% of samples)
24+
- Correctly identified `bubbleSort` inner loop (4.66%)
25+
- `fibonacci` appeared as 1.91%
26+
27+
**Pros:**
28+
- Very low overhead (~1-5%)
29+
- No code modification required
30+
- Built into Node.js
31+
32+
**Cons:**
33+
- Sampling-based: misses short operations
34+
- Only function-level granularity (not line-level)
35+
- Cannot distinguish individual lines within a function
36+
- 10μs minimum sampling interval limits precision
37+
38+
**Verdict:** Useful for high-level hotspot detection, but **not suitable** for line-level profiling.
39+
40+
---
41+
42+
### 2. Custom `process.hrtime.bigint()` Instrumentation
43+
44+
**How it works:** Insert timing calls around each statement, accumulate timings, report per-line statistics.
45+
46+
**Results:**
47+
48+
| Function | Baseline | Instrumented | Overhead |
49+
|----------|----------|--------------|----------|
50+
| fibonacci(30) | 132ns | 10.02μs | +7,511% |
51+
| reverseString | 8.66μs | 200μs | +2,209% |
52+
| bubbleSort | 343ns | 18.68μs | +5,341% |
53+
54+
**Timer Characteristics:**
55+
- Average timer overhead: ~962ns per call
56+
- Minimum: 0ns (cached)
57+
- Maximum: 4.35ms (occasional GC pause)
58+
59+
**JIT Warmup Effect:**
60+
- First batch: 189ns/call
61+
- After warmup (batch 2+): ~29ns/call
62+
- JIT stabilizes within 2,000 iterations (85% speedup)
63+
64+
**Accuracy Verification:**
65+
66+
Tested with known expensive/cheap operations:
67+
```
68+
Expected: Line 5 (array alloc) most expensive
69+
Actual: Line 5 = 49.8% of time ✓
70+
71+
Expected: toString() > arithmetic
72+
Actual: Line 3 (toString) = 14.9%, Line 4 (arithmetic) = 13.6% ✓
73+
```
74+
75+
**Line-Level Results for bubbleSort:**
76+
```
77+
Line 4 (inner loop): 28.1% of time, 44,000 calls
78+
Line 5 (comparison): 21.6% of time, 36,000 calls
79+
Line 6 (swap temp): 20.6% of time, 17,000 calls
80+
Line 8 (swap assign): 12.0% of time, 17,000 calls
81+
Line 7 (swap assign): 9.2% of time, 17,000 calls
82+
```
83+
84+
**Pros:**
85+
- Precise per-line timing
86+
- Correctly identifies relative costs
87+
- Works with any JavaScript code
88+
- No external dependencies
89+
90+
**Cons:**
91+
- High overhead (2000-7500%)
92+
- Requires AST transformation
93+
- Timer overhead dominates for very fast lines
94+
95+
**Verdict:** **Best approach** for detailed optimization analysis. Overhead is acceptable for profiling runs.
96+
97+
---
98+
99+
## Key Technical Findings
100+
101+
### 1. Timer Precision
102+
103+
`process.hrtime.bigint()` provides nanosecond precision but:
104+
- Minimum measurable time: ~28-30ns (after JIT warmup)
105+
- Timer call overhead: ~30-40ns best case, ~1μs average
106+
- Occasional spikes to milliseconds (GC/kernel scheduling)
107+
108+
### 2. JIT Impact
109+
110+
V8's JIT significantly affects measurements:
111+
- Cold code: ~190ns/call for fibonacci
112+
- Warm code: ~29ns/call (6.5x faster)
113+
- Stabilization: ~1,000-2,000 iterations
114+
- **Recommendation:** Always warmup before measuring
115+
116+
### 3. Measurement Consistency
117+
118+
Coefficient of variation across runs: 83.38% (high variance)
119+
- Caused by JIT warmup and GC pauses
120+
- Mitigation: Multiple runs, discard outliers, focus on relative %
121+
122+
### 4. Relative vs Absolute Accuracy
123+
124+
**Relative accuracy is excellent:**
125+
- Correctly ranks operations by cost
126+
- Identifies hot spots accurately
127+
- Percentage-based reporting is reliable
128+
129+
**Absolute accuracy is moderate:**
130+
- Timer overhead inflates small operations
131+
- Should not rely on absolute nanosecond values for fast lines
132+
- Use call counts + relative % instead
133+
134+
---
135+
136+
## Implementation Recommendations for Codeflash
137+
138+
### Recommended Architecture
139+
140+
```
141+
┌─────────────────────────────────────────────────────────────┐
142+
│ JavaScript Line Profiler │
143+
├─────────────────────────────────────────────────────────────┤
144+
│ 1. Parse with tree-sitter │
145+
│ 2. Identify statement boundaries │
146+
│ 3. Insert timing instrumentation │
147+
│ 4. Warmup for 1,000+ iterations │
148+
│ 5. Measure for 5,000+ iterations │
149+
│ 6. Report: per-line %, call counts, hot spots │
150+
└─────────────────────────────────────────────────────────────┘
151+
```
152+
153+
### Instrumentation Strategy
154+
155+
```javascript
156+
// Before:
157+
function example() {
158+
let sum = 0;
159+
for (let i = 0; i < n; i++) {
160+
sum += compute(i);
161+
}
162+
return sum;
163+
}
164+
165+
// After:
166+
function example() {
167+
let __t;
168+
169+
__t = process.hrtime.bigint();
170+
let sum = 0;
171+
__profiler.record('example', 2, process.hrtime.bigint() - __t);
172+
173+
__t = process.hrtime.bigint();
174+
for (let i = 0; i < n; i++) {
175+
__profiler.record('example', 3, process.hrtime.bigint() - __t);
176+
177+
__t = process.hrtime.bigint();
178+
sum += compute(i);
179+
__profiler.record('example', 4, process.hrtime.bigint() - __t);
180+
181+
__t = process.hrtime.bigint();
182+
}
183+
__profiler.record('example', 3, process.hrtime.bigint() - __t);
184+
185+
__t = process.hrtime.bigint();
186+
const __ret = sum;
187+
__profiler.record('example', 6, process.hrtime.bigint() - __t);
188+
return __ret;
189+
}
190+
```
191+
192+
### Special Cases to Handle
193+
194+
1. **Return statements:** Store value, record time, then return
195+
2. **Loops:** Time loop overhead separately from body
196+
3. **Conditionals:** Time condition evaluation and each branch
197+
4. **Try/catch:** Wrap carefully to preserve exception semantics
198+
5. **Async/await:** Handle promise timing correctly
199+
200+
### Output Format
201+
202+
```json
203+
{
204+
"function": "bubbleSort",
205+
"file": "sort.js",
206+
"lines": [
207+
{"line": 4, "percent": 28.1, "calls": 44000, "avgNs": 42},
208+
{"line": 5, "percent": 21.6, "calls": 36000, "avgNs": 40},
209+
{"line": 6, "percent": 20.6, "calls": 17000, "avgNs": 80}
210+
],
211+
"hotSpots": [4, 5, 6]
212+
}
213+
```
214+
215+
---
216+
217+
## Comparison Summary
218+
219+
| Approach | Line Granularity | Accuracy | Overhead | Complexity |
220+
|----------|------------------|----------|----------|------------|
221+
| V8 Sampling | Function only | Moderate | ~1-5% | Low |
222+
| Custom hrtime | Per-line | High | 2000-7500% | Medium |
223+
224+
**Winner: Custom hrtime instrumentation**
225+
226+
---
227+
228+
## Files in This Experiment
229+
230+
- `target-functions.js` - Test functions to profile
231+
- `custom-line-profiler.js` - Custom instrumentation implementation
232+
- `v8-inspector-profiler.js` - V8 inspector-based profiler
233+
- `run-experiment.js` - Main experiment runner
234+
- `experiment-results.json` - Detailed timing data
235+
- `RESULTS.md` - This summary document

0 commit comments

Comments
 (0)