Conversation
The tag-removal loop did `value = f"{value[:start]}{value[end + 1:]}"` for every
tag it found, which copies the whole remaining string once per tag and makes
striptags quadratic in the tag count.
Tags are now collected in a single left-to-right pass and joined once. That is
sound because the tag start mark is a single character: removing a tag can never
join two characters into a new `<`, so one pass finds exactly the same tags that
repeatedly searching from the beginning did. The comment-removal loop above is left
alone, since `<!--` is multi-character and splicing genuinely can forge a new mark
there.
Measured with time.process_time, alternating base and patched subprocesses,
min-of-5, on a paragraph of `<span class='x'>word</span>` repeated n times:
tags base new speedup base growth new growth
500 0.611ms 0.165ms 3.7x x2.33 x1.83
1000 6.415ms 0.365ms 17.6x x10.50 x2.21
2000 30.368ms 0.889ms 34.2x x4.73 x2.44
Base grows far faster than 2x per doubling while the patch stays near 2x, so the
quadratic term is gone rather than the constant factor being better.
Two costs, both measured:
Input with exactly ONE tag is about 6% slower (0.939x), because a single tag cannot
amortise the list allocation and join. Two tags is already 1.012x, four is 1.057x,
and a ten-tag bio string is 1.131x. I tried adding a single-tag fast path and it
simply moved the regression to the two-tag case (0.898x), so it is inherent to
batching at very small tag counts rather than something to engineer away.
Peak memory is higher on tag-dense input, because the chunk list exists alongside
the input until the join. On `Markup("<>" * 200000)`, tracemalloc peak goes from
1,200,119 to 2,072,916 bytes, about 1.7x. Upstream's peak is bounded by roughly 2x
the input; this is bounded by that plus about 8 bytes per tag for the list slot.
Behaviour is unchanged: 34 hand-written cases plus 40,000 randomized fuzz cases
over an alphabet of `<`, `>`, `<a>`, `</a>`, `<!--`, `-->`, newline, ampersand and
letters, comparing exact output. 0 mismatches. The cases cover unclosed tags,
comments interleaved with tags, `<>` pairs, nested tags, a tag containing a quoted
`>`, and non-ASCII inside a tag.
Suite: 79 passed, 1 skipped, rc=0, matching main. ruff check and ruff format
--check both clean.
Member
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Markup.striptagsremoves tags withvalue = f"{value[:start]}{value[end + 1:]}"inside a loop, which copies the whole remaining string once per tag. That makes it quadratic in the number of tags.Tags are now collected in a single left-to-right pass and joined once.
That is safe because the tag start mark is a single character: removing a tag can never join two neighbouring characters into a new
<, so one pass finds exactly the same tags that repeatedly searching from the beginning did. The comment-removal loop just above is deliberately left alone, since<!--is multi-character and splicing there genuinely can forge a new mark.Numbers
time.process_time, alternating base and patched subprocesses, min of 5, on a paragraph of<span class='x'>word</span>repeated n times:The growth columns are the point.
maingrows much faster than 2x per doubling while this stays near 2x, so what changes is the growth rate rather than the constant factor, and the gap keeps widening with tag count.striptagsis not in_speedups.c, so this is the code path that actually runs whether or not the C extension is compiled. I checked that before measuring.Two costs, both measured
Input with exactly one tag is about 6 percent slower (0.939x), because a single tag cannot amortise the list allocation and the join. Two tags is already 1.012x, four is 1.057x, and a ten-tag bio string is 1.131x. So the crossover is at two tags.
I tried adding a single-tag fast path to remove that. It worked for one tag and simply moved the regression to the two-tag case (0.898x), because the lookahead it needs is itself unamortised there. So this looks inherent to batching at very small tag counts rather than something to engineer away, and I would rather report it than keep trading it around.
Peak memory is higher on tag-dense input, because the chunk list coexists with the input until the join. On
Markup("<>" * 200000),tracemallocpeak goes from 1,200,119 to 2,072,916 bytes, about 1.7x.main's peak is bounded by roughly 2x the input; this is bounded by that plus about 8 bytes per tag for the list slot. For a template variable that is negligible, but it is a real change in the shape of the memory use and worth stating.Behaviour
Differential testing against
main: 34 hand-written cases plus 40,000 randomized fuzz cases over an alphabet of<,>,<a>,</a>,<!--,-->, newline, ampersand and letters, comparing exact output. 0 mismatches.The cases cover unclosed tags, comments interleaved with tags, bare
<>pairs, nested tags, a tag containing a quoted>, non-ASCII inside a tag, and the entity-looking strings<a>andA. I also checked the route a user actually reaches this by, Jinja's|striptagsfilter, on ordinary markup.Checks
pytestgives 79 passed, 1 skipped, exit code 0, matchingmain.ruff check srcandruff format --check srcare both clean.