Skip to content

Performance idea: add ascii skip char counting happy path. Worth the complexity?#118

Merged
ef4 merged 2 commits intoembroider-build:mainfrom
johanrd:skip-char-counting-for-ascii
Mar 17, 2026
Merged

Performance idea: add ascii skip char counting happy path. Worth the complexity?#118
ef4 merged 2 commits intoembroider-build:mainfrom
johanrd:skip-char-counting-for-ascii

Conversation

@johanrd
Copy link
Copy Markdown
Contributor

@johanrd johanrd commented Mar 16, 2026

Hey! I was profiling parse() and noticed that for ASCII-only source files (which most .gts files are), the Range computation does .chars().count() and .encode_utf16().count() scans even though byte/char/utf16 offsets are guaranteed to be identical for ASCII text.

This PR adds a simple src.is_ascii() check before the AST walk and skips those scans when true. Not sure if the added branch is worth the complexity, but the numbers seem meaningful — especially if parse() is called on a hot path (e.g. per-keystroke in a language server).

The change is small: one bool field on the visitor, one branch in Range::new. Non-ASCII files fall through to the existing logic unchanged.

Benchmark (native, release profile, Apple Silicon)

All three tables use the same 208-char base component as their first row, varying one dimension at a time.

By template count (same component repeated N times)

Templates File size Before After Speedup
1 208 chars 7.2µs 6.4µs 1.1x
2 416 chars 13.1µs 10.1µs 1.3x
5 1040 chars 38.8µs 22.9µs 1.7x
10 2080 chars 104.1µs 44.9µs 2.3x
20 4160 chars 320.3µs 88.3µs 3.6x

By template content size (same component, extra rows inside the template)

Extra rows File size Before After Speedup
0 208 chars 6.5µs 5.7µs 1.1x
10 659 chars 13.2µs 11.4µs 1.2x
50 2459 chars 39.8µs 34.4µs 1.2x
200 9209 chars 139.6µs 116.6µs 1.2x

By JS code before template (same component, extra JS lines prepended)

Extra JS lines File size Before After Speedup
0 208 chars 6.6µs 5.7µs 1.1x
10 758 chars 23.2µs 19.7µs 1.2x
50 2958 chars 87.5µs 72.7µs 1.2x
200 11208 chars 325.8µs 271.7µs 1.2x

The multi-template case shows the biggest win (3.6x at 20 templates) because the old code re-scans from byte 0 for every range offset. For single-template files, the improvement is a steady ~15-20% regardless of file size. Most of the absolute time is SWC parsing, which is unaffected by this change.

Non-ASCII files are unaffected by this change.

Test plan

  • All 35 Rust tests pass (including multibyte character tests)
  • All 27 JS/Node tests pass
  • Benchmark included in benches/parse_bench.rs for reproducibility

Cowritten by claude

@johanrd johanrd force-pushed the skip-char-counting-for-ascii branch from 4450f2f to c71c45b Compare March 16, 2026 15:04
@ef4
Copy link
Copy Markdown
Collaborator

ef4 commented Mar 17, 2026

Thanks, this seems good to me. Not much extra complexity (and I'm going to land a followup that introduces a real constructor for LocateContentTagVisitor which will make it even clearer).

@ef4 ef4 merged commit 44de46b into embroider-build:main Mar 17, 2026
1 check passed
@github-actions github-actions bot mentioned this pull request Mar 17, 2026
@ef4 ef4 added the bug Something isn't working label Mar 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants