Remembered places and the fallback's text: sibling and subtree rescans make one captured page cost minutes #588
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
performance
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo#588
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found on 2026-09-29 in a read-only review of postulo at
95348908d, reading the remembered-places reader and the link-farm measure and timing them on generated pages.Checked again on 2026-10-01 against
mainat0a5cad587: still present.plugins/builtin/has not changed; the API's calls toafter_captureare now atapi/api.py:547-551and the unthrottledhtmlpath atapi/api.py:605-607.The remembered-places reader finds what a label labels by scanning the label's parent from its first child every time, so a parent with k label-like children costs O(k²). The fallback's link-farm measure re-walks the whole subtree of every container it meets, which costs O(n × depth). Both run on every capture, inside the request or the capture errand, over pages of up to 2 MB, and a page of a few hundred kilobytes is enough to hold a worker for minutes.
plugins/builtin/hints.py:246-263(_labels) visits everydt,th,td,label,strong,bandspan. For each,_next_element(:183-191) scans the parent's children from the start until it finds the element; atdalso rebuilds the list of its row's cells (:237,cells = [c for c in element.parent.children if isinstance(c, Element)]); a "Label:" runs_rest_of_line(:202-225), which again starts from the parent's first child and callstext_ofon each child after the label.plugins/builtin/htmlutil.py:293-322(_link_farms) runselement.iter(),_text_length(element)and_text_lengthof every link for eachdiv,section,ul,ol,asideandnavthat is not already inside a farm, plus an ancestor walk (:310) for every element.jobs/remembered.py:276(after_capture, throughregistry.page_places) runs in the API request (api/api.py:545-549) and in the capture errand;hints.fillruns_labelsagain (hints.py:347) whenever a label place is remembered;main_text(htmlutil.py:325-357) runs_link_farmsfor every page the fallback reads.registry.page_places: one<tr>holding 2,500 / 5,000 / 10,000 closed<td>(25 / 50 / 100 KB) took 0.4 s / 1.6 s / 6.5 s; one<p>holding 2,500 / 5,000 / 10,000<span>a:</span> bpairs took 0.2 s / 0.8 s / 3.5 s; a<dl>of 2,500 / 5,000 pairs took 0.24 s / 0.87 s. Each doubling quadruples the time, so a 2 MB page of the same shape runs far past gunicorn's 120 s--timeout(docker/Dockerfile:354).main_textover 500 links and prose inside 100 / 200 / 400 nested<div>s (about 50 KB) took 0.4 s / 0.9 s / 1.9 s, linear in depth for the same size.htmlthroughPOST /api/v1/captureswith acapturestoken (that path is not throttled bythrottle.capture,api/api.py:603-605). The instance runs three workers (docker/Dockerfile:362), and an errand has no gunicorn timeout at all.MOST_BYTESandPER_KIND, cap the output after the work is done.parent.childrenin_labels(or record it in_TreeBuilder), check the "row of exactly two cells" condition once per row, and have_rest_of_linestart from the label's own index. In_link_farms, compute each node's text length and link-text length bottom-up in one post-order pass and decide farms from those totals, so the page costs O(n). Boundplacesand_labelsby the number of elements visited as well as by output size.Done when
tests/test_remembered_places.pyrunshints.placesover one<tr>of 20,000 cells and over a<p>of 20,000 "Label:" spans and finishes in well under a second (a generous bound such as 2 s, which the present code misses by more than an order of magnitude).tests/test_page_metadata.pyrunshtmlutil.main_textover 400 nested<div>s holding links and prose and stays within the same kind of bound.Landed on main with a green Forgejo CI run. Its entry in the CHANGELOG's Unreleased section ends in (#588).