1use super::{SegmentTier, TierThresholds};
4use crate::graph::refine::{RefinementPass, RefinementPasses};
5
6#[derive(Clone, Copy, Debug, Eq, PartialEq)]
8pub struct SegmentStats {
9 pub tier: SegmentTier,
11 pub row_count: u32,
13 pub dimensions: u32,
15 pub scheme: u16,
17}
18
19#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
21pub struct StoreStats {
22 pub graph_segment_count: u32,
24 pub total_graph_rows: u64,
26 pub largest_graph_segment_rows: u64,
28 pub scan_segment_count: u32,
30 pub total_scan_rows: u64,
32 pub scan_graph_min_rows: u32,
34}
35
36pub const CONSOLIDATE_MIN_SEGMENTS: u32 = 3;
42
43pub const CONSOLIDATE_DOMINANT_TENTHS: u128 = 7;
46
47#[derive(Clone, Copy, Debug, Eq, PartialEq)]
49pub enum StorePlan {
50 Stay,
52 Consolidate,
54 ConsolidateScan,
56}
57
58#[derive(Clone, Copy, Debug, Eq, PartialEq)]
60pub enum RefinementPlan {
61 Stay,
63 Apply(RefinementPass),
65}
66
67#[must_use]
69pub fn decide_refinement(applied: RefinementPasses) -> RefinementPlan {
70 RefinementPass::ALL
71 .into_iter()
72 .find(|pass| !applied.contains(*pass))
73 .map_or(RefinementPlan::Stay, RefinementPlan::Apply)
74}
75
76#[must_use]
82pub const fn decide_store(store: StoreStats) -> StorePlan {
83 if store.graph_segment_count >= CONSOLIDATE_MIN_SEGMENTS {
84 return StorePlan::Consolidate;
85 }
86 if store.graph_segment_count == 2 {
87 let largest = store.largest_graph_segment_rows as u128;
88 let total = store.total_graph_rows as u128;
89 if largest * 10 < total * CONSOLIDATE_DOMINANT_TENTHS {
90 return StorePlan::Consolidate;
91 }
92 }
93 if store.scan_segment_count >= 2
94 && store.scan_graph_min_rows != 0
95 && store.total_scan_rows >= store.scan_graph_min_rows as u64
96 {
97 return StorePlan::ConsolidateScan;
98 }
99 StorePlan::Stay
100}
101
102#[derive(Clone, Copy, Debug, Eq, PartialEq)]
104pub enum TierPlan {
105 Stay(SegmentTier),
107 Transition {
109 from: SegmentTier,
111 to: SegmentTier,
113 },
114}
115
116#[must_use]
118pub const fn decide(segment: SegmentStats, _store: StoreStats) -> TierPlan {
119 let thresholds = super::thresholds::for_bucket(segment.dimensions, segment.scheme);
120 decide_with_thresholds(segment, thresholds)
121}
122
123pub(crate) const fn decide_with_thresholds(
124 segment: SegmentStats,
125 thresholds: TierThresholds,
126) -> TierPlan {
127 match segment.tier {
128 SegmentTier::ActiveScan => TierPlan::Stay(SegmentTier::ActiveScan),
129 SegmentTier::SealedGraph => TierPlan::Stay(SegmentTier::SealedGraph),
130 SegmentTier::SealedScan => {
131 if segment.row_count >= thresholds.graph_min_rows {
132 TierPlan::Transition {
133 from: SegmentTier::SealedScan,
134 to: SegmentTier::SealedGraph,
135 }
136 } else {
137 TierPlan::Stay(SegmentTier::SealedScan)
138 }
139 }
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::{SegmentStats, StorePlan, StoreStats, TierPlan, decide, decide_store};
146 use crate::tier::{PROVISIONAL_TIER_THRESHOLDS, SegmentTier};
147
148 #[test]
149 fn policy_decides_graph_above_the_threshold_and_scan_below() {
150 let threshold = PROVISIONAL_TIER_THRESHOLDS.graph_min_rows;
151 let below = SegmentStats {
152 tier: SegmentTier::SealedScan,
153 row_count: threshold - 1,
154 dimensions: 128,
155 scheme: 4,
156 };
157 let above = SegmentStats {
158 row_count: threshold,
159 ..below
160 };
161
162 assert_eq!(
163 decide(below, StoreStats::default()),
164 TierPlan::Stay(SegmentTier::SealedScan)
165 );
166 assert_eq!(
167 decide(above, StoreStats::default()),
168 TierPlan::Transition {
169 from: SegmentTier::SealedScan,
170 to: SegmentTier::SealedGraph,
171 }
172 );
173 }
174
175 #[test]
176 fn policy_never_demotes_a_segment_that_already_has_a_graph() {
177 let segment = SegmentStats {
178 tier: SegmentTier::SealedGraph,
179 row_count: PROVISIONAL_TIER_THRESHOLDS.graph_min_rows - 1,
180 dimensions: 128,
181 scheme: 4,
182 };
183
184 assert_eq!(
185 decide(segment, StoreStats::default()),
186 TierPlan::Stay(SegmentTier::SealedGraph)
187 );
188 }
189
190 #[test]
191 fn store_policy_consolidates_at_three_or_more_graph_segments() {
192 for (count, expected) in [
193 (0, StorePlan::Stay),
194 (1, StorePlan::Stay),
195 (3, StorePlan::Consolidate),
196 (4, StorePlan::Consolidate),
197 (100, StorePlan::Consolidate),
198 ] {
199 assert_eq!(
200 decide_store(StoreStats {
201 graph_segment_count: count,
202 total_graph_rows: 1_000_000,
203 largest_graph_segment_rows: 999_999,
204 ..StoreStats::default()
205 }),
206 expected,
207 "graph_segment_count={count}"
208 );
209 }
210 }
211
212 #[test]
213 fn store_policy_at_two_segments_consolidates_only_without_a_dominant_segment() {
214 assert_eq!(
216 decide_store(StoreStats {
217 graph_segment_count: 2,
218 total_graph_rows: 1_000,
219 largest_graph_segment_rows: 500,
220 ..StoreStats::default()
221 }),
222 StorePlan::Consolidate
223 );
224 assert_eq!(
226 decide_store(StoreStats {
227 graph_segment_count: 2,
228 total_graph_rows: 1_000,
229 largest_graph_segment_rows: 699,
230 ..StoreStats::default()
231 }),
232 StorePlan::Consolidate
233 );
234 assert_eq!(
236 decide_store(StoreStats {
237 graph_segment_count: 2,
238 total_graph_rows: 1_000,
239 largest_graph_segment_rows: 700,
240 ..StoreStats::default()
241 }),
242 StorePlan::Stay
243 );
244 assert_eq!(
246 decide_store(StoreStats {
247 graph_segment_count: 2,
248 total_graph_rows: 1_000,
249 largest_graph_segment_rows: 950,
250 ..StoreStats::default()
251 }),
252 StorePlan::Stay
253 );
254 assert_eq!(
256 decide_store(StoreStats {
257 graph_segment_count: 2,
258 total_graph_rows: u64::MAX,
259 largest_graph_segment_rows: u64::MAX / 2,
260 ..StoreStats::default()
261 }),
262 StorePlan::Consolidate
263 );
264 }
265
266 #[test]
267 fn store_policy_consolidates_scan_segments_only_after_graph_threshold() {
268 assert_eq!(
269 decide_store(StoreStats {
270 scan_segment_count: 7,
271 total_scan_rows: 28_672,
272 scan_graph_min_rows: 30_000,
273 ..StoreStats::default()
274 }),
275 StorePlan::Stay
276 );
277 assert_eq!(
278 decide_store(StoreStats {
279 scan_segment_count: 8,
280 total_scan_rows: 32_768,
281 scan_graph_min_rows: 30_000,
282 ..StoreStats::default()
283 }),
284 StorePlan::ConsolidateScan
285 );
286 }
287}