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
//! Delta file summary info for a tracked file.
//!
//! tracked files are managed
//! by the UtilizationTracker.
use crate::file_summary::FileSummary;
/// Delta file summary info for a tracked file.
///
/// Tracked files are managed by the UtilizationTracker. The methods in this struct for reading
/// obsolete offsets may be used by multiple threads without synchronization even while another
/// thread is adding offsets. This is possible because elements are never deleted from the lists.
#[derive(Debug, Clone)]
pub struct TrackedFileSummary {
/// The file number being tracked.
file_number: u32,
/// The file summary counters.
summary: FileSummary,
/// Obsolete offsets tracked for this file.
obsolete_offsets: Vec<u32>,
/// Whether this summary has been modified since last flush.
modified: bool,
/// Whether to track obsolete offset details.
track_detail: bool,
/// Whether this summary is allowed to be flushed/evicted during cleaning.
/// JE `TrackedFileSummary.allowFlush` (default true); the cleaner pins a
/// file it is actively processing via `getUnflushableTrackedSummary`.
allow_flush: bool,
}
impl TrackedFileSummary {
/// Creates an empty tracked summary.
pub fn new(file_number: u32, track_detail: bool) -> Self {
Self {
file_number,
summary: FileSummary::new(),
obsolete_offsets: Vec::new(),
modified: false,
track_detail,
allow_flush: true,
}
}
/// Returns whether this summary may be flushed/evicted during cleaning.
///
/// JE `TrackedFileSummary.getAllowFlush`.
pub fn get_allow_flush(&self) -> bool {
self.allow_flush
}
/// Allows or prohibits this summary from being flushed/evicted.
///
/// JE `TrackedFileSummary.setAllowFlush`.
pub fn set_allow_flush(&mut self, allow_flush: bool) {
self.allow_flush = allow_flush;
}
/// Drops the obsolete-offset DETAIL while KEEPING the aggregate
/// `FileSummary` counters intact.
///
/// This is the in-memory adaptation of JE's eviction-of-detail path. In
/// JE, `UtilizationTracker.evictMemory` calls
/// `UtilizationProfile.flushFileSummary`, which writes a `FileSummaryLN`
/// (persisting the aggregate counts) and then calls
/// `TrackedFileSummary.reset` — `reset` sets `obsoleteOffsets = null` and
/// `super.reset()` zeroes the in-memory `FileSummary` *because the
/// aggregate has just been persisted*.
///
/// noxu's `UtilizationTracker` cannot reach `UtilizationProfile` to write
/// a `FileSummaryLN` from here (that would be a layering cycle), so the
/// in-memory budget cap is implemented by dropping only the per-LSN OFFSET
/// DETAIL (`obsoleteOffsets`) and KEEPING the aggregate counters that feed
/// the cleaner's util% file-selection. JE itself documents that the offset
/// detail is "an optimization for exact cleaning, not required for
/// correctness": dropping it only reduces exact-cleaning precision, never
/// the aggregate util%. Cite: `UtilizationTracker.evictMemory`,
/// `TrackedFileSummary.reset`.
pub fn discard_obsolete_detail(&mut self) {
self.obsolete_offsets = Vec::new();
self.modified = true;
}
/// Returns the file number being tracked.
pub fn get_file_number(&self) -> u32 {
self.file_number
}
/// Returns a reference to the file summary.
pub fn get_summary(&self) -> &FileSummary {
&self.summary
}
/// Returns a mutable reference to the file summary.
pub fn get_summary_mut(&mut self) -> &mut FileSummary {
self.modified = true;
&mut self.summary
}
/// Returns whether this summary has been modified.
pub fn is_modified(&self) -> bool {
self.modified
}
/// Clears the modified flag.
pub fn clear_modified(&mut self) {
self.modified = false;
}
/// Tracks the given offset as obsolete.
///
/// Must be called under the log write latch in the full implementation.
pub fn add_obsolete_offset(&mut self, offset: u32) {
if !self.track_detail {
return;
}
self.obsolete_offsets.push(offset);
self.modified = true;
}
/// Returns a reference to the obsolete offsets.
pub fn get_obsolete_offsets(&self) -> &[u32] {
&self.obsolete_offsets
}
/// Returns whether detail tracking is enabled.
pub fn is_track_detail(&self) -> bool {
self.track_detail
}
/// Resets the summary and clears obsolete offsets.
pub fn reset(&mut self) {
self.summary.reset();
self.obsolete_offsets.clear();
self.modified = false;
}
/// Adds the totals and offsets from another tracked summary.
pub fn add_tracked_summary(&mut self, other: &TrackedFileSummary) {
self.summary.add(&other.summary);
if self.track_detail && other.track_detail {
self.obsolete_offsets.extend_from_slice(&other.obsolete_offsets);
}
self.modified = true;
}
/// Returns the number of obsolete offsets tracked.
pub fn obsolete_offset_count(&self) -> usize {
self.obsolete_offsets.len()
}
/// Returns an estimate of memory usage in bytes.
pub fn memory_size(&self) -> usize {
// Base struct size + vector capacity
std::mem::size_of::<Self>()
+ (self.obsolete_offsets.capacity() * std::mem::size_of::<u32>())
}
/// Returns the bytes of obsolete-offset DETAIL only, excluding the
/// fixed per-object overhead.
///
/// JE budgets only the detail, not the object overhead:
/// `TrackedFileSummary.getMemorySize` returns `memSize`, which is bumped
/// only in `trackObsolete` (`TFS_LIST_*_OVERHEAD`). The class comment
/// states: "We only bother to budget obsolete detail, not the overhead
/// for this object." The budget cap (`evict_memory`) uses this value so
/// that dropping all detail can actually bring a file under budget.
pub fn detail_memory_size(&self) -> usize {
self.obsolete_offsets.capacity() * std::mem::size_of::<u32>()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new() {
let tracked = TrackedFileSummary::new(42, true);
assert_eq!(tracked.get_file_number(), 42);
assert!(tracked.get_summary().is_empty());
assert!(tracked.get_obsolete_offsets().is_empty());
assert!(!tracked.is_modified());
assert!(tracked.is_track_detail());
}
#[test]
fn test_new_no_detail() {
let tracked = TrackedFileSummary::new(42, false);
assert!(!tracked.is_track_detail());
}
#[test]
fn test_add_obsolete_offset() {
let mut tracked = TrackedFileSummary::new(42, true);
tracked.add_obsolete_offset(100);
tracked.add_obsolete_offset(200);
tracked.add_obsolete_offset(300);
assert_eq!(tracked.obsolete_offset_count(), 3);
assert_eq!(tracked.get_obsolete_offsets(), &[100, 200, 300]);
assert!(tracked.is_modified());
}
#[test]
fn test_add_obsolete_offset_no_detail() {
let mut tracked = TrackedFileSummary::new(42, false);
tracked.add_obsolete_offset(100);
tracked.add_obsolete_offset(200);
// Should not track when detail is disabled
assert_eq!(tracked.obsolete_offset_count(), 0);
}
#[test]
fn test_modify_summary() {
let mut tracked = TrackedFileSummary::new(42, true);
assert!(!tracked.is_modified());
{
let summary = tracked.get_summary_mut();
summary.total_count = 10;
summary.total_size = 1000;
}
assert!(tracked.is_modified());
assert_eq!(tracked.get_summary().total_count, 10);
}
#[test]
fn test_clear_modified() {
let mut tracked = TrackedFileSummary::new(42, true);
tracked.add_obsolete_offset(100);
assert!(tracked.is_modified());
tracked.clear_modified();
assert!(!tracked.is_modified());
}
#[test]
fn test_reset() {
let mut tracked = TrackedFileSummary::new(42, true);
tracked.get_summary_mut().total_count = 10;
tracked.add_obsolete_offset(100);
tracked.add_obsolete_offset(200);
tracked.reset();
assert!(tracked.get_summary().is_empty());
assert_eq!(tracked.obsolete_offset_count(), 0);
assert!(!tracked.is_modified());
}
#[test]
fn test_add_tracked_summary() {
let mut tracked1 = TrackedFileSummary::new(42, true);
tracked1.get_summary_mut().total_count = 10;
tracked1.get_summary_mut().total_size = 1000;
tracked1.add_obsolete_offset(100);
let mut tracked2 = TrackedFileSummary::new(43, true);
tracked2.get_summary_mut().total_count = 5;
tracked2.get_summary_mut().total_size = 500;
tracked2.add_obsolete_offset(200);
tracked1.add_tracked_summary(&tracked2);
assert_eq!(tracked1.get_summary().total_count, 15);
assert_eq!(tracked1.get_summary().total_size, 1500);
assert_eq!(tracked1.obsolete_offset_count(), 2);
assert_eq!(tracked1.get_obsolete_offsets(), &[100, 200]);
assert!(tracked1.is_modified());
}
#[test]
fn test_add_tracked_summary_mixed_detail() {
let mut tracked1 = TrackedFileSummary::new(42, true);
tracked1.add_obsolete_offset(100);
let mut tracked2 = TrackedFileSummary::new(43, false);
tracked2.get_summary_mut().total_count = 5;
tracked1.add_tracked_summary(&tracked2);
// Should only have offset from tracked1
assert_eq!(tracked1.obsolete_offset_count(), 1);
}
#[test]
fn test_memory_size() {
let mut tracked = TrackedFileSummary::new(42, true);
let base_size = tracked.memory_size();
tracked.add_obsolete_offset(100);
tracked.add_obsolete_offset(200);
tracked.add_obsolete_offset(300);
// Memory size should increase with offsets
assert!(tracked.memory_size() >= base_size);
}
#[test]
fn test_clone() {
let mut tracked1 = TrackedFileSummary::new(42, true);
tracked1.get_summary_mut().total_count = 10;
tracked1.add_obsolete_offset(100);
tracked1.add_obsolete_offset(200);
let tracked2 = tracked1.clone();
assert_eq!(tracked2.get_file_number(), 42);
assert_eq!(tracked2.get_summary().total_count, 10);
assert_eq!(tracked2.obsolete_offset_count(), 2);
assert_eq!(tracked2.get_obsolete_offsets(), &[100, 200]);
}
#[test]
fn test_get_summary_immutable() {
let mut tracked = TrackedFileSummary::new(42, true);
tracked.get_summary_mut().total_count = 10;
tracked.clear_modified();
// Getting immutable reference should not set modified flag
let _summary = tracked.get_summary();
assert!(!tracked.is_modified());
}
}