Found by the _pipeline/ sweep for #553.
Symptom. A name with many leading titles and many surname particles takes time proportional to the product of the two counts. Ratio for 4× the input (py3.11, 2026-09-28, best of 5), where 4× is linear:
| shape |
2.2.0 |
2.3.0 |
master |
"Dr. " * n + "Jan " + "van Berg " * n |
10.0× |
10.1× |
10.5× |
This has been present since at least 2.2.0.
Mechanism. In _pipeline/_group.py, chain() inside _group_segment evaluates all(is_leading_title(pieces[x], ...) for x in range(k)) at every ambiguous-particle chain site k, inside the while k < len(pieces) loop. The all() short-circuits only at the first non-title, so the cost is (leading titles) × (chain sites). At n=400, cProfile shows about 160,800 generator steps and 162k is_leading_title calls. da Silva behaves the same way.
Fix. Compute the length of the leading-title run once, before the loop, and compare k against it. Check that merge() cannot change that run while the loop is running, or recompute it only where it can. Add a guard row: a _PREFIXED_SHAPES row (#557) or a frame-ratio test, with a recorded negative control.
Found by the
_pipeline/sweep for #553.Symptom. A name with many leading titles and many surname particles takes time proportional to the product of the two counts. Ratio for 4× the input (py3.11, 2026-09-28, best of 5), where 4× is linear:
"Dr. " * n + "Jan " + "van Berg " * nThis has been present since at least 2.2.0.
Mechanism. In
_pipeline/_group.py,chain()inside_group_segmentevaluatesall(is_leading_title(pieces[x], ...) for x in range(k))at every ambiguous-particle chain site k, inside thewhile k < len(pieces)loop. Theall()short-circuits only at the first non-title, so the cost is (leading titles) × (chain sites). At n=400, cProfile shows about 160,800 generator steps and 162kis_leading_titlecalls.da Silvabehaves the same way.Fix. Compute the length of the leading-title run once, before the loop, and compare
kagainst it. Check thatmerge()cannot change that run while the loop is running, or recompute it only where it can. Add a guard row: a_PREFIXED_SHAPESrow (#557) or a frame-ratio test, with a recorded negative control.