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: |4 Generate a performance profile, identify hotspots, and optimize.5argument-hint: '[what to focus on]'6allowed-tools: Bash(profiler-md *)7---89Generate a performance profile, identify hotspots, and optimize.1011Focus: $ARGUMENTS1213# Workflow14151. Decide what to profile (e.g. time) and write an isolated script that16 exercises the slow path if nothing runs it directly. Run17 `profiler-md --help <language>` for profiling instructions182. Capture a baseline: generate a profile, convert it with19 `profiler-md path/to/profile`, and read the full report, focusing on:20 - Hottest functions: self % shows where time is spent or memory is allocated,21 rather than passed through22 - Hottest call stacks: the call path to the hot functions233. Identify the hottest 1-3 functions by self % and read their source:24 - A native function cannot be changed, but its cost may come from unnecessary25 work in its callers (e.g. parsing the same data twice)26 - Project functions are direct targets274. State a hypothesis (see "Hypotheses"). If the bottleneck is unclear, reread28 the hot function and its callers295. Apply the minimal change that removes the bottleneck, one optimization at a30 time31 - Leave unrelated code alone32 - Optimize the general case, not the benchmark: don't exploit quirks of the33 profiled input (its size, ordering, value distribution, or hardcoded34 special cases for it) unless they're guaranteed properties of real inputs35 - Prefer a change inside the hot function or its callers. A change that36 replaces a representation, an intermediate structure, or a layer boundary37 is a redesign. Follow "Redesigns" before starting one386. Run the relevant tests: if any newly fail, fix the optimization or revert it397. Capture a new report as in step 2 and compare self % for the targeted40 function(s) against the baseline: if the improvement is negligible or41 unclear, revert and go back to step 4. If the comparison is too noisy to42 call, collect more data (see "Measurement")438. If the goal is unmet and a clear bottleneck remains, repeat from step 3 with44 the new profile as the baseline. If no change inside a hot function can45 remove the remaining cost, see "Redesigns"469. Report:47 - Before vs after for the hot function(s)48 - Any other functions that moved significantly (regressions)4950# Measurement5152`profiler-md` reports are the ONLY measurements in this workflow. NEVER time53manually (e.g. `time` or `performance.now()`).5455Manual timing yields one noisy end-to-end number that can improve for reasons56unrelated to the change (machine load, caches, GC timing). Its instrumentation57also distorts the hot path. A profile attributes work to functions, so a58before/after comparison proves the optimization removed the targeted work, not59merely that one run was faster.6061If a profile has too little data to trust, collect more:6263- Run the workload longer or loop it more times64- Raise the sampling frequency if the profiler supports it (see65 `profiler-md --help <language>`)66- Regenerate the profile and compare again6768# Hypotheses6970Before changing anything, state:7172- What is the bottleneck? (e.g. "repeated object allocation in the hot loop",73 "O(n²) suffix scan", "redundant map lookups")74- What is the expected fix? (e.g. "hoist allocation outside loop", "use a75 two-pointer suffix scan", "cache the lookup result")76- Why will it be faster?7778# Redesigns7980A local optimization makes the existing design do its work faster. A redesign81replaces a representation, an intermediate structure, or a layer boundary so the82hot work never runs. It changes code the profile never ranked and can move work83instead of removing it, so it requires stronger evidence and a spike first.8485## When to consider one8687Only when the profile shows the cost is in the design rather than in one88function:8990- The profile is flat: many small functions implement the same abstraction (e.g.91 converting the same intermediate representation at each layer)92- The hot function does only what its interface requires, and every local change93 to it was reverted in step 794- The same local fix would repeat at many call sites95- The design sets the asymptotic bound (e.g. an intermediate tree built in full96 and walked once)9798Before designing anything, sum the self % of the functions the redesign would99remove. That sum is the ceiling on its gain. If the ceiling is below the goal,100report that the goal requires a different approach.101102## Spike103104NEVER start with the redesign itself:1051061. State the hypothesis as in "Hypotheses", plus which abstraction requires the107 work, what replaces it, which functions it removes, and the ceiling1082. Write a throwaway implementation for the profiled path alone: hardcode, skip109 edge cases and tests, and leave other call sites on the old code. Isolate it110 (a copy of the module, or its own commit or worktree) so discarding it is one111 operation. NEVER edit the existing implementation in place1123. Check it produces the same output as the baseline on the workload1134. Profile it and compare against the baseline: the removed functions should be114 gone, and no new function should absorb their cost1155. Proceed only if the gain meets the goal or a clear change closes the gap.116 Otherwise discard the spike and report the disproof as a finding1176. Get the user's approval before implementing. Present the hypothesis, the118 ceiling, the spike's profile against the baseline, and what the spike119 skipped. Stop until the user answers1207. Implement, then run steps 6 and 7 of the workflow. Compare the final profile121 with the spike's: a lost gain means the added generality reintroduced work.122 Treat the function that absorbed it as a new bottleneck123124Time-box the spike to a fraction of the redesign's cost. If it runs over, the125design is too unclear to implement. Discard it.126127Report a redesign with the ceiling and the spike's and final profiles against128the baseline.129130# Focus areas131132Sub-bullets are examples.133134- Time complexity: reduce the asymptotic work per input element135 - Replace `O(n²)` scans with single-pass or two-pointer algorithms136 - Use heaps for top-N instead of fully sorting137 - Sort once and binary search instead of repeated linear scans138- Redundant work: compute each result once139 - Cache or memoize repeated parses and lookups140 - Hoist loop-invariant computation out of loops141 - Build lookup tables once instead of searching repeatedly, and intern142 repeated values143 - Update incrementally instead of recomputing from scratch144 - Persist expensive results between invocations, keyed by input so stale145 entries are never served146- Work avoidance: skip or defer computation whose result may never be used147 - Exit early once the answer is known148 - Filter before expensive transforms149 - Order checks cheapest-first so expensive predicates run last and rarely150 - Add fast paths for the common input shape151 - Defer initialization and large dependency loading until first use,152 especially on the startup critical path153 - Debounce or throttle work triggered by bursty events154- Data structure selection: match the structure to the access pattern155 - Use sets/maps for membership and lookup instead of linear scans156 - Use bitsets for dense boolean membership157 - Use a ring buffer for FIFO access instead of shifting an array158- Cache locality: lay out data so hot loops read memory sequentially159 - Compressed sparse row (CSR) format160 - Data-oriented design (struct-of-arrays, packed primitive arrays)161 - Sparse arrays indexed by sequential IDs instead of integer-keyed hash maps162 - Iterate in storage order (e.g. row-major for nested arrays)163- Allocation and copying: allocate and copy less in hot paths164 - Hoist allocations out of hot loops, and reuse buffers instead of165 reallocating166 - Preallocate arrays and buffers to their known final size instead of growing167 incrementally168 - Fuse chained transforms into one pass instead of building an intermediate169 collection per step170 - Use views/slices over underlying data instead of copying, and defer cloning171 until mutation172- Memory footprint: retain only what the computation needs173 - Stream or process incrementally instead of materializing intermediates174 - Bound caches (e.g. LRU), and use weak references for object-keyed caches175 - Remove listeners, timers, and subscriptions when done176 - Avoid closures capturing large outer scopes, and drop references held by177 long-lived structures once no longer needed178- String handling: minimize the strings created and scanned in hot paths179 - Avoid repeated concatenation in loops180 - Work with indices or char codes into the original string instead of creating181 substrings182 - Minimize serialization/deserialization round-trips183 - Avoid regex in hot paths184- Runtime friendliness: stay on the JIT's optimized fast paths185 - Keep object shapes consistent (same fields, same initialization order)186 - Avoid polymorphic call sites and mixed-type arrays in hot paths187 - Keep numbers in the runtime's fast representations, and avoid deoptimization188 triggers in hot loops189- I/O and queries: make fewer round-trips and move fewer bytes190 - Read or write in large chunks instead of many small operations191 - Eliminate N+1 patterns by coalescing small queries or requests into one192 batch193 - Push filtering, aggregation, and pagination down to the data store, and add194 indexes matching the hot query's predicates195 - Compress or use binary encodings for large transfers, and send only the196 fields the consumer reads197- Concurrency: use idle hardware for independent work198 - Run independent async operations concurrently, bounded to what the awaited199 resource can absorb200 - Overlap I/O waits with computation instead of strictly sequencing them201 - Split CPU-bound work across threads or processes, and use SIMD where the202 platform supports it203- Contention and backpressure: keep concurrent parts from waiting on or204 overwhelming each other205 - Shrink critical sections to minimize time spent holding locks206 - Shard or partition shared state, and prefer immutable or thread-local data207 over shared mutable state208 - Bound queues so producers can't outrun consumers, and shed or coalesce load209 when the system is saturated210- Precision trade-offs: do cheaper approximate work where the consumer tolerates211 the difference212 - Use approximate algorithms (sampling, sketches, bloom filters)213 - Lower resolution or cap iteration counts when the output tolerance allows214- Observability overhead: keep instrumentation too cheap to distort the hot path215 - Keep logging, tracing, and assertions out of hot loops216 - Sample or gate expensive instrumentation behind flags