Skip to main content

microvms_core/
sizing.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Size classes: what `minimumMemoryInMiB` actually buys, and what it bills.
3//!
4//! `minimumMemoryInMiB` does not size a VM. It selects a *class*, and the class has
5//! two numbers that differ by 4x — the baseline you are billed for every running
6//! second, and the peak the guest reports in `/proc/meminfo`. Measured 2026-08-07,
7//! us-east-1: requesting 512 produced a guest reporting ~2 GB `MemTotal`; requesting
8//! 2048 produced ~8 GB. Both match AWS's documented sizing table exactly, so the
9//! number the caller writes down is neither the number they get nor, on its own,
10//! enough to predict the bill (`docs/PLATFORM.md`, "`minimumMemoryInMiB` selects a
11//! *baseline*, and the guest reports the *peak*").
12//!
13//! The peak is provisioned from the start (service team, confirmed 2026-08): there
14//! is no scaling event, nothing resizes during a run, and app code never observes a
15//! resource change. The baseline is the bill's floor — always paid while the VM
16//! runs — and usage above it is billed by consumption.
17//!
18//! # Why the table is data (TRAP-13)
19//!
20//! Every documented peak is exactly four times its baseline, which makes
21//! `baseline * 4` look like the obvious simplification. It is the one thing this
22//! module must not do. That regularity is AWS's and not ours: a sixth row, or a
23//! revision to an existing one, that breaks the pattern would silently get the
24//! pattern applied to it, and the report would name a provisioned ceiling the service
25//! does not offer. So [`SIZE_CLASSES`] is the only place any of these numbers appears, and
26//! every accessor reads a row out of it through one lookup — which is also what makes
27//! the guard testable: the test below drives the accessors over a table whose peak is
28//! *not* 4x, and a computed implementation cannot answer it.
29//!
30//! # Why an off-table request is refused (TRAP-10)
31//!
32//! Only the five table values are accepted. What the service does with an off-table
33//! request such as 1500 is undocumented and unmeasured, and the two plausible
34//! behaviours — round up to the enclosing class, or take the literal as a baseline —
35//! differ in both the memory the guest gets and the rate it is billed at. Rejecting
36//! locally costs the caller a second; guessing costs them a wrong bill they have no
37//! way to notice.
38
39use std::fmt;
40
41use crate::error::Error;
42
43/// MiB per GB as AWS's pricing and sizing tables use it.
44///
45/// The guest's `MemTotal` reads in KiB and lands slightly under (2 GB shows as
46/// 2037648 kB), so any comparison against a guest-reported figure is approximate by
47/// nature.
48pub const MIB_PER_GB: u32 = 1024;
49
50/// One row of the documented sizing table: a baseline and its provisioned ceiling.
51///
52/// Plain data with public fields, because that is what it is — a transcription of
53/// someone else's published table, not settings. The accessors on [`SizeClass`] are
54/// where a caller normally reads it; the row is public so the drift gate and the
55/// guard test can compare against the table itself.
56#[derive(Clone, Copy, Debug, PartialEq)]
57pub struct SizeRow {
58    /// What `minimumMemoryInMiB` must be set to in order to select this class.
59    pub baseline_mib: u32,
60    /// Billed per running second, alongside `baseline_vcpu`. Not what the guest
61    /// reports.
62    pub baseline_vcpu: f64,
63    /// What the guest reports as `MemTotal`: a fixed ceiling, provisioned from the
64    /// start and present the whole run. Usage above the baseline is billed by
65    /// consumption, per second.
66    pub peak_mib: u32,
67    pub peak_vcpu: f64,
68}
69
70/// The documented table (`microvms-images.html`), smallest first.
71///
72/// The single definition of all twenty numbers. See the module docs for why nothing
73/// derives one of them from another.
74pub const SIZE_CLASSES: [SizeRow; 5] = [
75    SizeRow {
76        baseline_mib: 512,
77        baseline_vcpu: 0.25,
78        peak_mib: 2048,
79        peak_vcpu: 1.0,
80    },
81    SizeRow {
82        baseline_mib: 1024,
83        baseline_vcpu: 0.5,
84        peak_mib: 4096,
85        peak_vcpu: 2.0,
86    },
87    SizeRow {
88        baseline_mib: 2048,
89        baseline_vcpu: 1.0,
90        peak_mib: 8192,
91        peak_vcpu: 4.0,
92    },
93    SizeRow {
94        baseline_mib: 4096,
95        baseline_vcpu: 2.0,
96        peak_mib: 16384,
97        peak_vcpu: 8.0,
98    },
99    SizeRow {
100        baseline_mib: 8192,
101        baseline_vcpu: 4.0,
102        peak_mib: 32768,
103        peak_vcpu: 16.0,
104    },
105];
106
107/// One of the five documented size classes.
108///
109/// Closed, so an off-table memory figure is not a value that exists — the Python
110/// client's S2 runtime check (`sizing.py:95` `size_class_for`) becomes S1 for every
111/// caller who has a `SizeClass` in hand. [`SizeClass::from_baseline_mib`] is the one
112/// boundary where a bare integer still has to be judged, which is where TRAP-10
113/// lives.
114///
115/// The variants are named for their baseline because that is the number the caller
116/// writes into `minimumMemoryInMiB`; the peak is deliberately not in any name, since
117/// naming both would suggest a caller gets to pick them independently.
118#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
119pub enum SizeClass {
120    Mib512,
121    Mib1024,
122    Mib2048,
123    Mib4096,
124    Mib8192,
125}
126
127impl SizeClass {
128    /// The platform's own default, and ours.
129    ///
130    /// Deliberately not the smallest class, and the reason is the CEILING: picking
131    /// 512 gives the guest a hard 2 GiB total — provisioned from the start, fixed
132    /// for the whole run, nothing resizes past it — and a real test suite breaches
133    /// it. Guest swap is absent (`SwapTotal: 0 kB`), so there is no paging phase to
134    /// absorb the mistake — pressure goes straight to the OOM killer. The default's
135    /// job is a ceiling that survives a real test suite, and 8 GiB does.
136    pub const DEFAULT: SizeClass = SizeClass::Mib2048;
137
138    /// Every class, smallest first, in [`SIZE_CLASSES`] order.
139    pub const ALL: [SizeClass; 5] = [
140        SizeClass::Mib512,
141        SizeClass::Mib1024,
142        SizeClass::Mib2048,
143        SizeClass::Mib4096,
144        SizeClass::Mib8192,
145    ];
146
147    /// The class `minimumMemoryInMiB = mib` selects (TRAP-10).
148    ///
149    /// Rejects anything not in the table rather than snapping to a neighbour: an
150    /// off-table request has two plausible readings that differ in both memory and
151    /// rate, and we have measured neither. This is the boundary a figure from a CLI
152    /// flag or a config file crosses.
153    pub fn from_baseline_mib(mib: u32) -> Result<SizeClass, Error> {
154        if let Some(class) = class_for_baseline_in(&SIZE_CLASSES, mib) {
155            return Ok(class);
156        }
157        let offered = SIZE_CLASSES
158            .iter()
159            .map(|row| row.baseline_mib.to_string())
160            .collect::<Vec<_>>()
161            .join(", ");
162        Err(Error::invalid_arg(format!(
163            "minimumMemoryInMiB={mib} is not a documented size class baseline. The field \
164             selects a class, it does not size a VM; pass one of {offered} MiB. See \
165             docs/PLATFORM.md, '`minimumMemoryInMiB` selects a *baseline*, and the guest \
166             reports the *peak*'."
167        )))
168    }
169
170    /// This class's row of the documented table.
171    ///
172    /// The one reader of [`SIZE_CLASSES`] for a known class, which is what keeps
173    /// every accessor below reading data rather than deriving it.
174    pub fn row(self) -> &'static SizeRow {
175        row_in(&SIZE_CLASSES, self)
176    }
177
178    /// The value to send as `minimumMemoryInMiB`. Billed per running second.
179    pub fn baseline_mib(self) -> u32 {
180        self.row().baseline_mib
181    }
182
183    /// What the guest reports as `MemTotal`: the provisioned ceiling, present from
184    /// the start of the run.
185    ///
186    /// Read from the table. Never `baseline_mib() * 4` — see the module docs.
187    pub fn peak_mib(self) -> u32 {
188        self.row().peak_mib
189    }
190
191    /// The static headroom above the baseline: peak minus baseline, both read from
192    /// this class's table row.
193    ///
194    /// Fixed at provision time — the headroom is always present, never granted by a
195    /// scaling event — and usage inside it is billed by consumption. Computed as a
196    /// difference of two *table* values, never as `baseline * 3`: the 4x regularity
197    /// is AWS's, not ours, and a row that breaks it must be reported as written
198    /// (TRAP-13, same discipline as [`SizeClass::peak_mib`]).
199    pub fn headroom_mib(self) -> u32 {
200        headroom_in(&SIZE_CLASSES, self)
201    }
202
203    /// Billed per running second, alongside [`SizeClass::baseline_mib`].
204    pub fn baseline_vcpu(self) -> f64 {
205        self.row().baseline_vcpu
206    }
207
208    /// The provisioned vCPU ceiling, present from the start. Read from the table.
209    pub fn peak_vcpu(self) -> f64 {
210        self.row().peak_vcpu
211    }
212
213    /// The figure a GB-second rate multiplies. Always the baseline, never the peak
214    /// (COST-5).
215    pub fn baseline_gb(self) -> f64 {
216        f64::from(self.baseline_mib()) / f64::from(MIB_PER_GB)
217    }
218
219    /// The peak in GB, for a report that shows the caller what the guest will say.
220    pub fn peak_gb(self) -> f64 {
221        f64::from(self.peak_mib()) / f64::from(MIB_PER_GB)
222    }
223
224    /// The position of this class in [`SIZE_CLASSES`].
225    ///
226    /// Private and `const`: it is the identity mapping between the enum and the
227    /// table, and the invariant test below pins it to the baselines so a reordered
228    /// table cannot quietly re-point every variant.
229    const fn index(self) -> usize {
230        match self {
231            SizeClass::Mib512 => 0,
232            SizeClass::Mib1024 => 1,
233            SizeClass::Mib2048 => 2,
234            SizeClass::Mib4096 => 3,
235            SizeClass::Mib8192 => 4,
236        }
237    }
238}
239
240/// One line naming both numbers, for an error message or a CLI.
241///
242/// Both numbers or neither: naming only the peak invites someone to budget for
243/// memory they will not be billed for, and naming only the baseline invites a
244/// memory-pressure test written against a ceiling four times lower than the one the
245/// guest enforces.
246impl fmt::Display for SizeClass {
247    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
248        let row = self.row();
249        write!(
250            f,
251            "{} GB / {} vCPU baseline (billed while running), with a fixed ceiling of {} GB / \
252             {} vCPU provisioned from the start (what the guest reports)",
253            self.baseline_gb(),
254            row.baseline_vcpu,
255            self.peak_gb(),
256            row.peak_vcpu,
257        )
258    }
259}
260
261/// The row a class occupies in `table`.
262///
263/// Takes the table as a parameter for one reason: it is what makes TRAP-13's guard
264/// falsifiable. Every accessor above goes through here, so a test can hand this
265/// function a table whose peak is not four times its baseline and see whether the
266/// answer follows the data or the pattern.
267fn row_in(table: &[SizeRow], class: SizeClass) -> &SizeRow {
268    &table[class.index()]
269}
270
271/// The headroom `table`'s row carries for `class`: its peak minus its baseline.
272///
273/// Table-parameterized for the same reason as [`row_in`]: a test can hand this a
274/// table whose peak is not four times its baseline and see whether the headroom
275/// follows the data or the pattern. Saturating rather than plain subtraction so a
276/// hostile test table whose peak is below its baseline reads as zero headroom
277/// rather than a panic.
278fn headroom_in(table: &[SizeRow], class: SizeClass) -> u32 {
279    let row = row_in(table, class);
280    row.peak_mib.saturating_sub(row.baseline_mib)
281}
282
283/// The class whose row in `table` carries `baseline_mib`, if any.
284///
285/// Table-parameterized for the same reason as [`row_in`], and a linear scan because
286/// five rows is five rows.
287fn class_for_baseline_in(table: &[SizeRow], baseline_mib: u32) -> Option<SizeClass> {
288    SizeClass::ALL
289        .into_iter()
290        .find(|class| table[class.index()].baseline_mib == baseline_mib)
291}
292
293#[cfg(test)]
294mod tests {
295    use proptest::prelude::*;
296
297    use super::*;
298    use crate::error::ErrorKind;
299
300    /// The measured table, written out as literals. This is the test a table edit
301    /// must fail: the twenty numbers are transcriptions of AWS's published table and
302    /// two of the rows were measured directly, so changing one is a claim about the
303    /// service that needs its own evidence.
304    #[test]
305    fn the_documented_table_carries_the_measured_rows() {
306        let rows: Vec<(u32, f64, u32, f64)> = SIZE_CLASSES
307            .iter()
308            .map(|r| (r.baseline_mib, r.baseline_vcpu, r.peak_mib, r.peak_vcpu))
309            .collect();
310        assert_eq!(
311            rows,
312            [
313                (512, 0.25, 2048, 1.0),
314                (1024, 0.5, 4096, 2.0),
315                (2048, 1.0, 8192, 4.0),
316                (4096, 2.0, 16384, 8.0),
317                (8192, 4.0, 32768, 16.0),
318            ]
319        );
320    }
321
322    /// TRAP-13. The lookup answers from the data, so a table whose peak breaks the
323    /// 4x pattern is reported as written rather than as the pattern predicts.
324    ///
325    /// This is the falsification the spec asks for: replace [`SizeClass::peak_mib`]'s
326    /// body with `self.baseline_mib() * 4` and this row's peak comes back 8192
327    /// instead of 9000. A test that only checked the shipped table could not tell the
328    /// two implementations apart, because every shipped peak *is* 4x its baseline.
329    #[test]
330    fn a_peak_that_is_not_four_times_its_baseline_is_read_not_computed() {
331        let mut table = SIZE_CLASSES;
332        table[SizeClass::Mib2048.index()].peak_mib = 9000;
333        table[SizeClass::Mib2048.index()].peak_vcpu = 3.5;
334
335        let row = row_in(&table, SizeClass::Mib2048);
336        assert_eq!(
337            row.peak_mib, 9000,
338            "the peak must come from the table, not from baseline * 4"
339        );
340        assert_eq!(row.peak_vcpu, 3.5);
341        // The baseline is untouched by the edit, which is what makes the assertion
342        // above about the peak rather than about the row being found at all.
343        assert_eq!(row.baseline_mib, 2048);
344        assert_eq!(row.peak_mib * 4 / 4, 9000);
345        assert_ne!(row.peak_mib, row.baseline_mib * 4);
346    }
347
348    /// The public accessors are that lookup applied to the shipped table, and
349    /// nothing else. Under a computed implementation this passes today and goes red
350    /// the moment a table row stops being 4x — which is exactly the regression
351    /// TRAP-13 names.
352    #[test]
353    fn every_accessor_returns_its_own_row_from_the_shipped_table() {
354        for class in SizeClass::ALL {
355            let row = &SIZE_CLASSES[class.index()];
356            assert_eq!(class.baseline_mib(), row.baseline_mib, "{class:?}");
357            assert_eq!(class.peak_mib(), row.peak_mib, "{class:?}");
358            assert_eq!(class.baseline_vcpu(), row.baseline_vcpu, "{class:?}");
359            assert_eq!(class.peak_vcpu(), row.peak_vcpu, "{class:?}");
360        }
361    }
362
363    /// The enum-to-row mapping is pinned to the baselines rather than to positions,
364    /// so a reordered table fails here instead of quietly re-pointing every variant
365    /// at its neighbour's numbers.
366    #[test]
367    fn each_variant_is_pinned_to_the_baseline_its_name_states() {
368        assert_eq!(SizeClass::Mib512.baseline_mib(), 512);
369        assert_eq!(SizeClass::Mib1024.baseline_mib(), 1024);
370        assert_eq!(SizeClass::Mib2048.baseline_mib(), 2048);
371        assert_eq!(SizeClass::Mib4096.baseline_mib(), 4096);
372        assert_eq!(SizeClass::Mib8192.baseline_mib(), 8192);
373    }
374
375    /// TRAP-10: the five documented baselines are accepted and answer with their own
376    /// class.
377    #[test]
378    fn the_five_documented_baselines_select_their_own_class() {
379        for class in SizeClass::ALL {
380            let selected = SizeClass::from_baseline_mib(class.baseline_mib())
381                .expect("a documented baseline is accepted");
382            assert_eq!(selected, class);
383        }
384    }
385
386    /// TRAP-10's falsification, at the values a caller actually reaches for: a round
387    /// number between two classes, the peak of a class mistaken for a request, and
388    /// zero. 1500 is the case in `sizing.py`'s own docstring — the reading is
389    /// ambiguous in a way that changes the bill, so it is refused rather than
390    /// snapped.
391    #[test]
392    fn an_off_table_baseline_is_refused_naming_the_finding() {
393        for mib in [0, 1, 511, 513, 1500, 3072, 8191, 16384, u32::MAX] {
394            let err = SizeClass::from_baseline_mib(mib)
395                .expect_err("only the five documented baselines are accepted");
396            assert_eq!(err.kind(), ErrorKind::InvalidArg, "{mib}");
397            let message = err.to_string();
398            assert!(
399                message.contains("not a documented size class baseline"),
400                "{message}"
401            );
402            assert!(
403                message.contains("selects a class, it does not size a VM"),
404                "must name the finding: {message}"
405            );
406        }
407    }
408
409    /// A peak is not a baseline. 2048 is both — the peak of the smallest class and
410    /// the baseline of the default — and 8192 is both the peak of the default and the
411    /// largest baseline, so a caller who read a `MemTotal` and passed it back gets a
412    /// silently different class rather than an error. Nothing can be done about that
413    /// (both are legal requests), which is why `Display` always names both numbers.
414    /// 16384 and 32768 are peaks that are *not* baselines and must be refused.
415    #[test]
416    fn a_peak_that_is_not_also_a_baseline_is_refused() {
417        assert_eq!(
418            SizeClass::from_baseline_mib(2048).expect("2048 is a baseline too"),
419            SizeClass::Mib2048
420        );
421        for peak_only in [16384, 32768] {
422            assert!(
423                SizeClass::from_baseline_mib(peak_only).is_err(),
424                "{peak_only}"
425            );
426        }
427    }
428
429    /// The default is the platform's, not the cheapest, and the reason is the
430    /// ceiling: a default of `Mib512` gives the guest a hard 2 GB total —
431    /// provisioned from the start, fixed for the whole run — which a real test
432    /// suite breaches, and there is no swap to absorb the breach. The default's
433    /// job is a ceiling that survives real work.
434    #[test]
435    fn the_default_class_is_two_gigabytes_of_baseline() {
436        assert_eq!(SizeClass::DEFAULT, SizeClass::Mib2048);
437        assert_eq!(SizeClass::DEFAULT.baseline_mib(), 2048);
438        assert_eq!(SizeClass::DEFAULT.baseline_gb(), 2.0);
439        assert_eq!(SizeClass::DEFAULT.peak_gb(), 8.0);
440    }
441
442    /// Both numbers or neither, so nobody budgets for memory they are not billed for
443    /// and nobody writes a pressure test against the wrong ceiling.
444    #[test]
445    fn display_names_the_baseline_and_the_peak_together() {
446        let described = SizeClass::DEFAULT.to_string();
447        assert!(described.contains("2 GB / 1 vCPU baseline"), "{described}");
448        assert!(described.contains("8 GB / 4 vCPU"), "{described}");
449        assert!(described.contains("billed while running"), "{described}");
450        assert!(described.contains("what the guest reports"), "{described}");
451        // The mechanism, stated as it is: a fixed ceiling present from the start,
452        // not a "burst" the service would have to grant.
453        assert!(
454            described.contains("provisioned from the start"),
455            "{described}"
456        );
457        assert!(!described.contains("burst"), "{described}");
458    }
459
460    /// TRAP-13 for the headroom: peak minus baseline, both read from the table.
461    ///
462    /// The falsification: replace [`SizeClass::headroom_mib`]'s body with
463    /// `self.baseline_mib() * 3` — arithmetically identical over the shipped table,
464    /// where every peak is 4x — and this row's headroom comes back 6144 instead of
465    /// 6952. A test over the shipped table alone could not tell the two apart.
466    #[test]
467    fn headroom_follows_a_table_whose_peak_is_not_four_times_its_baseline() {
468        let mut table = SIZE_CLASSES;
469        table[SizeClass::Mib2048.index()].peak_mib = 9000;
470        assert_eq!(
471            headroom_in(&table, SizeClass::Mib2048),
472            9000 - 2048,
473            "headroom must be a difference of table values, not baseline * 3"
474        );
475        // The shipped table's headroom for every class, through the public accessor.
476        for class in SizeClass::ALL {
477            let row = &SIZE_CLASSES[class.index()];
478            assert_eq!(
479                class.headroom_mib(),
480                row.peak_mib - row.baseline_mib,
481                "{class:?}"
482            );
483        }
484    }
485
486    /// The band where an off-table figure is *plausible*, sampled deliberately.
487    ///
488    /// A uniform `u32` draw is the wrong generator for TRAP-10 and measuring it
489    /// proved so: with the whole domain to pick from, essentially every case lands
490    /// above 8192, where the only reachable verdict is "refused" and a
491    /// round-up-to-the-enclosing-class implementation is indistinguishable from a
492    /// refusing one. Snapping can only happen *between* the smallest and largest
493    /// baseline, so the property has to be told to look there. The band is what
494    /// a caller actually types.
495    fn plausible_baseline_mib() -> impl Strategy<Value = u32> {
496        prop_oneof![
497            // The whole domain, so nothing outside the band goes unchecked.
498            2 => any::<u32>(),
499            // The band where an enclosing class exists to snap to.
500            5 => 1u32..=8192,
501            // The neighbourhoods of each documented baseline, where an off-by-one or
502            // a nearest-neighbour rule would hide.
503            3 => prop::sample::select(SIZE_CLASSES.to_vec())
504                .prop_flat_map(|row| (row.baseline_mib.saturating_sub(2))..=(row.baseline_mib + 2)),
505        ]
506    }
507
508    proptest! {
509        /// The verdict, over the whole `u32` domain plus the band where an off-table
510        /// figure is plausible: accepted exactly when the figure is one of the five,
511        /// refused as `ERR_INVALID_ARG` otherwise. Asserting the verdict rather than a
512        /// side effect is the lesson from
513        /// `.erpaval/solutions/test-failures/proptest-and-dst-tiers-need-verdict-assertions.md`
514        /// — a property that only checked "no panic" would pass against a function
515        /// that accepted everything.
516        ///
517        /// The `Ok` arm's second assertion is the one that catches snapping: a
518        /// rounding implementation returns a class whose baseline is *not* the figure
519        /// it was handed, so `class.baseline_mib() == mib` is what says the answer is
520        /// the caller's request rather than a nearby one chosen for them.
521        #[test]
522        fn from_baseline_mib_accepts_exactly_the_five_documented_baselines(
523            mib in plausible_baseline_mib(),
524        ) {
525            let documented = SIZE_CLASSES.iter().any(|row| row.baseline_mib == mib);
526            match SizeClass::from_baseline_mib(mib) {
527                Ok(class) => {
528                    prop_assert!(documented, "{mib} MiB is not a documented baseline but was accepted");
529                    prop_assert_eq!(class.baseline_mib(), mib);
530                }
531                Err(err) => {
532                    prop_assert!(!documented, "{} MiB is documented but was refused", mib);
533                    prop_assert_eq!(err.kind(), ErrorKind::InvalidArg);
534                    prop_assert_eq!(err.code(), "ERR_INVALID_ARG");
535                }
536            }
537        }
538
539        /// Whatever a table says, the lookup says. Generated peaks are deliberately
540        /// unrelated to their baselines, so a computed implementation disagrees with
541        /// the table on almost every input.
542        #[test]
543        fn the_lookup_reports_whatever_peak_the_table_carries(
544            peak_mib in 1u32..100_000,
545            peak_vcpu in 0.1f64..64.0,
546        ) {
547            let mut table = SIZE_CLASSES;
548            for class in SizeClass::ALL {
549                table[class.index()].peak_mib = peak_mib;
550                table[class.index()].peak_vcpu = peak_vcpu;
551            }
552            for class in SizeClass::ALL {
553                let row = row_in(&table, class);
554                prop_assert_eq!(row.peak_mib, peak_mib);
555                prop_assert_eq!(row.peak_vcpu, peak_vcpu);
556                // The baselines are untouched, so the row is still the right row.
557                prop_assert_eq!(row.baseline_mib, class.baseline_mib());
558                prop_assert_eq!(
559                    class_for_baseline_in(&table, class.baseline_mib()),
560                    Some(class)
561                );
562            }
563        }
564    }
565}