Repository navigation
JVM read path: fewer crossings per call (get_raw, one, count, lookup_by_key, transaction mark) - #214
Conversation
|
Timed A/B of this PR under concurrency on the JVM wheel (laptop timed lane, two timed cores,
Clean pairs per figure: 10. By the rule written before the run (gating upper end below 0.98, the 1-thread guard not clearly worse, the control median inside [0.97, 1.03], a clean A/A) this is a SHIP: the mixed window costs 0.489 [0.475, 0.499] of main's time at 2 threads, the gain is in the reader's share (0.476 [0.461, 0.487]), and the control stays inside its band. The single-thread study of the same head (TA-3) ended WEAK at its round cap, because its transaction-commit figure could not narrow enough on a busy laptop, so it reports nothing; the claim above rests on this study alone. The PR leaves draft at the end of the current batch of timed studies that pin main, with its docs, and merges on green. I can re-run either study on the branch after any change. |
ab15053 to
e979d3d
Compare
e979d3d to
789ac47
Compare
…has() then get() get() answers null for a property the record does not have, as it does for one set to null; Python reads None either way, so the has() call only repeated the scan. has_property() still tells the two apart. test_read_crossings.py counts the calls into the JVM on a proxy of the Java object and pins that the answers are the plain path's (absent, null-valued, mutable, vertex and edge). It is JVM-only (NATIVE_NOT_APPLICABLE); the native backend is unchanged. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…st() one() read hasNext()/next() per row and closed afterwards; count() crossed twice per row. RowAccess.upToTwoAndClose and countAndClose do the same work Java-side with the same errors and the same result-set state; the plain path stays for a missing bridge class. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…no Java arrays DbCalls.lookupFirstKey takes one key as scalars and lookupFirstKeys takes any number as the varargs of one call, so neither side builds a String[] and an Object[] through JPype per lookup. The engine receives the same arrays, and a different number of names and values reaches it as before. lookupFirst, which takes the arrays, stays. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The mark taken before every begin() (with db.transaction(), run_in_transaction) asked isTransactionActive() and getNestedTransactions() separately. DbCalls.stackMark returns both as one number, so with db.transaction() goes from 4 crossings to 3 and run_in_transaction from 6 to 5. The mark is still taken before begin(), so the interrupt and refused-begin rules of #9970 and #9813 are unchanged. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ndow at 2 Python threads) The study that adopts this PR is a mixed read/write window at 2 Python threads on the arcadedb-jvm package. The page gets the gating figure with its 95% interval, the 1-thread guard, the reader and writer shares and the Cypher control, all generated from the study's ab-score.md by script, and says what was not measured (4 or more threads, native, other hardware). The one-thread row is stated plainly: the gain is not specific to concurrency. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e skip list #214 and #233 each add one NATIVE_NOT_APPLICABLE entry at the same place, so merging one made the other conflict and cost a full CI run per rebase. Both branches now carry the same two entries in the same order; an entry for a test file that is not on this branch is only a collect_ignore name and changes nothing here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
789ac47 to
93df706
Compare
…e skip list #214 and #233 each add one NATIVE_NOT_APPLICABLE entry at the same place, so merging one made the other conflict and cost a full CI run per rebase. Both branches now carry the same two entries in the same order; an entry for a test file that is not on this branch is only a collect_ignore name and changes nothing here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…et iteration in one crossing each (#233) * JVM readers: a record or row to dict, the property names and result-set iteration in one crossing each (#227) Document.to_dict took 18 reads of Java objects for 3 properties, Result.to_dict 14, a property-names call 12, and each row of a result set two calls (hasNext, next). The new bridge class ReaderCalls writes a record or a row as the typed JSON of TypedRows (one call, one string read, the C json decoder), returns the names as one JSON text, and hands over the next row in one call. Values keep the types convert_java_to_python gives them; the side array is read only when the JSON refers to it; a result set is still read one row per call. Without the class the old paths run. Document.to_dict now also refreshes the cached names, so a property set after an earlier read is no longer missing from get_property_names(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Docs: the timed figures of the JVM reader crossings (to_dict and result iteration) The study that adopts this PR timed record to dict at 3 and 12 properties and iteration into dicts at 100 and 10,000 rows on the arcadedb-jvm package. The Results section gets those four gating figures with their 95% intervals, the unit (time per record or row), the control that the change does not touch, and the scope (JVM build, these workloads, one laptop). The numbers are generated from the study's ab-score.md by script. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * tests: list the read- and reader-crossing tests together in the native skip list #214 and #233 each add one NATIVE_NOT_APPLICABLE entry at the same place, so merging one made the other conflict and cost a full CI run per rebase. Both branches now carry the same two entries in the same order; an entry for a test file that is not on this branch is only a collect_ignore name and changes nothing here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Closes #213
The JVM wheel crossed into the JVM several times per read. A JPype call is a few microseconds of dispatch, and an untimed async-profiler pass put 10.7% of a warm point-read workload in JNI/libjvm and 5.2% in JPype C++, besides 5.4% of Java glue. This PR cuts the number of crossings of five common calls, each as its own commit, with the answer, the errors and the result-set state unchanged. It is a port of humemai/arcadedb-embedded-python#524 onto
main(the layout moved frombindings/python/src/arcadedb_embeddedtosrc/arcadedb; the Java and the Python hunks are otherwise the ones of that branch, re-applied and re-checked against the current code).What changes (crossings per call, counted by the new tests on a proxy of the Java object and the bridge classes)
Document.get_raw()(soget(),Vertex.get(),Edge.get()): 2 crossings and 2 scans of the record (has()thenget()) become 1 crossing and 1 scan.get()answers null for a property the record does not have, as it does for one set to null, and Python reads None either way;has_property()still tells the two apart.ResultSet.one():RowAccess.upToTwoAndClosereturns the first two rows and closes the set in one call, where the loop tookhasNext()/next()per row plus the close. Zero rows, several rows, an error while reading and a closed database raise the same exceptions and leave the same_closed/_exhaustedstate as the plain path, which stays for a missing bridge class.ResultSet.count():RowAccess.countAndClosecounts the rows left and drains and closes the set in one call, where the loop crossed twice per row. A statement error surfaces as before.Database.lookup_by_key(): it was already one crossing, but built aString[]and anObject[]through JPype first.DbCalls.lookupFirstKeytakes one key as scalars andlookupFirstKeystakes any number as the varargs of one call (names then values, so a different number of names and values reaches the engine as before).lookupFirst, which takes the arrays, stays.begin():DbCalls.stackMarkreturns active and nesting depth as one number, sowith db.transaction()goes from 4 crossings to 3 andrun_in_transaction()from 6 to 5. The mark is still taken beforebegin(), so the interrupt and refused-begin rules of #9970, #9813 and #7916 are unchanged.The native backend is not touched: nothing under
src/arcadedb/_nativeornative/changes, andtests/test_read_crossings.pyis listed inNATIVE_NOT_APPLICABLEbecause it counts JPype crossings.Tests
tests/test_read_crossings.py(21 tests, added with the commit each belongs to): crossing counts plus equivalence with the plain path, including typed and composite keys, error cases and the engine's own stack numbers. The counts fail onmainand the equivalence tests pass on both. Run in CI against the wheel built from this branch (the bridge jar is rebuilt there); nothing was run on the author's laptop, so CI is the first run of this port.What was measured A pre-registered timed A/B of this branch's JVM wheel against
main's under concurrency (A/A control, rule written before the run) shipped by its rule: figures, intervals and scope in this comment and in the new docs section. The earlier timed A/B without threads ended with too few clean pairs for a verdict, so nothing from it is reported.Docs:
how-to/concurrency.adochas a section with the concurrency figures of the timed study (mixed window at 2 Python threads onarcadedb-jvm, with the 1-thread row, the reader and writer shares, the control and what was not measured), added in ab15053.🤖 Generated with Claude Code
Edited 2026-11-05: the status line now gives the timed result; the PR is ready for review.