Merge pull request #33 from typst/cache-patterns

A test framework for incremental compilation
This commit is contained in:
Laurenz 2021-06-27 19:02:23 +02:00 committed by GitHub
commit d8d60207ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 309 additions and 59 deletions

View File

@ -108,6 +108,11 @@ impl Length {
self.raw + 1e-6 >= other.raw self.raw + 1e-6 >= other.raw
} }
/// Compares two lengths for whether they are approximately equal.
pub fn approx_eq(self, other: Self) -> bool {
self == other || (self - other).to_raw().abs() < 1e-6
}
/// Whether the length is zero. /// Whether the length is zero.
pub fn is_zero(self) -> bool { pub fn is_zero(self) -> bool {
self.raw == 0.0 self.raw == 0.0

View File

@ -56,6 +56,8 @@ struct GridLayouter<'a> {
cross: SpecAxis, cross: SpecAxis,
/// The axis of the main direction. /// The axis of the main direction.
main: SpecAxis, main: SpecAxis,
/// The original expand state of the target region.
expand: Spec<bool>,
/// The column tracks including gutter tracks. /// The column tracks including gutter tracks.
cols: Vec<TrackSizing>, cols: Vec<TrackSizing>,
/// The row tracks including gutter tracks. /// The row tracks including gutter tracks.
@ -135,7 +137,8 @@ impl<'a> GridLayouter<'a> {
let full = regions.current.get(main); let full = regions.current.get(main);
let rcols = vec![Length::zero(); cols.len()]; let rcols = vec![Length::zero(); cols.len()];
// We use the regions only for auto row measurement. // We use the regions only for auto row measurement and constraints.
let expand = regions.expand;
regions.expand = Gen::new(true, false).to_spec(main); regions.expand = Gen::new(true, false).to_spec(main);
Self { Self {
@ -144,8 +147,9 @@ impl<'a> GridLayouter<'a> {
cols, cols,
rows, rows,
children: &grid.children, children: &grid.children,
constraints: Constraints::new(regions.expand), constraints: Constraints::new(expand),
regions, regions,
expand,
rcols, rcols,
lrows: vec![], lrows: vec![],
full, full,
@ -506,7 +510,7 @@ impl<'a> GridLayouter<'a> {
self.used.main = Length::zero(); self.used.main = Length::zero();
self.fr = Fractional::zero(); self.fr = Fractional::zero();
self.finished.push(output.constrain(self.constraints)); self.finished.push(output.constrain(self.constraints));
self.constraints = Constraints::new(self.regions.expand); self.constraints = Constraints::new(self.expand);
} }
/// Get the node in the cell in column `x` and row `y`. /// Get the node in the cell in column `x` and row `y`.

View File

@ -1,4 +1,5 @@
use std::{collections::HashMap, ops::Deref}; use std::collections::{hash_map::Entry, HashMap};
use std::ops::Deref;
use super::*; use super::*;
@ -6,14 +7,18 @@ use super::*;
#[derive(Default, Debug, Clone)] #[derive(Default, Debug, Clone)]
pub struct LayoutCache { pub struct LayoutCache {
/// Maps from node hashes to the resulting frames and regions in which the /// Maps from node hashes to the resulting frames and regions in which the
/// frames are valid. /// frames are valid. The right hand side of the hash map is a vector of
pub frames: HashMap<u64, FramesEntry>, /// results because across one or more compilations, multiple different
/// layouts of the same node may have been requested.
pub frames: HashMap<u64, Vec<FramesEntry>>,
/// In how many compilations this cache has been used.
age: usize,
} }
impl LayoutCache { impl LayoutCache {
/// Create a new, empty layout cache. /// Create a new, empty layout cache.
pub fn new() -> Self { pub fn new() -> Self {
Self { frames: HashMap::new() } Self { frames: HashMap::new(), age: 0 }
} }
/// Clear the cache. /// Clear the cache.
@ -21,42 +26,143 @@ impl LayoutCache {
self.frames.clear(); self.frames.clear();
} }
/// Amount of items in the cache.
pub fn len(&self) -> usize {
self.frames.iter().map(|(_, e)| e.len()).sum()
}
/// Retains all elements for which the closure on the level returns `true`. /// Retains all elements for which the closure on the level returns `true`.
pub fn retain<F>(&mut self, mut f: F) pub fn retain<F>(&mut self, mut f: F)
where where
F: FnMut(usize) -> bool, F: FnMut(usize) -> bool,
{ {
self.frames.retain(|_, entry| f(entry.level)); for (_, entries) in self.frames.iter_mut() {
entries.retain(|entry| f(entry.level));
}
} }
/// Amount of items in the cache. /// Prepare the cache for the next round of compilation
pub fn len(&self) -> usize { pub fn turnaround(&mut self) {
self.frames.len() self.age += 1;
for entry in self.frames.iter_mut().flat_map(|(_, x)| x.iter_mut()) {
for i in 0 .. (entry.temperature.len() - 1) {
entry.temperature[i + 1] = entry.temperature[i];
}
entry.temperature[0] = 0;
entry.age += 1;
}
}
/// The amount of levels stored in the cache.
pub fn levels(&self) -> usize {
self.frames
.iter()
.flat_map(|(_, x)| x)
.map(|entry| entry.level + 1)
.max()
.unwrap_or(0)
}
/// Fetches the appropriate entry from the cache if there is any.
pub fn get(
&mut self,
hash: u64,
regions: Regions,
) -> Option<Vec<Constrained<Rc<Frame>>>> {
self.frames.get_mut(&hash).and_then(|frames| {
for frame in frames {
let res = frame.check(regions.clone());
if res.is_some() {
return res;
}
}
None
})
}
/// Inserts a new frame set into the cache.
pub fn insert(
&mut self,
hash: u64,
frames: Vec<Constrained<Rc<Frame>>>,
level: usize,
) {
let entry = FramesEntry::new(frames, level);
match self.frames.entry(hash) {
Entry::Occupied(o) => o.into_mut().push(entry),
Entry::Vacant(v) => {
v.insert(vec![entry]);
}
}
} }
} }
#[derive(Debug, Clone)]
/// Cached frames from past layouting. /// Cached frames from past layouting.
#[derive(Debug, Clone)]
pub struct FramesEntry { pub struct FramesEntry {
/// The cached frames for a node. /// The cached frames for a node.
pub frames: Vec<Constrained<Rc<Frame>>>, pub frames: Vec<Constrained<Rc<Frame>>>,
/// How nested the frame was in the context is was originally appearing in. /// How nested the frame was in the context is was originally appearing in.
pub level: usize, pub level: usize,
/// For how long the element already exists.
age: usize,
/// How much the element was accessed during the last five compilations, the
/// most recent one being the first element.
temperature: [usize; 5],
} }
impl FramesEntry { impl FramesEntry {
/// Construct a new instance.
pub fn new(frames: Vec<Constrained<Rc<Frame>>>, level: usize) -> Self {
Self {
frames,
level,
age: 1,
temperature: [0; 5],
}
}
/// Checks if the cached [`Frame`] is valid for the given regions. /// Checks if the cached [`Frame`] is valid for the given regions.
pub fn check(&self, mut regions: Regions) -> Option<Vec<Constrained<Rc<Frame>>>> { pub fn check(&mut self, mut regions: Regions) -> Option<Vec<Constrained<Rc<Frame>>>> {
for (i, frame) in self.frames.iter().enumerate() { for (i, frame) in self.frames.iter().enumerate() {
if (i != 0 && !regions.next()) || !frame.constraints.check(&regions) { if (i != 0 && !regions.next()) || !frame.constraints.check(&regions) {
return None; return None;
} }
} }
self.temperature[0] += 1;
Some(self.frames.clone()) Some(self.frames.clone())
} }
/// Get the amount of compilation cycles this item has remained in the
/// cache.
pub fn age(&self) -> usize {
self.age
}
/// Get the amount of consecutive cycles in which this item has not
/// been used.
pub fn cooldown(&self) -> usize {
let mut cycle = 0;
for &temp in &self.temperature[.. self.age] {
if temp > 0 {
return cycle;
}
cycle += 1;
}
cycle
}
/// Whether this element was used in the last compilation cycle.
pub fn hit(&self) -> bool {
self.temperature[0] != 0
}
} }
/// Describe regions that match them.
#[derive(Debug, Copy, Clone, PartialEq)] #[derive(Debug, Copy, Clone, PartialEq)]
pub struct Constraints { pub struct Constraints {
/// The minimum available length in the region. /// The minimum available length in the region.
@ -107,25 +213,40 @@ impl Constraints {
let base = regions.base.to_spec(); let base = regions.base.to_spec();
let current = regions.current.to_spec(); let current = regions.current.to_spec();
current.eq_by(&self.min, |x, y| y.map_or(true, |y| x >= &y)) current.eq_by(&self.min, |x, y| y.map_or(true, |y| x.fits(y)))
&& current.eq_by(&self.max, |x, y| y.map_or(true, |y| x < &y)) && current.eq_by(&self.max, |x, y| y.map_or(true, |y| x < &y))
&& current.eq_by(&self.exact, |x, y| y.map_or(true, |y| x == &y)) && current.eq_by(&self.exact, |x, y| y.map_or(true, |y| x.approx_eq(y)))
&& base.eq_by(&self.base, |x, y| y.map_or(true, |y| x == &y)) && base.eq_by(&self.base, |x, y| y.map_or(true, |y| x.approx_eq(y)))
} }
/// Changes all constraints by adding the argument to them if they are set. /// Changes all constraints by adding the `size` to them if they are `Some`.
pub fn mutate(&mut self, size: Size) { pub fn mutate(&mut self, size: Size, regions: &Regions) {
for x in &mut [self.min, self.max, self.exact, self.base] { for spec in std::array::IntoIter::new([
if let Some(horizontal) = x.horizontal.as_mut() { &mut self.min,
&mut self.max,
&mut self.exact,
&mut self.base,
]) {
if let Some(horizontal) = spec.horizontal.as_mut() {
*horizontal += size.width; *horizontal += size.width;
} }
if let Some(vertical) = x.vertical.as_mut() { if let Some(vertical) = spec.vertical.as_mut() {
*vertical += size.height; *vertical += size.height;
} }
} }
let current = regions.current.to_spec();
let base = regions.base.to_spec();
self.exact.horizontal.set_if_some(current.horizontal);
self.exact.vertical.set_if_some(current.vertical);
self.base.horizontal.set_if_some(base.horizontal);
self.base.vertical.set_if_some(base.vertical);
} }
} }
/// Carries an item that only applies to certain regions and the constraints
/// that describe these regions.
#[derive(Debug, Copy, Clone, PartialEq)] #[derive(Debug, Copy, Clone, PartialEq)]
pub struct Constrained<T> { pub struct Constrained<T> {
pub item: T, pub item: T,
@ -140,9 +261,19 @@ impl<T> Deref for Constrained<T> {
} }
} }
/// Extends length-related options by providing convenience methods for setting
/// minimum and maximum lengths on them, even if they are `None`.
pub trait OptionExt { pub trait OptionExt {
// Sets `other` as the value if the Option is `None` or if it contains a
// value larger than `other`.
fn set_min(&mut self, other: Length); fn set_min(&mut self, other: Length);
// Sets `other` as the value if the Option is `None` or if it contains a
// value smaller than `other`.
fn set_max(&mut self, other: Length); fn set_max(&mut self, other: Length);
/// Sets `other` as the value if the Option is `Some`.
fn set_if_some(&mut self, other: Length);
} }
impl OptionExt for Option<Length> { impl OptionExt for Option<Length> {
@ -159,4 +290,10 @@ impl OptionExt for Option<Length> {
None => *self = Some(other), None => *self = Some(other),
} }
} }
fn set_if_some(&mut self, other: Length) {
if self.is_some() {
*self = Some(other);
}
}
} }

View File

@ -102,18 +102,10 @@ impl Layout for AnyNode {
regions: &Regions, regions: &Regions,
) -> Vec<Constrained<Rc<Frame>>> { ) -> Vec<Constrained<Rc<Frame>>> {
ctx.level += 1; ctx.level += 1;
let frames = ctx let frames =
.cache ctx.cache.layout.get(self.hash, regions.clone()).unwrap_or_else(|| {
.layout
.frames
.get(&self.hash)
.and_then(|x| x.check(regions.clone()))
.unwrap_or_else(|| {
let frames = self.node.layout(ctx, regions); let frames = self.node.layout(ctx, regions);
ctx.cache.layout.frames.insert(self.hash, FramesEntry { ctx.cache.layout.insert(self.hash, frames.clone(), ctx.level - 1);
frames: frames.clone(),
level: ctx.level - 1,
});
frames frames
}); });
ctx.level -= 1; ctx.level -= 1;

View File

@ -15,9 +15,11 @@ impl Layout for PadNode {
ctx: &mut LayoutContext, ctx: &mut LayoutContext,
regions: &Regions, regions: &Regions,
) -> Vec<Constrained<Rc<Frame>>> { ) -> Vec<Constrained<Rc<Frame>>> {
let mut regions = regions.map(|size| size - self.padding.resolve(size).size()); let mut regions = regions.clone();
let mut frames = self.child.layout(
let mut frames = self.child.layout(ctx, &regions); ctx,
&regions.map(|size| size - self.padding.resolve(size).size()),
);
for frame in &mut frames { for frame in &mut frames {
let padded = solve(self.padding, frame.size); let padded = solve(self.padding, frame.size);
@ -28,7 +30,7 @@ impl Layout for PadNode {
let prev = std::mem::take(&mut frame.item); let prev = std::mem::take(&mut frame.item);
new.push_frame(origin, prev); new.push_frame(origin, prev);
frame.constraints.mutate(padding.size() * -1.0); frame.constraints.mutate(padding.size(), &regions);
if self.padding.left.is_relative() || self.padding.right.is_relative() { if self.padding.left.is_relative() || self.padding.right.is_relative() {
frame.constraints.base.horizontal = Some(regions.base.width); frame.constraints.base.horizontal = Some(regions.base.width);
@ -40,6 +42,7 @@ impl Layout for PadNode {
regions.next(); regions.next();
*Rc::make_mut(&mut frame.item) = new; *Rc::make_mut(&mut frame.item) = new;
} }
frames frames
} }
} }

View File

@ -190,6 +190,7 @@ impl<'a> ParLayouter<'a> {
// line cannot be broken up further. // line cannot be broken up further.
if !stack.regions.current.fits(line.size) { if !stack.regions.current.fits(line.size) {
if let Some((last_line, last_end)) = last.take() { if let Some((last_line, last_end)) = last.take() {
// The region must not fit this line for the result to be valid.
if !stack.regions.current.width.fits(line.size.width) { if !stack.regions.current.width.fits(line.size.width) {
stack.constraints.max.horizontal.set_min(line.size.width); stack.constraints.max.horizontal.set_min(line.size.width);
} }
@ -213,10 +214,31 @@ impl<'a> ParLayouter<'a> {
while !stack.regions.current.height.fits(line.size.height) while !stack.regions.current.height.fits(line.size.height)
&& !stack.regions.in_full_last() && !stack.regions.in_full_last()
{ {
stack.constraints.max.vertical.set_min(line.size.height); // Again, the line must not fit. It would if the space taken up
// plus the line height would fit, therefore the constraint
// below.
stack
.constraints
.max
.vertical
.set_min(stack.size.height + line.size.height);
stack.finish_region(ctx); stack.finish_region(ctx);
} }
// If the line does not fit vertically, we start a new region.
while !stack.regions.current.height.fits(line.size.height) {
if stack.regions.in_full_last() {
stack.overflowing = true;
break;
}
stack
.constraints
.max
.vertical
.set_min(stack.size.height + line.size.height);
stack.finish_region(ctx);
}
// If the line does not fit horizontally or we have a mandatory // If the line does not fit horizontally or we have a mandatory
// line break (i.e. due to "\n"), we push the line into the // line break (i.e. due to "\n"), we push the line into the
// stack. // stack.
@ -303,11 +325,13 @@ impl ParItem<'_> {
/// Stacks lines on top of each other. /// Stacks lines on top of each other.
struct LineStack<'a> { struct LineStack<'a> {
line_spacing: Length, line_spacing: Length,
full: Size,
regions: Regions, regions: Regions,
size: Size, size: Size,
lines: Vec<LineLayout<'a>>, lines: Vec<LineLayout<'a>>,
finished: Vec<Constrained<Rc<Frame>>>, finished: Vec<Constrained<Rc<Frame>>>,
constraints: Constraints, constraints: Constraints,
overflowing: bool,
} }
impl<'a> LineStack<'a> { impl<'a> LineStack<'a> {
@ -316,10 +340,12 @@ impl<'a> LineStack<'a> {
Self { Self {
line_spacing, line_spacing,
constraints: Constraints::new(regions.expand), constraints: Constraints::new(regions.expand),
full: regions.current,
regions, regions,
size: Size::zero(), size: Size::zero(),
lines: vec![], lines: vec![],
finished: vec![], finished: vec![],
overflowing: false,
} }
} }
@ -343,6 +369,12 @@ impl<'a> LineStack<'a> {
self.constraints.exact.horizontal = Some(self.regions.current.width); self.constraints.exact.horizontal = Some(self.regions.current.width);
} }
if self.overflowing {
self.constraints.min.vertical = None;
self.constraints.max.vertical = None;
self.constraints.exact = self.full.to_spec().map(Some);
}
let mut output = Frame::new(self.size, self.size.height); let mut output = Frame::new(self.size, self.size.height);
let mut offset = Length::zero(); let mut offset = Length::zero();
let mut first = true; let mut first = true;
@ -362,6 +394,7 @@ impl<'a> LineStack<'a> {
self.finished.push(output.constrain(self.constraints)); self.finished.push(output.constrain(self.constraints));
self.regions.next(); self.regions.next();
self.full = self.regions.current;
self.constraints = Constraints::new(self.regions.expand); self.constraints = Constraints::new(self.regions.expand);
self.size = Size::zero(); self.size = Size::zero();
} }

View File

@ -62,6 +62,8 @@ struct StackLayouter<'a> {
ruler: Align, ruler: Align,
/// The constraints for the current region. /// The constraints for the current region.
constraints: Constraints, constraints: Constraints,
/// Whether the last region can fit all the remaining content.
overflowing: bool,
/// Offset, alignment and frame for all children that fit into the current /// Offset, alignment and frame for all children that fit into the current
/// region. The exact positions are not known yet. /// region. The exact positions are not known yet.
frames: Vec<(Length, Gen<Align>, Rc<Frame>)>, frames: Vec<(Length, Gen<Align>, Rc<Frame>)>,
@ -87,11 +89,12 @@ impl<'a> StackLayouter<'a> {
stack, stack,
main, main,
expand, expand,
constraints: Constraints::new(regions.expand),
regions, regions,
full, full,
used: Gen::zero(), used: Gen::zero(),
ruler: Align::Start, ruler: Align::Start,
constraints: Constraints::new(expand),
overflowing: false,
frames: vec![], frames: vec![],
finished: vec![], finished: vec![],
} }
@ -142,10 +145,16 @@ impl<'a> StackLayouter<'a> {
} }
// Find a fitting region. // Find a fitting region.
while !self.regions.current.get(self.main).fits(size.main) while !self.regions.current.get(self.main).fits(size.main) {
&& !self.regions.in_full_last() if self.regions.in_full_last() {
{ self.overflowing = true;
self.constraints.max.get_mut(self.main).set_min(size.main); break;
}
self.constraints
.max
.get_mut(self.main)
.set_min(self.used.main + size.main);
self.finish_region(); self.finish_region();
} }
@ -188,7 +197,9 @@ impl<'a> StackLayouter<'a> {
// Make sure the stack's size satisfies the aspect ratio. // Make sure the stack's size satisfies the aspect ratio.
if let Some(aspect) = self.stack.aspect { if let Some(aspect) = self.stack.aspect {
self.constraints.exact = self.regions.current.to_spec().map(Some); self.constraints.exact = self.full.to_spec().map(Some);
self.constraints.min = Spec::splat(None);
self.constraints.max = Spec::splat(None);
let width = size let width = size
.width .width
.max(aspect.into_inner() * size.height) .max(aspect.into_inner() * size.height)
@ -198,6 +209,12 @@ impl<'a> StackLayouter<'a> {
size = Size::new(width, width / aspect.into_inner()); size = Size::new(width, width / aspect.into_inner());
} }
if self.overflowing {
self.constraints.min.vertical = None;
self.constraints.max.vertical = None;
self.constraints.exact = self.full.to_spec().map(Some);
}
let mut output = Frame::new(size, size.height); let mut output = Frame::new(size, size.height);
let mut first = true; let mut first = true;
@ -244,6 +261,6 @@ impl<'a> StackLayouter<'a> {
self.used = Gen::zero(); self.used = Gen::zero();
self.ruler = Align::Start; self.ruler = Align::Start;
self.finished.push(output.constrain(self.constraints)); self.finished.push(output.constrain(self.constraints));
self.constraints = Constraints::new(self.regions.expand); self.constraints = Constraints::new(expand);
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 264 B

After

Width:  |  Height:  |  Size: 325 B

View File

@ -7,3 +7,12 @@
rect(3cm, forest), rect(3cm, forest),
rect(1cm, conifer), rect(1cm, conifer),
) )
---
// Test overflowing stack.
#let rect(width, color) = rect(width: 1cm, height: 0.4cm, fill: color)
#box(height: 0.5cm, stack(
rect(3cm, forest),
rect(1cm, conifer),
))

View File

@ -16,13 +16,13 @@ use walkdir::WalkDir;
use typst::cache::Cache; use typst::cache::Cache;
use typst::color; use typst::color;
use typst::diag::{Diag, DiagSet, Level}; use typst::diag::{Diag, DiagSet, Level};
use typst::eval::{EvalContext, FuncArgs, FuncValue, Scope, Value}; use typst::eval::{eval, EvalContext, FuncArgs, FuncValue, Scope, Value};
use typst::exec::State; use typst::exec::{exec, State};
use typst::geom::{self, Length, Point, Sides, Size}; use typst::geom::{self, Length, Point, Sides, Size};
use typst::image::ImageId; use typst::image::ImageId;
use typst::layout::{Element, Fill, Frame, Shape, Text}; use typst::layout::{layout, Element, Fill, Frame, Shape, Text};
use typst::loading::FsLoader; use typst::loading::FsLoader;
use typst::parse::{LineMap, Scanner}; use typst::parse::{parse, LineMap, Scanner};
use typst::syntax::{Location, Pos}; use typst::syntax::{Location, Pos};
const TYP_DIR: &str = "./typ"; const TYP_DIR: &str = "./typ";
@ -224,14 +224,20 @@ fn test_part(
state.page.size = Size::new(Length::pt(120.0), Length::inf()); state.page.size = Size::new(Length::pt(120.0), Length::inf());
state.page.margins = Sides::splat(Some(Length::pt(10.0).into())); state.page.margins = Sides::splat(Some(Length::pt(10.0).into()));
// Clear cache between tests (for now). let parsed = parse(src);
cache.layout.clear(); let evaluated = eval(
loader,
cache,
Some(src_path),
Rc::new(parsed.output),
&scope,
);
let executed = exec(&evaluated.output.template, state.clone());
let mut layouted = layout(loader, cache, &executed.output);
let mut pass = typst::typeset(loader, cache, Some(src_path), &src, &scope, state); let mut diags = parsed.diags;
diags.extend(evaluated.diags);
if !compare_ref { diags.extend(executed.diags);
pass.output.clear();
}
let mut ok = true; let mut ok = true;
@ -247,11 +253,11 @@ fn test_part(
ok = false; ok = false;
} }
if pass.diags != ref_diags { if diags != ref_diags {
println!(" Subtest {} does not match expected diagnostics. ❌", i); println!(" Subtest {} does not match expected diagnostics. ❌", i);
ok = false; ok = false;
for diag in &pass.diags { for diag in &diags {
if !ref_diags.contains(diag) { if !ref_diags.contains(diag) {
print!(" Not annotated | "); print!(" Not annotated | ");
print_diag(diag, &map, lines); print_diag(diag, &map, lines);
@ -259,14 +265,58 @@ fn test_part(
} }
for diag in &ref_diags { for diag in &ref_diags {
if !pass.diags.contains(diag) { if !diags.contains(diag) {
print!(" Not emitted | "); print!(" Not emitted | ");
print_diag(diag, &map, lines); print_diag(diag, &map, lines);
} }
} }
} }
(ok, compare_ref, pass.output) let reference_cache = cache.layout.clone();
for level in 0 .. reference_cache.levels() {
cache.layout = reference_cache.clone();
cache.layout.retain(|x| x == level);
if cache.layout.frames.is_empty() {
continue;
}
cache.layout.turnaround();
let cached_result = layout(loader, cache, &executed.output);
let misses = cache
.layout
.frames
.iter()
.flat_map(|(_, e)| e)
.filter(|e| e.level == level && !e.hit() && e.age() == 2)
.count();
if misses > 0 {
ok = false;
println!(
" Recompilation had {} cache misses on level {} (Subtest {}) ❌",
misses, level, i
);
}
if cached_result != layouted {
ok = false;
println!(
" Recompilation of subtest {} differs from clean pass ❌",
i
);
}
}
cache.layout = reference_cache;
cache.layout.turnaround();
if !compare_ref {
layouted.clear();
}
(ok, compare_ref, layouted)
} }
fn parse_metadata(src: &str, map: &LineMap) -> (Option<bool>, DiagSet) { fn parse_metadata(src: &str, map: &LineMap) -> (Option<bool>, DiagSet) {