Breakwater

Sandboxes / #1064

perf: dual replace

pypa/packaging#1064 · merged 2026-01-19 · base d5398b8bc1 → head 9627a8821f · ran on Sandboxes

BLOCK

At least one behaviour change looks like an unintended regression.

Counterexamples · 2 groups of 7 classes: 7 likely bug

P1

Output changed

inputcanonicalize_name("Á")
basevalue: "Á"
headvalue: "á"
Likely bugvotes: 2 likely-bug, 1 intended

The PR is titled a performance improvement with no intent to change Unicode handling; no tests/docs were updated.

perf: dual replace
Much faster than re, and even faster than str.translate

Found by: Nemotron strategy search, Ultra's planned example, hostile mutation, search sample · also caught by: Plain Hypothesis, Mutations of the PR's own test strings, Nemotron plan only

ran onToken Factory Sandboxes · image bfdb727b-8008-42aa-8523-32888cb60192

Regression test verified passes on base, fails on head
def test_1064_p1_value_0():
    """Breakwater P1: base behaviour pinned (found by differential testing; label: likely-bug)."""
    from packaging.utils import canonicalize_name
    assert canonicalize_name('Á') == 'Á'
Reproduce
uv run breakwater probe --repo https://github.com/pypa/packaging --base d5398b8bc19f --head 9627a8821f09 --target packaging.utils:canonicalize_name --backend contree --input "Á"
2 near-duplicate classes same behaviour, inputs with extra traits
  • canonicalize_name("Á ") base value: "Á " · head value: "á " test verified
  • canonicalize_name("Á\u0000") base value: "Á\u0000" · head value: "á\u0000" test verified
P3

Equality / ordering changed

acanonicalize_name("Á")
bcanonicalize_name("ẞ")
basea > b → False
heada > b → True
Likely bugvotes: 2 likely-bug, 1 intended

Ordering changed because Unicode letters are now lower‑cased, not intended by the perf‑only PR.

perf: dual replace
Much faster than re, and even faster than str.translate

Found by: Ultra's planned example, relations pass · also caught by: Nemotron plan only

ran onToken Factory Sandboxes · image bfdb727b-8008-42aa-8523-32888cb60192

Regression test verified passes on base, fails on head
def test_1064_p3_gt_3():
    """Breakwater P3: base behaviour pinned (found by differential testing; label: likely-bug)."""
    from packaging.utils import canonicalize_name
    a = canonicalize_name('Á')
    b = canonicalize_name('ẞ')
    assert (a > b) is False
3 near-duplicate relation flipes same behaviour, inputs with extra traits
  • canonicalize_name("ẞ") vs canonicalize_name("Ω") base a > b → True · head a > b → False test verified
  • canonicalize_name("ẞ") vs canonicalize_name("Ω") base a < b → False · head a < b → True test verified
  • canonicalize_name("Á") vs canonicalize_name("ẞ") base a < b → True · head a < b → False test verified

Regression tests

7 of 7 classes have a generated pytest that pins the old behaviour. Each passed on the base commit and failed on the head commit in the sandbox before it was offered.

Download test_1064.py

Performance · canonicalize_name

Worst family with mostly valid inputs: 1.09× the instructions of base (Normal package names are short‑ish alphanumerics with only a few '_' or '.' characters. The new translate‑table + single‑pass replace handles this in linear tim).

head slowerhead faster0.12×0.25×0.5×1×101001,000input size (characters)R2R2: 1.06× at ~8 charsR2: 1.06× at ~12 charsR2: 1.06× at ~36 charsR2: 1.06× at ~132 charsR2: 1.07× at ~516 charsR2: 1.08× at ~2,052 charsR2: 1.07× at ~8,196 charsF5.2F5.2: 1.08× at ~6 charsF5.2: 1.06× at ~8 charsF5.2: 1.05× at ~20 charsF5.2: 1.04× at ~68 charsF5.2: 1.03× at ~260 charsF5.2: 1.03× at ~1,028 charsF5.2: 1.02× at ~4,100 charsF5F5: 1.08× at ~6 charsF5: 1.07× at ~8 charsF5: 1.05× at ~20 charsF5: 1.03× at ~68 charsF5: 1.02× at ~260 charsF5: 1.02× at ~1,028 charsF5: 1.02× at ~4,100 charsF5.1F5.1: 1.08× at ~6 charsF5.1: 1.07× at ~8 charsF5.1: 1.05× at ~20 charsF5.1: 1.03× at ~68 charsF5.1: 1.02× at ~260 charsF5.1: 1.02× at ~1,028 charsF5.1: 1.02× at ~4,100 charsR3R3: 0.79× at ~6 charsR3: 0.51× at ~8 charsR3: 0.20× at ~18 charsR3: 0.13× at ~64 charsR3: 0.25× at ~256 charsR3: 0.61× at ~1,024 charsR3: 1.09× at ~4,096 chars
  • R2
  • F5.2
  • F5
  • F5.1
  • R3
Head/base cachegrind instructions per call, by input size (log-log). Each line is one input family Nemotron proposed; points above 1× are where the "faster" code does more work.
FamilyInputsRatio range Slope base → headVerdict
R2many_underscores_and_dotssuspected slowdown · archive1.06× – 1.08×0.78 → 0.78slower
F5.2mixed_chars_replace_bothsuspected slowdown · expand:F51.02× – 1.08×0.74 → 0.74slower
F5long_name_many_underscoressuspected slowdown · plan1.02× – 1.08×0.76 → 0.75slower
F5.1long_name_dotssuspected slowdown · expand:F51.02× – 1.08×0.76 → 0.75slower
R3Normal package names are short‑ish alphanumerics with only a few '_' or '.' characters. The new translate‑table + single‑pass replace handles this in linear timclaimed speedup · archive0.13× – 1.09×0.43 → 0.52slower

Receipt

TargetBackendImageRun AuditAdjudication
packaging.utils:canonicalize_nameToken Factory Sandboxesbfdb727b-8008-42aa-8523-32888cb60192bt-1064-7598d7$0.046$0.067

Nemotron cost on Nebius Token Factory. Backend: where the untrusted code ran. Token Factory Sandboxes (a microVM branched from a kept image, cited by its UUID). Every image is content-addressed (repo at each commit + harness), so every card re-runs from the image and seed 0. Raw data: audit, adjudication.