Skip to content

Goal: high-rate ingest and large reads in both wheels: measures, audit results and the work list #315

Open

Description

@tae898

A goal for the Python bindings: bulk ingest and large reads should be fast and use little memory in both wheels, with the standard interfaces where they exist (Arrow C Data Interface, ADBC). This issue holds the measures and links the work.

Measures, each per wheel and warm: rows per second and CPU per row for insert_many, insert_columns, insert_arrow, insert_dataframe, graph batch and time-series ingest; seconds and peak memory per phase for large reads to Arrow and dataframes; the one-row call cost. Method: ab.py ABBA with an A/A control, a warmed process, the same SQL text for every call, four cores, per-phase memory from the cgroup, and the interval rule fixed before the run (a study whose intervals are wider than the rule is rerun and not reported).

Work, by issue: #312 (native large reads have no columnar read path), #313 (JVM cold start and idle memory), #314 (the two wheels return different floats from to_json_list), and the ingest findings below. Not to be redone: the closed issues listed in each of them.

Order: the cheap bindings-only changes first (a wheels-only 26.11.1.post1 is acceptable), then the changes that need a native image rebuild. Each change is measured against the released 26.11.1 with the same method before it is claimed.

Activity

  1. added
    pythonPull requests that update python code
    timeseriesTime-series ingest and queries
    on Nov 9, 2026
  2. added this to the 26.12.1 milestone on Nov 9, 2026
  3. tae898 commented on Nov 9, 2026

    @tae898
    CollaboratorAuthor

    Ranked work from the best-practice pass (the table of our paths against the practice has 18 rows, and every item below carries a measure and uses the method of this issue). In order: native Python-side read assembly (Python only, half a day to a day); native columnar read frames (3 to 5 days plus image rebuilds, with a decoder shared with the JVM wheel); reply handling without text copies; a batched Arrow reader and __arrow_c_stream__ (1 to 2 days, Python only); closing the native ingest fallbacks (temporal, bytes and Decimal kinds, vertex types) which needs an image rebuild; then the JVM string blob and offsets, a commit-size sweep, and a PEP 249 shim.

    Three decisions that are the owner's, not mine. First, whether the native library exports the Arrow C Stream interface itself (hand-written structs and a release callback under native-image, an ABI commitment, no speed reason) or only a Python-level __arrow_c_stream__; I recommend the Python-level one now. Second, whether to ship an ADBC driver, which is cheap only after the first option and carries a support commitment; the alternative is the PEP 249 shim plus __arrow_c_stream__, which covers most users. Third, whether to keep the JSON read path for the row APIs through post1 and add the columnar frames beside it; I recommend keeping it. A smaller point only if the commit-size sweep shows a clear curve: the native insert_many default of 1000 rows per commit is a failure-atomicity decision, not a tuning change.

  4. tae898 commented on Nov 9, 2026

    @tae898
    CollaboratorAuthor

    The owner answered the third decision above: no legacy burden. So the JSON read path is retired once the columnar frames exist, not kept beside them: one transport, and the row APIs build their dicts from typed buffers. That also forces one choice for the float32 rendering of #314, which I read as the exact float64 widening on both wheels. Dead and fallback paths go (for example NativeGraphBatch._create_vertices_json_bulk has no caller). Placement: the behaviour-neutral and parity changes can go in a wheels-only 26.11.1.post1; the redesign (single columnar transport, JSON read path retired, __arrow_c_stream__) goes into the next regular release, with a changelog entry that says what changes for users.

  5. tae898 commented on Nov 9, 2026

    @tae898
    CollaboratorAuthor

    Design principle for the redesign, so the two wheels do not drift: the interface and the format are shared, the plumbing is per wheel. Shared: the public API and types, one value semantics (including the float rule of #314), one frame format (typed column buffers laid out like Arrow), one Python decoder that wraps a buffer (np.frombuffer, pyarrow.foreign_buffer), and the golden parity test that runs on both wheels. Per wheel: the transport (the native wheel calls the C library through ctypes with buffers in native memory, the JVM wheel crosses through JPype with Java arrays or direct byte buffers), the crossing cost model (cheap on native, expensive through JPype, so the JVM side batches more), ownership and freeing (a release step on native, the garbage collector on the JVM), and the memory and start-up tuning (GC and heap flags on native; the heap fraction, a CDS archive and the AOT cache on the JVM).

    Release placement: the small, clearly better cuts (the float semantics, dead code) can go into 26.11.1.post1, which is the version the affected paper cells are rerun on (the four time-series cells that read large to_arrow results); the redesign goes into a 26.11.1.post2 or the next regular release, so the paper's rows do not wait for it. Every cut still needs a measured win under the method of this issue and a changelog line.

  6. tae898 commented on Nov 9, 2026

    @tae898
    CollaboratorAuthor

    Memory is a first-class measure of this work, not an afterthought, for three reasons. The benchmark's memory columns for the embedded cells are the cell's owned memory (anonymous plus shared), and that includes the Python process and the glue, so a lighter glue lowers a published number and a heavier one raises it. The big costs the audits found are memory costs too: a million Python dicts for one to_arrow read, rows rebuilt from columns on ingest, and a reply copied three times. And the two wheels start from different baselines (the JVM wheel's idle memory is a constant 54 to 66 MiB higher, and its default heap is half of the machine's RAM).

    Requirements for every glue change, next to the speed measure: report the peak and the steady resident memory, anonymous and file-backed separately, and the bytes copied per call; take the memory per phase from the cgroup inside the cell, not from the process high-water mark (the first study of this work used a last-cell high-water mark and that figure was withdrawn); bound the working set by design (batches of about 1 MB, a reader that yields batches so a read does not materialise the whole table, no intermediate row form for columnar paths); and accept a change that raises peak memory only with the reason written in the pull request. For the native wheel the young-generation sizing of the Serial GC is part of this (the eden issue I reported as oracle/graal#14758), and for the JVM wheel the heap fraction and a CDS archive are.

  7. self-assigned this
    on Nov 9, 2026
  8. tae898 commented on Nov 13, 2026

    @tae898
    CollaboratorAuthor

    Scope update: I am widening this goal from ingest and large reads to every create, read, update and delete path of both wheels, with the same method for each (a timed A/B with an A/A control, the interval rule fixed before the run, no claim from a run that ends weak).

    What I found while taking stock today:

    Order of work:

    1. Measure the post2 pair first: all 15 read paths, and the one-row create, update, lookup, select and delete paths, native against JVM. Issues for any gap that persists on post2 come after that, with generated tables.
    2. Fix pull requests as drafts, each with a test that fails on the old code. Two JVM ones are in preparation: the row readers skip the JSON hook when nothing in the batch is tagged, and timestamp and date columns cross to Java in bulk.
    3. 26.11.1.post3 is cut from a pinned candidate on a release branch, never from a moving main. Every study compares the released post2 wheel (A) against the post3 candidate (B) and post2 again (Z), native against native and JVM against JVM. It ships when the touched figures show no regression and the improvement it claims.

    I will update this issue as each study finishes.

  9. tae898 commented on Nov 13, 2026

    @tae898
    CollaboratorAuthor

    The first timed A/B of the post3 candidate against the released 26.11.1.post2 is finished. A is the released post2 wheel, B is the candidate (post2 plus the draft PRs #382, #383, #384, #385 and #386, built locally in Docker from one integration commit, engine unchanged), and Z is post2 again as the A/A control. Native is compared with native and JVM with JVM. It ran on one laptop (cores 6 and 7), ABBA rounds, one fresh process per run, rounds 0 to 20 with round 0 dropped. The ratio is B/A of the time per operation, so below 1 means the candidate is faster. The decoded and stored values of every figure were compared between the arms: no parity failure.

    Figures that pass the study's rule (at least 10 clean pairs, an interval no wider than 0.10 times the median, a clean A/A control), all named improved:

    PR figure B/A, 95% interval clean pairs verdict
    #382 JVM iter_dicts, 1 row 0.734 [0.714, 0.755] 20 improved
    #382 JVM iter_dicts, 100 rows 0.740 [0.730, 0.760] 20 improved
    #382 JVM iter_dicts, 100,000 rows 0.745 [0.740, 0.760] 20 improved
    #383 JVM insert_arrow, timestamp and date columns 0.308 [0.291, 0.314] 20 improved
    #383 JVM insert_dataframe, timestamp and date columns 0.288 [0.279, 0.293] 20 improved
    #384 native with db.transaction(), empty body 0.376 [0.372, 0.381] 20 improved
    #384 native with db.transaction() with one INSERT 0.874 [0.872, 0.877] 20 improved
    #384, #385 native one-row set and save() inside an explicit transaction, auto-transaction off (the default) 0.957 [0.951, 0.958] 20 improved

    No change: JVM insert_columns (1.002 [0.982, 1.026]) and native insert_columns of a vector column in chunks, the figure for #386 (1.000 [0.991, 1.008]). Not reported because the interval is still too wide at the round cap: the JVM to_list and typed-to_list figures at 1 and 100 rows, and the JVM to_list at 100,000 rows is unresolved. So I claim nothing for to_list from #382, only for iter_dicts, and the no-regression line of the study is not yet certified for those figures. The control figure of the native wheel (a point lookup that none of the PRs touch) moved by 1.007 [1.004, 1.012], which the study reports as drift and which voids nothing.

    What I do with it: #386 shows no measurable gain, and its change adds an integer to every native command, so I leave it out of post3. The other four go into the candidate. The release gate repeats this study on the wheels that CI builds from the tagged commit, with a higher round cap because the small-result to_list figures are noisy per process.

    Edited 2026-11-14: the one-row update figure ran inside explicit transactions with auto-transaction off, the default, and not with auto-transaction on as this text first said. It therefore does not time the path that #385 changes (#385 only changes save() with auto-transaction on, see its own crossing table); the figure measures the post3 build on the default path, and its gain may come from #384. A timed figure for the auto-transaction-on path is still missing.

  10. tae898 commented on Nov 14, 2026

    @tae898
    CollaboratorAuthor

    The release gate of 26.11.1.post3 is finished. It is the same study as before (A is the released 26.11.1.post2 wheel, B is the candidate, Z is post2 again as the A/A control, native against native and JVM against JVM, one laptop on cores 6 and 7, ABBA rounds, round 0 dropped), now on the wheels that CI built from the release-branch commit 55b427a (Test JVM run 43520459781, Test native run 43520464492), not on my local build. 504 runs were kept, rounds 0 to 20. The decoded and stored values of every figure matched between the arms: no parity failure. The engine of both candidate wheels is byte-identical to the one of 26.11.1 (JVM: 70 of 70 engine jars; both wheels: build number a3a5d124572608412aa187f9c4b290e071699473).

    Eight figures pass the rule of the study (at least 10 clean pairs, an interval no wider than 0.10 times the median, a clean A/A control) and are named improved. The ratio is B/A of the time per operation, so below 1 means the candidate is faster:

    PR figure B/A, 95% interval clean pairs verdict
    #382 JVM iter_dicts, 1 row 0.749 [0.735, 0.777] 20 improved
    #382 JVM iter_dicts, 100 rows 0.763 [0.733, 0.782] 20 improved
    #382 JVM iter_dicts, 100,000 rows 0.755 [0.744, 0.770] 20 improved
    #383 JVM insert_arrow, timestamp and date columns 0.295 [0.283, 0.311] 20 improved
    #383 JVM insert_dataframe, timestamp and date columns 0.283 [0.278, 0.290] 20 improved
    #384 native with db.transaction(), empty body 0.370 [0.368, 0.374] 20 improved
    #384 native with db.transaction() with one INSERT 0.875 [0.873, 0.878] 20 improved
    #384, #385 native one-row set and save() inside an explicit transaction, auto-transaction off (the default) 0.932 [0.928, 0.935] 20 improved

    No figure is a regression or slower: the summary line reads IMPROVED 8, FASTER 1, SAME 3, UNRESOLVED 5, REGRESSION 0, SLOWER 0, PARITY-FAIL 0, WEAK 4. JVM insert_columns (1.009 [0.983, 1.050]) and the native vector insert_columns (0.996 [0.990, 1.010]) show no change. The JVM to_list and typed-to_list figures at 1 and 100 rows are still too noisy at the round cap, so I report nothing for them and claim nothing for to_list. The native control (a point lookup no PR touches) moved by 0.992 [0.989, 0.995], which the study reports as drift and which voids nothing.

    On memory I claim no improvement. The peak resident size is only an information figure in this study. Its B over A ratio is 0.933 [0.871, 0.968] for iter_dicts at 100,000 rows and 0.918 [0.917, 0.919] for the empty native transaction. For JVM insert_arrow it is 1.066 [1.014, 1.194] and for insert_dataframe it is 1.202 [1.059, 1.299], so the faster timestamp path holds more memory at peak, which I have not looked into yet and will not call a gain.

    26.11.1.post3 is tagged on 55b427a and is on PyPI for both packages (arcadedb and arcadedb-jvm, published 2026-11-14). I ran the engine identity check on the published linux x86_64 wheels of both: the engine part is byte-identical to 26.11.1 (70 of 70 engine jars on the JVM wheel, build number a3a5d124572608412aa187f9c4b290e071699473 on both). #386 stays out of it.

    Edited 2026-11-14: the one-row update figure ran inside explicit transactions with auto-transaction off, the default, and not with auto-transaction on as this text first said. It therefore does not time the path that #385 changes (#385 only changes save() with auto-transaction on, see its own crossing table); the figure measures the post3 build on the default path, and its gain may come from #384. A timed figure for the auto-transaction-on path is still missing.

  11. tae898 commented on Nov 14, 2026

    @tae898
    CollaboratorAuthor

    I corrected two of my comments above (the first A/B result and the release-gate result). The one-row update figure (set then save()) was labelled "auto-transaction on", but the study runs it inside explicit transactions with auto-transaction off, the default. So it does not time the path that #385 changes, and its gain may come from #384; I changed the label and added a note at the end of each comment. A timed figure for save() with auto-transaction on is still missing, and I make no speed claim for #385. The figures and intervals are unchanged.

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

Metadata

Metadata

Assignees

Labels

pythonPull requests that update python codetimeseriesTime-series ingest and queries

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions