|
| 1 | +# Performance Benchmarks |
| 2 | + |
| 3 | +This file records performance work that affects the template parser and |
| 4 | +renderer. It includes experiments that were kept and approaches that were |
| 5 | +removed or rejected after evaluation. |
| 6 | + |
| 7 | +## Reproduction |
| 8 | + |
| 9 | +The measurements below were collected on 2026-08-12 with Go 1.26.5 on an |
| 10 | +Apple M1 (`darwin/arm64`). Each result is the median of eight one-second runs |
| 11 | +with one logical processor: |
| 12 | + |
| 13 | +```bash |
| 14 | +GOMAXPROCS=1 go test -run '^$' \ |
| 15 | + -bench '^(BenchmarkEngine_Parse|BenchmarkTemplate_Render|BenchmarkTemplate_RenderStructProperty|BenchmarkTemplate_RenderIncludes)$' \ |
| 16 | + -benchmem -benchtime=1s -count=8 . |
| 17 | +``` |
| 18 | + |
| 19 | +Absolute timings vary across machines and thermal conditions. Allocation |
| 20 | +counts and large relative changes are more stable. |
| 21 | + |
| 22 | +## Overall result |
| 23 | + |
| 24 | +| Benchmark | Time before | Time after | Change | Bytes before | Bytes after | Change | Allocations before | Allocations after | Change | |
| 25 | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | |
| 26 | +| Parse | 48.47 ms | 41.16 ms | -15% | 62.61 MB | 13.74 MB | -78% | 136,095 | 130,077 | -4% | |
| 27 | +| Loop render | 540.87 µs | 441.59 µs | -18% | 315,880 B | 147,856 B | -53% | 12,248 | 11,247 | -8% | |
| 28 | +| Struct property render | 462.43 µs | 340.41 µs | -26% | 207,880 B | 95,856 B | -54% | 8,748 | 7,747 | -11% | |
| 29 | +| 100 repeated includes | 467.51 µs | 97.05 µs | -79% | 1,331,160 B | 84,272 B | -94% | 4,610 | 1,433 | -69% | |
| 30 | + |
| 31 | +## Incremental trials |
| 32 | + |
| 33 | +The table records adjacent benchmark sweeps, so each row isolates one change |
| 34 | +more closely than the overall comparison. |
| 35 | + |
| 36 | +| Trial | Benchmark | Time | Bytes/op | Allocs/op | Decision | |
| 37 | +| --- | --- | ---: | ---: | ---: | --- | |
| 38 | +| Pool generated yacc parsers | Parse | 48.47 → 36.21 ms | 62.61 → 19.61 MB | 136,095 → 128,100 | Kept: large time and memory reduction | |
| 39 | +| Preallocate scanned tokens | Parse | 36.21 → 36.81 ms | 19.61 → 13.71 MB | 128,100 → 128,077 | Kept: neutral time, 30% fewer bytes, three-line change | |
| 40 | +| Pass render contexts by pointer | Loop render | 540.87 → 477.73 µs | 315,880 → 163,888 B | 12,248 → 12,249 | Kept: 12% faster and 48% fewer bytes | |
| 41 | +| Precompute literal values | Loop render | 477.73 → 430.79 µs | 163,888 → 147,856 B | 12,249 → 11,247 | Kept: 10% faster and 1,002 fewer allocations | |
| 42 | +| Compile include arguments once | Repeated includes | 461.66 → 377.95 µs | 272,883 → 232,078 B | 4,511 → 3,811 | Kept: 18% faster | |
| 43 | +| Stream partial output | Repeated includes | 377.95 → 343.03 µs | 232,078 → 219,277 B | 3,811 → 3,411 | Kept: 9% faster and avoids a temporary result string | |
| 44 | +| Render-scoped compiled partial cache | Repeated includes | 343.03 → 107.12 µs | 219,277 → 100,273 B | 3,411 → 1,633 | Kept: 69% faster while preserving mutable stores | |
| 45 | +| Cache struct metadata | Struct property render | 441.93 → 336.65 µs | 207,904 → 95,904 B | 8,748 → 7,748 | Kept: 24% faster and 54% fewer bytes | |
| 46 | + |
| 47 | +Literal values now allocate their runtime wrappers during parsing instead of |
| 48 | +during every render. This slightly offsets the parser pool's allocation-count |
| 49 | +reduction, but benefits every subsequent render of a compiled template. |
| 50 | + |
| 51 | +## Tried and not kept |
| 52 | + |
| 53 | +- **Process-wide compiled partial cache:** Rejected after the design trial. A |
| 54 | + cache shared across renders would require template-store invalidation, |
| 55 | + concurrency control, and configuration revision tracking. The render-scoped |
| 56 | + cache obtains the large repeated-partial win, rereads the store, compares the |
| 57 | + source bytes, and cannot become stale across top-level renders. |
| 58 | +- **Eager render-scoped cache allocation:** Implemented initially, then removed. |
| 59 | + It charged templates that never render partials and allocated a discarded map |
| 60 | + for every child context. The final implementation initializes the map only on |
| 61 | + the first partial compilation. |
| 62 | +- **Caching missing struct properties:** Implemented initially, then removed. |
| 63 | + It could retain an unbounded set of user-controlled property names in a |
| 64 | + process-wide map. The final cache stores only successful lookups, whose count |
| 65 | + is bounded by the fields and methods of encountered types. |
| 66 | +- **Handwritten default-delimiter scanner:** Rejected before implementation. |
| 67 | + After parser pooling, regular-expression matching was no longer the dominant |
| 68 | + allocator. Reimplementing delimiter, trimming, and malformed-token behavior |
| 69 | + would add substantial compatibility risk for a smaller remaining CPU target. |
0 commit comments