Skip to content

Treebuilder.inspecificscope optimizations#2485

Open
JH-A-Kim wants to merge 6 commits intojhy:masterfrom
JH-A-Kim:treebuilder.inspecificscope-optimizations
Open

Treebuilder.inspecificscope optimizations#2485
JH-A-Kim wants to merge 6 commits intojhy:masterfrom
JH-A-Kim:treebuilder.inspecificscope-optimizations

Conversation

@JH-A-Kim
Copy link
Copy Markdown

@JH-A-Kim JH-A-Kim commented Apr 9, 2026

Performance Optimization: Algorithmic Improvement for HtmlTreeBuilder.inSpecificScope

Executive Summary

This PR addresses a significant performance bottleneck in the HTML parser's scope-checking logic. Through profiling with Java Flight Recorder (JFR), we identified that HtmlTreeBuilder.inSpecificScope and its dependency on Arrays.binarySearch were responsible for a dominant portion of CPU cycles.

By replacing repeated binary search lookups with a pre-calculated bit flag on the Tag object, we have shifted the algorithmic complexity of stack traversal from $O(n \log m)$ to a strictly linear $O(n)$.

Key Performance Gains

In synthetic workloads testing deep nesting (depths up to 120), the following improvements were observed:

CPU Profiling (JFR Samples)

Method Before (% / Samples) After (% / Samples) Improvement
Arrays.binarySearch 33.8% / 52 19.2% / 25 ~52%
HtmlTreeBuilder.inSpecificScope 46 samples 25 samples ~46%
TreeBuilder.stepParser 30 samples < 5 samples ~83%+
Tokeniser.read 29 samples < 5 samples ~83%+

Call Stack Hot Paths

Method Before (Samples) After (Samples) Delta
StringUtil.inSorted 52 25 -27
HtmlTreeBuilder.inSpecificScope 46 25 -21
HtmlTreeBuilderState.process 34 5 -29

Algorithmic Improvement

  • Original Approach: As the parser walks the open-element stack, it performs membership checks using StringUtil.inSorted, which triggers Arrays.binarySearch for every step. Total complexity: $O(n \log m)$ where $n$ is stack depth and $m$ is the count of boundary tags ($m \approx 9$).
  • Optimized Approach: Canonical boundary tags (e.g., applet, caption, html, table, etc.) are now pre-marked with a Tag.InScope bit flag during tag setup. The traversal now uses a constant-time bitwise check (el.tag().is(Tag.InScope)), resulting in a linear $O(n)$ walk.

Memory & Stability

Memory behavior remains neutral. The gains are strictly algorithmic and do not increase heap pressure or Garbage Collection frequency.

Metric Before After Status
Sampled Allocation Weight 1320.1 MB 1319.3 MB Neutral
Total GC Pause 16.9 ms 17.3 ms Neutral
Max Heap after GC 6.88 MB 7.01 MB Neutral

Justification

This optimization fundamentally improves the scalability of the parser for modern, deeply nested web documents. By reducing the per-step overhead of scope checking to a single bitwise instruction, we ensure Jsoup remains performant and deterministic in high-throughput or resource-constrained environments.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants