sqry-core 6.0.22

Core library for sqry - semantic code search engine
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
//! Cache budget controller for enforcing memory limits.
//!
//! This module provides adaptive cache budgeting to prevent unbounded memory growth
//! while maintaining high cache hit rates. The controller uses synchronous clamping
//! logic to enforce entry and memory limits across multiple caches.

use std::sync::atomic::{AtomicUsize, Ordering};

/// Configuration for cache budget limits
#[derive(Debug, Clone)]
pub struct BudgetConfig {
    /// Maximum number of entries across all caches (default: 10,000)
    pub max_entries: usize,

    /// Maximum memory in bytes (default: 100 MB)
    /// This is a soft limit based on estimated size tracking
    pub max_memory_bytes: usize,

    /// Estimated bytes per symbol (default: 512 bytes)
    /// Used for rough memory estimation when actual size unavailable
    pub estimated_symbol_size: usize,

    /// Estimated bytes per parse tree (default: 2048 bytes)
    pub estimated_parse_tree_size: usize,
}

impl Default for BudgetConfig {
    fn default() -> Self {
        Self {
            max_entries: 10_000,
            max_memory_bytes: 100 * 1024 * 1024, // 100 MB
            estimated_symbol_size: 512,
            estimated_parse_tree_size: 2048,
        }
    }
}

/// Budget controller for tracking and enforcing cache limits
pub struct CacheBudgetController {
    config: BudgetConfig,

    /// Current total entries across all caches
    total_entries: AtomicUsize,

    /// Estimated total memory usage in bytes
    estimated_memory: AtomicUsize,

    /// Number of clamp operations performed
    clamp_count: AtomicUsize,
}

impl CacheBudgetController {
    /// Create a new budget controller with default configuration
    #[must_use]
    pub fn new() -> Self {
        Self::with_config(BudgetConfig::default())
    }

    /// Create a new budget controller with custom configuration
    #[must_use]
    pub fn with_config(config: BudgetConfig) -> Self {
        Self {
            config,
            total_entries: AtomicUsize::new(0),
            estimated_memory: AtomicUsize::new(0),
            clamp_count: AtomicUsize::new(0),
        }
    }

    /// Record an insert operation
    ///
    /// # Arguments
    ///
    /// * `entry_count` - Number of entries being inserted
    /// * `estimated_bytes` - Estimated memory size of the entries
    pub fn record_insert(&self, entry_count: usize, estimated_bytes: usize) {
        self.total_entries.fetch_add(entry_count, Ordering::Relaxed);
        self.estimated_memory
            .fetch_add(estimated_bytes, Ordering::Relaxed);
    }

    /// Record a remove/eviction operation
    ///
    /// # Arguments
    ///
    /// * `entry_count` - Number of entries being removed
    /// * `estimated_bytes` - Estimated memory size of the entries
    pub fn record_remove(&self, entry_count: usize, estimated_bytes: usize) {
        self.total_entries.fetch_sub(entry_count, Ordering::Relaxed);
        self.estimated_memory
            .fetch_sub(estimated_bytes, Ordering::Relaxed);
    }

    /// Check if budget limits are exceeded
    ///
    /// Returns `ClampAction` indicating how to adjust caches
    pub fn check_budget(&self) -> ClampAction {
        let entries = self.total_entries.load(Ordering::Relaxed);
        let memory = self.estimated_memory.load(Ordering::Relaxed);

        let entries_over = entries.saturating_sub(self.config.max_entries);
        let memory_over = memory.saturating_sub(self.config.max_memory_bytes);

        if entries_over > 0 || memory_over > 0 {
            // Calculate how many entries to evict based on whichever limit is more exceeded
            let entries_to_evict_for_count = entries_over;
            let entries_to_evict_for_memory = if memory_over > 0 {
                // Estimate entries needed to free memory (conservative)
                (memory_over / self.config.estimated_symbol_size).max(1)
            } else {
                0
            };

            let entries_to_evict = entries_to_evict_for_count.max(entries_to_evict_for_memory);

            ClampAction::Evict {
                count: entries_to_evict,
                reason: if entries_over > memory_over {
                    ClampReason::EntryLimit
                } else {
                    ClampReason::MemoryLimit
                },
            }
        } else {
            ClampAction::None
        }
    }

    /// Record that a clamp operation was performed
    pub fn record_clamp(&self) {
        self.clamp_count.fetch_add(1, Ordering::Relaxed);
    }

    /// Get current budget statistics
    pub fn stats(&self) -> BudgetStats {
        BudgetStats {
            total_entries: self.total_entries.load(Ordering::Relaxed),
            estimated_memory_bytes: self.estimated_memory.load(Ordering::Relaxed),
            clamp_count: self.clamp_count.load(Ordering::Relaxed),
            max_entries: self.config.max_entries,
            max_memory_bytes: self.config.max_memory_bytes,
        }
    }

    /// Reset budget tracking (used when clearing all caches)
    pub fn reset(&self) {
        self.total_entries.store(0, Ordering::Relaxed);
        self.estimated_memory.store(0, Ordering::Relaxed);
        // Note: We don't reset clamp_count as it's a cumulative statistic
    }

    /// Get the current configuration
    pub fn config(&self) -> &BudgetConfig {
        &self.config
    }
}

impl Default for CacheBudgetController {
    fn default() -> Self {
        Self::new()
    }
}

/// Action to take when budget is exceeded
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ClampAction {
    /// No action needed, within budget
    None,

    /// Evict entries to get back within budget
    Evict {
        /// Number of entries to evict
        count: usize,
        /// Reason for clamping
        reason: ClampReason,
    },
}

/// Reason why clamping is needed
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClampReason {
    /// Entry count limit exceeded
    EntryLimit,

    /// Memory limit exceeded
    MemoryLimit,
}

/// Statistics about current budget usage
#[derive(Debug, Clone)]
pub struct BudgetStats {
    /// Current total entries
    pub total_entries: usize,

    /// Estimated current memory usage
    pub estimated_memory_bytes: usize,

    /// Number of times clamping was performed
    pub clamp_count: usize,

    /// Maximum allowed entries
    pub max_entries: usize,

    /// Maximum allowed memory
    pub max_memory_bytes: usize,
}

impl BudgetStats {
    /// Calculate entry utilization as a percentage (0.0-1.0)
    #[must_use]
    #[allow(
        clippy::cast_precision_loss,
        reason = "Utilization percentages are informational; precision is sufficient"
    )]
    pub fn entry_utilization(&self) -> f64 {
        if self.max_entries == 0 {
            0.0
        } else {
            self.total_entries as f64 / self.max_entries as f64
        }
    }

    /// Calculate memory utilization as a percentage (0.0-1.0)
    #[must_use]
    #[allow(
        clippy::cast_precision_loss,
        reason = "Utilization percentages are informational; precision is sufficient"
    )]
    pub fn memory_utilization(&self) -> f64 {
        if self.max_memory_bytes == 0 {
            0.0
        } else {
            self.estimated_memory_bytes as f64 / self.max_memory_bytes as f64
        }
    }

    /// Check if budget is exceeded
    #[must_use]
    pub fn is_over_budget(&self) -> bool {
        self.total_entries > self.max_entries || self.estimated_memory_bytes > self.max_memory_bytes
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use approx::assert_abs_diff_eq;

    #[test]
    fn test_default_config() {
        let config = BudgetConfig::default();
        assert_eq!(config.max_entries, 10_000);
        assert_eq!(config.max_memory_bytes, 100 * 1024 * 1024);
        assert_eq!(config.estimated_symbol_size, 512);
    }

    #[test]
    fn test_record_insert() {
        let controller = CacheBudgetController::new();

        controller.record_insert(10, 5120);

        let stats = controller.stats();
        assert_eq!(stats.total_entries, 10);
        assert_eq!(stats.estimated_memory_bytes, 5120);
    }

    #[test]
    fn test_record_remove() {
        let controller = CacheBudgetController::new();

        controller.record_insert(20, 10240);
        controller.record_remove(5, 2560);

        let stats = controller.stats();
        assert_eq!(stats.total_entries, 15);
        assert_eq!(stats.estimated_memory_bytes, 7680);
    }

    #[test]
    fn test_budget_within_limits() {
        let config = BudgetConfig {
            max_entries: 100,
            max_memory_bytes: 10240,
            ..Default::default()
        };
        let controller = CacheBudgetController::with_config(config);

        controller.record_insert(50, 5000);

        let action = controller.check_budget();
        assert_eq!(action, ClampAction::None);
    }

    #[test]
    fn test_budget_entry_limit_exceeded() {
        let config = BudgetConfig {
            max_entries: 100,
            max_memory_bytes: 100_000,
            ..Default::default()
        };
        let controller = CacheBudgetController::with_config(config);

        controller.record_insert(150, 5000);

        let action = controller.check_budget();
        match action {
            ClampAction::Evict { count, reason } => {
                assert_eq!(count, 50);
                assert_eq!(reason, ClampReason::EntryLimit);
            }
            ClampAction::None => panic!("Expected eviction"),
        }
    }

    #[test]
    fn test_budget_memory_limit_exceeded() {
        let config = BudgetConfig {
            max_entries: 1000,
            max_memory_bytes: 10_000,
            estimated_symbol_size: 512,
            ..Default::default()
        };
        let controller = CacheBudgetController::with_config(config);

        controller.record_insert(50, 15_000);

        let action = controller.check_budget();
        match action {
            ClampAction::Evict { count, reason } => {
                assert!(count > 0);
                assert_eq!(reason, ClampReason::MemoryLimit);
            }
            ClampAction::None => panic!("Expected eviction"),
        }
    }

    #[test]
    fn test_clamp_count_tracking() {
        let controller = CacheBudgetController::new();

        assert_eq!(controller.stats().clamp_count, 0);

        controller.record_clamp();
        controller.record_clamp();

        assert_eq!(controller.stats().clamp_count, 2);
    }

    #[test]
    fn test_reset() {
        let controller = CacheBudgetController::new();

        controller.record_insert(100, 5000);
        controller.record_clamp();

        controller.reset();

        let stats = controller.stats();
        assert_eq!(stats.total_entries, 0);
        assert_eq!(stats.estimated_memory_bytes, 0);
        assert_eq!(stats.clamp_count, 1); // Clamp count not reset
    }

    #[test]
    fn test_budget_stats_utilization() {
        let config = BudgetConfig {
            max_entries: 100,
            max_memory_bytes: 10_000,
            ..Default::default()
        };
        let controller = CacheBudgetController::with_config(config);

        controller.record_insert(50, 5_000);

        let stats = controller.stats();
        assert_abs_diff_eq!(stats.entry_utilization(), 0.5, epsilon = 1e-10);
        assert_abs_diff_eq!(stats.memory_utilization(), 0.5, epsilon = 1e-10);
        assert!(!stats.is_over_budget());
    }

    #[test]
    fn test_budget_stats_over_budget() {
        let config = BudgetConfig {
            max_entries: 100,
            max_memory_bytes: 10_000,
            ..Default::default()
        };
        let controller = CacheBudgetController::with_config(config);

        controller.record_insert(150, 5_000);

        let stats = controller.stats();
        assert!(stats.is_over_budget());
        assert!(stats.entry_utilization() > 1.0);
    }

    #[test]
    fn test_multiple_inserts_and_removes() {
        let controller = CacheBudgetController::new();

        controller.record_insert(10, 1000);
        controller.record_insert(20, 2000);
        controller.record_remove(5, 500);
        controller.record_insert(15, 1500);
        controller.record_remove(10, 1000);

        let stats = controller.stats();
        assert_eq!(stats.total_entries, 30);
        assert_eq!(stats.estimated_memory_bytes, 3000);
    }
}