1use std::fmt::{self, Display, Formatter};
4
5use crate::{Axis, ComputeScope, Coverage, Level, Partitioning, Space, Spread};
6
7const LEAF: char = '◦';
9const MARGIN: &str = " ";
10const LEVEL: &str = " ";
11const TIMES: &str = " × ";
12const GAP: &str = " ";
13
14pub struct LevelTable<'a> {
16 partitioning: &'a Partitioning,
17 labels: &'a [(Axis, &'a str)],
18}
19
20impl<'a> LevelTable<'a> {
21 pub(crate) fn new(partitioning: &'a Partitioning, labels: &'a [(Axis, &'a str)]) -> Self {
22 LevelTable {
23 partitioning,
24 labels,
25 }
26 }
27
28 fn label(&self, axis: Axis) -> String {
29 match self.labels.iter().find(|&&(named, _)| named == axis) {
30 Some(&(_, name)) => name.to_string(),
31 None => format!("a{}", axis.0),
32 }
33 }
34
35 fn rows(&self) -> Vec<Row> {
37 let axes: Vec<Axis> = self.partitioning.space().axes().collect();
38 let mut space = self.partitioning.space().clone();
39 let mut rows: Vec<Row> = self
40 .partitioning
41 .levels()
42 .iter()
43 .map(|level| {
44 let row = Row::of(level, &space, &axes);
45 space = level.child(&space);
46 row
47 })
48 .collect();
49 rows.reverse();
50 rows.insert(0, Row::leaf(&space, &axes));
51 rows
52 }
53}
54
55impl Display for LevelTable<'_> {
56 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
57 let axes: Vec<Axis> = self.partitioning.space().axes().collect();
58 let header: Vec<String> = axes.iter().map(|&axis| self.label(axis)).collect();
59 let rows = self.rows();
60
61 let counts = Block::new("count", &header, rows.iter().map(|row| &row.counts));
62 let tiles = Block::new("tile", &header, rows.iter().map(|row| &row.tile));
63 let coverage_wide = rows
64 .iter()
65 .map(|row| row.coverage.chars().count())
66 .max()
67 .unwrap_or(0)
68 + GAP.chars().count();
69 let indent = format!("{MARGIN} {LEVEL}{:coverage_wide$}", "");
70
71 writeln!(
72 f,
73 "{indent}{}{}{GAP}{}{}",
74 counts.indent(),
75 counts.line(&header),
76 tiles.indent(),
77 tiles.line(&header)
78 )?;
79 writeln!(f)?;
80 for row in &rows {
81 writeln!(
82 f,
83 "{MARGIN}{}{LEVEL}{:coverage_wide$}{}{}{GAP}{}{}",
84 row.glyph,
85 row.coverage,
86 counts.indent(),
87 counts.line(&row.counts),
88 tiles.indent(),
89 tiles.line(&row.tile)
90 )?;
91 }
92 writeln!(f)?;
93 write!(f, "{indent}{}{GAP}{}", counts.rule(), tiles.rule())
94 }
95}
96
97impl Display for Partitioning {
99 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
100 self.table(&[]).fmt(f)
101 }
102}
103
104struct Row {
106 glyph: char,
107 coverage: String,
108 counts: Vec<String>,
109 tile: Vec<String>,
110}
111
112impl Row {
113 fn of(level: &Level, space: &Space, axes: &[Axis]) -> Row {
114 Row {
115 glyph: Row::glyph(level.coverage()),
116 coverage: Row::coverage(level, space),
117 counts: axes
118 .iter()
119 .map(|&axis| Row::count(level, space, axis))
120 .collect(),
121 tile: axes.iter().map(|&axis| Row::extent(space, axis)).collect(),
122 }
123 }
124
125 fn leaf(space: &Space, axes: &[Axis]) -> Row {
127 Row {
128 glyph: LEAF,
129 coverage: String::new(),
130 counts: axes.iter().map(|_| "·".to_string()).collect(),
131 tile: axes.iter().map(|&axis| Row::extent(space, axis)).collect(),
132 }
133 }
134
135 fn glyph(coverage: Coverage) -> char {
137 match coverage {
138 Coverage::Distribute(ComputeScope::Cube) => '▣',
139 Coverage::Distribute(ComputeScope::Plane) => '▤',
140 Coverage::Distribute(ComputeScope::PlaneGroup { .. }) => '▥',
141 Coverage::Distribute(ComputeScope::Unit) => '▪',
142 Coverage::Walk => '↻',
143 }
144 }
145
146 fn coverage(level: &Level, space: &Space) -> String {
148 let counts: Vec<Option<usize>> = level
149 .axes()
150 .iter()
151 .map(|&axis| level.tiles_const(space, axis))
152 .collect();
153 let total = counts
154 .iter()
155 .try_fold(1usize, |acc, count| count.map(|n| acc * n));
156 let many = match total {
157 Some(n) => n.to_string(),
158 None => "?".to_string(),
159 };
160 let interleaved = level
161 .axes()
162 .iter()
163 .any(|&axis| level.spread(axis) == Some(Spread::Interleaved));
164 let mut note = match level.coverage() {
165 Coverage::Distribute(ComputeScope::Cube) => match level.shared_by() {
166 Some(cubes) => format!("{cubes} cubes sharing {many} boxes"),
167 None => format!("{many} cubes"),
168 },
169 Coverage::Distribute(ComputeScope::Plane) => match level.shared_by() {
170 Some(planes) => format!("{planes} planes sharing {many} boxes"),
171 None => format!("{many} planes a cube"),
172 },
173 Coverage::Distribute(ComputeScope::PlaneGroup { planes }) => match level.shared_by() {
174 Some(groups) => format!("{groups} groups of {planes} planes sharing {many} boxes"),
175 None => format!("{many} groups of {planes} planes a cube"),
176 },
177 Coverage::Distribute(ComputeScope::Unit) => format!("{many} units"),
178 Coverage::Walk => format!("{many} steps"),
179 };
180 if interleaved {
181 note += " interleaved";
182 }
183 match level.fillers() {
184 0 => {}
185 1 => note += ", 1 filling plane",
186 n => note += &format!(", {n} filling planes"),
187 }
188 note
189 }
190
191 fn count(level: &Level, space: &Space, axis: Axis) -> String {
193 match level.count(axis) {
194 None => "·".to_string(),
195 Some(_) => match level.tiles_const(space, axis) {
196 Some(tiles) => tiles.to_string(),
197 None => "?".to_string(),
198 },
199 }
200 }
201
202 fn extent(space: &Space, axis: Axis) -> String {
204 match space.is_dynamic(axis) {
205 true => "?".to_string(),
206 false => space.extent(axis).to_string(),
207 }
208 }
209}
210
211struct Block<'n> {
214 name: &'n str,
215 widths: Vec<usize>,
217}
218
219impl<'n> Block<'n> {
220 fn new<'c>(
221 name: &'n str,
222 header: &[String],
223 cells: impl Iterator<Item = &'c Vec<String>>,
224 ) -> Self {
225 let widths = cells.fold(
226 header.iter().map(|label| label.chars().count()).collect(),
227 |widest: Vec<usize>, row| {
228 widest
229 .iter()
230 .zip(row)
231 .map(|(&widest, cell)| widest.max(cell.chars().count()))
232 .collect()
233 },
234 );
235 Block { name, widths }
236 }
237
238 fn line(&self, cells: &[String]) -> String {
240 cells
241 .iter()
242 .zip(&self.widths)
243 .map(|(cell, &width)| format!("{cell:>width$}"))
244 .collect::<Vec<_>>()
245 .join(TIMES)
246 }
247
248 fn rule(&self) -> String {
250 format!(
251 "└─ {} {}┘",
252 self.name,
253 "─".repeat(self.wide() - self.ruled() + 1)
254 )
255 }
256
257 fn indent(&self) -> String {
259 " ".repeat(self.wide() - self.spanned())
260 }
261
262 fn wide(&self) -> usize {
264 self.spanned().max(self.ruled())
265 }
266
267 fn spanned(&self) -> usize {
269 self.widths.iter().sum::<usize>()
270 + TIMES.chars().count() * (self.widths.len().saturating_sub(1))
271 }
272
273 fn ruled(&self) -> usize {
275 self.name.chars().count() + 6
276 }
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282 use crate::{Levels, Space};
283
284 const B: Axis = Axis(0);
285 const M: Axis = Axis(1);
286 const N: Axis = Axis(2);
287 const K: Axis = Axis(3);
288
289 fn staged() -> Partitioning {
291 Partitioning::new(
292 Space::new(&[(B, 4), (M, 512), (N, 1024), (K, 4096)]),
293 Levels::leaf(&[(M, 16), (N, 16), (K, 16)])
294 .walk(&[(M, 2), (N, 2)])
295 .walk(&[(K, 2)])
296 .planes(&[(M, 2), (N, 2)])
297 .walk_every(&[K])
298 .cubes(&[M, N])
299 .batches(&[B])
300 .build(),
301 )
302 }
303
304 #[test]
305 fn a_row_is_the_row_below_it_times_its_count() {
306 let labels = [(B, "b"), (M, "m"), (N, "n"), (K, "k")];
307
308 assert_eq!(
309 staged().table(&labels).to_string(),
310 [
311 " b × m × n × k b × m × n × k",
312 "",
313 " ◦ · × · × · × · 1 × 16 × 16 × 16",
314 " ↻ 4 steps · × 2 × 2 × · 1 × 32 × 32 × 16",
315 " ↻ 2 steps · × · × · × 2 1 × 32 × 32 × 32",
316 " ▤ 4 planes a cube · × 2 × 2 × · 1 × 64 × 64 × 32",
317 " ↻ 128 steps · × · × · × 128 1 × 64 × 64 × 4096",
318 " ▣ 512 cubes 4 × 8 × 16 × · 4 × 512 × 1024 × 4096",
319 "",
320 " └─ count ──────┘ └─ tile ────────────┘",
321 ]
322 .join("\n")
323 );
324 }
325
326 #[test]
328 fn an_unlabelled_axis_prints_its_index() {
329 let table = staged().to_string();
330
331 assert!(table.contains("a0 × a1 × a2 × a3"), "{table}");
332 }
333
334 #[test]
336 fn a_dynamic_axis_prints_a_question() {
337 let partitioning = Partitioning::new(
338 Space::new(&[(M, 512), (K, 4096)]).with_dynamic(&[K]),
339 Levels::leaf(&[(M, 64), (K, 32)])
340 .walk_every(&[K])
341 .cubes(&[M])
342 .build(),
343 );
344 let table = partitioning.table(&[(M, "m"), (K, "k")]).to_string();
345
346 assert!(table.lines().any(|line| line.contains("× ?")), "{table}");
347 }
348
349 #[test]
351 fn a_shared_level_still_prints_as_the_cube_grid() {
352 let partitioning = Partitioning::new(
353 Space::new(&[(M, 512), (N, 1024)]),
354 Levels::leaf(&[(M, 64), (N, 64)])
355 .cubes(&[M, N])
356 .shared_by(8)
357 .build(),
358 );
359
360 assert!(partitioning.to_string().contains('▣'));
361 }
362
363 #[test]
365 fn an_overhanging_axis_counts_its_partial_tile() {
366 let partitioning = Partitioning::new(
367 Space::new(&[(M, 500), (K, 4096)]),
368 Levels::leaf(&[(M, 128), (K, 32)])
369 .walk_every(&[K])
370 .cubes(&[M])
371 .build(),
372 );
373
374 assert_eq!(
375 partitioning.table(&[(M, "m"), (K, "k")]).to_string(),
376 [
377 " m × k m × k",
378 "",
379 " ◦ · × · 128 × 32",
380 " ↻ 128 steps · × 128 128 × 4096",
381 " ▣ 4 cubes 4 × · 500 × 4096",
382 "",
383 " └─ count ─┘ └─ tile ─┘",
384 ]
385 .join("\n")
386 );
387 }
388}