qem/document/
compaction.rs1use super::*;
2
3const DEFAULT_COMPACTION_MIN_TOTAL_BYTES: usize = 1024 * 1024;
4const DEFAULT_COMPACTION_MIN_PIECES: usize = 1024;
5const DEFAULT_COMPACTION_SMALL_PIECE_BYTES: usize = 1024;
6const DEFAULT_COMPACTION_MAX_AVERAGE_PIECE_BYTES: usize = 4096;
7const DEFAULT_COMPACTION_MIN_RATIO: f64 = 0.35;
8const DEFAULT_COMPACTION_FORCED_PIECES: usize = 8192;
9const DEFAULT_COMPACTION_FORCED_RATIO: f64 = 0.50;
10
11#[derive(Clone, Copy, Debug, PartialEq)]
13pub struct CompactionPolicy {
14 pub min_total_bytes: usize,
16 pub min_piece_count: usize,
18 pub small_piece_threshold_bytes: usize,
20 pub max_average_piece_bytes: usize,
22 pub min_fragmentation_ratio: f64,
24 pub forced_piece_count: usize,
26 pub forced_fragmentation_ratio: f64,
28}
29
30impl Default for CompactionPolicy {
31 fn default() -> Self {
32 Self {
33 min_total_bytes: DEFAULT_COMPACTION_MIN_TOTAL_BYTES,
34 min_piece_count: DEFAULT_COMPACTION_MIN_PIECES,
35 small_piece_threshold_bytes: DEFAULT_COMPACTION_SMALL_PIECE_BYTES,
36 max_average_piece_bytes: DEFAULT_COMPACTION_MAX_AVERAGE_PIECE_BYTES,
37 min_fragmentation_ratio: DEFAULT_COMPACTION_MIN_RATIO,
38 forced_piece_count: DEFAULT_COMPACTION_FORCED_PIECES,
39 forced_fragmentation_ratio: DEFAULT_COMPACTION_FORCED_RATIO,
40 }
41 }
42}
43
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
46pub enum CompactionUrgency {
47 Deferred,
49 Forced,
51}
52
53#[derive(Clone, Copy, Debug, PartialEq)]
55pub struct CompactionRecommendation {
56 urgency: CompactionUrgency,
57 stats: FragmentationStats,
58}
59
60impl CompactionRecommendation {
61 pub fn urgency(self) -> CompactionUrgency {
63 self.urgency
64 }
65
66 pub fn stats(self) -> FragmentationStats {
68 self.stats
69 }
70}
71
72#[derive(Clone, Copy, Debug, PartialEq)]
74pub enum IdleCompactionOutcome {
75 NotNeeded,
77 Compacted(CompactionRecommendation),
79 ForcedPending(CompactionRecommendation),
82}
83
84impl IdleCompactionOutcome {
85 pub const fn recommendation(self) -> Option<CompactionRecommendation> {
87 match self {
88 Self::NotNeeded => None,
89 Self::Compacted(recommendation) | Self::ForcedPending(recommendation) => {
90 Some(recommendation)
91 }
92 }
93 }
94
95 pub const fn is_compacted(self) -> bool {
97 matches!(self, Self::Compacted(_))
98 }
99
100 pub const fn is_forced_pending(self) -> bool {
102 matches!(self, Self::ForcedPending(_))
103 }
104}
105
106impl Document {
107 pub fn compaction_recommendation(&self) -> Option<CompactionRecommendation> {
112 self.compaction_recommendation_with_policy(CompactionPolicy::default())
113 }
114
115 pub fn compaction_recommendation_with_policy(
117 &self,
118 policy: CompactionPolicy,
119 ) -> Option<CompactionRecommendation> {
120 self.piece_table
121 .as_ref()
122 .and_then(|piece_table| piece_table.compaction_recommendation(policy))
123 }
124
125 pub fn compact_piece_table(&mut self) -> io::Result<bool> {
132 let Some(piece_table) = self.piece_table.as_mut() else {
133 return Ok(false);
134 };
135 piece_table.compact_current_state()
136 }
137
138 pub fn compact_piece_table_if_recommended(
142 &mut self,
143 policy: CompactionPolicy,
144 ) -> io::Result<Option<CompactionRecommendation>> {
145 let Some(recommendation) = self.compaction_recommendation_with_policy(policy) else {
146 return Ok(None);
147 };
148 if self.compact_piece_table()? {
149 Ok(Some(recommendation))
150 } else {
151 Ok(None)
152 }
153 }
154
155 pub fn run_idle_compaction(&mut self) -> io::Result<IdleCompactionOutcome> {
162 self.run_idle_compaction_with_policy(CompactionPolicy::default())
163 }
164
165 pub fn run_idle_compaction_with_policy(
167 &mut self,
168 policy: CompactionPolicy,
169 ) -> io::Result<IdleCompactionOutcome> {
170 let Some(recommendation) = self.compaction_recommendation_with_policy(policy) else {
171 return Ok(IdleCompactionOutcome::NotNeeded);
172 };
173 if recommendation.urgency() == CompactionUrgency::Forced {
174 return Ok(IdleCompactionOutcome::ForcedPending(recommendation));
175 }
176 if self.compact_piece_table()? {
177 Ok(IdleCompactionOutcome::Compacted(recommendation))
178 } else {
179 Ok(IdleCompactionOutcome::NotNeeded)
180 }
181 }
182}
183
184impl PieceTable {
185 pub(super) fn compact_current_state(&mut self) -> io::Result<bool> {
186 let stats = self.fragmentation_stats();
187 if stats.piece_count <= 1 && self.full_index {
188 return Ok(false);
189 }
190
191 let mut compacted = self.read_range(0, self.total_len);
192 let compacted_len = compacted.len();
193 let line_breaks = count_line_breaks_in_bytes(&compacted);
194 let add_start = self.add.len();
195 self.add.append(&mut compacted);
196
197 let replacement = if compacted_len == 0 {
198 Vec::new()
199 } else {
200 vec![Piece {
201 src: PieceSource::Add,
202 start: add_start,
203 len: compacted_len,
204 line_breaks,
205 }]
206 };
207 self.pieces.replace_current_root_with_pieces(replacement);
208 self.total_len = compacted_len;
209 self.known_byte_len = compacted_len;
210 self.full_index = true;
211 self.refresh_known_line_count();
212 self.schedule_session_flush()?;
213 Ok(true)
214 }
215
216 pub(super) fn compaction_recommendation(
217 &self,
218 policy: CompactionPolicy,
219 ) -> Option<CompactionRecommendation> {
220 if self.total_len < policy.min_total_bytes {
221 return None;
222 }
223
224 let stats = self.fragmentation_stats_with_threshold(policy.small_piece_threshold_bytes);
225 if stats.piece_count < policy.min_piece_count {
226 return None;
227 }
228
229 let ratio = stats.fragmentation_ratio();
230 if stats.piece_count >= policy.forced_piece_count
231 && ratio >= policy.forced_fragmentation_ratio
232 {
233 return Some(CompactionRecommendation {
234 urgency: CompactionUrgency::Forced,
235 stats,
236 });
237 }
238
239 if ratio >= policy.min_fragmentation_ratio
240 && stats.average_piece_bytes() <= policy.max_average_piece_bytes as f64
241 {
242 return Some(CompactionRecommendation {
243 urgency: CompactionUrgency::Deferred,
244 stats,
245 });
246 }
247
248 None
249 }
250}