Repository navigation
Fix quadratic-time id generation in toc extension - #1673
Conversation
`unique` probes `base`, `base_1`, `base_2` and so on from the start on every call, so with many headers that share the same text the n-th header checks about n ids and the whole document costs time that grows with the square of the header count. Add an optional `counters` cache to `unique` which records, per base id, how far the run of taken suffixes is already known to extend, and pass one cache per document from `TocTreeprocessor.run`. The cache is only used when the probe run is contiguous with it, so the result is the same as the plain probe even when explicit ids leave gaps or an id such as `foo_07` collides numerically but not as a string. Calls without the cache behave as before.
waylan
left a comment
There was a problem hiding this comment.
As a general observation, I understand the problem being solved and I understand how the provided code solves it, but I feel like there has to be a more elegant solution. I'm just not sure what it is.
|
Agreed it's more involved than I'd like. Most of the extra logic is there so the output stays exactly what the plain probe gives when |
That is the crux of the issue. If we don't maintain the preexisting behavior, then we break/change users' permalinks. We don't want to do that, so we will leave it as-is. |
Description
uniquein thetocextension finds a free id by probingbase,base_1,base_2and so on until one is not inids.TocTreeprocessor.runcalls it once per header with the slugified text, so when many headers share the same text (or all slugify to the same thing, such as headers made only of punctuation) the n-th call probes about n ids and the total work grows with the square of the header count. Timings formarkdown.markdown('# Header\n' * n, extensions=['toc'])on CPython 3.11:uniquenow takes an optionalcountersdict which records, per base id, the highestnfor whichbase_1throughbase_nare all known to be taken, and the treeprocessor passes one dict for the whole document. A repeated id resumes the probe after that run instead of from the start. The cache is only consulted or extended when the probe run is contiguous with it, which keeps the result identical to the plain probe whenused_idsalready holds explicit ids fromattr_listthat leave gaps, or ids such asfoo_07that collide numerically but not as strings. I compared the new function with the old one on about 780k calls over random id sequences and pre-seeded sets, with and without the cache, and the returned ids and final sets matched in every case. Callingunique(id, ids)without the cache behaves as before.Added a unit test for the cache edge cases and a rendering test with 10,000 identical headers (about 14 s before the change, instant with it), plus a changelog entry. The full suite and flake8 pass.
AI Assistance Disclosure
No AI tools were used in preparing this PR.
AI tools were used and I have disclosed which ones, and have fully reviewed and verified their output.
AI tools were use to assist with the following:
Checklist