Execute one checkpoint tick: compute pages to flush per engine. Returns `(engine_name, pages_to_flush)` pairs. The caller is responsible for actually performing the I/O and calling `record_flush()` after completion. Returns empty vec if the tick interval hasn't elapsed or there are no dirty pages.
(&mut self)
| 127 | /// Returns empty vec if the tick interval hasn't elapsed or |
| 128 | /// there are no dirty pages. |
| 129 | pub fn tick(&mut self) -> Vec<(String, usize)> { |
| 130 | let now = Instant::now(); |
| 131 | |
| 132 | // Respect tick interval. |
| 133 | if let Some(last) = self.last_tick |
| 134 | && now.duration_since(last) < self.config.tick_interval |
| 135 | { |
| 136 | return Vec::new(); |
| 137 | } |
| 138 | self.last_tick = Some(now); |
| 139 | |
| 140 | let mut flush_plan = Vec::new(); |
| 141 | let mut budget_remaining = self.config.io_budget_bytes_per_tick; |
| 142 | // Assume 4 KiB per page for budget calculation. |
| 143 | let page_size = 4096; |
| 144 | |
| 145 | for engine in &self.engines { |
| 146 | if engine.dirty_pages == 0 { |
| 147 | continue; |
| 148 | } |
| 149 | let target = engine.pages_to_flush(&self.config); |
| 150 | let budget_pages = budget_remaining / page_size; |
| 151 | let actual = target.min(budget_pages); |
| 152 | if actual > 0 { |
| 153 | flush_plan.push((engine.engine_name.clone(), actual)); |
| 154 | budget_remaining = budget_remaining.saturating_sub(actual * page_size); |
| 155 | } |
| 156 | } |
| 157 | |
| 158 | if !flush_plan.is_empty() { |
| 159 | debug!( |
| 160 | engines = flush_plan.len(), |
| 161 | total_pages = flush_plan.iter().map(|(_, p)| p).sum::<usize>(), |
| 162 | "checkpoint tick: flushing" |
| 163 | ); |
| 164 | } |
| 165 | |
| 166 | flush_plan |
| 167 | } |
| 168 | |
| 169 | /// Record completed flush for an engine. |
| 170 | pub fn record_flush(&mut self, engine: &str, count: usize) { |