Line data Source code
1 : //! A tool for visualizing the arrangement of layerfiles within a timeline.
2 : //!
3 : //! It reads filenames from stdin and prints a svg on stdout. The image is a plot in
4 : //! page-lsn space, where every delta layer is a rectangle and every image layer is a
5 : //! thick line. Legend:
6 : //! - The x axis (left to right) represents page index.
7 : //! - The y axis represents LSN, growing upwards.
8 : //!
9 : //! Coordinates in both axis are compressed for better readability.
10 : //! (see <https://medium.com/algorithms-digest/coordinate-compression-2fff95326fb>)
11 : //!
12 : //! Example use:
13 : //! ```bash
14 : //! $ ls test_output/test_pgbench\[neon-45-684\]/repo/tenants/$TENANT/timelines/$TIMELINE | \
15 : //! $ grep "__" | cargo run --release --bin pagectl draw-timeline-dir > out.svg
16 : //! $ firefox out.svg
17 : //! ```
18 : //!
19 : //! This API was chosen so that we can easily work with filenames extracted from ssh,
20 : //! or from pageserver log files.
21 : //!
22 : //! TODO Consider shipping this as a grafana panel plugin:
23 : //! <https://grafana.com/tutorials/build-a-panel-plugin/>
24 : use anyhow::Result;
25 : use pageserver::repository::Key;
26 : use pageserver::METADATA_FILE_NAME;
27 : use std::cmp::Ordering;
28 : use std::io::{self, BufRead};
29 : use std::path::PathBuf;
30 : use std::str::FromStr;
31 : use std::{
32 : collections::{BTreeMap, BTreeSet},
33 : ops::Range,
34 : };
35 : use svg_fmt::{rectangle, rgb, BeginSvg, EndSvg, Fill, Stroke};
36 : use utils::{lsn::Lsn, project_git_version};
37 :
38 : project_git_version!(GIT_VERSION);
39 :
40 : // Map values to their compressed coordinate - the index the value
41 : // would have in a sorted and deduplicated list of all values.
42 0 : fn build_coordinate_compression_map<T: Ord + Copy>(coords: Vec<T>) -> BTreeMap<T, usize> {
43 0 : let set: BTreeSet<T> = coords.into_iter().collect();
44 0 :
45 0 : let mut map: BTreeMap<T, usize> = BTreeMap::new();
46 0 : for (i, e) in set.iter().enumerate() {
47 0 : map.insert(*e, i);
48 0 : }
49 :
50 0 : map
51 0 : }
52 :
53 0 : fn parse_filename(name: &str) -> (Range<Key>, Range<Lsn>) {
54 0 : let split: Vec<&str> = name.split("__").collect();
55 0 : let keys: Vec<&str> = split[0].split('-').collect();
56 0 : let mut lsns: Vec<&str> = split[1].split('-').collect();
57 0 : if lsns.len() == 1 {
58 0 : lsns.push(lsns[0]);
59 0 : }
60 :
61 0 : let keys = Key::from_hex(keys[0]).unwrap()..Key::from_hex(keys[1]).unwrap();
62 0 : let lsns = Lsn::from_hex(lsns[0]).unwrap()..Lsn::from_hex(lsns[1]).unwrap();
63 0 : (keys, lsns)
64 0 : }
65 :
66 0 : pub fn main() -> Result<()> {
67 0 : // Parse layer filenames from stdin
68 0 : let mut ranges: Vec<(Range<Key>, Range<Lsn>)> = vec![];
69 0 : let stdin = io::stdin();
70 0 : for line in stdin.lock().lines() {
71 0 : let line = line.unwrap();
72 0 : let line = PathBuf::from_str(&line).unwrap();
73 0 : let filename = line.file_name().unwrap();
74 0 : let filename = filename.to_str().unwrap();
75 0 : if filename == METADATA_FILE_NAME {
76 : // Don't try and parse "metadata" like a key-lsn range
77 0 : continue;
78 0 : }
79 0 : let range = parse_filename(filename);
80 0 : ranges.push(range);
81 : }
82 :
83 : // Collect all coordinates
84 0 : let mut keys: Vec<Key> = vec![];
85 0 : let mut lsns: Vec<Lsn> = vec![];
86 0 : for (keyr, lsnr) in &ranges {
87 0 : keys.push(keyr.start);
88 0 : keys.push(keyr.end);
89 0 : lsns.push(lsnr.start);
90 0 : lsns.push(lsnr.end);
91 0 : }
92 :
93 : // Analyze
94 0 : let key_map = build_coordinate_compression_map(keys);
95 0 : let lsn_map = build_coordinate_compression_map(lsns);
96 0 :
97 0 : // Initialize stats
98 0 : let mut num_deltas = 0;
99 0 : let mut num_images = 0;
100 0 :
101 0 : // Draw
102 0 : let stretch = 3.0; // Stretch out vertically for better visibility
103 0 : println!(
104 0 : "{}",
105 0 : BeginSvg {
106 0 : w: key_map.len() as f32,
107 0 : h: stretch * lsn_map.len() as f32
108 0 : }
109 0 : );
110 0 : for (keyr, lsnr) in &ranges {
111 0 : let key_start = *key_map.get(&keyr.start).unwrap();
112 0 : let key_end = *key_map.get(&keyr.end).unwrap();
113 0 : let key_diff = key_end - key_start;
114 0 : let lsn_max = lsn_map.len();
115 0 :
116 0 : if key_start >= key_end {
117 0 : panic!("Invalid key range {}-{}", key_start, key_end);
118 0 : }
119 0 :
120 0 : let lsn_start = *lsn_map.get(&lsnr.start).unwrap();
121 0 : let lsn_end = *lsn_map.get(&lsnr.end).unwrap();
122 0 :
123 0 : let mut lsn_diff = (lsn_end - lsn_start) as f32;
124 0 : let mut fill = Fill::None;
125 0 : let mut ymargin = 0.05 * lsn_diff; // Height-dependent margin to disambiguate overlapping deltas
126 0 : let xmargin = 0.05; // Height-dependent margin to disambiguate overlapping deltas
127 0 : let mut lsn_offset = 0.0;
128 0 :
129 0 : // Fill in and thicken rectangle if it's an
130 0 : // image layer so that we can see it.
131 0 : match lsn_start.cmp(&lsn_end) {
132 0 : Ordering::Less => num_deltas += 1,
133 0 : Ordering::Equal => {
134 0 : num_images += 1;
135 0 : lsn_diff = 0.3;
136 0 : lsn_offset = -lsn_diff / 2.0;
137 0 : ymargin = 0.05;
138 0 : fill = Fill::Color(rgb(0, 0, 0));
139 0 : }
140 0 : Ordering::Greater => panic!("Invalid lsn range {}-{}", lsn_start, lsn_end),
141 : }
142 :
143 0 : println!(
144 0 : " {}",
145 0 : rectangle(
146 0 : key_start as f32 + stretch * xmargin,
147 0 : stretch * (lsn_max as f32 - (lsn_end as f32 - ymargin - lsn_offset)),
148 0 : key_diff as f32 - stretch * 2.0 * xmargin,
149 0 : stretch * (lsn_diff - 2.0 * ymargin)
150 0 : )
151 0 : .fill(fill)
152 0 : .stroke(Stroke::Color(rgb(0, 0, 0), 0.1))
153 0 : .border_radius(0.4)
154 0 : );
155 : }
156 0 : println!("{}", EndSvg);
157 0 :
158 0 : eprintln!("num_images: {}", num_images);
159 0 : eprintln!("num_deltas: {}", num_deltas);
160 0 :
161 0 : Ok(())
162 0 : }
|