Skip to content

[java][bidi] Decode nested RemoteValues from the parsed tree instead of re-serializing each subtree - #18095

Merged
diemol merged 4 commits into
SeleniumHQ:trunkfrom
yashp676:fix-18076-remotevalue-decode
Oct 6, 2026
Merged

diemol merged 4 commits into
SeleniumHQ:trunkfrom
yashp676:fix-18076-remotevalue-decode

Conversation

@yashp676

@yashp676 yashp676 commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Related Issues

Fixes #18076

What does this PR do?

RemoteValue.deserializeValue turned each child back into a RemoteValue by serializing it to JSON text and parsing it again. Each subtree was re-serialized once for every level above it. That made decoding a large log.entryAdded argument cost gigabytes and seconds, and it failed outright once a value was deeper than JsonOutput.MAX_DEPTH.

This PR adds a package-private RemoteValue.fromMap that builds a RemoteValue directly from the already-parsed Map/List tree, so each node is visited once:

  • ARRAY / SET / NODE_LIST / MAP / OBJECT: children (and non-string map keys) are decoded through fromMap.
  • NODE: NodeProperties isn't a leaf. children and shadowRoot are remote values, and the shared ConstructorCoercer round-trips them too. NodeProperties.fromMap now decodes them through RemoteValue.fromMap.
  • REGULAR_EXPRESSION / WINDOW: unchanged. They are genuine leaves (strings only), so a single round trip is bounded.
  • Scalar fields (nodeType, childNodeCount, localName, attribute values, mode, handle, internalId, sharedId) go through RemoteValue.coerceScalar, which applies the same JSON coercers as before (so "1" still becomes 1L, and 123 still becomes "123"). Objects and arrays are rejected rather than serialized.

RemoteValue.fromJson(JsonInput) keeps its signature and is still the only static fromJson, so StaticInitializerCoercer continues to bind it. The new factory is deliberately named fromMap. No public API changes: all new methods are package-private.

Before / after: one decode of each payload, measured with ThreadMXBean.getCurrentThreadAllocatedBytes (OpenJDK 21, Linux, after one warm-up decode):

payload wire trunk this PR
10,000 × {index, nested: {value}} (same shape as the .NET check on the issue) 1.4 MB 3,160 MB / 4.6 s 50 MB / 0.24 s
Array.from({length: 100000}, (_, i) => i) 3.1 MB 1,739 MB / 2.4 s 88 MB / 0.52 s
object tree, depth 6 × 3 keys 37 KB 354 MB / 0.7 s 1 MB / 9 ms
single chain, depth 30 1 KB 100 MB / 0.3 s < 1 MB / 4 ms

Depth ceiling: on trunk, a single-key object chain decodes up to depth 34 and fails from depth 35 with Reached the maximum depth of 100 while writing JSON. With this PR, decoding is no longer bounded by JsonOutput.MAX_DEPTH. It goes as deep as the JSON parser itself can read (about 300 levels on a default thread stack, and 1,000+ with a larger one).

Implementation Notes

  • Behaviour is unchanged apart from the fix. I ran 60,000 randomly generated payloads (valid, lenient and malformed: numeric strings, numbers/booleans in string fields, JSON null in place of values, extra or missing map-entry items, unknown fields, bad types) through trunk and this branch. Decoded results, concrete collection types (ArrayList, HashMap, LinkedHashMap) and rejected inputs (JsonException) are identical across all of them.
  • As on trunk, a JSON null where a remote value is expected decodes to null, and a map entry with more than two items uses the first two. A map entry with fewer than two items, a missing type, or a non-array container value throws JsonException. New error messages name the JSON type (object, array, …) rather than echoing the value, because these payloads can be megabytes.
  • NodeProperties.fromMap keeps the ConstructorCoercer rules: nodeType and childNodeCount are required, the rest are optional, and mode is matched case-insensitively.

Tests

New RemoteValueTest (small / unit, no browser) in java/test/org/openqa/selenium/bidi/script, with a small-tests target. It's excluded from the large-tests glob. It has 12 tests:

  • object chain at depths 20, 40 and 200; node tree 40 levels deep through children
  • 10,000 nested records, map with number and object keys, set, nodelist, node children and shadowRoot, regexp and window leaves, ids on nested values, missing type rejected
  • scalar coercion matching the JSON layer (numeric-string node counts; numbers/booleans in string fields), and null / extra map-entry items handled as on trunk

To confirm the tests catch the bug, I reverted RemoteValue/NodeProperties to trunk with the new tests in place. The two depth tests failed with Reached the maximum depth of 100 while writing JSON, and the other ten passed, because they pin the existing behaviour. With the fix, all twelve pass. The tests assert structure, not timing, so they won't be flaky in CI.

Additional Considerations

Out of scope here, as discussed on the issue:

  • The unbounded BiDi Connection cached thread pool. Decoding is now much cheaper, but a flood of events can still queue without backpressure.
  • A client-side depth/breadth bound for log.entryAdded.

One related spot I didn't change: Script.createEvaluateResult decodes the evaluate result with JSON.convert(response.get("result"), RemoteValue.class). That is a single top-level round trip, so the cost is linear, but script.evaluate results nested deeper than ~33 levels still hit JsonOutput.MAX_DEPTH. Fixing it would mean calling fromMap from bidi.module, which needs it to be public. Happy to do that here or in a follow-up if you want it.

Types of changes

  • Bug fix (backwards compatible)

@qodo-code-review

Copy link
Copy Markdown
Contributor

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@titusfortner

Copy link
Copy Markdown
Member

/agentic_review

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

The BiDi wire decoder change needs human compatibility review, and node decoding currently rejects previously accepted numeric-string counts.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)
What changed in this PR

This PR addresses #18076 by decoding nested Java BiDi RemoteValue data from the parsed tree instead of repeatedly serializing subtrees.

Changes:

  • Decode container values and node descendants directly from parsed maps and lists.
  • Add unit tests for deep and large values, and a small Bazel test target.
File Description
java/​test/​org/​openqa/​selenium/​bidi/​script/​RemoteValueTest.java Adds decoding regression tests.
java/​test/​org/​openqa/​selenium/​bidi/​script/​BUILD.bazel Runs the new tests as a small target.
java/​src/​org/​openqa/​selenium/​bidi/​script/​RemoteValue.java Decodes nested values from the parsed tree.
java/​src/​org/​openqa/​selenium/​bidi/​script/​NodeProperties.java Decodes node children and shadow roots directly.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread java/src/org/openqa/selenium/bidi/script/NodeProperties.java Outdated
@qodo-code-review

qodo-code-review Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider

Great, no issues found!

Qodo reviewed your code and found no material issues that require review

Grey Divider

Tip of the day
💡 Did you know, you can add REVIEW.md to your repo root and Qodo follows it on every PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Previous reviews

Review updated until commit 4d0bc70 ⚖️ Balanced

Results up to commit 3f4a1ca ⚖️ Balanced


No changes from previous review

Grey Divider

Qodo Logo

@qodo-code-review

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit 4d0bc70

@qodo-code-review

qodo-code-review Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

No code changes since the last review — review skipped

Qodo Logo

This was referenced Oct 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

B-build Includes scripting, bazel and CI integrations B-devtools Includes everything BiDi or Chrome DevTools related C-java Java Bindings

Projects

None yet

5 participants