Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 33 additions & 23 deletions impl/entity.ml
Original file line number Diff line number Diff line change
Expand Up @@ -42,32 +42,29 @@ 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
| Some values -> (d.a, d.v :: values) :: List.remove_assoc d.a groups
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 sorted_forward_entity_attrs context db entity_id =
group_forward_entity_attrs context db entity_id
|> List.sort (fun (left, _) (right, _) -> Util.compare_attr 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
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 =
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, _) -> Util.compare_attr left right)

let reverse_entity_attr context db entity_id attr =
let forward_attr = context.reverse_ref attr in
let values =
Expand All @@ -82,17 +79,30 @@ 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
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
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
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)
; materialize_attrs = (fun () -> Lazy.force materialized)
; lookup_attr = lookup
; materialize_attrs = (fun () -> sorted_forward_entity_attrs context db entity_id)
}

let materialized_entity context db entity_id attrs =
Expand Down
6 changes: 6 additions & 0 deletions impl/platform.mli
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions impl/platform/jsoo/platform.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions impl/platform/melange/platform.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions impl/platform/native/platform.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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
19 changes: 17 additions & 2 deletions impl/storage.ml
Original file line number Diff line number Diff line change
Expand Up @@ -160,15 +160,30 @@ 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)
; storage_index_order_version = index_order_version
}

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
Expand Down
Loading