Skip to content

[java] Speed up JSON serialisation and parsing#17750

Open
shs96c wants to merge 5 commits into
SeleniumHQ:trunkfrom
shs96c:json-speed
Open

[java] Speed up JSON serialisation and parsing#17750
shs96c wants to merge 5 commits into
SeleniumHQ:trunkfrom
shs96c:json-speed

Conversation

@shs96c

@shs96c shs96c commented Jul 4, 2026

Copy link
Copy Markdown
Member

Performance work on org.openqa.selenium.json, guided by benchmarks and JFR profiles. Serialisation strategies are resolved once per class instead of reflection-probing for every value written, string escaping streams through a lookup table without materialising escaped copies, strings and digit runs are read from the input buffer in bulk, coercion functions are cached and resolved once per container, bean accessors are compiled to direct calls via LambdaMetafactory, and decimals take Clinger's fast path with results pinned bit-for-bit to Double.parseDouble by a differential test.

On representative payloads this brings us to similar speeds as either Gson or Jackson. JSON output is byte-identical; the only observable change is that error messages now truncate multi-megabyte payloads rather than embedding them wholesale.

This PR also enables compact mode for our responses, which also helps speed.

🤖 Generated with Claude Code

shs96c and others added 3 commits July 4, 2026 11:04
- Resolve JsonOutput write strategies once per class instead of probing
  with reflection (and exceptions) for every value written
- Stream string escaping through a lookup table without materialising
  an escaped copy of the value
- Read strings and digit runs from the input buffer in bulk, with a
  zero-copy path for escape-free strings
- Cache property descriptors, instance writers and coercer lookups
- Coerce scalar constructor arguments directly instead of round-tripping
  them through JSON text

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Fold null handling into cached coercion functions and resolve element
  coercers once per container instead of per value
- Compile bean getters and setters to direct calls via LambdaMetafactory,
  falling back to reflective invocation where compilation isn't possible
- Memoise the pending token type in JsonInput; it was recomputed several
  times per value
- Skip whitespace in bulk and fast-path the common scalar classes when
  writing

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Profile-guided improvements to the JSON read path:

- Test for the four JSON whitespace characters before consulting
  Character.isWhitespace, which was a third of wire-payload parse time
- Parse decimals with Clinger's fast path when the significand fits
  exactly in a double, falling back to Double.parseDouble otherwise;
  a differential test pins results bit-for-bit to the JDK
- Replace the container deque with a byte-array stack and let
  ObjectCoercer handle nulls itself, trimming per-value overhead

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@selenium-ci selenium-ci added the C-java Java Bindings label Jul 4, 2026
shs96c and others added 2 commits July 4, 2026 22:21
…directly

JsonTypeCoercer now stores coercion functions for plain Class keys in a
ClassValue, so classes (and their loaders) stay collectable now that Json
shares a single coercer for the life of the JVM. The remaining generic-type
map is capped, and LazyCoercer memoizes property types locally instead of
publishing them to the shared cache.

Json.toType(String, ...) reads the string directly rather than through a
StringReader, skipping the Reader indirection and the 16KB read buffer that
dominated the cost of parsing small payloads: element-sized responses go
from 1.7M to 8.5M parses/second, faster than both Gson and Jackson on the
same input.

Also remove the ineffective synchronized block in JsonInput.addCoercers
(the field it guarded is written elsewhere without synchronization) and
document that JsonInput instances are single-threaded.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
JsonOutput pretty-prints by default, so every command body, response body,
and Grid payload carried newlines and indentation. Emit compact JSON at the
wire boundaries instead: the command and response codecs, Contents.asJson,
the new session payload, and the GraphQL handler. This trims nested
payloads by roughly a third and aligns Java with the other bindings, which
already send compact JSON. Json.toJson is unchanged and still
pretty-prints.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@shs96c

shs96c commented Jul 4, 2026

Copy link
Copy Markdown
Member Author

Throughput on WebDriver-protocol-shaped payloads (JMH 1.37, 1 forked JVM per
run, 3×2s warmup + 5×2s measurement, Apple Silicon, Temurin-compatible JDK 26).
Reads deserialize a String to Map via Json.MAP_TYPE; writes serialize
plain Map/List graphs.

Payload trunk this PR speedup
read element response (94 B) 1.60 M ops/s 8.50 M ops/s 5.3×
read findElements ×100 (~7 KB) 26.9 K ops/s 102 K ops/s 3.8×
read new-session response (~1.6 KB) 237 K ops/s 722 K ops/s 3.1×
read script response (120 KB) 1,480 ops/s 3,271 ops/s 2.2×
write locator command (72 B) 112 K ops/s 8.81 M ops/s 78×
write new-session map (~1.6 KB) 14.9 K ops/s 482 K ops/s 32×
write script response (120 KB) 56 ops/s 3,334 ops/s 59×

For context, the same harness against Gson 2.14.0 and Jackson 2.21.3 (the
versions pinned in this repo):

Payload this PR Gson Jackson
read element response (94 B) 8.50 M 4.05 M 6.36 M
read findElements ×100 (~7 KB) 102 K 149 K 114 K
read new-session response (~1.6 KB) 722 K 630 K 671 K
read script response (120 KB) 3.3 K 3.4 K 4.0 K
write locator command (72 B) 8.81 M 3.38 M 9.78 M
write new-session map (~1.6 KB) 482 K 860 K 1.30 M
write script response (120 KB) 3.3 K 4.2 K 5.7 K

Trunk was 2.5–4× slower than both libraries on reads and 30–100× slower on
writes (the per-value reflective toJson probing dominated). With this PR the
library is fastest of the three on the two payload shapes that dominate real
sessions — element-command responses and session/capability blobs — and within
0.7–0.9× elsewhere.

@shs96c shs96c marked this pull request as ready for review July 4, 2026 21:37
@qodo-code-review

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

[java] Speed up JSON serialisation and parsing in org.openqa.selenium.json

✨ Enhancement 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Resolves serialisation strategies once per class via ClassValue/LambdaMetafactory instead of
 reflective probing per value; compiles bean getters/setters to direct calls.
• Streams string escaping through a lookup table without materialising escaped copies; reads strings
 and digit runs from the input buffer in bulk with a zero-copy fast path for escape-free strings.
• Caches property descriptors, coercion functions, and container-element coercers; memoises the
 pending token type in JsonInput; replaces Deque with primitive byte arrays.
• Implements Clinger's fast path for decimal parsing, falling back to Double.parseDouble for
 bit-identical results; adds a differential test covering 40 000 random doubles.
• Enables compact (non-pretty-printed) JSON output for HTTP responses, GraphQL handler, and session
 payloads; truncates multi-megabyte payloads in error messages.
Diagram

graph TD
    A["Json.java\n(entry point)"] --> B["JsonOutput.java\n(serialiser)"]
    A --> C["JsonInput.java\n(deserialiser)"]
    B --> D["ClassValue CONVERTERS\n(per-class strategy cache)"]
    B --> E["ASCII_ESCAPES table\n(O(1) escape lookup)"]
    B --> F["SimplePropertyDescriptor\n(getter compiler)"]
    C --> G["Input.java\n(buffered char reader)"]
    C --> H["JsonTypeCoercer\n(coercion cache)"]
    H --> I["LambdaMetafactory setters\n(InstanceCoercer)"]
    H --> J["Clinger fast-path\n(decimal parser)"]
    K["HTTP codecs / GraphQL\n(callers)"] --> A

    subgraph Legend
      direction LR
      _mod["Module"] ~~~ _cache[("Cache")] ~~~ _ext{{"External caller"}}
    end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Replace custom JSON with Jackson or Gson
  • ➕ Mature, battle-tested parsers with extensive optimisation history
  • ➕ Eliminates maintenance burden of a custom JSON stack
  • ➖ Selenium's coercion API (TypeCoercer, PropertySetting, fromJson/toJson conventions) has no direct equivalent — a thick adapter layer would be needed
  • ➖ Introduces a hard runtime dependency on a third-party library
  • ➖ Large migration scope with high regression risk
2. Use a memory-mapped or off-heap buffer for large payloads
  • ➕ Could further reduce GC pressure for very large responses
  • ➖ Adds significant complexity for a marginal gain on the small payloads that dominate WebDriver traffic
  • ➖ Not compatible with the existing Appendable/Reader API surface

Recommendation: The PR's approach is well-chosen for a performance-critical, already-deployed library. The main alternative worth considering is replacing the custom JSON implementation with Jackson or Gson entirely, which would give mature, heavily-optimised parsers for free. However, Selenium's JSON layer carries significant semantic baggage (custom coercers, toJson/fromJson conventions, PropertySetting modes, JavaBean reflection) that would require a non-trivial adapter layer on top of any third-party library — and the PR description notes that the result already reaches parity with Gson/Jackson on representative payloads. A second alternative is using java.io.StringWriter with synchronized stripped (via a custom unsynchronised writer) instead of StringBuilder; this is a minor point and the PR's StringBuilder choice is strictly better. The incremental optimisation approach taken here is the right call: it preserves the existing API surface, avoids a large migration risk, and achieves the performance goal.

Files changed (18) +1225 / -405

Enhancement (17) +1098 / -405
JsonOutput.javaResolve serialisation strategies once per class; stream string escaping via lookup table +316/-235

Resolve serialisation strategies once per class; stream string escaping via lookup table

• Replaces the per-instance 'LinkedHashMap<Predicate, DepthAwareConsumer>' with a static 'ClassValue<DepthAwareConsumer>' that resolves and caches the write strategy once per class. String escaping is rewritten to stream runs of plain characters directly to the appendable through a 128-entry 'ASCII_ESCAPES' array, avoiding materialising an escaped copy. Adds fast paths for 'String', 'Long', 'Integer', 'Double', and 'Boolean' in 'write0'. Separates 'beginValue' bookkeeping from the 'write' method and adds 'rawAppend' to reduce intermediate string allocations. Compact-mode fields ('separator', 'objectStart', 'arrayStart') are pre-computed in 'setPrettyPrint'.

java/src/org/openqa/selenium/json/JsonOutput.java

JsonInput.javaReplace Deque container stack with byte arrays; memoize peeked token type; bulk digit reads +222/-81

Replace Deque container stack with byte arrays; memoize peeked token type; bulk digit reads

• Replaces 'Deque<Container>' and 'Deque<Boolean>' with parallel primitive byte/boolean arrays, eliminating boxing and deque overhead on every value read. The pending token type is memoised in 'peekedType' and cleared on consumption. Digit-run reading delegates to 'Input.appendDigits' instead of per-character loops. Adds a string-backed constructor that bypasses the 'Reader' indirection for small payloads. Implements Clinger's fast-path decimal parser with a 'POW_10' table, falling back to 'Double.parseDouble' for bit-identical results.

java/src/org/openqa/selenium/json/JsonInput.java

Input.javaAdd bulk read methods and string-backed constructor to Input +145/-2

Add bulk read methods and string-backed constructor to Input

• Adds a 'String'-backed constructor that pre-buffers the entire input, avoiding 'Reader' calls and the 16 KB read buffer for small payloads. Adds 'readSimpleString' (zero-copy fast path for escape-free strings), 'appendStringContent' (bulk append of plain string chars), 'appendDigits' (bulk digit append), and 'skipWhitespace' (bulk whitespace skip with RFC 8259 fast check). Increases the streaming buffer size from 4 096 to 16 384 chars.

java/src/org/openqa/selenium/json/Input.java

JsonTypeCoercer.javaCache coercion functions per class via ClassValue; add LazyCoercer and resolve/lazyResolve API +112/-23

Cache coercion functions per class via ClassValue; add LazyCoercer and resolve/lazyResolve API

• Introduces a 'ClassValue'-backed 'classCoercers' cache for plain 'Class' keys (entries live on the class itself, allowing GC of dynamic classes) and caps the existing 'ConcurrentHashMap' for generic types at 256 entries. Adds 'resolve' and 'lazyResolve' methods so containers can obtain a coercer once and reuse it per element. Null handling is folded into the coercion function returned by 'buildCoercer', removing the per-call null check from 'coerce'. Adds 'LazyCoercer' to defer resolution safely from within 'TypeCoercer.apply'.

java/src/org/openqa/selenium/json/JsonTypeCoercer.java

SimplePropertyDescriptor.javaCache property descriptors per class; compile getters via LambdaMetafactory +64/-11

Cache property descriptors per class; compile getters via LambdaMetafactory

• Adds a 'ClassValue<SimplePropertyDescriptor[]>' cache so method scanning happens once per class rather than on every serialisation. Bean getter methods are compiled to direct 'Function' calls via 'LambdaMetafactory', falling back to reflective 'Method.invoke' for inaccessible methods.

java/src/org/openqa/selenium/json/SimplePropertyDescriptor.java

InstanceCoercer.javaCache writer maps per PropertySetting; compile setters via LambdaMetafactory +71/-16

Cache writer maps per PropertySetting; compile setters via LambdaMetafactory

• Writer maps are computed once per 'PropertySetting' strategy using 'ConcurrentHashMap.computeIfAbsent' and reused across instances. Property coercers are resolved once per type via 'lazyResolve' and stored in 'TypeAndWriter.coercion'. Bean setter methods are compiled to direct 'BiConsumer' calls via 'LambdaMetafactory', with reflective fallback.

java/src/org/openqa/selenium/json/InstanceCoercer.java

CollectionCoercer.javaResolve element coercer once per container type instead of per element +4/-1

Resolve element coercer once per container type instead of per element

• Calls 'coercer.lazyResolve(valueType)' outside the lambda so the coercion function is resolved once when the container coercer is built, rather than on every element read.

java/src/org/openqa/selenium/json/CollectionCoercer.java

MapCoercer.javaResolve key and value coercers once per map type +9/-4

Resolve key and value coercers once per map type

• Moves 'stringKey' detection and 'lazyResolve' calls for key and value coercers outside the per-instance lambda, so cache lookups happen once per map type rather than per entry.

java/src/org/openqa/selenium/json/MapCoercer.java

ObjectCoercer.javaPre-resolve target coercers and handle null inline in ObjectCoercer +23/-14

Pre-resolve target coercers and handle null inline in ObjectCoercer

• Resolves coercers for 'Boolean', 'String', 'Number', 'List', and 'Map' once via 'lazyResolve' outside the per-value lambda. Adds a 'NULL' case directly in the switch, and overrides 'handlesNull()' to return 'true' so 'JsonTypeCoercer' does not double-handle nulls.

java/src/org/openqa/selenium/json/ObjectCoercer.java

ConstructorCoercer.javaShort-circuit scalar coercion without JSON round-trip in ConstructorCoercer +71/-0

Short-circuit scalar coercion without JSON round-trip in ConstructorCoercer

• Adds 'tryDirectCoercion' to convert already-parsed scalar values (numbers, booleans, strings, enums) directly to the target type, avoiding the expensive serialize-to-JSON-then-reparse round-trip for constructor arguments.

java/src/org/openqa/selenium/json/ConstructorCoercer.java

TypeCoercer.javaAdd handlesNull() hook to TypeCoercer +8/-0

Add handlesNull() hook to TypeCoercer

• Adds a 'handlesNull()' method (default 'false') that lets individual coercers declare they handle JSON null themselves, allowing 'JsonTypeCoercer.buildCoercer' to skip the null-guard wrapper.

java/src/org/openqa/selenium/json/TypeCoercer.java

Json.javaShare a single DEFAULT_COERCER; use StringBuilder and string-backed JsonInput +28/-13

Share a single DEFAULT_COERCER; use StringBuilder and string-backed JsonInput

• Promotes 'JsonTypeCoercer' to a static 'DEFAULT_COERCER' so its cache accumulates across all 'Json' instances. 'toJson' now writes into a 'StringBuilder' instead of a 'StringWriter' (avoiding synchronisation). 'toType(String, ...)' constructs a string-backed 'JsonInput' directly, bypassing 'StringReader'. Error messages are truncated to 512 characters via 'abbreviate'.

java/src/org/openqa/selenium/json/Json.java

GraphqlHandler.javaUse compact JSON output for GraphQL responses +11/-2

Use compact JSON output for GraphQL responses

• Adds a 'toCompactJson' helper that writes with 'setPrettyPrint(false)' and switches both the success and error response paths to use it.

java/src/org/openqa/selenium/grid/graphql/GraphqlHandler.java

AbstractHttpCommandCodec.javaEncode HTTP command bodies with compact JSON +6/-2

Encode HTTP command bodies with compact JSON

• Replaces 'json.toJson(parameters)' with an explicit 'JsonOutput' that has 'setPrettyPrint(false)' set, removing whitespace from outgoing command payloads.

java/src/org/openqa/selenium/remote/codec/AbstractHttpCommandCodec.java

AbstractHttpResponseCodec.javaEncode HTTP responses with compact JSON +6/-1

Encode HTTP responses with compact JSON

• Replaces 'json.toJson(...)' with an explicit compact 'JsonOutput', removing whitespace from outgoing HTTP response bodies.

java/src/org/openqa/selenium/remote/codec/AbstractHttpResponseCodec.java

Contents.javaDisable pretty-printing in Contents.asJson +1/-0

Disable pretty-printing in Contents.asJson

• Adds 'out.setPrettyPrint(false)' to the 'asJson' helper so JSON content bodies are compact.

java/src/org/openqa/selenium/remote/http/Contents.java

NewSessionPayload.javaWrite new-session payload as compact JSON +1/-0

Write new-session payload as compact JSON

• Adds 'json.setPrettyPrint(false)' to 'writeTo' so session negotiation payloads are compact.

java/src/org/openqa/selenium/remote/NewSessionPayload.java

Tests (1) +127 / -0
NumberParsingTest.javaAdd differential test pinning decimal parser results to Double.parseDouble +127/-0

Add differential test pinning decimal parser results to Double.parseDouble

• New test class with two tests: one covering known edge-case decimal strings and one running 40 000 randomised doubles (JDK-rendered and adversarial). Both assert bit-for-bit identity with 'Double.parseDouble' to validate the Clinger fast-path implementation.

java/test/org/openqa/selenium/json/NumberParsingTest.java

@qodo-code-review

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (1) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 18 rules

Grey Divider


Action required

1. Json.toType truncation untested 📘 Rule violation ▣ Testability
Description
Json.toType(String, ...) now truncates the echoed source in parse exception messages via
abbreviate(...), changing user-visible behavior for large payloads. The change set adds no test
that verifies the truncation boundary/format, risking regressions in this behavior.
Code

java/src/org/openqa/selenium/json/Json.java[R176-195]

  public <T> T toType(String source, Type typeOfT, PropertySetting setter) {
-    try (StringReader reader = new StringReader(source)) {
-      return toType(reader, typeOfT, setter);
+    Require.nonNull("Mechanism for setting properties", setter);
+
+    // Read the string directly rather than through a StringReader: string-backed input skips the
+    // Reader indirection and the large read buffer, which dominate the cost of parsing the small
+    // payloads typical of WebDriver commands and responses.
+    try (JsonInput json = new JsonInput(source, fromJson, PropertySetting.BY_NAME)) {
+      return fromJson.coerce(json, typeOfT, setter);
    } catch (JsonException e) {
-      throw new JsonException("Unable to parse: " + source, e);
+      throw new JsonException("Unable to parse: " + abbreviate(source), e);
    }
  }

+  /**
+   * Limit how much of the source is echoed into an exception message; sources can be many
+   * megabytes, and the parse position is already reported by the underlying cause.
+   */
+  private static String abbreviate(String text) {
+    return text.length() <= 512 ? text : text.substring(0, 512) + "...";
+  }
Evidence
PR Compliance ID 389273 requires adding/updating tests for new functionality and behavior changes.
The updated Json.toType(String, ...) now abbreviates the source included in exception messages,
but the only added test (NumberParsingTest) is about numeric parsing and does not validate the new
truncation behavior.

Rule 389273: Require tests for all new functionality and bug fixes
java/src/org/openqa/selenium/json/Json.java[176-195]
java/test/org/openqa/selenium/json/NumberParsingTest.java[27-126]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`Json.toType(String, ...)` now truncates the JSON source included in thrown `JsonException` messages (via `abbreviate(...)`). This is a user-visible behavior change for large inputs, but there is no automated test in this PR that asserts the truncation length and suffix, so future edits could accidentally remove or alter the truncation.

## Issue Context
The implementation now throws `new JsonException("Unable to parse: " + abbreviate(source), e)` and `abbreviate` truncates to 512 characters plus `...`.

## Fix Focus Areas
- java/src/org/openqa/selenium/json/Json.java[176-195]
- java/test/org/openqa/selenium/json/JsonTest.java[134-150]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. String parsing doubles memory 🐞 Bug ☼ Reliability
Description
Json.toType(String, ...) now routes through the string-backed JsonInput/Input path, which copies the
entire JSON text into a new char[] via String#toCharArray. For large JSON strings this can roughly
double peak memory for the payload and can trigger avoidable OOMs compared to streaming the existing
String through a Reader.
Code

java/src/org/openqa/selenium/json/Json.java[R176-186]

  public <T> T toType(String source, Type typeOfT, PropertySetting setter) {
-    try (StringReader reader = new StringReader(source)) {
-      return toType(reader, typeOfT, setter);
+    Require.nonNull("Mechanism for setting properties", setter);
+
+    // Read the string directly rather than through a StringReader: string-backed input skips the
+    // Reader indirection and the large read buffer, which dominate the cost of parsing the small
+    // payloads typical of WebDriver commands and responses.
+    try (JsonInput json = new JsonInput(source, fromJson, PropertySetting.BY_NAME)) {
+      return fromJson.coerce(json, typeOfT, setter);
    } catch (JsonException e) {
-      throw new JsonException("Unable to parse: " + source, e);
+      throw new JsonException("Unable to parse: " + abbreviate(source), e);
    }
Evidence
Json.toType(String, ...) now constructs a string-backed JsonInput, which in turn constructs
Input from the same string; Input(String) copies the entire string into a new char[] via
toCharArray(), increasing peak memory proportional to the full payload size.

java/src/org/openqa/selenium/json/Json.java[176-186]
java/src/org/openqa/selenium/json/JsonInput.java[71-77]
java/src/org/openqa/selenium/json/Input.java[70-85]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`Json.toType(String, ...)` now constructs `JsonInput` from the full `String`, and `Input(String)` immediately copies the entire string via `toCharArray()`. This increases peak memory usage proportional to input size and can become problematic for large JSON payloads.

### Issue Context
The optimization targets the small-payload fast path, but `Json.toType(String, ...)` is a public API and may be used with large strings.

### Fix Focus Areas
- java/src/org/openqa/selenium/json/Json.java[176-186]
- java/src/org/openqa/selenium/json/JsonInput.java[71-77]
- java/src/org/openqa/selenium/json/Input.java[79-85]

### Suggested fix
Implement a size-based strategy or a non-copying string input:
1. In `Json.toType(String, ...)`, if `source.length()` exceeds a threshold (e.g., 64KB/256KB), fall back to the existing streaming path (`new StringReader(source)` + `toType(reader, ...)`) to avoid duplicating the payload.
2. Alternatively (or additionally), rework `Input(String)` to avoid `toCharArray()` by reading directly from the `String` (e.g., store `String` + index, use `charAt`, and keep a small debug window buffer for `toString()`).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Throwable swallowed in compilation 🐞 Bug ⚙ Maintainability
Description
Accessor compilation via LambdaMetafactory catches Throwable and silently falls back to reflective
invocation. This broad catch can unintentionally swallow serious JVM Errors (e.g., OutOfMemoryError)
during compilation/setup rather than failing fast.
Code

java/src/org/openqa/selenium/json/InstanceCoercer.java[R267-285]

+    private static @Nullable BiConsumer<Object, Object> compileSetter(Method method) {
+      try {
+        MethodHandles.Lookup lookup = MethodHandles.lookup();
+        MethodHandle handle = lookup.unreflect(method);
+        CallSite site =
+            LambdaMetafactory.metafactory(
+                lookup,
+                "accept",
+                MethodType.methodType(BiConsumer.class),
+                MethodType.methodType(void.class, Object.class, Object.class),
+                handle,
+                handle.type().changeReturnType(void.class));
+        @SuppressWarnings("unchecked")
+        BiConsumer<Object, Object> setter =
+            (BiConsumer<Object, Object>) site.getTarget().invokeExact();
+        return setter;
+      } catch (Throwable t) {
+        return null;
+      }
Evidence
Both setter and getter compilation paths explicitly catch Throwable during LambdaMetafactory setup
and degrade to reflection/return null, which by definition includes JVM Errors.

java/src/org/openqa/selenium/json/InstanceCoercer.java[250-305]
java/src/org/openqa/selenium/json/SimplePropertyDescriptor.java[149-181]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`compileSetter` / `compileGetter` use `catch (Throwable)` to decide whether to fall back to reflection. This is broader than needed and can swallow serious JVM `Error`s during lambda compilation.

### Issue Context
The fallback is fine for expected reflective/linkage failures, but `Error`s typically indicate unrecoverable conditions and should not be converted into a silent performance downgrade.

### Fix Focus Areas
- java/src/org/openqa/selenium/json/InstanceCoercer.java[267-285]
- java/src/org/openqa/selenium/json/SimplePropertyDescriptor.java[149-181]

### Suggested fix
Narrow the catch or rethrow `Error`:
- Prefer catching expected exception types (e.g., `ReflectiveOperationException`, `RuntimeException`, `LambdaConversionException`, `IllegalAccessException`).
- If keeping a broad catch for safety, do:
 - `catch (Throwable t) { if (t instanceof Error) throw (Error) t; return null; }`
 and similarly in `compileGetter`.
This preserves the intended fallback behavior without masking fatal JVM conditions.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment on lines 176 to +195
public <T> T toType(String source, Type typeOfT, PropertySetting setter) {
try (StringReader reader = new StringReader(source)) {
return toType(reader, typeOfT, setter);
Require.nonNull("Mechanism for setting properties", setter);

// Read the string directly rather than through a StringReader: string-backed input skips the
// Reader indirection and the large read buffer, which dominate the cost of parsing the small
// payloads typical of WebDriver commands and responses.
try (JsonInput json = new JsonInput(source, fromJson, PropertySetting.BY_NAME)) {
return fromJson.coerce(json, typeOfT, setter);
} catch (JsonException e) {
throw new JsonException("Unable to parse: " + source, e);
throw new JsonException("Unable to parse: " + abbreviate(source), e);
}
}

/**
* Limit how much of the source is echoed into an exception message; sources can be many
* megabytes, and the parse position is already reported by the underlying cause.
*/
private static String abbreviate(String text) {
return text.length() <= 512 ? text : text.substring(0, 512) + "...";
}

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.

Action required

1. json.totype truncation untested 📘 Rule violation ▣ Testability

Json.toType(String, ...) now truncates the echoed source in parse exception messages via
abbreviate(...), changing user-visible behavior for large payloads. The change set adds no test
that verifies the truncation boundary/format, risking regressions in this behavior.
Agent Prompt
## Issue description
`Json.toType(String, ...)` now truncates the JSON source included in thrown `JsonException` messages (via `abbreviate(...)`). This is a user-visible behavior change for large inputs, but there is no automated test in this PR that asserts the truncation length and suffix, so future edits could accidentally remove or alter the truncation.

## Issue Context
The implementation now throws `new JsonException("Unable to parse: " + abbreviate(source), e)` and `abbreviate` truncates to 512 characters plus `...`.

## Fix Focus Areas
- java/src/org/openqa/selenium/json/Json.java[176-195]
- java/test/org/openqa/selenium/json/JsonTest.java[134-150]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines 176 to 186
public <T> T toType(String source, Type typeOfT, PropertySetting setter) {
try (StringReader reader = new StringReader(source)) {
return toType(reader, typeOfT, setter);
Require.nonNull("Mechanism for setting properties", setter);

// Read the string directly rather than through a StringReader: string-backed input skips the
// Reader indirection and the large read buffer, which dominate the cost of parsing the small
// payloads typical of WebDriver commands and responses.
try (JsonInput json = new JsonInput(source, fromJson, PropertySetting.BY_NAME)) {
return fromJson.coerce(json, typeOfT, setter);
} catch (JsonException e) {
throw new JsonException("Unable to parse: " + source, e);
throw new JsonException("Unable to parse: " + abbreviate(source), e);
}

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.

Remediation recommended

2. String parsing doubles memory 🐞 Bug ☼ Reliability

Json.toType(String, ...) now routes through the string-backed JsonInput/Input path, which copies the
entire JSON text into a new char[] via String#toCharArray. For large JSON strings this can roughly
double peak memory for the payload and can trigger avoidable OOMs compared to streaming the existing
String through a Reader.
Agent Prompt
### Issue description
`Json.toType(String, ...)` now constructs `JsonInput` from the full `String`, and `Input(String)` immediately copies the entire string via `toCharArray()`. This increases peak memory usage proportional to input size and can become problematic for large JSON payloads.

### Issue Context
The optimization targets the small-payload fast path, but `Json.toType(String, ...)` is a public API and may be used with large strings.

### Fix Focus Areas
- java/src/org/openqa/selenium/json/Json.java[176-186]
- java/src/org/openqa/selenium/json/JsonInput.java[71-77]
- java/src/org/openqa/selenium/json/Input.java[79-85]

### Suggested fix
Implement a size-based strategy or a non-copying string input:
1. In `Json.toType(String, ...)`, if `source.length()` exceeds a threshold (e.g., 64KB/256KB), fall back to the existing streaming path (`new StringReader(source)` + `toType(reader, ...)`) to avoid duplicating the payload.
2. Alternatively (or additionally), rework `Input(String)` to avoid `toCharArray()` by reading directly from the `String` (e.g., store `String` + index, use `charAt`, and keep a small debug window buffer for `toString()`).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +267 to +285
private static @Nullable BiConsumer<Object, Object> compileSetter(Method method) {
try {
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle handle = lookup.unreflect(method);
CallSite site =
LambdaMetafactory.metafactory(
lookup,
"accept",
MethodType.methodType(BiConsumer.class),
MethodType.methodType(void.class, Object.class, Object.class),
handle,
handle.type().changeReturnType(void.class));
@SuppressWarnings("unchecked")
BiConsumer<Object, Object> setter =
(BiConsumer<Object, Object>) site.getTarget().invokeExact();
return setter;
} catch (Throwable t) {
return null;
}

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.

Remediation recommended

3. Throwable swallowed in compilation 🐞 Bug ⚙ Maintainability

Accessor compilation via LambdaMetafactory catches Throwable and silently falls back to reflective
invocation. This broad catch can unintentionally swallow serious JVM Errors (e.g., OutOfMemoryError)
during compilation/setup rather than failing fast.
Agent Prompt
### Issue description
`compileSetter` / `compileGetter` use `catch (Throwable)` to decide whether to fall back to reflection. This is broader than needed and can swallow serious JVM `Error`s during lambda compilation.

### Issue Context
The fallback is fine for expected reflective/linkage failures, but `Error`s typically indicate unrecoverable conditions and should not be converted into a silent performance downgrade.

### Fix Focus Areas
- java/src/org/openqa/selenium/json/InstanceCoercer.java[267-285]
- java/src/org/openqa/selenium/json/SimplePropertyDescriptor.java[149-181]

### Suggested fix
Narrow the catch or rethrow `Error`:
- Prefer catching expected exception types (e.g., `ReflectiveOperationException`, `RuntimeException`, `LambdaConversionException`, `IllegalAccessException`).
- If keeping a broad catch for safety, do:
  - `catch (Throwable t) { if (t instanceof Error) throw (Error) t; return null; }`
  and similarly in `compileGetter`.
This preserves the intended fallback behavior without masking fatal JVM conditions.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

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

Labels

C-java Java Bindings

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants