| 1 | # Schema construction profile, 2026-09-25 |
| 2 | |
| 3 | The largest attributable costs depend on the schema. AST creation and copying dominate the ready-field wide structs |
| 4 | and the checked/encoded fixtures. Central schema-object construction is substantial when many small schemas are composed. |
| 5 | Type projection is more prominent with new transformations than with ordinary checks. These measurements do not support |
| 6 | assigning one universal percentage to `Schema.make` or predicting an optimization's speedup from CPU shares. |
| 7 | |
| 8 | Only measurement code was added. The measured library is `main` at |
| 9 | `e36c35c656bb5cc6dd1170d863ea8abb75c24279`. Environment: Apple M3, macOS Darwin 23.6.0 arm64, |
| 10 | Node v24.12.0, V8 13.6.233.17-node.37. The source tree was checked clean and the revision unchanged before and after each run. |
| 11 | |
| 12 | ## Elapsed construction time |
| 13 | |
| 14 | Times below are medians from 24 ordinary fresh Node processes per fixture, without the profiler. |
| 15 | Each process constructs 1,000 roots. MAD is the median absolute deviation, not a confidence interval. |
| 16 | Imports and first decode are excluded. Child construction is included. |
| 17 | |
| 18 | | Fixture | Median ms | MAD ms | |
| 19 | | ------------------------------------------ | --------: | -----: | |
| 20 | | Literal | 0.648 | 0.008 | |
| 21 | | Struct, 2 dynamically generated fields | 1.249 | 0.018 | |
| 22 | | Struct, 32 dynamically generated fields | 6.126 | 0.412 | |
| 23 | | Struct, 256 dynamically generated fields | 43.040 | 0.655 | |
| 24 | | Nested structs | 5.356 | 0.110 | |
| 25 | | Arrays, checks, optional keys and literals | 18.455 | 0.310 | |
| 26 | | New string-to-number transformations | 12.360 | 0.108 | |
| 27 | | Union of 8 new tagged members | 22.030 | 0.180 | |
| 28 | | Struct, 32 ready fields | 3.955 | 0.043 | |
| 29 | | Struct, 256 ready fields | 26.528 | 0.197 | |
| 30 | |
| 31 | The dynamic-field fixtures include `Object.fromEntries` and its input arrays. The ready-field fixtures isolate |
| 32 | `Schema.Struct(fields)` with a previously prepared field map. Neither dynamic generation nor reused fields is a timing |
| 33 | model for a freshly written object literal. The ready-field cases were measured in a subsequent cohort; the difference |
| 34 | between the two tables' cases is not a paired estimate of field-map creation cost. |
| 35 | |
| 36 | ## Attributable internal work |
| 37 | |
| 38 | These are mean per-process sample percentages at a requested 100 microsecond interval, rounded to one decimal place. |
| 39 | Every sample belongs to one category. Other includes API preparation, separately identifiable maker factories, the caller, |
| 40 | and runtime frames. Library work that V8 folds into the caller can remain in Other. |
| 41 | |
| 42 | | Fixture | Initial AST and AST changes | Central schema constructor | `toType` | GC | Other | |
| 43 | | ------------------------------------------ | --------------------------: | -------------------------: | -------: | ----: | ----: | |
| 44 | | Struct, 256 ready fields | 49.7% | 1.7% | 9.3% | 15.8% | 23.5% | |
| 45 | | Nested structs | 14.4% | 26.6% | 6.9% | 26.9% | 25.2% | |
| 46 | | Arrays, checks, optional keys and literals | 33.6% | 21.4% | 3.7% | 23.1% | 18.2% | |
| 47 | | New string-to-number transformations | 30.9% | 21.9% | 19.3% | 12.5% | 15.4% | |
| 48 | |
| 49 | Representative 95% process-bootstrap intervals: |
| 50 | |
| 51 | | Fixture | Initial AST and AST changes | `toType` | |
| 52 | | ------------------------------------------ | --------------------------- | -------------- | |
| 53 | | Struct, 256 ready fields | 48.8% to 50.6% | 8.4% to 10.2% | |
| 54 | | Arrays, checks, optional keys and literals | 32.5% to 34.8% | 3.1% to 4.4% | |
| 55 | | New string-to-number transformations | 28.8% to 33.0% | 17.4% to 21.1% | |
| 56 | |
| 57 | The intervals describe process-to-process sampling variability. They do not include profiler bias, missed boundary |
| 58 | samples, or attribution errors caused by optimized code. These shares must not be multiplied by ordinary elapsed time |
| 59 | and presented as directly measured phase durations. |
| 60 | |
| 61 | The retained stacks point to these concrete operations: |
| 62 | |
| 63 | - Checked schemas spend many AST samples in `modifyOwnPropertyDescriptors` and `replaceChecks` in |
| 64 | [SchemaAST.ts](../../../src/SchemaAST.ts). Updating checks copies property descriptors and creates another AST object. |
| 65 | - Encoded schemas also spend substantial time in `replaceEncoding`. The stacks separate copies needed while defining |
| 66 | transformations from copies made later by `toType` while preparing `.make`. |
| 67 | - Ready-field structs spend many AST samples in `struct` and the `Objects` constructor. Those enumerate fields, create |
| 68 | property signatures and check duplicate names. `toType` then traverses the property signatures, using memoized children. |
| 69 | - The [central constructor](../../../src/internal/schema/make.ts) remains a substantial cost for nested, checked, |
| 70 | encoded and union fixtures. Its samples include function/prototype/property setup and inseparable inlined work. |
| 71 | They do not isolate the cost of each property assignment or closure allocation. |
| 72 | - GC is visible in several fixtures. The CPU profile cannot assign a GC pause to the allocation that caused it. |
| 73 | Small or zero samples in `makeEffect`/`makeOption` therefore do not establish that their allocations are cheap. |
| 74 | |
| 75 | ## Profiler perturbation and resolution |
| 76 | |
| 77 | Both frequencies were paired with ordinary execution in the same round. Entries are median paired changes in elapsed |
| 78 | time, with 95% intervals. This is measurement overhead, not a source-code regression. |
| 79 | |
| 80 | | Fixture | Profiling at 100 us | Profiling at 500 us | |
| 81 | | ------------------------------------------ | ---------------------- | ---------------------- | |
| 82 | | Literal | +13.8%, 12.4% to 16.5% | +13.0%, 12.0% to 15.4% | |
| 83 | | Struct, 2 dynamic fields | +15.7%, 11.9% to 18.2% | +14.6%, 12.6% to 17.6% | |
| 84 | | Nested structs | +6.2%, 3.7% to 7.7% | +2.2%, 0.6% to 4.5% | |
| 85 | | Arrays, checks, optional keys and literals | +6.1%, 3.8% to 7.6% | +3.2%, 1.1% to 4.7% | |
| 86 | | New string-to-number transformations | +6.4%, 5.4% to 8.0% | +4.4%, 2.4% to 5.9% | |
| 87 | | Struct, 256 ready fields | +5.8%, 4.0% to 7.0% | +2.9%, 2.2% to 3.3% | |
| 88 | |
| 89 | The checked and encoded fixtures give similar broad proportions at both frequencies. At 500 us, their `toType` shares |
| 90 | are 3.9% and 19.4%, respectively. This supports distinguishing the workloads, not treating the percentages as exact costs. |
| 91 | |
| 92 | Literal has only 147 total construction samples at 100 us and 26 at 500 us across all 24 processes. The 500 us median |
| 93 | sampled span is zero because most processes have only one observed construction sample. Struct-2 and ready-field Struct-32 |
| 94 | also have sparse 500 us profiles. Their precise elapsed times do not provide a comparably precise internal decomposition. |
| 95 | |
| 96 | Union-8 exposes a boundary limitation: the 500 us median sampled span covers about 81% of elapsed construction time. |
| 97 | Its GC share changes from 34.7% at 100 us to 26.1% at 500 us, while central construction stays near 28%. |
| 98 | Do not use its GC point estimate to quantify a prospective allocation optimization. Caller-only attribution is also |
| 99 | ambiguous: even ready-field Struct-256 assigns about 21% to its tiny calling function, showing that this includes inlined |
| 100 | library work. It must not be described as 21% benchmark overhead. |
| 101 | |
| 102 | ## Reproduction and validation |
| 103 | |
| 104 | The [protocol](./README.md) describes timing boundaries, attribution and sparse-profile handling. Two complete cohorts |
| 105 | ran sequentially, with no builds, tests or other benchmarks started alongside them. They contain 720 measured processes |
| 106 | and 10 separate semantic validation processes. All configured rounds succeeded; no observations were discarded. |
| 107 | An earlier two-round pilot and an incomplete sampler-resolution probe are separate from these results. |
| 108 | |
| 109 | ```sh |
| 110 | node packages/effect/runtimeperf/suites/schema-construction/run.mts tmp/schema-construction-rerun 24 1000 literal,struct-2,struct-32,struct-256,nested,composed,encoded,union-8 |
| 111 | node packages/effect/runtimeperf/suites/schema-construction/run.mts tmp/schema-construction-ready-rerun 24 1000 struct-ready-32,struct-ready-256 |
| 112 | ``` |
| 113 | |
| 114 | Raw reports: [main cohort](../../../../../tmp/schema-construction-main/results.json) and |
| 115 | [ready-field cohort](../../../../../tmp/schema-construction-ready/results.json). Their sibling `profiles/` directories |
| 116 | contain the CPU profiles. Their `harness/` directories preserve the measured analysis files. The fixture list and runner |
| 117 | archival support were extended between cohorts; each report records its own hashes. Library sources and measurement |
| 118 | settings were unchanged. |
| 119 | |
| 120 | Validation passed: all 26 runtimeperf infrastructure tests, including four attribution tests; valid/invalid fixture |
| 121 | checks and type-side makers in separate workers; post-timing root checks; `pnpm lint-fix`; focused `.mts` formatting; |
| 122 | and `pnpm check`. No library optimization was implemented or measured. |