Skip to content

perf: make html_to_text linear in the number of nodes - #2372

Open
B4nan wants to merge 2 commits into
masterfrom
claude/html-to-text-linear-bacf3b
Open

B4nan wants to merge 2 commits into
masterfrom
claude/html-to-text-linear-bacf3b

Conversation

@B4nan

@B4nan B4nan commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

Both html_to_text() implementations (BeautifulSoup and Parsel) built the result by appending to one growing string. On every text node and block element they then ran a regex or endswith() over that whole buffer to decide whether a leading space or a newline was needed, so each check re-read everything emitted so far and the traversal was quadratic in the number of nodes. On CPython 3.13 a 0.6 MB document of 64000 inline elements took 53 s, 160000 elements took minutes, and the first 1 MB of a real Wikipedia article took 10 s.

The output is now collected in a list of parts, the checks look only at the last emitted character, and the parts are joined once at the end. The 64000-element document takes 0.3 s and the 160000-element one about a second. The result is byte-identical to the old code on the existing fixtures and on 1200 randomly generated documents. The two regex constants that only served the old checks are gone. A new test converts 20000 and then 80000 nodes with each parser and asserts the second run takes less than ten times as long, which holds for the linear code (ratio 3.5 to 4.8) and fails for the old one (ratio 14 to 15), with or without coverage tracing. Same change as apify/crawlee#4322 on the JS side.

Both the BeautifulSoup and the Parsel implementation appended to a
growing string and, on every text node and block element, ran a regex
or endswith() over that whole buffer to decide whether a leading space
or a newline was needed. Each check re-read everything emitted so far,
so the traversal was quadratic in the number of nodes: a 0.6 MB
document of 64000 inline elements took 53 s on CPython 3.13 and 160000
elements took minutes.

The output is now collected in a list of parts, the checks look only
at the last emitted character, and the parts are joined once at the
end. The same inputs take 0.3 s and 0.8 s, and the output is unchanged
on the existing fixtures and on randomly generated markup. The two
regex constants that only served the old checks are removed.
@B4nan B4nan added t-tooling Issues with this label are in the ownership of the tooling team. adhoc Ad-hoc unplanned task added during the sprint. labels Oct 30, 2026
@apify-service-account apify-service-account added the tested Temporary label used only programatically for some analytics. label Oct 30, 2026
@B4nan
B4nan requested a review from vdusek October 30, 2026 20:25
The CI unit tests run under coverage, where 160000 nodes take 13-15 s
even with the linear implementation, so the fixed 2 s bound failed on
every runner. The test now times 20000 and 80000 nodes and asserts the
ratio stays under 10: the linear code measures 3.5-4.8 and the previous
quadratic code 14-15, with or without coverage tracing.
@codecov

codecov Bot commented Oct 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.13%. Comparing base (242be00) to head (52fe368).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #2372      +/-   ##
==========================================
+ Coverage   94.06%   94.13%   +0.06%     
==========================================
  Files         184      184              
  Lines       13654    13662       +8     
==========================================
+ Hits        12844    12861      +17     
+ Misses        810      801       -9     
Flag Coverage Δ
unit 94.13% <100.00%> (+0.06%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

adhoc Ad-hoc unplanned task added during the sprint. t-tooling Issues with this label are in the ownership of the tooling team. tested Temporary label used only programatically for some analytics.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants