Generate a performance profile, identify hotspots, and optimize.
Focus: $ARGUMENTS
Workflow
- Decide what to profile (e.g. time) and write an isolated script that
exercises the slow path if nothing runs it directly. Run
profiler-md --help <language> for profiling instructions
- Capture a baseline: generate a profile, convert it with
profiler-md path/to/profile, and read the full report, focusing on:
- Hottest functions: self % shows where time is spent or memory is allocated,
rather than passed through
- Hottest call stacks: the call path to the hot functions
- Identify the hottest 1-3 functions by self % and read their source:
- A native function cannot be changed, but its cost may come from unnecessary
work in its callers (e.g. parsing the same data twice)
- Project functions are direct targets
- State a hypothesis (see "Hypotheses"). If the bottleneck is unclear, reread
the hot function and its callers
- Apply the minimal change that removes the bottleneck, one optimization at a
time
- Leave unrelated code alone
- Optimize the general case, not the benchmark: don't exploit quirks of the
profiled input (its size, ordering, value distribution, or hardcoded
special cases for it) unless they're guaranteed properties of real inputs
- Prefer a change inside the hot function or its callers. A change that
replaces a representation, an intermediate structure, or a layer boundary
is a redesign. Follow "Redesigns" before starting one
- Run the relevant tests: if any newly fail, fix the optimization or revert it
- Capture a new report as in step 2 and compare self % for the targeted
function(s) against the baseline: if the improvement is negligible or
unclear, revert and go back to step 4. If the comparison is too noisy to
call, collect more data (see "Measurement")
- If the goal is unmet and a clear bottleneck remains, repeat from step 3 with
the new profile as the baseline. If no change inside a hot function can
remove the remaining cost, see "Redesigns"
- Report:
- Before vs after for the hot function(s)
- Any other functions that moved significantly (regressions)
Measurement
profiler-md reports are the ONLY measurements in this workflow. NEVER time
manually (e.g. time or performance.now()).
Manual timing yields one noisy end-to-end number that can improve for reasons
unrelated to the change (machine load, caches, GC timing). Its instrumentation
also distorts the hot path. A profile attributes work to functions, so a
before/after comparison proves the optimization removed the targeted work, not
merely that one run was faster.
If a profile has too little data to trust, collect more:
- Run the workload longer or loop it more times
- Raise the sampling frequency if the profiler supports it (see
profiler-md --help <language>)
- Regenerate the profile and compare again
Hypotheses
Before changing anything, state:
- What is the bottleneck? (e.g. "repeated object allocation in the hot loop",
"O(n²) suffix scan", "redundant map lookups")
- What is the expected fix? (e.g. "hoist allocation outside loop", "use a
two-pointer suffix scan", "cache the lookup result")
- Why will it be faster?
Redesigns
A local optimization makes the existing design do its work faster. A redesign
replaces a representation, an intermediate structure, or a layer boundary so the
hot work never runs. It changes code the profile never ranked and can move work
instead of removing it, so it requires stronger evidence and a spike first.
When to consider one
Only when the profile shows the cost is in the design rather than in one
function:
- The profile is flat: many small functions implement the same abstraction (e.g.
converting the same intermediate representation at each layer)
- The hot function does only what its interface requires, and every local change
to it was reverted in step 7
- The same local fix would repeat at many call sites
- The design sets the asymptotic bound (e.g. an intermediate tree built in full
and walked once)
Before designing anything, sum the self % of the functions the redesign would
remove. That sum is the ceiling on its gain. If the ceiling is below the goal,
report that the goal requires a different approach.
Spike
NEVER start with the redesign itself:
- State the hypothesis as in "Hypotheses", plus which abstraction requires the
work, what replaces it, which functions it removes, and the ceiling
- Write a throwaway implementation for the profiled path alone: hardcode, skip
edge cases and tests, and leave other call sites on the old code. Isolate it
(a copy of the module, or its own commit or worktree) so discarding it is one
operation. NEVER edit the existing implementation in place
- Check it produces the same output as the baseline on the workload
- Profile it and compare against the baseline: the removed functions should be
gone, and no new function should absorb their cost
- Proceed only if the gain meets the goal or a clear change closes the gap.
Otherwise discard the spike and report the disproof as a finding
- Get the user's approval before implementing. Present the hypothesis, the
ceiling, the spike's profile against the baseline, and what the spike
skipped. Stop until the user answers
- Implement, then run steps 6 and 7 of the workflow. Compare the final profile
with the spike's: a lost gain means the added generality reintroduced work.
Treat the function that absorbed it as a new bottleneck
Time-box the spike to a fraction of the redesign's cost. If it runs over, the
design is too unclear to implement. Discard it.
Report a redesign with the ceiling and the spike's and final profiles against
the baseline.
Focus areas
Sub-bullets are examples.
- Time complexity: reduce the asymptotic work per input element
- Replace
O(n²) scans with single-pass or two-pointer algorithms
- Use heaps for top-N instead of fully sorting
- Sort once and binary search instead of repeated linear scans
- Redundant work: compute each result once
- Cache or memoize repeated parses and lookups
- Hoist loop-invariant computation out of loops
- Build lookup tables once instead of searching repeatedly, and intern
repeated values
- Update incrementally instead of recomputing from scratch
- Persist expensive results between invocations, keyed by input so stale
entries are never served
- Work avoidance: skip or defer computation whose result may never be used
- Exit early once the answer is known
- Filter before expensive transforms
- Order checks cheapest-first so expensive predicates run last and rarely
- Add fast paths for the common input shape
- Defer initialization and large dependency loading until first use,
especially on the startup critical path
- Debounce or throttle work triggered by bursty events
- Data structure selection: match the structure to the access pattern
- Use sets/maps for membership and lookup instead of linear scans
- Use bitsets for dense boolean membership
- Use a ring buffer for FIFO access instead of shifting an array
- Cache locality: lay out data so hot loops read memory sequentially
- Compressed sparse row (CSR) format
- Data-oriented design (struct-of-arrays, packed primitive arrays)
- Sparse arrays indexed by sequential IDs instead of integer-keyed hash maps
- Iterate in storage order (e.g. row-major for nested arrays)
- Allocation and copying: allocate and copy less in hot paths
- Hoist allocations out of hot loops, and reuse buffers instead of
reallocating
- Preallocate arrays and buffers to their known final size instead of growing
incrementally
- Fuse chained transforms into one pass instead of building an intermediate
collection per step
- Use views/slices over underlying data instead of copying, and defer cloning
until mutation
- Memory footprint: retain only what the computation needs
- Stream or process incrementally instead of materializing intermediates
- Bound caches (e.g. LRU), and use weak references for object-keyed caches
- Remove listeners, timers, and subscriptions when done
- Avoid closures capturing large outer scopes, and drop references held by
long-lived structures once no longer needed
- String handling: minimize the strings created and scanned in hot paths
- Avoid repeated concatenation in loops
- Work with indices or char codes into the original string instead of creating
substrings
- Minimize serialization/deserialization round-trips
- Avoid regex in hot paths
- Runtime friendliness: stay on the JIT's optimized fast paths
- Keep object shapes consistent (same fields, same initialization order)
- Avoid polymorphic call sites and mixed-type arrays in hot paths
- Keep numbers in the runtime's fast representations, and avoid deoptimization
triggers in hot loops
- I/O and queries: make fewer round-trips and move fewer bytes
- Read or write in large chunks instead of many small operations
- Eliminate N+1 patterns by coalescing small queries or requests into one
batch
- Push filtering, aggregation, and pagination down to the data store, and add
indexes matching the hot query's predicates
- Compress or use binary encodings for large transfers, and send only the
fields the consumer reads
- Concurrency: use idle hardware for independent work
- Run independent async operations concurrently, bounded to what the awaited
resource can absorb
- Overlap I/O waits with computation instead of strictly sequencing them
- Split CPU-bound work across threads or processes, and use SIMD where the
platform supports it
- Contention and backpressure: keep concurrent parts from waiting on or
overwhelming each other
- Shrink critical sections to minimize time spent holding locks
- Shard or partition shared state, and prefer immutable or thread-local data
over shared mutable state
- Bound queues so producers can't outrun consumers, and shed or coalesce load
when the system is saturated
- Precision trade-offs: do cheaper approximate work where the consumer tolerates
the difference
- Use approximate algorithms (sampling, sketches, bloom filters)
- Lower resolution or cap iteration counts when the output tolerance allows
- Observability overhead: keep instrumentation too cheap to distort the hot path
- Keep logging, tracing, and assertions out of hot loops
- Sample or gate expensive instrumentation behind flags
1---2name: profile-optimize3description: Generate a performance profile, identify hotspots, and optimize.4---56Generate a performance profile, identify hotspots, and optimize.78Focus: $ARGUMENTS910# Workflow11121. Decide what to profile (e.g. time) and write an isolated script that13 exercises the slow path if nothing runs it directly. Run14 `profiler-md --help <language>` for profiling instructions152. Capture a baseline: generate a profile, convert it with16 `profiler-md path/to/profile`, and read the full report, focusing on:17 - Hottest functions: self % shows where time is spent or memory is allocated,18 rather than passed through19 - Hottest call stacks: the call path to the hot functions203. Identify the hottest 1-3 functions by self % and read their source:21 - A native function cannot be changed, but its cost may come from unnecessary22 work in its callers (e.g. parsing the same data twice)23 - Project functions are direct targets244. State a hypothesis (see "Hypotheses"). If the bottleneck is unclear, reread25 the hot function and its callers265. Apply the minimal change that removes the bottleneck, one optimization at a27 time28 - Leave unrelated code alone29 - Optimize the general case, not the benchmark: don't exploit quirks of the30 profiled input (its size, ordering, value distribution, or hardcoded31 special cases for it) unless they're guaranteed properties of real inputs32 - Prefer a change inside the hot function or its callers. A change that33 replaces a representation, an intermediate structure, or a layer boundary34 is a redesign. Follow "Redesigns" before starting one356. Run the relevant tests: if any newly fail, fix the optimization or revert it367. Capture a new report as in step 2 and compare self % for the targeted37 function(s) against the baseline: if the improvement is negligible or38 unclear, revert and go back to step 4. If the comparison is too noisy to39 call, collect more data (see "Measurement")408. If the goal is unmet and a clear bottleneck remains, repeat from step 3 with41 the new profile as the baseline. If no change inside a hot function can42 remove the remaining cost, see "Redesigns"439. Report:44 - Before vs after for the hot function(s)45 - Any other functions that moved significantly (regressions)4647# Measurement4849`profiler-md` reports are the ONLY measurements in this workflow. NEVER time50manually (e.g. `time` or `performance.now()`).5152Manual timing yields one noisy end-to-end number that can improve for reasons53unrelated to the change (machine load, caches, GC timing). Its instrumentation54also distorts the hot path. A profile attributes work to functions, so a55before/after comparison proves the optimization removed the targeted work, not56merely that one run was faster.5758If a profile has too little data to trust, collect more:5960- Run the workload longer or loop it more times61- Raise the sampling frequency if the profiler supports it (see62 `profiler-md --help <language>`)63- Regenerate the profile and compare again6465# Hypotheses6667Before changing anything, state:6869- What is the bottleneck? (e.g. "repeated object allocation in the hot loop",70 "O(n²) suffix scan", "redundant map lookups")71- What is the expected fix? (e.g. "hoist allocation outside loop", "use a72 two-pointer suffix scan", "cache the lookup result")73- Why will it be faster?7475# Redesigns7677A local optimization makes the existing design do its work faster. A redesign78replaces a representation, an intermediate structure, or a layer boundary so the79hot work never runs. It changes code the profile never ranked and can move work80instead of removing it, so it requires stronger evidence and a spike first.8182## When to consider one8384Only when the profile shows the cost is in the design rather than in one85function:8687- The profile is flat: many small functions implement the same abstraction (e.g.88 converting the same intermediate representation at each layer)89- The hot function does only what its interface requires, and every local change90 to it was reverted in step 791- The same local fix would repeat at many call sites92- The design sets the asymptotic bound (e.g. an intermediate tree built in full93 and walked once)9495Before designing anything, sum the self % of the functions the redesign would96remove. That sum is the ceiling on its gain. If the ceiling is below the goal,97report that the goal requires a different approach.9899## Spike100101NEVER start with the redesign itself:1021031. State the hypothesis as in "Hypotheses", plus which abstraction requires the104 work, what replaces it, which functions it removes, and the ceiling1052. Write a throwaway implementation for the profiled path alone: hardcode, skip106 edge cases and tests, and leave other call sites on the old code. Isolate it107 (a copy of the module, or its own commit or worktree) so discarding it is one108 operation. NEVER edit the existing implementation in place1093. Check it produces the same output as the baseline on the workload1104. Profile it and compare against the baseline: the removed functions should be111 gone, and no new function should absorb their cost1125. Proceed only if the gain meets the goal or a clear change closes the gap.113 Otherwise discard the spike and report the disproof as a finding1146. Get the user's approval before implementing. Present the hypothesis, the115 ceiling, the spike's profile against the baseline, and what the spike116 skipped. Stop until the user answers1177. Implement, then run steps 6 and 7 of the workflow. Compare the final profile118 with the spike's: a lost gain means the added generality reintroduced work.119 Treat the function that absorbed it as a new bottleneck120121Time-box the spike to a fraction of the redesign's cost. If it runs over, the122design is too unclear to implement. Discard it.123124Report a redesign with the ceiling and the spike's and final profiles against125the baseline.126127# Focus areas128129Sub-bullets are examples.130131- Time complexity: reduce the asymptotic work per input element132 - Replace `O(n²)` scans with single-pass or two-pointer algorithms133 - Use heaps for top-N instead of fully sorting134 - Sort once and binary search instead of repeated linear scans135- Redundant work: compute each result once136 - Cache or memoize repeated parses and lookups137 - Hoist loop-invariant computation out of loops138 - Build lookup tables once instead of searching repeatedly, and intern139 repeated values140 - Update incrementally instead of recomputing from scratch141 - Persist expensive results between invocations, keyed by input so stale142 entries are never served143- Work avoidance: skip or defer computation whose result may never be used144 - Exit early once the answer is known145 - Filter before expensive transforms146 - Order checks cheapest-first so expensive predicates run last and rarely147 - Add fast paths for the common input shape148 - Defer initialization and large dependency loading until first use,149 especially on the startup critical path150 - Debounce or throttle work triggered by bursty events151- Data structure selection: match the structure to the access pattern152 - Use sets/maps for membership and lookup instead of linear scans153 - Use bitsets for dense boolean membership154 - Use a ring buffer for FIFO access instead of shifting an array155- Cache locality: lay out data so hot loops read memory sequentially156 - Compressed sparse row (CSR) format157 - Data-oriented design (struct-of-arrays, packed primitive arrays)158 - Sparse arrays indexed by sequential IDs instead of integer-keyed hash maps159 - Iterate in storage order (e.g. row-major for nested arrays)160- Allocation and copying: allocate and copy less in hot paths161 - Hoist allocations out of hot loops, and reuse buffers instead of162 reallocating163 - Preallocate arrays and buffers to their known final size instead of growing164 incrementally165 - Fuse chained transforms into one pass instead of building an intermediate166 collection per step167 - Use views/slices over underlying data instead of copying, and defer cloning168 until mutation169- Memory footprint: retain only what the computation needs170 - Stream or process incrementally instead of materializing intermediates171 - Bound caches (e.g. LRU), and use weak references for object-keyed caches172 - Remove listeners, timers, and subscriptions when done173 - Avoid closures capturing large outer scopes, and drop references held by174 long-lived structures once no longer needed175- String handling: minimize the strings created and scanned in hot paths176 - Avoid repeated concatenation in loops177 - Work with indices or char codes into the original string instead of creating178 substrings179 - Minimize serialization/deserialization round-trips180 - Avoid regex in hot paths181- Runtime friendliness: stay on the JIT's optimized fast paths182 - Keep object shapes consistent (same fields, same initialization order)183 - Avoid polymorphic call sites and mixed-type arrays in hot paths184 - Keep numbers in the runtime's fast representations, and avoid deoptimization185 triggers in hot loops186- I/O and queries: make fewer round-trips and move fewer bytes187 - Read or write in large chunks instead of many small operations188 - Eliminate N+1 patterns by coalescing small queries or requests into one189 batch190 - Push filtering, aggregation, and pagination down to the data store, and add191 indexes matching the hot query's predicates192 - Compress or use binary encodings for large transfers, and send only the193 fields the consumer reads194- Concurrency: use idle hardware for independent work195 - Run independent async operations concurrently, bounded to what the awaited196 resource can absorb197 - Overlap I/O waits with computation instead of strictly sequencing them198 - Split CPU-bound work across threads or processes, and use SIMD where the199 platform supports it200- Contention and backpressure: keep concurrent parts from waiting on or201 overwhelming each other202 - Shrink critical sections to minimize time spent holding locks203 - Shard or partition shared state, and prefer immutable or thread-local data204 over shared mutable state205 - Bound queues so producers can't outrun consumers, and shed or coalesce load206 when the system is saturated207- Precision trade-offs: do cheaper approximate work where the consumer tolerates208 the difference209 - Use approximate algorithms (sampling, sketches, bloom filters)210 - Lower resolution or cap iteration counts when the output tolerance allows211- Observability overhead: keep instrumentation too cheap to distort the hot path212 - Keep logging, tracing, and assertions out of hot loops213 - Sample or gate expensive instrumentation behind flags