Repository navigation
perf: lazy pandoc probe, bounded content-type cache, one-walk url rewrite - #303
Conversation
getHTML evaluated defaultPandoc() as a default parameter, so the first call of every process ran execFileSync('pandoc --list-input-formats') even for plain HTML. That blocked the event loop: synchronous part of the first getHTML call 33-47ms (5 fresh processes), now 7-10ms. The probe now runs only when a response is an office document, through tinyspawn, once per process. Explicit `pandoc` values and the exported sync defaultPandoc() behave as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The cache was keyed by the raw remote header and never evicted: 200k distinct headers retained 64.6MB. It now resets after 1000 entries. Hit path unchanged (15.7 vs 16.6ns per two lookups, median of 5). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rewriteHtmlUrls ran one full-document selector query per url attribute (10 walks). It now selects every url-bearing tag once and checks that tag's attributes. Output is identical: an element removed for a local address is dropped either way, and surviving elements get the same rewrites (checked on every test fixture plus crafted nested local-address cases, 48 renders). html rewriteUrls on: 143.0 -> 111.2ms (-22.2%, 6 alternating runs per side). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review.
|
| Layer / File(s) | Summary |
|---|---|
Element URL processing src/html.js, test/html/rewrite-urls.js |
The combined selector finds URL-bearing elements once. Non-string values and non-HTTP protocols skip only the current attribute. HTTP(S) URLs are made absolute, and an element is removed when an attribute resolves to a local address. Tests cover these cases and early termination after a local address is found. |
Office conversion
| Layer / File(s) | Summary |
|---|---|
Lazy converter resolution src/index.js, test/office.js |
The default Pandoc runner is resolved lazily. Its supported-format probe and converter are cached. Tests cover deferred probing, custom and disabled runners, and probe failures. |
Office detection and conversion routing src/index.js, test/office.js |
Office-format detection no longer depends on a configured runner. Conversion runs only when a converter is resolved. Tests cover fallback behavior for PDF-labeled office files when Pandoc is disabled. |
Content-type cache
| Layer / File(s) | Summary |
|---|---|
Bounded cache and lookup checks src/util.js, test/util/get-content-type.js |
The cache clears before an insertion would exceed 1,000 entries. Cache hits are checked against undefined. Tests cover overflow, reparsing, and subsequent caching. |
Priority:
Estimated code review effort: 3 (Moderate) | ~25 minutes
Change: Refactor
Sequence Diagram(s)
sequenceDiagram
participant Request as Office conversion path
participant Resolver as resolvePandoc
participant Pandoc as Pandoc executable
participant Converter as Office converter
Request->>Resolver: Resolve converter for detected office format
Resolver->>Pandoc: Probe supported input formats on first use
Pandoc-->>Resolver: Return supported input formats
Resolver-->>Request: Return converter
Request->>Converter: Convert detected format and temporary file
Possibly related PRs
- microlinkhq/html-get#302: Adds benchmarks for URL rewriting and cold module loading, which measure behaviors affected by this change.
Merge Risk: 
Minimal · up to 657c7
-
No material merge risks were identified.
Pre-merge checks: 
5
-
Full details: Docstring Coverage
-
Explanation
No eligible functions found in the changed files. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 0 functions across 6 files.
-
Finishing Touches
-
📝 Generate docstrings
-
- Commit to this branch
- Create a new PR
🧪 Generate unit tests (beta)
-
- Commit to this branch
- Create a new PR
-
- Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
Comment @coderabbitai help to get the list of available commands.
There was a problem hiding this comment.
html-get
Details
| Benchmark suite | Current: 657c7d0 | Previous: 4fec2ee | Ratio |
|---|---|---|---|
html rewriteUrls off |
147 ms (133.2-189.0) |
163 ms (146.3-226.4) |
0.90 |
html rewriteUrls on |
223.8 ms (215.5-291.6) |
326.3 ms (308.8-390.2) |
0.69 |
pdf extract |
98.3 ms (92.2-129.8) |
150.8 ms (145.4-213.8) |
0.65 |
cold require |
236.1 ms (220.2-244.1) |
317.9 ms (302.5-328.1) |
0.74 |
This comment was automatically generated by workflow using github-action-benchmark.
The new branches still passed when inverted. Assert the cache cap, one shared probe, and url attributes that share an element. Co-authored-by: Cursor <cursoragent@cursor.com>
Three separately measured commits.
1. Probe the default pandoc lazily and async
pandoc = defaultPandoc()was a default parameter, so the firstgetHTMLcall of every process ranexecFileSync('pandoc', ['--list-input-formats']), even for plain HTML. The probe now runs only when a response is an office document, async through tinyspawn, once per process (memoized promise).The benchmark harness has no first-call case, so this is a one-off measurement: a fresh process serving a local plain HTML page, timing the synchronous part of the first
getHTML()call, 5 processes each.The remaining ~4-5ms of that is
defaultMutool()runningwhich mutoolthe same way (not touched here).Unchanged: explicit
pandocvalues (function orfalse), the exported syncdefaultPandoc(), and the branching (an office file mislabeledapplication/pdfwith no pandoc installed still goes to mutool, because pandoc availability is resolved before that branch).2. Bound the content-type cache
src/util.jscached parsed content-types keyed by the raw remote header, unbounded: 200k distinct headers retained 64.6MB. It now resets after 1000 entries. Hit path microbench (4 hot headers,getContentType+getCharset): 16.6ns base vs 15.7ns change, so no regression. 200k distinct headers now retain ~0MB.3. Rewrite url attributes in one tree walk
rewriteHtmlUrlsran one full-document selector query per url attribute inhtml-urlsTAGS (10 walks;domutils findwas the top self-time frame). It now selects every url-bearing tag once and checks that tag's attributes in the same order.Output is identical. An element removed for a local address ends up removed either way, and surviving elements get the same rewrites. Checked by rendering old and new on every HTML fixture under
test/plus crafted cases (nested local-address elements,ping/href,formaction/srcon the same element, uppercase tags, invalid and non-http urls), 3 base urls, rewriteUrls on/off: 48 renders, byte-identical.A/B
benchmark/index.jsunchanged. 6 alternating runs per side, median of per-run medians. node v24.16.0, arm64 (12 cores), load1 8.8-9.4.Behavior changes
None. The test suite passes. Two tests added. One checks that a plain HTML call never runs pandoc and the first office document probes it exactly once (it fails on master). The other checks that the content-type cache keeps resolving after eviction.
How tested
npm test(standard + c8 ava): 143 passed, 54 skipped (pre-existing skips). The one live-network test (from image URL that returns HTML markup,timing < 3000) flaked once and passed on rerun.🤖 Generated with Claude Code
Note
Medium Risk
Touches core
getHTMLfetch branching (office, PDF, pandoc/mutool) and HTML URL sanitization; changes are tested but alter when external binaries are spawned and how document types are classified.Overview
This PR bundles three performance-oriented changes to the HTML fetch pipeline, with expanded tests around each area.
Lazy async default pandoc:
getHTMLno longer runs a synchronouspandoc --list-input-formatsprobe on the first call via a default parameter. The default is a sentinel resolved withresolvePandoconly when an office format is detected, using a memoized asyncloadDefaultPandoc. Plain HTML requests avoid the spawn cost; concurrent office fetches share one probe.Bounded content-type cache: Parsed
Content-Typeheaders inutil.jsare capped at 1000 distinct keys, with a full cache reset on overflow, replacing an unbounded in-memory map.Single-pass URL rewriting:
rewriteHtmlUrlsbuilds a tag→attributes map fromhtml-urlsand walks URL-bearing elements once, rewriting every applicable attribute per element (including continuing after skipped protocols and removing the node on the first local-address hit).Office-vs-PDF routing now always runs
getOfficeFormatand only attempts pandoc when a converter is available (convertOffice), which keeps mislabeled office files on the pandoc path when enabled and allows PDF-labeled office bytes to fall through to mutool when pandoc is off—behavior covered by new integration tests.Reviewed by Cursor Bugbot for commit 657c7d0. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by CodeRabbit
Bug Fixes
Tests