You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Evidence repository: dpol1/hugegraph-rust-rfc at commit b094369. Its README explains how to reproduce the runs. Paths below in its rfc, spike and results directories link to it.
Benchmarks and harness: benchmark/rfc on the branch prototype/rfc-spike of dpol1/hugegraph (commit 1c62d1fe7), not proposed for merge.
Paths that start with a HugeGraph module name are in apache/hugegraph at 83ef9f3fa.
1. Objectives, scope, non-goals
Question: which Server-core candidate offers a verifiable benefit large enough to justify the interoperability, compatibility, and maintenance cost of a Rust implementation?
Latency criterion, committed before any measurement (rfc/thresholds.md): T_native / T_Java <= 0.5. T_native covers the crossing, conversions, copies, the operation, and building the result.
Scope: the candidates of the #3145 inventory (LongEncoding, NumericUtil, both BytesBuffer copies, IntSet/IntMap, IntMapByDynamicHash) and one non-codec candidate, the Jieba analyzer. Server core is the perimeter this RFC proposes; the maintainers have not chosen it.
All numbers are JMH average times per call on one Linux x86_64 laptop (section 6). The table uses these terms:
Java cost (stage A): time per call of the current Java operation.
Calibration (stage B): time per call of the Java-to-Rust crossings that a native version of the operation would need.
Declared path: how those crossings are made, JNI through the jni-rs with_env wrapper around raw JNI calls on stack buffers (section 3).
Loads: small draws values from 0 to 999, mixed from the full long range, and large from the 1000 values below Long.MAX_VALUE. For validB64Char the character is early in the alphabet, late in it, or absent.
Stop and explore: for one operation and load, the declared path stops when the lower bound of the calibration exceeds half the upper bound of the Java cost; both bounds come from 99.9% confidence intervals (rfc/thresholds.md). Otherwise the verdict is explore, which means only that the rule does not allow a stop. A stop applies to the declared path and that load, not to every possible native implementation.
Stage C: measurement of a complete native implementation for an operation that reaches explore. It was not run.
Not distinguishable from the harness: the Java cost does not separate from the harness baseline, a benchmark method that reads the same inputs without running the operation. The rule is not applied to such a case.
Outcomes: evidence contrary when the declared path stops; evidence insufficient when the available evidence leaves the question open; not evaluated when nothing was measured, for the reason given in the row.
Java 38 to 182 ns. Creating the result string across the boundary alone costs 166 to 171 ns (rfc/stage-c-gate.md)
LongEncoding.decodeSignedB64
Against the current Java: stop on small and large; evidence insufficient on mixed (the gate says explore; stage C was not run). Against an improved Java decoder (same behaviour on the verified cases, section 4): evidence contrary for the declared path on all loads
Blocked by #3090. The two copies already meet on one live path: the store ScanPartition service decodes server-written rows with the struct copy and passes no schema, while the core property format relies on schema metadata. rfc/matrix.md gives the call chain. Vermeer loads graphs through this service. apache/hugegraph-computer#361 reports Can't construct Cardinality from code 0 in the struct readProperty there, an error consistent with the mismatch; it was not reproduced here, and the report lists Store and PD images tagged latest
IntSet / IntMap
Evidence insufficient for Rust
The motivation is maintenance (sun.misc.Unsafe) and memory, both outside the latency criterion. JEP 471 (https://openjdk.org/jeps/471) maps every member they use to VarHandle; that Java path was not measured here
IntMapByDynamicHash
Not a candidate today
No production instantiation at 83ef9f3fa (rfc/matrix.md)
Jieba analyzer
Evidence insufficient
Tokens are persisted as search-index row keys. The jieba-rs README says nothing about compatibility with jieba-analysis 1.0.2, and a token-equality run on real text is missing (rfc/matrix.md)
Recommendation: on the measured loads, the Java optimization makes it unjustified to continue the studied JNI path with respect to the 2x objective. Every mapped operation and load stops on this path, with two exceptions:
numberToSortableLong(Long) on the small load, whose cost is not distinguishable from the harness.
The current decodeSignedB64 on the mixed load, which ends as evidence insufficient (the gate says explore; stage C was not run). Against the improved Java decoder (21.7 ns instead of 280 ns on that load), the declared path stops on every load.
The spike on decodeSignedB64 therefore ends here: no stage C, and the wrapper question stays deferred. The other candidates keep the outcomes in the table.
The conclusion is unfavourable to the measured JNI path, on the measured loads, with respect to the 2x objective; it does not extend to every fine-grained call or to every native implementation. Nor does it show that Rust is without value for codecs in general.
The conclusion is compatible with the boundary principle of the refactor/rust-rewrite-design branch: explicit RPC or a C ABI, avoiding fine-grained cross-language calls (principle 5 of its design document, linked in the next section). Coarse-grained or batched boundaries were not measured here; a later candidate of that kind would need its own evidence, against the #3223 baselines.
Relation to refactor/rust-rewrite-design
The upstream branch refactor/rust-rewrite-design (apache/hugegraph, head a92da16a3 of 2026-09-13) is earlier work on the same roadmap. It covers PD and storage; this RFC does not.
What it proposes (review drafts, in Chinese):
a phased rewrite in which Rust services coexist with Java behind gRPC (docs/rust-rewrite-design.md, sections 3 and 5)
the boundary principle "explicit RPC or C ABI, avoiding fine-grained cross-language calls" (principle 5 in section 2, line 21)
a phase 0 of baselines and contracts, with golden samples generated by the Java version that a Rust version must pass (section 8.1)
data semantics compared through normalized comparators rather than internal bytes (section 8.2)
Where the two meet: on the measured path and loads, this RFC's measurements are compatible with principle 5. The byte-exact corpus of #3146 and the branch's normalized comparison answer different questions: persisted encoded keys need exact bytes; external semantics can be compared after normalization.
3. Boundary
Each candidate faced three options:
Improved Java. Argued first; measured when it decides the recommendation, as it did for decodeSignedB64 (section 2). For IntSet/IntMap it is argued only (JEP 471).
In-process JNI. The declared path: the jni-rs 0.22.4 with_env wrapper around raw JNI calls on stack buffers (rfc/thresholds.md). The wrapper catches Rust panics that unwind and turns them into Java exceptions. spike/rust/src/lib.rs states the JNI obligations of each call; section 4 lists the checks. FFM is final only from JDK 22 (JEP 454), so it needs an explicit baseline decision; it was not measured.
Out-of-process. Excluded from the initial perimeter of scalar operations as a scope choice, not by measurement.
A crossing through the declared path with no JNI call inside the closure (passLong) costs 59 ns (57.9 to 60.1), against a harness baseline of 3.6 ns (results/20260923T134724Z-stage-b). That cost belongs to the measured path; it is not attributed to JNI or to the panic capture. It sets the verdicts for the scalar operations, whose budgets (half the Java cost) are about 2 to 13 ns.
Removing with_env would not force panic = "abort": a directly written boundary could keep catch_unwind with panic = "unwind". Its exception handling, references, and error paths would then need their own design and checks, and nothing here shows it would be faster or as robust.
If a future candidate is recommended, its shape would be:
The existing Java class keeps its interface.
A Java implementation and a native one sit behind an internal seam chosen at class initialisation.
The Java implementation is the fallback for availability and compatibility, not for recovery from a fatal native failure.
The native library loads where HugeSecurityManager allows it; it refuses loads from Gremlin workers (hugegraph-server/hugegraph-core/.../security/HugeSecurityManager.java:144-149). Loading at startup is one option.
Corpus: the test(commons): add golden fixture corpus for LongEncoding #3146 corpus is an oracle of recorded outcomes; it records values and exception classes, not messages. On its 740 decodeSignedB64 cases the improved decoder gives the recorded values and exception classes.
Differential test: a separate comparison with the original decoder on 20,020 generated cases, including invalid input, overflow, null, and surrogates. Values, exception classes, and messages are the same, except the NullPointerException message, which the check does not compare. Both checks cover the verified cases, not every possible input.
Measurement guards: each run refuses uncommitted changes, rebuilds from the recorded revision, checks the discovered benchmarks against the mapping and the loaded code against the recorded build, and records its commits (rfc/appendix-method.md).
Boundary checks: deterministic results, null references, a panic, and an out-of-range array each give the expected Java outcome, with and without -Xcheck:jni, which reported nothing. The out-of-memory branches (a NULL result from NewStringUTF or NewByteArray) were not exercised.
Criteria for a future proof of concept, not verified here:
concurrency and resource release for stateful candidates
fallback when the native library is absent or of the wrong version
observability of which implementation runs
upgrade and rollback where a persisted format is involved
5. CI, packaging, security
No candidate is recommended, so this section sizes nothing. Constraints for any later candidate:
The engineering standard on refactor/rust-rewrite-design (docs/rust-engineering-standard.md) already lists toolchain pinning, clippy -D warnings, cargo audit, cargo deny, an unsafe policy, and no panic across FFI.
6. Risks, limits, open questions
Limits of the evidence:
One laptop (Intel Core Ultra 7 155H, powersave governor, no CPU pinning), one JDK (Temurin 11.0.30), one Rust toolchain (1.85.0); aarch64 not measured.
Stage A and stage B ran in different campaigns, and other work ran on the machine during stage A (rfc/appendix-method.md).
The borderline decodeSignedB64 large case changed verdict between two campaigns: explore in the first, stop in the second.
How the loads relate to production: on the index read path, decodeSignedB64 decodes the index label id, which is a schema id. User schema ids need at most 6 characters in this encoding. The other ofStoredString(..., LONG) callers are reached only through TableSerializer.readId, and no concrete TableSerializer exists at 83ef9f3fa. Section 6 of rfc/RFC.md gives the full chain. This bounds the length, not the distribution of lengths in production. The mixed load covers the full long range, the large load the 1000 values below Long.MAX_VALUE, and the small load values from 0 to 999 (1 to 2 characters). On the small load the improved Java costs 9.5 ns instead of 25.2 ns; that saving is measured on that load and not established per index row in production.
Mentor and Rust reviewers: to be defined. This RFC names no one as responsible.
Is the improved decodeSignedB64 wanted as a small optimization? A checked prototype exists: ImprovedLongEncoding, a class in hugegraph-test with a copy of the alphabet. This RFC claims no application-level benefit. The prefix conversion has a different motivation and would be evaluated separately.
Is a Jieba token-equality run worth doing?
BREAKING CHANGE(server): upgrade Java17 + TP3.7 + Groovy4 #3117 timing: when the Java baseline moves, should the Java measurements be repeated before the maintainers decide? A raw extern "system" no-op control without with_env would also show whether the validB64Char verdicts on the late and absent loads depend on the wrapper. That control is deferred (section 3).
How do the maintainers want to bring this proposal to discussion and decision on the public mailing list?
2026-09-24: section 2 defines its terms before the table, and the text is reorganised into shorter sentences and lists. Verification chains now link to the evidence repository instead of being repeated here. The evidence links point to commit b094369. Since publication, the evidence repository marks rfc/RFC.md as the text first published here, corrects the load-average note in the appendix, states the stage A window in UTC, and removes repeated tables from rfc/matrix.md. No number, source or conclusion changed.
Status: draft for review. Parent: #3110. Groundwork: #3145, #3146. Snapshot: apache/hugegraph
83ef9f3fa.b094369. Its README explains how to reproduce the runs. Paths below in itsrfc,spikeandresultsdirectories link to it.benchmark/rfcon the branchprototype/rfc-spikeof dpol1/hugegraph (commit1c62d1fe7), not proposed for merge.83ef9f3fa.1. Objectives, scope, non-goals
Question: which Server-core candidate offers a verifiable benefit large enough to justify the interoperability, compatibility, and maintenance cost of a Rust implementation?
Latency criterion, committed before any measurement (
rfc/thresholds.md): T_native / T_Java <= 0.5. T_native covers the crossing, conversions, copies, the operation, and building the result.Scope: the candidates of the #3145 inventory (
LongEncoding,NumericUtil, bothBytesBuffercopies,IntSet/IntMap,IntMapByDynamicHash) and one non-codec candidate, the Jieba analyzer. Server core is the perimeter this RFC proposes; the maintainers have not chosen it.Non-goals:
apache/hugegraphBytesBufferbefore Track HStore core-test exclusions and property codec mismatch #3090 is decided2. Candidates and decision
All numbers are JMH average times per call on one Linux x86_64 laptop (section 6). The table uses these terms:
with_envwrapper around raw JNI calls on stack buffers (section 3).smalldraws values from 0 to 999,mixedfrom the fulllongrange, andlargefrom the 1000 values belowLong.MAX_VALUE. ForvalidB64Charthe character is early in the alphabet, late in it, or absent.rfc/thresholds.md). Otherwise the verdict is explore, which means only that the rule does not allow a stop. A stop applies to the declared path and that load, not to every possible native implementation.LongEncodingencoders (encodeNumber,encodeSortable,encodeSignedB64)rfc/stage-c-gate.md)LongEncoding.decodeSignedB64rfc/stage-c-gate-d1.md)LongEncoding.validB64Charrfc/stage-c-gate.md)NumericUtil(numberToSortableLong,numberToSortableBytes,sortableBytesToNumber)rfc/stage-c-gate.md)BytesBuffer(core and struct)ScanPartitionservice decodes server-written rows with the struct copy and passes no schema, while the core property format relies on schema metadata.rfc/matrix.mdgives the call chain. Vermeer loads graphs through this service. apache/hugegraph-computer#361 reportsCan't construct Cardinality from code 0in the structreadPropertythere, an error consistent with the mismatch; it was not reproduced here, and the report lists Store and PD images taggedlatestIntSet/IntMapsun.misc.Unsafe) and memory, both outside the latency criterion. JEP 471 (https://openjdk.org/jeps/471) maps every member they use toVarHandle; that Java path was not measured hereIntMapByDynamicHash83ef9f3fa(rfc/matrix.md)rfc/matrix.md)Recommendation: on the measured loads, the Java optimization makes it unjustified to continue the studied JNI path with respect to the 2x objective. Every mapped operation and load stops on this path, with two exceptions:
numberToSortableLong(Long)on the small load, whose cost is not distinguishable from the harness.decodeSignedB64on the mixed load, which ends as evidence insufficient (the gate says explore; stage C was not run). Against the improved Java decoder (21.7 ns instead of 280 ns on that load), the declared path stops on every load.The spike on
decodeSignedB64therefore ends here: no stage C, and the wrapper question stays deferred. The other candidates keep the outcomes in the table.The conclusion is unfavourable to the measured JNI path, on the measured loads, with respect to the 2x objective; it does not extend to every fine-grained call or to every native implementation. Nor does it show that Rust is without value for codecs in general.
The conclusion is compatible with the boundary principle of the
refactor/rust-rewrite-designbranch: explicit RPC or a C ABI, avoiding fine-grained cross-language calls (principle 5 of its design document, linked in the next section). Coarse-grained or batched boundaries were not measured here; a later candidate of that kind would need its own evidence, against the #3223 baselines.Relation to
refactor/rust-rewrite-designThe upstream branch
refactor/rust-rewrite-design(apache/hugegraph, heada92da16a3of 2026-09-13) is earlier work on the same roadmap. It covers PD and storage; this RFC does not.docs/rust-rewrite-design.md, sections 3 and 5)docs/rust-engineering-standard.md)tools/rust-partition-poc)tools/rust-store-reader)tools/rust-rewrite-cluster)tools/raft-linearizability)Where the two meet: on the measured path and loads, this RFC's measurements are compatible with principle 5. The byte-exact corpus of #3146 and the branch's normalized comparison answer different questions: persisted encoded keys need exact bytes; external semantics can be compared after normalization.
3. Boundary
Each candidate faced three options:
decodeSignedB64(section 2). ForIntSet/IntMapit is argued only (JEP 471).with_envwrapper around raw JNI calls on stack buffers (rfc/thresholds.md). The wrapper catches Rust panics that unwind and turns them into Java exceptions.spike/rust/src/lib.rsstates the JNI obligations of each call; section 4 lists the checks. FFM is final only from JDK 22 (JEP 454), so it needs an explicit baseline decision; it was not measured.A crossing through the declared path with no JNI call inside the closure (
passLong) costs 59 ns (57.9 to 60.1), against a harness baseline of 3.6 ns (results/20260923T134724Z-stage-b). That cost belongs to the measured path; it is not attributed to JNI or to the panic capture. It sets the verdicts for the scalar operations, whose budgets (half the Java cost) are about 2 to 13 ns.Removing
with_envwould not forcepanic = "abort": a directly written boundary could keepcatch_unwindwithpanic = "unwind". Its exception handling, references, and error paths would then need their own design and checks, and nothing here shows it would be faster or as robust.If a future candidate is recommended, its shape would be:
HugeSecurityManagerallows it; it refuses loads from Gremlin workers (hugegraph-server/hugegraph-core/.../security/HugeSecurityManager.java:144-149). Loading at startup is one option.Unpaired surrogates: on the declared read path (
GetStringRegion), UTF-16 units arrive unchanged (results/20260923T134724Z-stage-b/check.txt).4. Validation
Verified by the spike (
spike/VALIDATION.md,results/):decodeSignedB64cases the improved decoder gives the recorded values and exception classes.rfc/appendix-method.md).-Xcheck:jni, which reported nothing. The out-of-memory branches (a NULL result fromNewStringUTForNewByteArray) were not exercised.Criteria for a future proof of concept, not verified here:
5. CI, packaging, security
No candidate is recommended, so this section sizes nothing. Constraints for any later candidate:
approved_patterns.ymlof https://github.com/apache/infrastructure-actions.LICENSE/NOTICEmatch its contents.--enable-native-accessis documented (JEP 472, JDK 24, https://openjdk.org/jeps/472).The engineering standard on
refactor/rust-rewrite-design(docs/rust-engineering-standard.md) already lists toolchain pinning,clippy -D warnings,cargo audit,cargo deny, anunsafepolicy, and no panic across FFI.6. Risks, limits, open questions
Limits of the evidence:
powersavegovernor, no CPU pinning), one JDK (Temurin 11.0.30), one Rust toolchain (1.85.0); aarch64 not measured.rfc/appendix-method.md).decodeSignedB64large case changed verdict between two campaigns:explorein the first,stopin the second.How the loads relate to production: on the index read path,
decodeSignedB64decodes the index label id, which is a schema id. User schema ids need at most 6 characters in this encoding. The otherofStoredString(..., LONG)callers are reached only throughTableSerializer.readId, and no concreteTableSerializerexists at83ef9f3fa. Section 6 ofrfc/RFC.mdgives the full chain. This bounds the length, not the distribution of lengths in production. Themixedload covers the fulllongrange, thelargeload the 1000 values belowLong.MAX_VALUE, and thesmallload values from 0 to 999 (1 to 2 characters). On the small load the improved Java costs 9.5 ns instead of 25.2 ns; that saving is measured on that load and not established per index row in production.Java-side findings, not Rust questions (
rfc/java-findings.md):CollectionFactoryalways returns the concurrent segmentedIntSet/IntMap.Open questions for the maintainers:
refactor/rust-rewrite-designand the toolchain priority in [Roadmap] Incremental Rust modernization for HugeGraph Toolchain hugegraph-toolchain#748?decodeSignedB64wanted as a small optimization? A checked prototype exists:ImprovedLongEncoding, a class inhugegraph-testwith a copy of the alphabet. This RFC claims no application-level benefit. The prefix conversion has a different motivation and would be evaluated separately.extern "system"no-op control withoutwith_envwould also show whether thevalidB64Charverdicts on the late and absent loads depend on the wrapper. That control is deferred (section 3).Guardrails from #3110
BytesBufferrows,LongEncoding/NumericUtiloutputs inside index keys, analyzer tokens) not changedrfc/thresholds.md; this document andrfc/appendix-method.mdChanges since publication
b094369. Since publication, the evidence repository marksrfc/RFC.mdas the text first published here, corrects the load-average note in the appendix, states the stage A window in UTC, and removes repeated tables fromrfc/matrix.md. No number, source or conclusion changed.