From 209e4a3ffd41757cc5bb9b53fcf4dcb2b459ae20 Mon Sep 17 00:00:00 2001 From: Thomas Clegg Date: Wed, 5 Aug 2026 17:03:34 -0500 Subject: [PATCH] test: measure the parse, validate and format phases separately --- .../PhoneNumberWorkflowBenchmark.cs | 62 +++++++++++++++++++ csharp/PhoneNumbers.PerformanceTest/README.md | 5 +- 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/csharp/PhoneNumbers.PerformanceTest/Benchmarks/PhoneNumberWorkflowBenchmark.cs b/csharp/PhoneNumbers.PerformanceTest/Benchmarks/PhoneNumberWorkflowBenchmark.cs index 226e24712..521f14ae6 100644 --- a/csharp/PhoneNumbers.PerformanceTest/Benchmarks/PhoneNumberWorkflowBenchmark.cs +++ b/csharp/PhoneNumbers.PerformanceTest/Benchmarks/PhoneNumberWorkflowBenchmark.cs @@ -3,12 +3,19 @@ namespace PhoneNumbers.PerformanceTest.Benchmarks { + /// + /// The end-to-end workflow plus each of its three phases measured separately, so a cost can be + /// attributed rather than only totalled. Parse and Format allocate for quite different reasons - + /// StringBuilder round trips on one side, regex replacement producing a fresh string per step on + /// the other - and those want different fixes. + /// [MemoryDiagnoser] [SimpleJob(RuntimeMoniker.Net10_0)] public class PhoneNumberWorkflowBenchmark { private PhoneNumberUtil _phoneNumberUtil = null!; private PhoneNumberBenchmarkCase[] _phoneNumbers = null!; + private PhoneNumber[] _parsedNumbers = null!; [Params(1000)] public int PhoneNumberCount { get; set; } @@ -18,8 +25,20 @@ public void Setup() { _phoneNumberUtil = PhoneNumberUtil.GetInstance(); _phoneNumbers = PhoneNumberBenchmarkData.Create(_phoneNumberUtil, PhoneNumberCount); + + // Pre-parsed, so the validate and format phases measure only their own work. + _parsedNumbers = new PhoneNumber[_phoneNumbers.Length]; + for (var i = 0; i < _phoneNumbers.Length; i++) + { + _parsedNumbers[i] = + _phoneNumberUtil.Parse(_phoneNumbers[i].NumberToParse, _phoneNumbers[i].DefaultRegion); + } } + /// + /// Kept alongside the phases: parsing warms metadata the later phases reuse, so the whole is + /// not simply the sum, and this is the figure tracked across previous changes. + /// [Benchmark] public int ParseValidateAndFormatPhoneNumbers() { @@ -38,5 +57,48 @@ public int ParseValidateAndFormatPhoneNumbers() return checksum; } + + [Benchmark] + public int ParseOnly() + { + var checksum = 0; + for (var i = 0; i < _phoneNumbers.Length; i++) + { + var phoneNumber = _phoneNumbers[i]; + checksum += _phoneNumberUtil + .Parse(phoneNumber.NumberToParse, phoneNumber.DefaultRegion).CountryCode; + } + + return checksum; + } + + [Benchmark] + public int ValidateOnly() + { + var checksum = 0; + for (var i = 0; i < _parsedNumbers.Length; i++) + { + if (_phoneNumberUtil.IsValidNumber(_parsedNumbers[i])) + checksum++; + } + + return checksum; + } + + /// + /// INTERNATIONAL rather than E164: E164 takes an early exit that skips pattern formatting, + /// so it would not exercise the regex replacement chain in FormatNsnUsingPattern. + /// + [Benchmark] + public int FormatOnly() + { + var checksum = 0; + for (var i = 0; i < _parsedNumbers.Length; i++) + { + checksum += _phoneNumberUtil.Format(_parsedNumbers[i], PhoneNumberFormat.INTERNATIONAL).Length; + } + + return checksum; + } } } diff --git a/csharp/PhoneNumbers.PerformanceTest/README.md b/csharp/PhoneNumbers.PerformanceTest/README.md index d9c98b531..b98b3e86b 100644 --- a/csharp/PhoneNumbers.PerformanceTest/README.md +++ b/csharp/PhoneNumbers.PerformanceTest/README.md @@ -22,7 +22,10 @@ dotnet run -c Release --framework net10.0 -- --filter "*PhoneNumberWorkflowBench ``` The `PhoneNumberWorkflowBenchmark` exercises the widest slice of the library; the full suite -completes in a few minutes on a single runtime. +completes in a few minutes on a single runtime. Alongside the end-to-end +`ParseValidateAndFormatPhoneNumbers` it measures `ParseOnly`, `ValidateOnly` and `FormatOnly` +against the same data, so a cost — allocation especially — can be attributed to a phase instead of +only being visible as a total. Other available benchmarks: