Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
coverage:
status:
project:
default:
target: auto
# Allow overall coverage to drop by up to 1% without failing the build.
# Catches gradual rot without flagging tiny noise from refactors.
threshold: 1%
patch:
default:
# Patch coverage on changed lines must be at least 90%. Catches PRs that
# ship large uncovered blocks while tolerating a few hard-to-test lines
# (resource-loader plumbing, AOT-only branches, etc.).
target: 90%
13 changes: 7 additions & 6 deletions csharp/PhoneNumbers.MetadataBuilder/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
Expand Down Expand Up @@ -137,8 +138,8 @@ private static int BuildGeocoding(string inputDir, string outputDir)
var countryCode = Path.GetFileNameWithoutExtension(txtPath);
var map = ParseAreaCodeText(txtPath);
var outPath = Path.Combine(outputDir, $"{lang}.{countryCode}");
using var fs = File.Create(outPath);
BuildPrefixMapFromBin.WriteAreaCodeMap(fs, map);
using var gz = new GZipStream(File.Create(outPath), CompressionLevel.SmallestSize);
BuildPrefixMapFromBin.WriteAreaCodeMap(gz, map);
Comment thread
wmundev marked this conversation as resolved.
Comment thread
wmundev marked this conversation as resolved.
Comment thread
wmundev marked this conversation as resolved.
written++;
}
}
Expand All @@ -162,8 +163,8 @@ private static int BuildTimezones(string inputFile, string outputFile)
Directory.CreateDirectory(Path.GetDirectoryName(outputFile)!);

var map = ParseTimezoneText(inputFile, splitter: '&');
using var fs = File.Create(outputFile);
BuildPrefixMapFromBin.WriteTimezoneMap(fs, map);
using var gz = new GZipStream(File.Create(outputFile), CompressionLevel.SmallestSize);
BuildPrefixMapFromBin.WriteTimezoneMap(gz, map);
Console.Out.WriteLine($"PhoneNumbers.MetadataBuilder: wrote {map.Count} timezone entries to {outputFile}");
return 0;
}
Expand Down Expand Up @@ -278,8 +279,8 @@ private static int BuildPerRegion(
{
var key = MakeFileNameKey(metadata, isAlternateFormatsMetadata);
var path = Path.Combine(outputDir, $"{filePrefix}_{key}");
using var fs = File.Create(path);
BuildMetadataFromBin.WriteMetadata(fs, metadata);
using var gz = new GZipStream(File.Create(path), CompressionLevel.SmallestSize);
BuildMetadataFromBin.WriteMetadata(gz, metadata);
written++;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;

namespace PhoneNumbers.PerformanceTest.Benchmarks
{
[MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net48)]
[SimpleJob(RuntimeMoniker.Net80)]
[SimpleJob(RuntimeMoniker.Net90)]
public class AsYouTypeFormatterBenchmark
{
#if NETFRAMEWORK
private PhoneNumberUtil _phoneNumberUtil = null;
private PhoneNumberBenchmarkCase[] _phoneNumbers = null;
#else
private PhoneNumberUtil _phoneNumberUtil = null!;
private PhoneNumberBenchmarkCase[] _phoneNumbers = null!;
#endif

[Params(1000, 10000)]
public int PhoneNumberCount { get; set; }

[GlobalSetup]
public void Setup()
{
_phoneNumberUtil = PhoneNumberUtil.GetInstance();
_phoneNumbers = PhoneNumberBenchmarkData.Create(_phoneNumberUtil, PhoneNumberCount);
}

[Benchmark]
public int InputDigitPerKeystroke()
{
var checksum = 0;

for (var i = 0; i < _phoneNumbers.Length; i++)
{
var phoneNumber = _phoneNumbers[i];
var formatter = _phoneNumberUtil.GetAsYouTypeFormatter(phoneNumber.DefaultRegion);

var input = phoneNumber.NumberToParse;
for (var c = 0; c < input.Length; c++)
checksum += formatter.InputDigit(input[c]).Length;
}

return checksum;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Engines;
using BenchmarkDotNet.Jobs;

namespace PhoneNumbers.PerformanceTest.Benchmarks
{
/// <summary>
/// Cold-start measurements. Each invocation builds a fresh <see cref="PhoneNumberUtil"/> so the
/// embedded-resource metadata cache is empty — this is the cost a consumer pays on their first
/// use of the library, before any region metadata has been loaded.
/// </summary>
[MemoryDiagnoser]
[SimpleJob(RunStrategy.ColdStart, RuntimeMoniker.Net48, launchCount: 1, warmupCount: 1, iterationCount: 20, invocationCount: 1)]
[SimpleJob(RunStrategy.ColdStart, RuntimeMoniker.Net80, launchCount: 1, warmupCount: 1, iterationCount: 20, invocationCount: 1)]
[SimpleJob(RunStrategy.ColdStart, RuntimeMoniker.Net90, launchCount: 1, warmupCount: 1, iterationCount: 20, invocationCount: 1)]
public class ColdStartBenchmark
{
// The country-code-to-region map and one fresh PhoneNumberUtil are kept around so the
// FirstRegionLookup benchmark has a pre-constructed util whose region cache has NOT been
// touched for the target region (we pick a region we never look up during setup).
#if NETFRAMEWORK
private PhoneNumberUtil _warmInstance = null;
private string[] _supportedRegions = null;
#else
private PhoneNumberUtil _warmInstance = null!;
private string[] _supportedRegions = null!;
#endif

// Region selected for FirstRegionLookup. Chosen as a small-but-real region so its metadata
// payload size is representative of the average region rather than an outlier like US/CN.
private const string TargetRegion = "CH";

[GlobalSetup]
public void Setup()
{
// Force JIT of the metadata-loading path so we measure steady-state cold-start cost
// rather than first-ever-invocation JIT noise. We deliberately use a different region
// than TargetRegion so the per-region cache stays cold for that one in FirstRegionLookup.
_warmInstance = PhoneNumberUtil.GetInstance();
_supportedRegions = new string[_warmInstance.GetSupportedRegions().Count];
_warmInstance.GetSupportedRegions().CopyTo(_supportedRegions);
}

/// <summary>
/// Bare construction: builds the country-code map and runs the constructor. No region
/// metadata is loaded — that all happens lazily on first <see cref="PhoneNumberUtil.Parse"/>.
/// </summary>
[Benchmark]
public PhoneNumberUtil CreateInstance()
{
return new PhoneNumberUtil(
new EmbeddedResourceMetadataLoader(),
CountryCodeToRegionCodeMap.GetCountryCodeToRegionCodeMap());
}

/// <summary>
/// Construct + force-load every region's metadata. Represents a long-running process that
/// will eventually touch every region — the total cold cost they pay across their lifetime.
/// </summary>
[Benchmark]
public int CreateInstanceAndLoadAllRegions()
{
var util = new PhoneNumberUtil(
new EmbeddedResourceMetadataLoader(),
CountryCodeToRegionCodeMap.GetCountryCodeToRegionCodeMap());

var checksum = 0;
for (var i = 0; i < _supportedRegions.Length; i++)
{
var meta = util.GetMetadataForRegion(_supportedRegions[i]);
if (meta != null)
checksum++;
}
return checksum;
}

/// <summary>
/// Isolated per-region lazy load against a pre-constructed instance. Builds one fresh util
/// per invocation so <see cref="PhoneNumberUtil.GetMetadataForRegion"/> hits the binary
/// loader instead of the in-memory cache.
/// </summary>
[Benchmark]
public PhoneMetadata FirstRegionLookup()
{
var util = new PhoneNumberUtil(
new EmbeddedResourceMetadataLoader(),
CountryCodeToRegionCodeMap.GetCountryCodeToRegionCodeMap());
return util.GetMetadataForRegion(TargetRegion);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;

namespace PhoneNumbers.PerformanceTest.Benchmarks
{
[MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net48)]
[SimpleJob(RuntimeMoniker.Net80)]
[SimpleJob(RuntimeMoniker.Net90)]
public class ParsingHelpersBenchmark
{
#if NETFRAMEWORK
private string[] _inputs = null;
private string[] _inputsWithLeadingJunk = null;
#else
private string[] _inputs = null!;
private string[] _inputsWithLeadingJunk = null!;
#endif

[Params(1000, 10000)]
public int PhoneNumberCount { get; set; }

[GlobalSetup]
public void Setup()
{
var phoneNumberUtil = PhoneNumberUtil.GetInstance();
var cases = PhoneNumberBenchmarkData.Create(phoneNumberUtil, PhoneNumberCount);

_inputs = new string[cases.Length];
_inputsWithLeadingJunk = new string[cases.Length];
for (var i = 0; i < cases.Length; i++)
{
_inputs[i] = cases[i].NumberToParse;
// Forces ExtractPossibleNumber to actually slice (the common "clean input" case
// is measured separately by _inputs).
_inputsWithLeadingJunk[i] = "abc " + cases[i].NumberToParse;
}
}

[Benchmark]
public int ExtractPossibleNumber_CleanInput()
{
var checksum = 0;
for (var i = 0; i < _inputs.Length; i++)
checksum += PhoneNumberUtil.ExtractPossibleNumber(_inputs[i]).Length;
return checksum;
}

[Benchmark]
public int ExtractPossibleNumber_WithLeadingJunk()
{
var checksum = 0;
for (var i = 0; i < _inputsWithLeadingJunk.Length; i++)
checksum += PhoneNumberUtil.ExtractPossibleNumber(_inputsWithLeadingJunk[i]).Length;
return checksum;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using System.Text;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;

namespace PhoneNumbers.PerformanceTest.Benchmarks
{
[MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net48)]
[SimpleJob(RuntimeMoniker.Net80)]
[SimpleJob(RuntimeMoniker.Net90)]
public class PhoneNumberMatcherBenchmark
{
// Filler text interleaved between embedded numbers so the matcher has to skip non-number
// content. Kept short to keep total input length proportional to PhoneNumberCount.
private const string Filler = " Lorem ipsum dolor sit amet, consectetur adipiscing elit. Call ";

#if NETFRAMEWORK
private PhoneNumberUtil _phoneNumberUtil = null;
private string _defaultRegion = null;
private string _text = null;
#else
private PhoneNumberUtil _phoneNumberUtil = null!;
private string _defaultRegion = null!;
private string _text = null!;
#endif

[Params(100, 1000)]
public int PhoneNumberCount { get; set; }

[GlobalSetup]
public void Setup()
{
_phoneNumberUtil = PhoneNumberUtil.GetInstance();
var cases = PhoneNumberBenchmarkData.Create(_phoneNumberUtil, PhoneNumberCount);

// FindNumbers takes a single default region. Pick the most common one in the seed
// set so a meaningful share of the numbers parse against region-local formats.
_defaultRegion = cases[0].DefaultRegion;

var sb = new StringBuilder(PhoneNumberCount * (Filler.Length + 16));
for (var i = 0; i < cases.Length; i++)
{
sb.Append(Filler);
sb.Append(cases[i].NumberToParse);
}
_text = sb.ToString();
}

[Benchmark]
public int FindNumbers_Valid()
{
var checksum = 0;
foreach (var match in _phoneNumberUtil.FindNumbers(_text, _defaultRegion))
checksum += match.RawString.Length;
return checksum;
}

// STRICT_GROUPING exercises AllNumberGroupsRemainGrouped, which the default VALID leniency
// does not. Useful to measure the matcher's group-formatting validation path.
[Benchmark]
public int FindNumbers_StrictGrouping()
{
var checksum = 0;
foreach (var match in _phoneNumberUtil.FindNumbers(_text, _defaultRegion,
PhoneNumberUtil.Leniency.STRICT_GROUPING, long.MaxValue))
checksum += match.RawString.Length;
return checksum;
}
}
}
13 changes: 13 additions & 0 deletions csharp/PhoneNumbers.PerformanceTest/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,19 @@ dotnet run -c Release --framework net9.0 -- --filter "*PhoneNumberWorkflowBenchm
The full benchmark includes the `100000` phone-number data set and may take several minutes,
especially when multiple runtime jobs are available on the machine.

Other available benchmarks:

- `*AsYouTypeFormatterBenchmark*` — per-keystroke cost of `AsYouTypeFormatter.InputDigit` over
a representative set of regional numbers.
- `*PhoneNumberMatcherBenchmark*` — `PhoneNumberUtil.FindNumbers` over a synthetic text body
with phone numbers embedded between filler sentences.
- `*ParsingHelpersBenchmark*` — `PhoneNumberUtil.ExtractPossibleNumber` measured separately
for clean inputs (no leading junk) and inputs that force the strip path.
- `*ColdStartBenchmark*` — cost a consumer pays the first time they touch the library: bare
`PhoneNumberUtil` construction, construction plus lazy-load of every region's metadata,
and an isolated first-region lookup. Uses BDN's `RunStrategy.ColdStart` with
`invocationCount: 1` so each measurement is a genuine first-use, not a steady-state loop.

The benchmark data is generated from valid example numbers in the bundled metadata and expanded
deterministically to the configured `PhoneNumberCount` values, up to 100,000 inputs. Each benchmark
iteration parses, validates, and formats every number in that data set.
Expand Down
8 changes: 7 additions & 1 deletion csharp/PhoneNumbers/AsYouTypeFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -632,7 +632,13 @@ private bool AttemptToExtractIdd()
isCompleteNumber = true;
var startOfCountryCallingCode = iddMatcher.Length;
nationalNumber.Length = 0;
nationalNumber.Append(accruedInputWithoutFormatting.ToString().Substring(startOfCountryCallingCode));
#if NETSTANDARD2_0
for (var k = startOfCountryCallingCode; k < accruedInputWithoutFormatting.Length; k++)
nationalNumber.Append(accruedInputWithoutFormatting[k]);
#else
nationalNumber.Append(accruedInputWithoutFormatting, startOfCountryCallingCode,
accruedInputWithoutFormatting.Length - startOfCountryCallingCode);
#endif
prefixBeforeNationalNumber.Length = 0;
prefixBeforeNationalNumber.Append(iddMatcher.Value);
if (accruedInputWithoutFormatting[0] != PhoneNumberUtil.PLUS_SIGN)
Expand Down
Loading
Loading