Skip to main content

max / makenotwork

7.2 KB · 174 lines History Blame Raw
1 //! Forward fence, the hand-written stylesheet ratchet.
2 //!
3 //! Every page ships both frontends: the hand-written sheets and the sheets
4 //! makeover-build generates. The hand-written half dwarfs the generated one and
5 //! is paid on every page load by every visitor, signed-out ones included, which
6 //! is the largest standing cost of the conversion.
7 //!
8 //! # Parked
9 //!
10 //! The ratchet below is `#[ignore]`d until the conversion is finished and the
11 //! duplicated CSS is gone. See the test's own doc comment for what turns it back
12 //! on. `the_two_lists_agree` is unaffected and still runs.
13 //!
14 //! # Why this seal exists rather than a note saying the same thing
15 //!
16 //! The conversion's payoff is the hand-written sheet shrinking. Without a
17 //! number, "we converted a screen" and "we removed its cost" are the same
18 //! claim. A seal makes the second claim checkable, the same way
19 //! `frontend_globals` makes the script half
20 //! checkable and `DEAD_VOCABULARY_HIGH_WATER` (in `build.rs`) makes the
21 //! generated half checkable. This is the third of the three.
22 //!
23 //! # Kibibytes rather than bytes
24 //!
25 //! A byte-exact seal on a hand-edited file fails on whitespace, and a guard
26 //! that fires on a reflow is a guard people learn to edit rather than read. A
27 //! KiB is coarse enough that ordinary editing is free and fine enough that a
28 //! deleted rule block shows up.
29 //!
30 //! # One-sided, and which side
31 //!
32 //! Growing fails. Under the feature freeze, adding to a hand-written sheet
33 //! wants an argument, and the argument belongs in the commit that raises the
34 //! number rather than nowhere.
35 //!
36 //! Shrinking fails too, asking for the seal to be lowered. That is the half
37 //! that makes it a ratchet rather than a ceiling: a conversion batch that
38 //! deletes a screen's CSS and leaves the seal where it was has left the next
39 //! batch room to grow back into.
40 //!
41 //! `check_vocabulary_use` deliberately only warns on the way down, and the
42 //! reasoning does not carry here: dead vocabulary falls as a side effect of
43 //! work aimed elsewhere, and this number falls only when somebody deleted CSS
44 //! on purpose. Somebody who did that can lower a constant.
45
46 use std::fs;
47
48 /// Total hand-written CSS, in whole kibibytes, that this repo may ship.
49 ///
50 /// Nothing about this number is a target: it is the record of where the
51 /// conversion started, and it is what the finished conversion gets measured
52 /// against.
53 ///
54 /// Lower it whenever the total falls. Raising it is a decision, not a fix.
55 const CSS_KIB_HIGH_WATER: usize = 336;
56
57 /// The sheets this repo writes by hand.
58 ///
59 /// Kept in step with `HAND_WRITTEN_CSS` in `build.rs`, which is the list the
60 /// breakpoint and vocabulary guards read. A sheet in one list and not the other
61 /// is a sheet that can take bytes from a sealed neighbour and read as a
62 /// deletion, so `the_two_lists_agree` below holds them together.
63 ///
64 /// `layout.css`, `geometry.css` and `embed-geometry.css` are excluded because
65 /// they are generated: their size is makeover's answer, and shrinking them is
66 /// not this repo's work to do.
67 const SHEETS: [&str; 4] = [
68 "static/style.css",
69 "static/wizard.css",
70 "static/media-player.css",
71 "static/no-js.css",
72 ];
73
74 /// Total bytes across [`SHEETS`], and the per-sheet breakdown for the message.
75 fn measure() -> (usize, Vec<(&'static str, usize)>) {
76 let each: Vec<(&str, usize)> = SHEETS
77 .iter()
78 .map(|path| {
79 let bytes = fs::read(path)
80 .unwrap_or_else(|e| panic!("read {path}: {e}"))
81 .len();
82 (*path, bytes)
83 })
84 .collect();
85 (each.iter().map(|(_, bytes)| bytes).sum(), each)
86 }
87
88 /// Parked, and the module doc above argues for enforcing it, so the argument
89 /// gets answered rather than ignored.
90 ///
91 /// What the doc says: a guard people edit rather than read is not a guard, and
92 /// without a number "we converted a screen" and "we removed its cost" are the
93 /// same claim. Both points stand. What they do not survive is the assertion
94 /// pair below: this test fails on any decrease as well as any increase, so mid
95 /// conversion it fires on work going in the direction it exists to reward, and
96 /// the number gets re-baselined every batch. A seal that is re-baselined on
97 /// schedule measures nothing, and it teaches exactly the editing habit the doc
98 /// warns about.
99 ///
100 /// So the number is kept and the enforcement is suspended, rather than the seal
101 /// being quietly lowered batch by batch.
102 ///
103 /// WHAT BRINGS IT BACK: the conversion complete and the duplicated CSS removed.
104 /// At that point the number stops moving in both directions, and the decision
105 /// deferred with it becomes worth taking: seal the minified size rather than raw
106 /// bytes. `lightningcss` is already a dependency of this repo, so that costs no
107 /// new dependency.
108 #[test]
109 #[ignore = "parked until the conversion is finished and the duplicated CSS is gone: see this test's doc comment"]
110 fn hand_written_css_does_not_grow() {
111 let (total, each) = measure();
112 let kib = total / 1024;
113
114 let breakdown = each
115 .iter()
116 .map(|(path, bytes)| format!(" {path}: {bytes}"))
117 .collect::<Vec<_>>()
118 .join("\n");
119
120 assert!(
121 kib <= CSS_KIB_HIGH_WATER,
122 "hand-written CSS grew to {kib} KiB ({total} bytes), above the sealed \
123 {CSS_KIB_HIGH_WATER}:\n{breakdown}\n\n\
124 The conversion is supposed to move this number down. If a rule really \
125 belongs in a hand-written sheet rather than in makeover-webview, raise \
126 CSS_KIB_HIGH_WATER to {kib} and say why in the same commit."
127 );
128
129 assert_eq!(
130 kib, CSS_KIB_HIGH_WATER,
131 "hand-written CSS fell to {kib} KiB ({total} bytes). Lower \
132 CSS_KIB_HIGH_WATER to {kib} so it cannot grow back:\n{breakdown}"
133 );
134 }
135
136 #[test]
137 fn the_two_lists_agree() {
138 // `build.rs` is not a module this test can import, so the list is compared
139 // as text. Crude, and it is the only thing standing between the two lists
140 // drifting apart, which would let bytes move from a weighed sheet to an
141 // unweighed one and read as a deletion.
142 let build = fs::read_to_string("build.rs").expect("read build.rs");
143 let (start, _) = build
144 .split_once("const HAND_WRITTEN_CSS")
145 .expect("build.rs declares HAND_WRITTEN_CSS");
146 // On `= [` rather than `[`, because the declaration's first bracket is the
147 // type annotation (`: [&str; 4]`) and matching that reads the length back
148 // as the list.
149 let declared = &build[start.len()..];
150 let list = declared
151 .split_once("= [")
152 .and_then(|(_, rest)| rest.split_once(']'))
153 .map(|(inner, _)| inner)
154 .expect("HAND_WRITTEN_CSS is an array literal");
155
156 for sheet in SHEETS {
157 assert!(
158 list.contains(sheet),
159 "{sheet} is weighed by this seal but is not in build.rs's \
160 HAND_WRITTEN_CSS, so it is not breakpoint- or vocabulary-checked."
161 );
162 }
163
164 let in_build = list.matches("static/").count();
165 assert_eq!(
166 in_build,
167 SHEETS.len(),
168 "build.rs's HAND_WRITTEN_CSS lists {in_build} sheets and this seal \
169 weighs {}. A sheet in one list and not the other can take bytes from \
170 a sealed neighbour and read as a deletion.",
171 SHEETS.len()
172 );
173 }
174