feat(html): opt-in HTML backends (nokolexbor + rust) behind env switch - #501
Draft
gildesmarais wants to merge 20 commits into
Draft
feat(html): opt-in HTML backends (nokolexbor + rust) behind env switch#501gildesmarais wants to merge 20 commits into
gildesmarais wants to merge 20 commits into
Conversation
Hide HTML parse construction behind Html::Backend so Response and domain callers no longer construct or type-check raw Nokogiri HTML documents. Sanitize and XML/XPath paths stay on Nokogiri.
Wire Lexbor via nokolexbor (dev dependency), CSS compatibility shims for :first/:not(:first-child), and XPath pager fallback when Lexbor rejects selectors. Full suite green under both backends.
Extend dual-backend perf harness (heuristic + suite wall-clock), capture numbers, and document no-go for production default while facade stays.
Add rb-sys/magnus workspace under ext/html2rss_parser with rake compile only (no gemspec.extensions) so nokogiri default installs stay zero-Rust.
Parse once with scraper, normalize in Rust mirroring Normalizer rules, and map IR to SST via Magnus so the :rust path can skip the Ruby walk.
Expose NativeEngine Document/Node CSS and attr ducks, register Backend::Rust behind HTML2RSS_HTML_BACKEND=rust, and keep sitemap/XML on Nokogiri.
Add NodeSet/#text/#attr ducks, named descendant text helpers, sitemap Nokogiri policy, and NativeNode identity so Microdata and selectors work.
Wire rust into perf harnesses, refresh suite/auto-source numbers, and update the backend experiment memo with promotion gates still unmet.
Document rake compile and sandbox BUNDLE_PATH caveat for the opt-in Rust HTML backend experiment.
Add criterion microbenches for string normalize vs today's serialize+reparse Document path, feature-gate magnus so benches link without libruby, and snapshot Phase 0 before numbers.
Walk scraper Html in normalize_from_html so Document#to_sst skips serialize+reparse; keep string to_sst as parse+reuse; assert Document≡string.
Move Attrs/Node/Index/Document construction into SST::Hydrator and have Rust emit one nested Hash IR, deleting per-node Magnus eval/build.
Detach comments for real, document Path A short-circuit, fix perf-suite empty-array under set -u, and refresh baselines plus BACKEND_EXPERIMENT honesty on the ≥15% wall checkpoint miss.
Path A materializes Attrs/Node/Index/Document via cached Magnus classes, dropping the O(nodes) Ruby Hash nest that dominated wall and allocations vs nokogiri on page_1.
Path A SST now beats nokogiri on page_1 wall and alloc; auto-source alloc flips to a clear drop while fidelity counts remain 69/71/17.
Record page_1 SST wall/alloc ≤ nokogiri and auto-source alloc drop after killing nested Hash IR; fidelity gate still open.
Mend html5ever adoption-agency splits into libxml-like nested li/a chains and blackhole following siblings into the still-open parent anchor so page_1 semantic/html/auto_source counts match 65/61/13.
Both parity gates are met: Path A SST wall/alloc ≤ nokogiri on page_1 and admission counts 65/61/13; suite green under HTML2RSS_HTML_BACKEND=rust.
CI characterization so mend_lists regressions fail the suite instead of only drifting remasure docs.
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.
What changed
Html::Document/Html::Nodefacade andHtml::Backendregistry (HTML2RSS_HTML_BACKEND=nokogiri|nokolexbor|rust, defaultnokogiri).Backend::Nokolexbor) as a measured experiment.ext/html2rss_parser(rb-sys + magnus + scraper):NativeEngine, direct SST construction, thin CSS/attr DOM ducks — compile viarake compile(nogemspec.extensions).make perf-baseline/make perf-suitefor all three backends; records baselines underspec/perf/.lib/html2rss/html/BACKEND_EXPERIMENT.md.Why
Nokogiri dominates parse/normalize cost on the auto-source path. The goal was a second engine that can beat it on wall time and allocations without breaking admission fidelity. Nokolexbor and Rust both land as opt-in so we can measure; neither is promoted to default.
Benchmarks
Auto-source (
make perf-baseline, Ruby 4.0.0, median of 5)Suite wall (
make perf-suite, seed 42, 1887 examples)Source:
spec/perf/baseline-auto-source.md,spec/perf/baseline-suite-wall.md.Insights
page_1.htmladmission drifts (Nokogiri 65/61/13 vs Lexbor 69/71/17). FFI node wrappers reintroduce GC/dispatch cost.rake compile(1 pending: XPath-only pager). Samepage_1drift as Lexbor. Auto-source ~parity with nokogiri (~0% wall / ~−3% alloc) — thin DOM path still pays Ruby wrapper cost; SST short-circuit exists but does not yet deliver the promotion bar (>30% wall / >50% alloc).gemspec.extensionsuntil packaging (precompiled gems) is ready.method_missingnative leak.Risk
:rustrequires localrake compile+ Rust toolchain; LoadError if missing.Review map
lib/html2rss/html/backend.rb+backend/{nokogiri,nokolexbor,rust}.rb— env registry and adapter contractsext/html2rss_parser/— pureparse/sstvs magnusruby/boundary; optional compile wiring inrakefile.rblib/html2rss/sst/normalizer.rb+spec/.../native_engine_sst_spec.rb— SST short-circuit and golden fidelitylib/html2rss/html/BACKEND_EXPERIMENT.md+spec/perf/baseline-*.md— verdicts and numbersValidation
HTML2RSS_HTML_BACKEND=rustfull RSpec (seed 42): 1887 examples, 0 failures, 1 pendingbin/heuristic-perf-baseline --backend all→spec/perf/baseline-auto-source.mdbin/perf-suite --write …→spec/perf/baseline-suite-wall.mdTest plan
mise exec -- bundle exec rake compilemise exec -- bundle exec rspec --no-fail-fastHTML2RSS_HTML_BACKEND=rust mise exec -- bundle exec rspec --no-fail-fastmake perf-baselineand confirmpage_1counts vs nokogiri