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 : //! The plain text API was chosen so that we can easily work with filenames from various
13 : //! sources; see the Usage section below for examples.
14 : //!
15 : //! # Usage
16 : //!
17 : //! ## Producing the SVG
18 : //!
19 : //! ```bash
20 : //!
21 : //! # local timeline dir
22 : //! ls test_output/test_pgbench\[neon-45-684\]/repo/tenants/$TENANT/timelines/$TIMELINE | \
23 : //! grep "__" | cargo run --release --bin pagectl draw-timeline-dir > out.svg
24 : //!
25 : //! # Layer map dump from `/v1/tenant/$TENANT/timeline/$TIMELINE/layer`
26 : //! (jq -r '.historic_layers[] | .layer_file_name' | cargo run -p pagectl draw-timeline) < layer-map.json > out.svg
27 : //!
28 : //! # From an `index_part.json` in S3
29 : //! (jq -r '.layer_metadata | keys[]' | cargo run -p pagectl draw-timeline ) < index_part.json-00000016 > out.svg
30 : //!
31 : //! ```
32 : //!
33 : //! ## Viewing
34 : //!
35 : //! **Inkscape** is better than the built-in viewers in browsers.
36 : //!
37 : //! After selecting a layer file rectangle, use "Open XML Editor" (Ctrl|Cmd + Shift + X)
38 : //! to see the layer file name in the comment field.
39 : //!
40 : //! ```bash
41 : //!
42 : //! # Linux
43 : //! inkscape out.svg
44 : //!
45 : //! # macOS
46 : //! /Applications/Inkscape.app/Contents/MacOS/inkscape out.svg
47 : //!
48 : //! ```
49 : //!
50 :
51 : use anyhow::Result;
52 : use pageserver::repository::Key;
53 : use pageserver::METADATA_FILE_NAME;
54 : use std::cmp::Ordering;
55 : use std::io::{self, BufRead};
56 : use std::path::PathBuf;
57 : use std::str::FromStr;
58 : use std::{
59 : collections::{BTreeMap, BTreeSet},
60 : ops::Range,
61 : };
62 : use svg_fmt::{rectangle, rgb, BeginSvg, EndSvg, Fill, Stroke};
63 : use utils::{lsn::Lsn, project_git_version};
64 :
65 : project_git_version!(GIT_VERSION);
66 :
67 : // Map values to their compressed coordinate - the index the value
68 : // would have in a sorted and deduplicated list of all values.
69 0 : fn build_coordinate_compression_map<T: Ord + Copy>(coords: Vec<T>) -> BTreeMap<T, usize> {
70 0 : let set: BTreeSet<T> = coords.into_iter().collect();
71 0 :
72 0 : let mut map: BTreeMap<T, usize> = BTreeMap::new();
73 0 : for (i, e) in set.iter().enumerate() {
74 0 : map.insert(*e, i);
75 0 : }
76 :
77 0 : map
78 0 : }
79 :
80 0 : fn parse_filename(name: &str) -> (Range<Key>, Range<Lsn>) {
81 0 : let split: Vec<&str> = name.split("__").collect();
82 0 : let keys: Vec<&str> = split[0].split('-').collect();
83 0 : let mut lsns: Vec<&str> = split[1].split('-').collect();
84 0 : if lsns.len() == 1 {
85 0 : lsns.push(lsns[0]);
86 0 : }
87 :
88 0 : let keys = Key::from_hex(keys[0]).unwrap()..Key::from_hex(keys[1]).unwrap();
89 0 : let lsns = Lsn::from_hex(lsns[0]).unwrap()..Lsn::from_hex(lsns[1]).unwrap();
90 0 : (keys, lsns)
91 0 : }
92 :
93 0 : pub fn main() -> Result<()> {
94 0 : // Parse layer filenames from stdin
95 0 : struct Layer {
96 0 : filename: String,
97 0 : key_range: Range<Key>,
98 0 : lsn_range: Range<Lsn>,
99 0 : }
100 0 : let mut files: Vec<Layer> = vec![];
101 0 : let stdin = io::stdin();
102 0 : for line in stdin.lock().lines() {
103 0 : let line = line.unwrap();
104 0 : let line = PathBuf::from_str(&line).unwrap();
105 0 : let filename = line.file_name().unwrap();
106 0 : let filename = filename.to_str().unwrap();
107 0 : if filename == METADATA_FILE_NAME {
108 : // Don't try and parse "metadata" like a key-lsn range
109 0 : continue;
110 0 : }
111 0 : let (key_range, lsn_range) = parse_filename(filename);
112 0 : files.push(Layer {
113 0 : filename: filename.to_owned(),
114 0 : key_range,
115 0 : lsn_range,
116 0 : });
117 : }
118 :
119 : // Collect all coordinates
120 0 : let mut keys: Vec<Key> = vec![];
121 0 : let mut lsns: Vec<Lsn> = vec![];
122 : for Layer {
123 0 : key_range: keyr,
124 0 : lsn_range: lsnr,
125 : ..
126 0 : } in &files
127 0 : {
128 0 : keys.push(keyr.start);
129 0 : keys.push(keyr.end);
130 0 : lsns.push(lsnr.start);
131 0 : lsns.push(lsnr.end);
132 0 : }
133 :
134 : // Analyze
135 0 : let key_map = build_coordinate_compression_map(keys);
136 0 : let lsn_map = build_coordinate_compression_map(lsns);
137 0 :
138 0 : // Initialize stats
139 0 : let mut num_deltas = 0;
140 0 : let mut num_images = 0;
141 0 :
142 0 : // Draw
143 0 : let stretch = 3.0; // Stretch out vertically for better visibility
144 0 : println!(
145 0 : "{}",
146 0 : BeginSvg {
147 0 : w: key_map.len() as f32,
148 0 : h: stretch * lsn_map.len() as f32
149 0 : }
150 0 : );
151 : for Layer {
152 0 : filename,
153 0 : key_range: keyr,
154 0 : lsn_range: lsnr,
155 0 : } in &files
156 : {
157 0 : let key_start = *key_map.get(&keyr.start).unwrap();
158 0 : let key_end = *key_map.get(&keyr.end).unwrap();
159 0 : let key_diff = key_end - key_start;
160 0 : let lsn_max = lsn_map.len();
161 0 :
162 0 : if key_start >= key_end {
163 0 : panic!("Invalid key range {}-{}", key_start, key_end);
164 0 : }
165 0 :
166 0 : let lsn_start = *lsn_map.get(&lsnr.start).unwrap();
167 0 : let lsn_end = *lsn_map.get(&lsnr.end).unwrap();
168 0 :
169 0 : let mut lsn_diff = (lsn_end - lsn_start) as f32;
170 0 : let mut fill = Fill::None;
171 0 : let mut ymargin = 0.05 * lsn_diff; // Height-dependent margin to disambiguate overlapping deltas
172 0 : let xmargin = 0.05; // Height-dependent margin to disambiguate overlapping deltas
173 0 : let mut lsn_offset = 0.0;
174 0 :
175 0 : // Fill in and thicken rectangle if it's an
176 0 : // image layer so that we can see it.
177 0 : match lsn_start.cmp(&lsn_end) {
178 0 : Ordering::Less => num_deltas += 1,
179 0 : Ordering::Equal => {
180 0 : num_images += 1;
181 0 : lsn_diff = 0.3;
182 0 : lsn_offset = -lsn_diff / 2.0;
183 0 : ymargin = 0.05;
184 0 : fill = Fill::Color(rgb(0, 0, 0));
185 0 : }
186 0 : Ordering::Greater => panic!("Invalid lsn range {}-{}", lsn_start, lsn_end),
187 : }
188 :
189 0 : println!(
190 0 : " {}",
191 0 : rectangle(
192 0 : key_start as f32 + stretch * xmargin,
193 0 : stretch * (lsn_max as f32 - (lsn_end as f32 - ymargin - lsn_offset)),
194 0 : key_diff as f32 - stretch * 2.0 * xmargin,
195 0 : stretch * (lsn_diff - 2.0 * ymargin)
196 0 : )
197 0 : .fill(fill)
198 0 : .stroke(Stroke::Color(rgb(0, 0, 0), 0.1))
199 0 : .border_radius(0.4)
200 0 : .comment(filename)
201 0 : );
202 : }
203 0 : println!("{}", EndSvg);
204 0 :
205 0 : eprintln!("num_images: {}", num_images);
206 0 : eprintln!("num_deltas: {}", num_deltas);
207 0 :
208 0 : Ok(())
209 0 : }
|