Skip to content

Commit e8b517e

Browse files
committed
perf(storage): O(1) storage_tail append via newest-first batch list
conn.storage_tail now keeps per-transaction datom batches newest-first and conses each report's tx_data, replacing the O(#batches) list append on every transaction. The forward (tx, then in-tx) order is restored with List.rev only where the flat tail is consumed: store_tail writes, the Conn.storage_tail accessor, and restore seeding — so the persisted Storage_tail payload is order-identical to before. Storage.tail_datom_count folds group lengths instead of concat |> length, and persist_transact_tail (the db-level transact path) computes the compaction count before materializing groups @ [tx_data], so a compaction-triggering tx skips the append entirely. Regression coverage in test_storage: on-disk Storage_tail group order across transacts, restore_conn, and compaction; Conn.storage_tail accessor order; tail_datom_count over grouped tails.
1 parent 952c0be commit e8b517e

5 files changed

Lines changed: 93 additions & 11 deletions

File tree

‎docs/design.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,16 @@ Not every value near the DB should become an ordered persistent index.
159159
Moving these to PSS would not improve parity with upstream DataScript and would
160160
make serialization and equality behavior more complex.
161161

162+
## Storage Tail
163+
164+
`conn.storage_tail` holds one `datom list` batch per committed transaction,
165+
stored newest-first so appending a transaction's `tx_data` is O(1). The
166+
batches are flattened back to transaction order only where the flat tail is
167+
consumed: `store_tail` writes, `storage_tail_datom_count`, the compaction
168+
threshold check, and the `Conn.storage_tail` accessor. The persisted
169+
`Storage_tail` payload keeps upstream's transaction-order-then-in-transaction
170+
datom order.
171+
162172
## Persistent Sorted Set Dependency
163173

164174
The repo depends on `persistent_sorted_set_ocaml` through an opam pin to

‎impl/conn.ml‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ type t =
55
; mutable listeners : (string * (tx_report -> unit)) list
66
; mutable next_listener_id : int
77
; storage : storage option
8-
; mutable storage_tail : datom list list
8+
; (* per-transaction datom batches, newest first: consing each report's
9+
tx_data is O(1); reversed back to on-disk tx order only when the
10+
tail is written or read out *)
11+
mutable storage_tail : datom list list
912
}
1013

1114
type creation_context =
@@ -70,7 +73,7 @@ let make ?storage ?(storage_tail = []) db =
7073
| None -> db
7174
| Some _ -> { db with storage_ref = storage }
7275
in
73-
{ db; listeners = []; next_listener_id = 0; storage; storage_tail }
76+
{ db; listeners = []; next_listener_id = 0; storage; storage_tail = List.rev storage_tail }
7477

7578
let create (context : creation_context) ?schema ?storage () =
7679
let db = context.empty_db ?schema ?storage () in
@@ -97,7 +100,7 @@ let db conn = conn.db
97100
store/notify side effects, for bookkeeping fields like :max-tx. *)
98101
let update_db conn f = conn.db <- f conn.db
99102

100-
let storage_tail conn = conn.storage_tail
103+
let storage_tail conn = List.rev conn.storage_tail
101104

102105
let is_conn (_ : t) = true
103106

@@ -145,13 +148,13 @@ let transact (context : transact_context) ?(tx_meta = []) conn tx_data =
145148
| None -> ()
146149
| Some storage ->
147150
if report.tx_data <> [] then begin
148-
let tail = conn.storage_tail @ [ report.tx_data ] in
151+
let tail = report.tx_data :: conn.storage_tail in
149152
if context.storage_tail_datom_count tail > context.storage_tail_compaction_threshold report.db_after then begin
150153
conn.db <- context.store ~storage report.db_after;
151154
conn.storage_tail <- []
152155
end else begin
153156
conn.storage_tail <- tail;
154-
context.store_tail storage conn.storage_tail
157+
context.store_tail storage (List.rev conn.storage_tail)
155158
end
156159
end);
157160
notify_listeners conn report;
@@ -173,13 +176,13 @@ let apply_report (context : transact_context) conn (report : tx_report) =
173176
| None -> ()
174177
| Some storage ->
175178
if report.tx_data <> [] then begin
176-
let tail = conn.storage_tail @ [ report.tx_data ] in
179+
let tail = report.tx_data :: conn.storage_tail in
177180
if context.storage_tail_datom_count tail > context.storage_tail_compaction_threshold db_after then begin
178181
conn.db <- context.store ~storage db_after;
179182
conn.storage_tail <- []
180183
end else begin
181184
conn.storage_tail <- tail;
182-
context.store_tail storage conn.storage_tail
185+
context.store_tail storage (List.rev conn.storage_tail)
183186
end
184187
end);
185188
let report = { report with db_after } in

‎impl/datascript.ml‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -903,11 +903,14 @@ let persist_transact_tail ~tx_meta db tx_data =
903903
match db.storage_ref with
904904
| None -> db
905905
| Some storage ->
906-
let tail = restore_tail_groups storage @ [ tx_data ] in
907-
if storage_tail_datom_count tail > storage_tail_compaction_threshold db then
906+
let groups = restore_tail_groups storage in
907+
if
908+
storage_tail_datom_count groups + List.length tx_data
909+
> storage_tail_compaction_threshold db
910+
then
908911
store ~storage db
909912
else begin
910-
store_tail storage tail;
913+
store_tail storage (groups @ [ tx_data ]);
911914
db
912915
end
913916

‎impl/storage.ml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ let tail_compaction_threshold (db : db) =
280280
(PSet.settings db.eavt_index).branching_factor
281281

282282
let tail_datom_count tail =
283-
tail |> List.concat |> List.length
283+
List.fold_left (fun count group -> count + List.length group) 0 tail
284284

285285
let restore_root_snapshot storage =
286286
match storage.storage_restore root_address with

‎test/test_storage.ml‎

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,70 @@ let test_storage__test_conn () =
379379
(datoms restored_db Eavt ())
380380
| None -> failwith "conn_from_datoms should store attached datoms"
381381

382+
let tail_groups storage =
383+
match storage.storage_restore Storage.tail_address with
384+
| Some (Storage_tail groups) -> groups
385+
| _ -> failwith "storage tail address missing"
386+
387+
let flatten_tail_groups groups =
388+
List.map (fun group -> List.map (fun d -> d.e, d.a, d.v) group) groups
389+
390+
let test_storage__test_conn_tail_write_order () =
391+
let storage = memory_storage () in
392+
let conn = create_conn ~storage () in
393+
let assert_tail label conn expected =
394+
let stored = flatten_tail_groups (tail_groups storage) in
395+
if stored <> expected then
396+
failf "%s: stored tail order mismatch: %s" label
397+
(String.concat ";" (List.map (fun g -> string_of_int (List.length g)) stored));
398+
if flatten_tail_groups (Conn.storage_tail conn) <> expected then
399+
failwith (label ^ ": conn storage_tail accessor order mismatch")
400+
in
401+
ignore (transact_conn conn [ Add (Entity_id 1, "a", String "1") ]);
402+
ignore
403+
(transact_conn conn
404+
[ Add (Entity_id 2, "a", String "2"); Add (Entity_id 2, "b", String "3") ]);
405+
assert_tail "two txs" conn
406+
[ [ 1, "a", String "1" ]
407+
; [ 2, "a", String "2"; 2, "b", String "3" ]
408+
];
409+
(* a restored conn seeds its tail from disk and keeps appending in tx
410+
order; the on-disk order must survive the internal reversal *)
411+
let conn =
412+
match restore_conn storage with
413+
| Some conn -> conn
414+
| None -> failwith "restore_conn should return a conn"
415+
in
416+
ignore (transact_conn conn [ Add (Entity_id 3, "a", String "4") ]);
417+
assert_tail "restored conn" conn
418+
[ [ 1, "a", String "1" ]
419+
; [ 2, "a", String "2"; 2, "b", String "3" ]
420+
; [ 3, "a", String "4" ]
421+
];
422+
(* crossing the compaction threshold clears the stored tail *)
423+
ignore
424+
(transact_conn conn
425+
(List.init 40 (fun i -> Add (Entity_id (i + 10), "a", String (string_of_int i)))));
426+
(match tail_groups storage with
427+
| [] -> ()
428+
| _ -> failwith "compaction should clear the stored tail");
429+
ignore (transact_conn conn [ Add (Entity_id 99, "a", String "9") ]);
430+
assert_tail "post-compaction" conn [ [ 99, "a", String "9" ] ];
431+
assert_equal_triples
432+
"all datoms queryable after compaction"
433+
[ 1, "a", String "1"; 2, "a", String "2"; 2, "b", String "3"
434+
; 3, "a", String "4"; 99, "a", String "9" ]
435+
(datoms (conn_db conn) Eavt ~e:1 ()
436+
@ datoms (conn_db conn) Eavt ~e:2 ()
437+
@ datoms (conn_db conn) Eavt ~e:3 ()
438+
@ datoms (conn_db conn) Eavt ~e:99 ()
439+
|> List.sort compare)
440+
441+
let test_storage__test_tail_datom_count () =
442+
let d tx e = datom ~tx ~e ~a:"a" ~v:(String "v") () in
443+
assert_equal_int "tail_datom_count sums group sizes" 4
444+
(Storage.tail_datom_count [ [ d 1 1 ]; [ d 2 2; d 2 3; d 2 4 ]; [] ])
445+
382446
let test_storage__test_db_with_tail () =
383447
let db =
384448
empty_db ~schema:[ "block/updated-at", indexed; "block/uuid", unique_identity ] ()
@@ -525,4 +589,6 @@ let () =
525589
test_storage__test_transact_after_restore_uses_index_slices ();
526590
test_storage__test_conn_repeated_transacts_store_incrementally ();
527591
test_storage__test_conn ();
592+
test_storage__test_conn_tail_write_order ();
593+
test_storage__test_tail_datom_count ();
528594
test_storage__test_db_with_tail ();

0 commit comments

Comments
 (0)