perf(core,classes): optimize synchronous map() hot path and createMap startup#632
Merged
nartc merged 4 commits intoJun 23, 2026
Merged
Conversation
Benchmarks for the synchronous map() hot path and createMap startup: - bench.ts: identity, forMember(mapFrom), and snake->camel naming groups (rotating 64-object pool to defeat constant-folding); runs under --expose-gc so mitata reports heap + gc. - startup-bench.ts: many-createMap, tail-skewed class-size distribution; reports createMap wall time + retained heap. - profile-formember.ts + analyze-cpuprofile.ts: clean CPU self-time attribution (no mitata overhead). - verify.ts: cross-version correctness gate + independent hrtime timing, so a benchmark run can't be a silently no-op'd or wrong map. Scripts: bench, bench:startup, bench:profile (+ :analyze). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… startup Behavior-preserving optimizations (characterization tests added first; full suite green across all packages). ~2x faster per map and ~3-5x less allocation vs the previous branch baseline. map() runtime: - precompute the unmapped-key residual at createMap (no per-call Set allocation or writable-key scan in assertUnmappedProperties) - replace the per-member setMemberFn closure factory with a direct (path, dest, value) writer; build a closure only on the async arm - specialize the mapFrom step in compileStep: call the selector directly, dropping the per-member thunk and the mapMember type switch - MapInitialize scalar fast-path + Symbol.toStringTag File check - get(): single-segment fast path - hoist the per-element options object out of mapArray/mutateArray loops createMap / memory: - storeMetadata O(P^2) -> O(P) (build the list in place) - drop the unused props/configuredKeys from the retained compiled plan - getPathRecursive: module-level Set, drop redundant own-name dedup - size-gated metadata index + extend Set for very wide classes - AutoMap decorator O(P^2) -> O(1) own-list push (inheritance-safe seed) - naming / applyMetadata / getMetadataList compile-time cleanups Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Quality cleanups from review (no behavior change; full suite green):
- extract a shared pathKey(path) helper for the null-byte path-key idiom
that was open-coded across create-initial-mapping and extend
- collapse extend()'s size-gated dual loop into a single Set-based pass
(compile-time only, so the Set is cheap at any size)
- extract isFileTagged() for the duplicated File-tag check in compileStep
- reuse one nested-options object across nested map() recursion instead of
allocating { extraArgs } per nested object / array element
- drop an unnecessary export on getWritableKeys
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- extract the shared classes-strategy model/factory/mapping setup into fixtures.ts (imported by bench, verify, profile-formember) instead of copy-pasting it in each; drop a dead keep-alive fixture - analyze-cpuprofile: read the newest profile from the fixed ./profiles dir instead of an optional CLI argument — removes the path-injection surface entirely (no external input reaches the filesystem path) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
b23fb02 to
d9af7e5
Compare
nartc
approved these changes
Jun 23, 2026
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Summary
Optimizes the synchronous
map()hot path andcreateMapstartup on top ofchore/9.0-migrate's compiled-mapping engine. Every change is behavior-preserving (characterization tests written first; full suite green across all packages) and the PR adds a benchmark + verifier harness so these paths stay measurable.Against the
chore/9.0-migratebase: per-callmap()is ~1.7–2.2× faster with ~3–5× less allocation, andcreateMapstartup is ~13% faster with ~0.5 MB less retained memory.Benchmarks
M3 Max, Node v22.22.3, same harness run sequentially. Numbers are machine-specific — treat as ratios. Reproduce with the commands below.
map()— per call (ns): base (chore/9.0-migrate) → this PRmapArray(1000)scales the same (e.g. classes/flat 528 µs → 215 µs).Allocation — per element via
mapArrayunder--expose-gc: base → this PRLower allocation → smaller GC windows.
createMapstartup (many plans, tail-skewed class sizes)What changed
Runtime (
map()):createMap, soassertUnmappedPropertiesdoes no per-callSetallocation or writable-key scan (it runs on every map / every array element).setMemberFnclosure factory with a direct(path, dest, value)writer; a closure is built only on the async branch.mapFromstep incompileStep: call the selector directly, dropping the per-member thunk and themapMembertype switch.MapInitializescalar fast-path (primitives short-circuit before Date/File checks) +Symbol.toStringTagFile detection (replacingObject.prototype.toString.call(x).slice(...)).get()single-segment fast path.mapArray/mutateArrayloops.createMap/ memory:storeMetadataO(P²) → O(P) (build the list in place).props/configuredKeysarrays from the retained compiled plan.getPathRecursive: module-levelSetfor excluded keys; drop a redundant own-nameSetdedup.extendSetfor very wide classes (small classes keep the cheaper.find).@AutoMapdecorator O(P²) → O(1) (push onto the class's own metadata list; inheritance-safe sliced seed).applyMetadata/getMetadataListcompile-time cleanups.Correctness
Behavior-preserving throughout — sync/async, mutate-vs-return,
MapMemberErrorwrapping, and pre/condition handling unchanged. Each change has a characterization test written before it: File/Date/arrayMapInitialize;mapFromreturn/mutate/throw/skip-undefined;get()single-segment;getPathRecursivediamond; wide (>30-prop) size-gated paths;@AutoMapinheritance (parent metadata not mutated).Full test suite green across all packages (core, classes, integration-test, pojos, nestjs, mikro, sequelize, zod).
Method: mitata under
--expose-gc(heap + gc), corroborated by an independenthrtimeloop, plus a verifier that asserts each scenario maps correctly so a timed run can't be a silently no-op'd map.New tooling (
packages/benchmark-core)bench.ts— identity /forMember(mapFrom)/ naming groups under--expose-gc.startup-bench.ts— many-createMapwall + retained heap.profile-formember.ts+analyze-cpuprofile.ts— CPU self-time attribution.verify.ts— correctness gate + independent timing.Reproduce
🤖 Generated with Claude Code