Skip to main content

krishiv_plan/optimizer/
small_file.rs

1//! Small-file scan planner.
2
3/// Per-file metadata used by [`SmallFilePlanner`].
4#[non_exhaustive]
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct FileStats {
7    pub path: String,
8    pub size_bytes: u64,
9}
10
11/// Advice produced by [`SmallFilePlanner`]: a list of scan groups where each
12/// group of file paths should be handled by a single executor task.
13#[non_exhaustive]
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct SplitPlanAdvice {
16    /// Each inner `Vec` is one task's worth of files.
17    pub task_groups: Vec<Vec<String>>,
18}
19
20/// Plans scan parallelism for a set of files.
21///
22/// When individual files are smaller than `target_bytes`, multiple files are
23/// grouped into a single task so each task processes roughly `target_bytes` of
24/// data. Files larger than `target_bytes` each get their own task (splitting
25/// within a file is not yet supported).
26pub struct SmallFilePlanner {
27    target_bytes: u64,
28}
29
30impl SmallFilePlanner {
31    /// Create a planner with the given target bytes per task.
32    pub fn new(target_bytes: u64) -> Self {
33        Self { target_bytes }
34    }
35
36    /// Produce a scan plan for the given file list.
37    ///
38    /// Files are grouped greedily: accumulate until the next file would push the
39    /// group over `target_bytes`, then start a new group. This ensures each
40    /// group is at most `target_bytes + max_single_file_bytes`.
41    pub fn plan(&self, files: &[FileStats]) -> SplitPlanAdvice {
42        if files.is_empty() {
43            return SplitPlanAdvice {
44                task_groups: Vec::new(),
45            };
46        }
47
48        let mut groups: Vec<Vec<String>> = Vec::new();
49        let mut current: Vec<String> = Vec::new();
50        let mut current_bytes = 0u128;
51        let target_bytes = u128::from(self.target_bytes);
52
53        for file in files {
54            let file_bytes = u128::from(file.size_bytes);
55            if !current.is_empty() && current_bytes + file_bytes > target_bytes {
56                groups.push(std::mem::take(&mut current));
57                current_bytes = 0;
58            }
59            current.push(file.path.clone());
60            current_bytes += file_bytes;
61        }
62        if !current.is_empty() {
63            groups.push(current);
64        }
65
66        SplitPlanAdvice {
67            task_groups: groups,
68        }
69    }
70}