Claude Fable 5.1 & GPT-6 Astra packages are live

Cpu

Free

CPU-bound work — profiling with flame graphs, algorithmic complexity, keeping event loops free, parallelism, and knowing when CPU is not the problem.

197 lines8.7 KB Open Ai Performance
targetModels
GPT-6 AstraGPT-5.6GPT-5.5GPT-5 FamilyFuture GPT Models
name
cpu
category
Performance
description
CPU-bound work — profiling with flame graphs, algorithmic complexity, keeping event loops free, parallelism, and knowing when CPU is not the problem.
license
MIT
author
Agent.md maintainers
last-verified
reviewed-by
unreviewed
<!-- Generated from models/_canonical by scripts/build-model-variants.js. Edit the canonical source, not this file. Behavioural profile for ChatGPT: scripts/model-profiles.json -->

#Scope contract

FILE_ISOLATION: Modify only files inside the scope the task names. Reading elsewhere is allowed; writing outside it is not, and a needed out-of-scope change is reported, not made. SIGNATURE_PINNING: Before implementing, write the exact signatures you will add or change (name, parameters, return type). Implement to those signatures; if one must change, say so before changing it. TYPE_CONTRACTS: Every public function carries explicit parameter and return types. No any, untyped dict, or interface{} at a module boundary.


#Purpose

Rules for CPU-bound performance. The first rule is a filter: most web-service latency is waiting, not computing. Before optimising CPU, confirm it is actually the constraint.

ObservationMeaning
CPU near saturation, latency highGenuinely CPU-bound — this package applies
CPU low, latency highWaiting: I/O, locks, pool, dependency → Performance/database
CPU low, throughput cappedConcurrency limit or a serialisation point
CPU spiky with flat loadGC, a cron job, or a compaction
High system CPU, low userSyscalls, context switching, or throttling

#Profile with a flame graph

bash
npx 0x -- node dist/server.js              # Node
py-spy record -o profile.svg -- python app.py
go tool pprof -http=: http://localhost:6060/debug/pprof/profile?seconds=30
java -jar async-profiler.jar -e cpu -d 30 -f flame.html <pid>

Read it as: width is total time, stacked bars are call depth. A wide plateau is where the time goes; a tall thin spike is deep but cheap.

Two distinctions that change the diagnosis:

  • Self time versus total time. A function with high total but low self time is not the problem; its callee is.
  • On-CPU versus off-CPU. A standard profiler samples running threads only. Time spent blocked on a lock or I/O does not appear at all — which is why a flat-looking profile with high latency means you are looking at the wrong tool. Use tracing for wall-clock time. → Backend/monitoring

Profile under realistic load and data. A profile taken on ten rows shows startup cost; a profile on production-shaped data shows the algorithm.


#Complexity beats constants

ChangeTypical gain
O(n²) → O(n) with a hash lookup100–10,000× at scale
Repeated work → computed once10–100×
Interpreted hot loop → native/SIMD library5–50×
Micro-optimising a tight loop1.1–2×
ts
// O(n × m) — nested scan, fine at 100 rows, quadratic at 100,000
const enriched = orders.map(o => ({ ...o, customer: customers.find(c => c.id === o.customerId) }));

// O(n + m) — build an index once
const byId = new Map(customers.map(c => [c.id, c]));
const enriched = orders.map(o => ({ ...o, customer: byId.get(o.customerId) }));

Look for these first, in order:

  1. A find/includes/indexOf inside a loop — always a hash lookup instead.
  2. Repeated computation of an invariant inside a loop — hoist it.
  3. Re-parsing or re-compiling per call — a regex literal recompiled each invocation, a schema rebuilt per request. Build once at module scope.
  4. Sorting inside a loop, or sorting when a single pass would do.
  5. Deep copies (structuredClone, JSON.parse(JSON.stringify(x))) of large objects on a hot path.

Also cheap and frequently significant: serialisation. JSON.stringify of a large response is real CPU time on the main thread, and it grows with payload size — another reason to project only the fields you need. → Performance/queries


#Do not block a single-threaded runtime

In Node, one thread serves every request. A synchronous 200 ms operation adds 200 ms to every concurrent request, not just its own.

ts
// Blocks the event loop for every concurrent request
const hash = crypto.pbkdf2Sync(pw, salt, 600_000, 32, "sha512");

// Off the event loop, onto the threadpool
const hash = await promisify(crypto.pbkdf2)(pw, salt, 600_000, 32, "sha512");

Common blockers: synchronous crypto, JSON.parse of megabyte payloads, readFileSync in a handler, large sorts, zlib sync variants, and a regex with catastrophic backtracking — which is also a denial-of-service vector, since input controls the runtime.

Monitor event-loop delay (perf_hooks.monitorEventLoopDelay); a p99 above ~50 ms means something is blocking. → Backend/node

For genuinely CPU-heavy work: worker_threads, a separate service, or a queue. Adding async concurrency to a blocked event loop does nothing.


#Parallelism, and its limits

  • Independent work runs concurrently: Promise.all, goroutines, a thread pool. Bound the fan-out — unbounded parallelism exhausts pools and adds context switching. → Backend/workers
  • Amdahl's law: the serial fraction bounds the speedup. Work that is 10% serial cannot exceed 10× no matter how many cores. Find and shrink the serial part before adding cores.
  • Adding cores to a lock-contended workload makes it slower — more threads, more contention.
  • In containers, a CPU limit throttles the process even when the node is idle, which appears as unexplained p99 latency. Prefer requests without limits for latency-sensitive services. → DevOps/kubernetes

Check throttling explicitly before concluding you need more CPU: container_cpu_cfs_throttled_seconds_total rising means the limit is the constraint, not the code. nproc inside the container also does not reflect the CPU limit, so runtimes that size thread pools from it (GOMAXPROCS, UV_THREADPOOL_SIZE, JVM parallel GC threads) over-allocate and thrash — set them explicitly.

Scaling out is a legitimate answer once the code is efficient — but it pays rent forever, so establish the algorithm is not quadratic first.


#Anti-patterns

Anti-patternWhy it failsFix
Optimising CPU when the service is I/O-boundNo gain; the wait is elsewhereConfirm saturation first
Guessing at the hot pathUsually wrongFlame graph
Profiling on toy dataShows startup, not the algorithmRealistic load and volume
Reading total time as self timeBlames the callerCheck self time
CPU profiler on a blocking problemOff-CPU time is invisibleTrace wall-clock time
Micro-optimising firstConstant factors on the wrong codeFix complexity
find inside a loopQuadratic at scaleBuild a Map
Recompiling regexes or schemas per callRepeated setup costHoist to module scope
Deep-cloning large objects on hot pathsAllocation and copy costStructural sharing
Synchronous crypto or I/O in a handlerBlocks every concurrent requestAsync variants
Regex with catastrophic backtrackingUnbounded CPU; a DoS vectorBounded patterns, timeouts
Async concurrency for CPU-bound workThe thread is still one threadWorker threads
Unbounded parallel fan-outPool exhaustion, context switchingBound it
Adding cores to contended codeContention increasesReduce the serial fraction
CPU limits on latency-sensitive servicesThrottled on an idle nodeRequests without limits
Scaling out to hide an O(n²)Pays rent foreverFix the algorithm

#Checklist

  • CPU is confirmed as the constraint before optimising it
  • A flame graph identifies the hot path under realistic load and data
  • Self time is distinguished from total time
  • Off-CPU waiting is measured with tracing, not a CPU profiler
  • Nested scans are replaced with hash lookups
  • Invariant work is hoisted out of loops
  • Regexes, schemas and compiled artefacts are built once at module scope
  • Large deep copies are avoided on hot paths
  • Response payloads are projected to reduce serialisation cost
  • No synchronous crypto, file or compression call runs in a request path
  • Regex patterns are checked for catastrophic backtracking
  • Event-loop delay is monitored
  • CPU-heavy work runs in worker threads, a separate service, or a queue
  • Parallel fan-out is bounded
  • The serial fraction is understood before adding cores
  • CPU limits are omitted for latency-sensitive containers
  • Scaling out follows algorithmic fixes rather than replacing them