Skip to main content

nodedb_types/config/tuning/
engines.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Per-engine tuning: Vector, Sparse, Graph, Timeseries.
4
5use serde::{Deserialize, Serialize};
6
7/// Vector engine tuning (HNSW, PQ, IVF).
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct VectorTuning {
10    #[serde(default = "default_flat_index_threshold")]
11    pub flat_index_threshold: usize,
12    #[serde(default = "default_seal_threshold")]
13    pub seal_threshold: usize,
14    #[serde(default = "default_pq_m")]
15    pub default_pq_m: usize,
16    #[serde(default = "default_ivf_cells")]
17    pub default_ivf_cells: usize,
18    #[serde(default = "default_ivf_nprobe")]
19    pub default_ivf_nprobe: usize,
20}
21
22impl Default for VectorTuning {
23    fn default() -> Self {
24        Self {
25            flat_index_threshold: default_flat_index_threshold(),
26            seal_threshold: default_seal_threshold(),
27            default_pq_m: default_pq_m(),
28            default_ivf_cells: default_ivf_cells(),
29            default_ivf_nprobe: default_ivf_nprobe(),
30        }
31    }
32}
33
34fn default_flat_index_threshold() -> usize {
35    10_000
36}
37fn default_seal_threshold() -> usize {
38    65_536
39}
40fn default_pq_m() -> usize {
41    8
42}
43fn default_ivf_cells() -> usize {
44    256
45}
46fn default_ivf_nprobe() -> usize {
47    16
48}
49
50/// Sparse/metadata engine tuning (BM25, GSI, HyperLogLog).
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct SparseTuning {
53    #[serde(default = "default_bm25_k1")]
54    pub bm25_k1: f32,
55    #[serde(default = "default_bm25_b")]
56    pub bm25_b: f32,
57    #[serde(default = "default_max_gsis_per_collection")]
58    pub max_gsis_per_collection: usize,
59    #[serde(default = "default_hll_m")]
60    pub hll_registers: usize,
61    #[serde(default = "default_hll_p")]
62    pub hll_precision: u32,
63}
64
65impl Default for SparseTuning {
66    fn default() -> Self {
67        Self {
68            bm25_k1: default_bm25_k1(),
69            bm25_b: default_bm25_b(),
70            max_gsis_per_collection: default_max_gsis_per_collection(),
71            hll_registers: default_hll_m(),
72            hll_precision: default_hll_p(),
73        }
74    }
75}
76
77fn default_bm25_k1() -> f32 {
78    1.2
79}
80fn default_bm25_b() -> f32 {
81    0.75
82}
83fn default_max_gsis_per_collection() -> usize {
84    4
85}
86fn default_hll_m() -> usize {
87    256
88}
89fn default_hll_p() -> u32 {
90    8
91}
92
93/// Default cap on visited nodes during BFS traversals.
94/// Prevents supernode fan-out explosion from consuming unbounded memory.
95pub const DEFAULT_MAX_VISITED: usize = 100_000;
96
97/// Default maximum BFS traversal depth.
98pub const DEFAULT_MAX_DEPTH: usize = 10;
99
100/// Default hard cap on results emitted from a single variable-length MATCH
101/// expansion before it must page via cross-shard resume.
102pub const DEFAULT_VARLEN_MAX_RESULTS: usize = 100_000;
103
104/// Default hard cap on the live frontier of a single variable-length MATCH
105/// expansion before it must page via cross-shard resume.
106pub const DEFAULT_VARLEN_MAX_FRONTIER: usize = 100_000;
107
108/// Default cap on the edges one gathered graph algorithm run collects. Each
109/// gathered edge holds its three names and a weight, around 100 bytes with
110/// typical keys, so the default bounds one run near 1 GB before its CSR.
111pub const DEFAULT_MAX_GATHERED_ALGO_EDGES: usize = 10_000_000;
112
113/// Graph engine tuning (traversal limits, LCC algorithm).
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct GraphTuning {
116    #[serde(default = "default_max_visited")]
117    pub max_visited: usize,
118    #[serde(default = "default_max_depth")]
119    pub max_depth: usize,
120    #[serde(default = "default_lcc_high_degree_threshold")]
121    pub lcc_high_degree_threshold: usize,
122    #[serde(default = "default_lcc_sample_pairs")]
123    pub lcc_sample_pairs: usize,
124    /// Hard cap on results emitted from a single variable-length MATCH
125    /// (`[*min..max]`) expansion. When an expansion would exceed this it
126    /// truncates at the current hop boundary and surfaces a resume cursor so
127    /// the remainder is paged across follow-up rounds (cross-shard or local) —
128    /// no row is silently dropped. Bounds peak per-expansion result allocation.
129    #[serde(default = "default_varlen_max_results")]
130    pub varlen_max_results: usize,
131    /// Hard cap on the live (per-hop) frontier of a single variable-length
132    /// MATCH expansion. When a single hop would grow the frontier past this it
133    /// truncates and pages via resume, bounding peak intermediate allocation on
134    /// dense / bidirectional traversals.
135    #[serde(default = "default_varlen_max_frontier")]
136    pub varlen_max_frontier: usize,
137    /// Hard cap on the distinct edges a gathered graph algorithm collects from
138    /// every owner before it runs on one core. The algorithm needs every edge
139    /// in one CSR, so a run over more edges is refused with the edge count
140    /// rather than answered from part of the graph.
141    #[serde(default = "default_max_gathered_algo_edges")]
142    pub max_gathered_algo_edges: usize,
143}
144
145impl Default for GraphTuning {
146    fn default() -> Self {
147        Self {
148            max_visited: default_max_visited(),
149            max_depth: default_max_depth(),
150            lcc_high_degree_threshold: default_lcc_high_degree_threshold(),
151            lcc_sample_pairs: default_lcc_sample_pairs(),
152            varlen_max_results: default_varlen_max_results(),
153            varlen_max_frontier: default_varlen_max_frontier(),
154            max_gathered_algo_edges: default_max_gathered_algo_edges(),
155        }
156    }
157}
158
159fn default_max_visited() -> usize {
160    DEFAULT_MAX_VISITED
161}
162fn default_max_depth() -> usize {
163    DEFAULT_MAX_DEPTH
164}
165fn default_lcc_high_degree_threshold() -> usize {
166    2_000
167}
168fn default_lcc_sample_pairs() -> usize {
169    10_000
170}
171fn default_varlen_max_results() -> usize {
172    DEFAULT_VARLEN_MAX_RESULTS
173}
174fn default_varlen_max_frontier() -> usize {
175    DEFAULT_VARLEN_MAX_FRONTIER
176}
177fn default_max_gathered_algo_edges() -> usize {
178    DEFAULT_MAX_GATHERED_ALGO_EDGES
179}
180
181/// Timeseries engine tuning (memtable budgets, block sizes).
182#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct TimeseriesToning {
184    /// Soft memtable budget: reaching it schedules a flush at the next record
185    /// boundary. Rows already in the memtable are never refused because of it.
186    #[serde(default = "default_memtable_budget_bytes")]
187    pub memtable_budget_bytes: usize,
188    /// Hard memtable ceiling. Reaching it forces a flush BEFORE the next WAL
189    /// record is ingested, never partway through one: by the time the Data
190    /// Plane sees a record its WAL append has already committed, so the
191    /// memtable must take the record whole or the write is silently lost.
192    /// Consequently a single record can carry the memtable up to its own
193    /// decoded size past this ceiling; see the admission gate in the
194    /// timeseries ingest handler for the bound that costs.
195    #[serde(default = "default_memtable_hard_limit_bytes")]
196    pub memtable_hard_limit_bytes: usize,
197    /// Maximum distinct values per symbol (tag / string) column in one
198    /// memtable generation. Flushing resets the dictionaries, so this bounds
199    /// dictionary size between flushes rather than over the collection's life.
200    #[serde(default = "default_max_tag_cardinality")]
201    pub max_tag_cardinality: u32,
202    #[serde(default = "default_total_budget_bytes")]
203    pub total_budget_bytes: usize,
204    #[serde(default = "default_ts_block_size")]
205    pub block_size: usize,
206    /// Sealed partitions one maintenance tick reads to fold into continuous
207    /// aggregates. Bounds the time a tick holds the core while an aggregate
208    /// backfills after a CREATE or a restart.
209    #[serde(default = "default_cagg_fold_partitions_per_tick")]
210    pub cagg_fold_partitions_per_tick: usize,
211    /// Partition groups one maintenance tick merges. Late rows flush into a
212    /// new partition beside the one their interval already has; a merge
213    /// combines the partitions of one interval into one.
214    #[serde(default = "default_merge_groups_per_tick")]
215    pub merge_groups_per_tick: usize,
216    /// Most rows one merge reads and writes. Bounds the memory a merge holds.
217    /// A larger interval merges in several groups.
218    #[serde(default = "default_merge_max_rows")]
219    pub merge_max_rows: u64,
220    /// How long after an interval ends its partitions merge. Late rows for
221    /// an interval still arriving within this window flush first, so the
222    /// interval merges once.
223    #[serde(default = "default_merge_quiet_ms")]
224    pub merge_quiet_ms: u64,
225}
226
227impl Default for TimeseriesToning {
228    fn default() -> Self {
229        Self {
230            memtable_budget_bytes: default_memtable_budget_bytes(),
231            memtable_hard_limit_bytes: default_memtable_hard_limit_bytes(),
232            max_tag_cardinality: default_max_tag_cardinality(),
233            total_budget_bytes: default_total_budget_bytes(),
234            block_size: default_ts_block_size(),
235            cagg_fold_partitions_per_tick: default_cagg_fold_partitions_per_tick(),
236            merge_groups_per_tick: default_merge_groups_per_tick(),
237            merge_max_rows: default_merge_max_rows(),
238            merge_quiet_ms: default_merge_quiet_ms(),
239        }
240    }
241}
242
243fn default_memtable_budget_bytes() -> usize {
244    64 * 1024 * 1024
245}
246fn default_memtable_hard_limit_bytes() -> usize {
247    80 * 1024 * 1024
248}
249fn default_max_tag_cardinality() -> u32 {
250    100_000
251}
252fn default_total_budget_bytes() -> usize {
253    100 * 1024 * 1024
254}
255fn default_ts_block_size() -> usize {
256    1024
257}
258fn default_cagg_fold_partitions_per_tick() -> usize {
259    8
260}
261fn default_merge_groups_per_tick() -> usize {
262    2
263}
264fn default_merge_max_rows() -> u64 {
265    4_000_000
266}
267fn default_merge_quiet_ms() -> u64 {
268    60_000
269}
270
271/// KV engine tuning (hash table, expiry wheel, slab allocator).
272///
273/// Controls the per-core hash table parameters, incremental rehash behavior,
274/// expiry wheel tick rate, and the per-tick reap budget that prevents reactor
275/// stalls during mass-expiry events.
276#[derive(Debug, Clone, Serialize, Deserialize)]
277pub struct KvTuning {
278    /// Default initial hash table capacity per collection (number of slots).
279    /// Should be a power of two. Larger values reduce early rehash churn for
280    /// collections that will grow quickly. Default: 16384.
281    #[serde(default = "default_kv_capacity")]
282    pub default_capacity: usize,
283
284    /// Hash table load factor threshold that triggers incremental rehash.
285    /// When `entries / capacity > rehash_load_factor`, the table begins
286    /// doubling. Range: 0.5–0.9. Default: 0.75 (standard Robin Hood threshold).
287    #[serde(default = "default_kv_rehash_load_factor")]
288    pub rehash_load_factor: f32,
289
290    /// Number of entries rehashed per PUT during incremental rehash.
291    /// Higher values complete rehash faster but add per-PUT latency.
292    /// Default: 8.
293    #[serde(default = "default_kv_rehash_batch_size")]
294    pub rehash_batch_size: usize,
295
296    /// Default inline value threshold in bytes. Values at or below this size
297    /// are stored directly in the hash entry (no pointer chase). Larger values
298    /// overflow to slab-allocated Binary Tuples. Default: 64.
299    #[serde(default = "default_kv_inline_threshold")]
300    pub default_inline_threshold: usize,
301
302    /// Maximum expirations processed per reactor tick (event loop iteration).
303    /// Prevents mass-expiry events (e.g., 10M keys with identical TTL) from
304    /// stalling the TPC core. Expired-but-not-yet-reaped keys are invisible
305    /// to GET (lazy fallback). Default: 1024.
306    #[serde(default = "default_kv_expiry_reap_budget")]
307    pub expiry_reap_budget: usize,
308
309    /// Expiry wheel tick interval in milliseconds. Determines the granularity
310    /// of TTL expiration. Lower = more precise but more CPU overhead.
311    /// Default: 1000 (1 second).
312    #[serde(default = "default_kv_expiry_tick_ms")]
313    pub expiry_tick_ms: u64,
314}
315
316impl Default for KvTuning {
317    fn default() -> Self {
318        Self {
319            default_capacity: default_kv_capacity(),
320            rehash_load_factor: default_kv_rehash_load_factor(),
321            rehash_batch_size: default_kv_rehash_batch_size(),
322            default_inline_threshold: default_kv_inline_threshold(),
323            expiry_reap_budget: default_kv_expiry_reap_budget(),
324            expiry_tick_ms: default_kv_expiry_tick_ms(),
325        }
326    }
327}
328
329fn default_kv_capacity() -> usize {
330    16_384
331}
332fn default_kv_rehash_load_factor() -> f32 {
333    0.75
334}
335fn default_kv_rehash_batch_size() -> usize {
336    8
337}
338fn default_kv_inline_threshold() -> usize {
339    64
340}
341fn default_kv_expiry_reap_budget() -> usize {
342    1024
343}
344fn default_kv_expiry_tick_ms() -> u64 {
345    1000
346}