Skip to main content

tuning-hot-paths

Measure and tune the parse, format, match, geocode or cold-start hot paths with the BenchmarkDotNet harness in csharp/PhoneNumbers.PerformanceTest. Use before and after any change under csharp/PhoneNumbers/ that touches parsing, formatting, matching, regex handling or allocation, when a PR's benchmark comparison comment reports a regression, or when someone proposes RegexOptions.Compiled.

インストールへ移動

ソース情報

リポジトリ
twcclegg/libphonenumber-csharp
ソースの最終更新活動
2026年9月17日 01:20
検出された SKILL.md の言語
英語
スター
922
フォーク
157

インストール方法

デフォルトでは、最初にソースを確認する Prompt が選択されています。直接コマンドに切り替えるか、ローカルコピーをダウンロードすることもできます。

ソースファイルを確認

インストールを決める前に、SKILL.md と SkillsMP に表示されている付属ファイルをお読みください。

SKILL.md を表示中

SKILL.md
ソースの指示 · 読み取り専用プレビュー
name
tuning-hot-paths
description
Measure and tune the parse, format, match, geocode or cold-start hot paths with the BenchmarkDotNet harness in csharp/PhoneNumbers.PerformanceTest. Use before and after any change under csharp/PhoneNumbers/ that touches parsing, formatting, matching, regex handling or allocation, when a PR's benchmark comparison comment reports a regression, or when someone proposes RegexOptions.Compiled.
# Tuning a hot path Parsing and formatting are the library's whole job and callers run them in tight loops, so this repo treats allocation and throughput as correctness-adjacent. **Measure a hot-path change; do not reason about it** — the JIT, the regex cache and the frozen lookup tables routinely invert intuition. ## Conventions to keep - Match against spans and slices rather than materialising substrings or `Match` objects. - Go through `RegexCache` / `PhoneRegex`; never construct a `Regex` on a call path. - Build lookup tables once, into frozen collections, rather than per call. - Nothing on a public path may need reflection or dynamic code — `IsAotCompatible` is on and the trim/AOT analyzers are errors. The Blazor WASM demo runs the library trimmed and depends on this. ## Never compile metadata regexes Do not build a metadata-derived pattern with `RegexOptions.Compiled`. This shipped as a regression three times (8.8.0, 8.13.0, 9.0.30), each time on "compiled should be faster" reasoning. Measured end to end, compiling the metadata patterns is 34x slower for 1,000 operations across 245 regions, 3.7x slower for 100,000, and still 1.7x slower at 1,000,000 — the break-even math is in `README.md` under "Regex compilation and startup cost" and in the harness README under "Why metadata regexes are not compiled". `TestPhoneRegex.MetadataPatternsAreNeverCompiled` asserts on the options the metadata regexes are actually built with; the library's own small fixed set of regexes is a different case and stays compiled. ## Measuring locally ```bash cd csharp/PhoneNumbers.PerformanceTest dotnet run -c Release --framework net10.0 -- --filter "*PhoneNumberWorkflowBenchmark*" ``` `--filter "*"` runs everything (a few minutes). Results land in `BenchmarkDotNet.Artifacts/results`. Pick the benchmark that isolates your change: | Filter | Covers | | --- | --- | | `*PhoneNumberWorkflowBenchmark*` | Widest slice — end-to-end, plus `ParseOnly` / `ValidateOnly` / `FormatOnly` over the same data so a cost can be attributed to a phase | | `*ParsingHelpersBenchmark*` | `ExtractPossibleNumber`, split by clean input vs input that forces the strip path | | `*AsYouTypeFormatterBenchmark*` | Per-keystroke `InputDigit` cost | | `*PhoneNumberMatcherBenchmark*` | `FindNumbers` over a synthetic text body | | `*PhoneNumberOfflineGeocoderBenchmark*` | `GetDescriptionForNumber`, plus `Locale.GetDisplayCountry` alone | | `*ColdStartBenchmark*` | First-use cost: construction, lazy metadata load, first region lookup | | `*ProcessStartBenchmark*` | Whole-process startup to first parse | Benchmark the same filter on the unmodified base commit and compare — a single run of the changed code proves nothing on its own. Watch the **Allocated** column as closely as **Mean**; allocation regressions are the ones that hurt callers in a loop. **Allocated does not measure retained memory.** `MemoryDiagnoser` runs after every static initializer has completed, so state the library keeps forever is invisible to it — a 3.4 MB retained-state regression once moved `ProcessStartBenchmark`'s Allocated by 15 KB. Retained memory is audited by a separate mode that CI also runs: ```bash dotnet run -c Release --framework net10.0 -- --retained-memory # exits non-zero over budget ``` `csharp/PhoneNumbers.PerformanceTest/README.md` explains what each benchmark exercises and why. ## The PR comparison, and its blind spot Changes under `csharp/PhoneNumbers/`, the benchmark project, `resources/`, or the shared build files trigger `run_performance_tests.yml`, which runs the base commit and the branch on the same runner, compares them with `csharp/PhoneNumbers.BenchmarkTools/` (Welch's t-test), fails the job through `lib/fail-on-benchmark-regression.sh` on a significant regression, and posts a comparison comment via `post_performance_test_comment.yml`. Read that comment rather than guessing. It flags a case only when Welch's t-test clears p < 0.001 **and** the mean moved by at least **20%**. That floor is deliberate — base and branch each get one process launch, and launch-to-launch drift alone measured a 17% spread on identical code. The consequence: **a real 5–15% regression will pass CI silently.** If you are tuning something that matters at that scale, measure it locally with repeated runs; a green benchmark check is not evidence of no regression. BenchmarkDotNet exits 0 even when a benchmark throws, reporting the row as `NA`. The workflow has a separate step for that — if you see `NA` in results, the benchmark failed, it did not measure zero. ## Adding a benchmark New benchmarks go in `csharp/PhoneNumbers.PerformanceTest/Benchmarks/`, target `net10.0` only, and use `[MemoryDiagnoser]` + `[SimpleJob(RuntimeMoniker.Net10_0)]` like the existing ones. Reuse `PhoneNumberBenchmarkData` for input so cases stay comparable across benchmarks.
GitHubで見る