From 80774407295a0d5d414f4d93432801de4ba1a927 Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Sat, 26 Sep 2026 19:37:01 -0700 Subject: [PATCH 1/3] perf(entity): cache forward attr lookups on lazy entity --- impl/entity.ml | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/impl/entity.ml b/impl/entity.ml index d96d94b..2196ffe 100644 --- a/impl/entity.ml +++ b/impl/entity.ml @@ -59,15 +59,6 @@ let sorted_forward_entity_attrs context db entity_id = group_forward_entity_attrs context db entity_id |> List.sort (fun (left, _) (right, _) -> compare left right) -let forward_entity_attr context db entity_id attr = - context.datoms_by_entity db entity_id - |> Seq.filter_map (fun d -> if d.a = attr then Some d.v else None) - |> List.of_seq - |> entity_visible_attr_values context db attr - |> function - | [] -> None - | values -> Some (tx_value_of_attr_values context db attr values) - let reverse_entity_attr context db entity_id attr = let forward_attr = context.reverse_ref attr in let values = @@ -83,15 +74,27 @@ let reverse_entity_attr context db entity_id attr = let lazy_entity context db entity_id = let materialized = lazy (sorted_forward_entity_attrs context db entity_id) in + (* upstream caches each queried attr on the entity; without a cache every + lookup re-scans all of the entity's datoms. *) + let lookup_cache : (attr, tx_value option) Hashtbl.t = Hashtbl.create 8 in + let lookup attr = + match Hashtbl.find_opt lookup_cache attr with + | Some cached -> cached + | None -> + let result = + if context.is_reverse_ref attr then + reverse_entity_attr context db entity_id attr + else + (* force the single full scan once, then assoc lookups *) + List.assoc_opt attr (Lazy.force materialized) + in + Hashtbl.replace lookup_cache attr result; + result + in { id = entity_id ; db ; attrs = [] - ; lookup_attr = - (fun attr -> - if context.is_reverse_ref attr then - reverse_entity_attr context db entity_id attr - else - forward_entity_attr context db entity_id attr) + ; lookup_attr = lookup ; materialize_attrs = (fun () -> Lazy.force materialized) } From 8be3288840fb50e2afa2e49ae88af70e884c821f Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Sat, 26 Sep 2026 19:37:01 -0700 Subject: [PATCH 2/3] perf(storage): strong index node cache on native --- impl/platform.mli | 6 ++++++ impl/platform/jsoo/platform.ml | 1 + impl/platform/melange/platform.ml | 1 + impl/platform/native/platform.ml | 1 + impl/storage.ml | 13 ++++++++++++- 5 files changed, 21 insertions(+), 1 deletion(-) diff --git a/impl/platform.mli b/impl/platform.mli index af4ca75..09f5c74 100644 --- a/impl/platform.mli +++ b/impl/platform.mli @@ -37,3 +37,9 @@ val split_regex : regex -> string -> string list (** Split a string around regex matches, producing at most the requested number of parts. *) val split_regex_limited : regex -> string -> int -> string list + +(** Whether storage-backed index nodes should be cached with strong + references. True on native, where the OCaml GC clears weak slots on + every major collection and would thrash the node cache; false on JS + runtimes where WeakRef behaves like upstream DataScript. *) +val strong_index_node_cache : bool diff --git a/impl/platform/jsoo/platform.ml b/impl/platform/jsoo/platform.ml index 6fbacb2..67568e9 100644 --- a/impl/platform/jsoo/platform.ml +++ b/impl/platform/jsoo/platform.ml @@ -48,3 +48,4 @@ let split_regex_limited regex value limit = if limit = 1 then [ value ] else if limit <= 0 then Regexp.split regex value else Regexp.bounded_split regex value limit +let strong_index_node_cache = false diff --git a/impl/platform/melange/platform.ml b/impl/platform/melange/platform.ml index d16eac3..cde318e 100644 --- a/impl/platform/melange/platform.ml +++ b/impl/platform/melange/platform.ml @@ -75,3 +75,4 @@ let split_regex_limited pattern value limit = let split_regex pattern value = split_regex_limited pattern value 0 +let strong_index_node_cache = false diff --git a/impl/platform/native/platform.ml b/impl/platform/native/platform.ml index d2dacc8..8c1bac8 100644 --- a/impl/platform/native/platform.ml +++ b/impl/platform/native/platform.ml @@ -206,3 +206,4 @@ let split_regex_limited regex value limit = if limit = 1 then [ value ] else if limit <= 0 then Str.split regex value else Str.bounded_split regex value limit +let strong_index_node_cache = true diff --git a/impl/storage.ml b/impl/storage.ml index 075fe02..6dcc632 100644 --- a/impl/storage.ml +++ b/impl/storage.ml @@ -158,11 +158,22 @@ let root_of_stored_indexes db ~eavt_metadata ~aevt_metadata ~avet_metadata eavt_ ; storage_ref_type = settings.ref_type } -let settings_of_root root = +let stored_settings_of_root root = { PSet.branching_factor = root.storage_branching_factor ; ref_type = root.storage_ref_type } +(* Upstream caches restored index nodes behind js/WeakRef, which survives + V8 minor GCs; the OCaml GC clears weak slots on every major collection, + so hot slices keep paying a sqlite reload + transit decode on native. + Strong refs reproduce the effective upstream cache lifetime there. The + stored metadata is untouched — this only affects the in-memory cache. *) +let settings_of_root root = + if Platform.strong_index_node_cache then + { (stored_settings_of_root root) with PSet.ref_type = PSet.Strong } + else + stored_settings_of_root root + let storage_backed_index node_storage index index_set = let cmp = Util.compare_datom index in let settings = PSet.settings index_set in From 808c0f198ec967b2ed98611df5214d7d10db4da3 Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Sat, 26 Sep 2026 19:51:41 -0700 Subject: [PATCH 3/3] fix(entity): keep per-attr conversion lazy; persist weak ref-type on native --- impl/entity.ml | 27 +++++++++++++++++---------- impl/storage.ml | 6 +++++- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/impl/entity.ml b/impl/entity.ml index 2196ffe..1bfecf0 100644 --- a/impl/entity.ml +++ b/impl/entity.ml @@ -42,7 +42,10 @@ let entity_visible_attr_values context db attr values = else values -let group_forward_entity_attrs context db entity_id = +(* Raw forward groups without ref wrapping / visibility filtering — a + single index scan. Conversion happens per attr so that reading one + attribute never pays (or raises on) another attribute's values. *) +let raw_forward_entity_attrs context db entity_id = let add_attr groups d = match List.assoc_opt d.a groups with | None -> (d.a, [ d.v ]) :: groups @@ -50,13 +53,16 @@ let group_forward_entity_attrs context db entity_id = in context.datoms_by_entity db entity_id |> Seq.fold_left add_attr [] - |> List.filter_map (fun (attr, values) -> - match entity_visible_attr_values context db attr values with - | [] -> None - | values -> Some (attr, tx_value_of_attr_values context db attr values)) + +let tx_value_of_raw_attr context db attr values = + match entity_visible_attr_values context db attr values with + | [] -> None + | values -> Some (tx_value_of_attr_values context db attr values) let sorted_forward_entity_attrs context db entity_id = - group_forward_entity_attrs context db entity_id + raw_forward_entity_attrs context db entity_id + |> List.filter_map (fun (attr, values) -> + Option.map (fun v -> attr, v) (tx_value_of_raw_attr context db attr values)) |> List.sort (fun (left, _) (right, _) -> compare left right) let reverse_entity_attr context db entity_id attr = @@ -73,7 +79,7 @@ let reverse_entity_attr context db entity_id attr = | values -> Some (Many_values values) let lazy_entity context db entity_id = - let materialized = lazy (sorted_forward_entity_attrs context db entity_id) in + let raw_attrs = lazy (raw_forward_entity_attrs context db entity_id) in (* upstream caches each queried attr on the entity; without a cache every lookup re-scans all of the entity's datoms. *) let lookup_cache : (attr, tx_value option) Hashtbl.t = Hashtbl.create 8 in @@ -85,8 +91,9 @@ let lazy_entity context db entity_id = if context.is_reverse_ref attr then reverse_entity_attr context db entity_id attr else - (* force the single full scan once, then assoc lookups *) - List.assoc_opt attr (Lazy.force materialized) + Option.bind + (List.assoc_opt attr (Lazy.force raw_attrs)) + (tx_value_of_raw_attr context db attr) in Hashtbl.replace lookup_cache attr result; result @@ -95,7 +102,7 @@ let lazy_entity context db entity_id = ; db ; attrs = [] ; lookup_attr = lookup - ; materialize_attrs = (fun () -> Lazy.force materialized) + ; materialize_attrs = (fun () -> sorted_forward_entity_attrs context db entity_id) } let materialized_entity context db entity_id attrs = diff --git a/impl/storage.ml b/impl/storage.ml index 6dcc632..c4c07cd 100644 --- a/impl/storage.ml +++ b/impl/storage.ml @@ -155,7 +155,11 @@ let root_of_stored_indexes db ~eavt_metadata ~aevt_metadata ~avet_metadata eavt_ ; storage_duplicate_datoms = db.duplicate_datoms ; storage_max_addr = !max_storage_addr ; storage_branching_factor = settings.branching_factor - ; storage_ref_type = settings.ref_type + (* ref-type is an in-memory node-cache policy. Native forces Strong at + restore (see settings_of_root); don't let that leak into stored + metadata — keep writing what a JS restore expects. *) + ; storage_ref_type = + (if Platform.strong_index_node_cache then PSet.Weak else settings.ref_type) } let stored_settings_of_root root =