use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::sync::Arc;
use alloc::vec::Vec;
use core::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use crate::image::format::DecodedImage;
pub const DEFAULT_BUDGET_BYTES: usize = 64 * 1024 * 1024;
struct Entry {
image: Arc<DecodedImage>,
bytes: usize,
used_at: u64,
}
struct PixelEntry {
pixels: Arc<Vec<u8>>,
bytes: usize,
used_at: u64,
}
struct SourceEntry {
bytes: Arc<Vec<u8>>,
size: usize,
stamp: SourceStamp,
used_at: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct SourceStamp {
len: u64,
modified_nanos: u128,
}
enum Victim {
Image(u64),
Pixels(u64),
Source(alloc::vec::Vec<u8>),
}
struct DecodeCache {
entries: BTreeMap<u64, Entry>,
pixels: BTreeMap<u64, PixelEntry>,
sources: BTreeMap<alloc::vec::Vec<u8>, SourceEntry>,
used_bytes: usize,
budget_bytes: usize,
tick: u64,
evictions: u64,
}
impl DecodeCache {
fn new(budget_bytes: usize) -> Self {
Self {
entries: BTreeMap::new(),
pixels: BTreeMap::new(),
sources: BTreeMap::new(),
used_bytes: 0,
budget_bytes,
tick: 0,
evictions: 0,
}
}
fn get(&mut self, key: u64) -> Option<Arc<DecodedImage>> {
self.tick += 1;
let tick = self.tick;
let entry = self.entries.get_mut(&key)?;
entry.used_at = tick;
Some(Arc::clone(&entry.image))
}
fn get_pixels(&mut self, key: u64) -> Option<Arc<Vec<u8>>> {
self.tick += 1;
let tick = self.tick;
let entry = self.pixels.get_mut(&key)?;
entry.used_at = tick;
Some(Arc::clone(&entry.pixels))
}
fn get_source(&mut self, path_key: &[u8], stamp: SourceStamp) -> Option<Arc<Vec<u8>>> {
self.tick += 1;
let tick = self.tick;
let entry = self.sources.get_mut(path_key)?;
if entry.stamp != stamp {
if let Some(old) = self.sources.remove(path_key) {
self.used_bytes = self.used_bytes.saturating_sub(old.size);
}
return None;
}
entry.used_at = tick;
Some(Arc::clone(&entry.bytes))
}
fn insert_source(
&mut self,
path_key: alloc::vec::Vec<u8>,
bytes: Arc<Vec<u8>>,
stamp: SourceStamp,
) {
let size = bytes.len();
if size > self.budget_bytes {
return;
}
if let Some(old) = self.sources.remove(&path_key) {
self.used_bytes = self.used_bytes.saturating_sub(old.size);
}
self.evict_until_fits(size);
self.tick += 1;
let used_at = self.tick;
self.sources.insert(path_key, SourceEntry { bytes, size, stamp, used_at });
self.used_bytes += size;
}
fn insert(&mut self, key: u64, image: Arc<DecodedImage>, bytes: usize) {
if bytes > self.budget_bytes {
return;
}
if let Some(old) = self.entries.remove(&key) {
self.used_bytes = self.used_bytes.saturating_sub(old.bytes);
}
self.evict_until_fits(bytes);
self.tick += 1;
let used_at = self.tick;
self.entries.insert(key, Entry { image, bytes, used_at });
self.used_bytes += bytes;
}
fn insert_pixels(&mut self, key: u64, pixels: Arc<Vec<u8>>, bytes: usize) {
if bytes > self.budget_bytes {
return;
}
if let Some(old) = self.pixels.remove(&key) {
self.used_bytes = self.used_bytes.saturating_sub(old.bytes);
}
self.evict_until_fits(bytes);
self.tick += 1;
let used_at = self.tick;
self.pixels.insert(key, PixelEntry { pixels, bytes, used_at });
self.used_bytes += bytes;
}
fn evict_until_fits(&mut self, incoming: usize) {
while self.used_bytes + incoming > self.budget_bytes {
let Some(victim) = self.least_recently_used() else { break };
let removed_bytes = match victim {
Victim::Image(key) => self.entries.remove(&key).map(|e| e.bytes),
Victim::Pixels(key) => self.pixels.remove(&key).map(|e| e.bytes),
Victim::Source(key) => self.sources.remove(&key).map(|e| e.size),
};
match removed_bytes {
Some(bytes) => {
self.used_bytes = self.used_bytes.saturating_sub(bytes);
self.evictions += 1;
}
None => break,
}
}
}
fn least_recently_used(&self) -> Option<Victim> {
let mut oldest: Option<(u64, Victim)> = None;
let mut consider = |used_at: u64, victim: Victim| {
if oldest.as_ref().is_none_or(|(t, _)| used_at < *t) {
oldest = Some((used_at, victim));
}
};
for (key, entry) in &self.entries {
consider(entry.used_at, Victim::Image(*key));
}
for (key, entry) in &self.pixels {
consider(entry.used_at, Victim::Pixels(*key));
}
for (key, entry) in &self.sources {
consider(entry.used_at, Victim::Source(key.clone()));
}
oldest.map(|(_, victim)| victim)
}
fn clear(&mut self) {
self.entries.clear();
self.pixels.clear();
self.sources.clear();
self.used_bytes = 0;
}
}
static REQUESTS: AtomicU64 = AtomicU64::new(0);
static HITS: AtomicU64 = AtomicU64::new(0);
static MISSES: AtomicU64 = AtomicU64::new(0);
static EVICTIONS: AtomicU64 = AtomicU64::new(0);
static STORED_BYTES: AtomicUsize = AtomicUsize::new(0);
static FILE_REQUESTS: AtomicU64 = AtomicU64::new(0);
static FILE_HITS: AtomicU64 = AtomicU64::new(0);
static FILE_MISSES: AtomicU64 = AtomicU64::new(0);
#[cfg(all(feature = "image", not(alloc_frugal)))]
static CACHE: std::sync::Mutex<Option<DecodeCache>> = std::sync::Mutex::new(None);
#[cfg(all(feature = "image", not(alloc_frugal)))]
fn with_cache<R>(f: impl FnOnce(&mut DecodeCache) -> R) -> R {
let mut guard = match CACHE.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
let cache = guard.get_or_insert_with(|| DecodeCache::new(DEFAULT_BUDGET_BYTES));
f(cache)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct DecodeCacheStats {
pub requests: u64,
pub hits: u64,
pub misses: u64,
pub evictions: u64,
pub bytes: usize,
pub budget_bytes: usize,
pub file_requests: u64,
pub file_hits: u64,
pub file_misses: u64,
}
impl DecodeCacheStats {
pub fn avoided(&self) -> u64 {
self.hits
}
pub fn hit_rate(&self) -> f32 {
if self.requests == 0 {
0.0
} else {
self.hits as f32 / self.requests as f32
}
}
}
pub fn stats() -> DecodeCacheStats {
DecodeCacheStats {
requests: REQUESTS.load(Ordering::Relaxed),
hits: HITS.load(Ordering::Relaxed),
misses: MISSES.load(Ordering::Relaxed),
evictions: EVICTIONS.load(Ordering::Relaxed),
bytes: STORED_BYTES.load(Ordering::Relaxed),
budget_bytes: DEFAULT_BUDGET_BYTES,
file_requests: FILE_REQUESTS.load(Ordering::Relaxed),
file_hits: FILE_HITS.load(Ordering::Relaxed),
file_misses: FILE_MISSES.load(Ordering::Relaxed),
}
}
#[cfg(test)]
pub(crate) fn stats_test_guard() -> crate::compat::MutexGuard<'static, ()> {
static GUARD: std::sync::OnceLock<std::sync::Mutex<()>> = std::sync::OnceLock::new();
let mutex = GUARD.get_or_init(|| std::sync::Mutex::new(()));
match mutex.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
}
}
pub fn clear() {
#[cfg(all(feature = "image", not(alloc_frugal)))]
{
with_cache(|cache| cache.clear());
STORED_BYTES.store(0, Ordering::Relaxed);
}
}
pub fn set_budget_bytes(budget_bytes: usize) {
#[cfg(all(feature = "image", not(alloc_frugal)))]
{
let (used, evictions) = with_cache(|cache| {
cache.budget_bytes = budget_bytes;
cache.evict_until_fits(0);
(cache.used_bytes, cache.evictions)
});
STORED_BYTES.store(used, Ordering::Relaxed);
EVICTIONS.store(evictions, Ordering::Relaxed);
}
}
fn decoded_bytes(image: &DecodedImage) -> usize {
use crate::image::format::ImageData;
match &image.data {
ImageData::Rgba8(v) | ImageData::Rgb8(v) | ImageData::Grayscale8(v) => v.len(),
ImageData::Grayscale16(v) => v.len(),
ImageData::Rgba16(v) | ImageData::Rgb16(v) => v.len(),
}
}
fn content_key(data: &[u8], format_tag: u8) -> u64 {
let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
let mut mix = |byte: u8| {
hash ^= byte as u64;
hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
};
mix(width_tag(data.len()));
for &byte in data {
mix(byte);
}
mix(format_tag);
hash
}
fn width_tag(len: usize) -> u8 {
(len & 0xff) as u8
}
const FORMAT_TAG_RGBA8: u8 = 1;
#[cfg(all(feature = "image", not(alloc_frugal)))]
const PIXELS_TAG_RGBA8: u8 = 2;
#[cfg(all(feature = "image", not(alloc_frugal)))]
pub fn decode_to_rgba8_cached(data: &[u8]) -> Result<Arc<DecodedImage>, String> {
REQUESTS.fetch_add(1, Ordering::Relaxed);
let key = content_key(data, FORMAT_TAG_RGBA8);
if let Some(image) = with_cache(|cache| cache.get(key)) {
HITS.fetch_add(1, Ordering::Relaxed);
return Ok(image);
}
MISSES.fetch_add(1, Ordering::Relaxed);
let decoded = crate::image::decoder::decode_to_rgba8(data)?;
let bytes = decoded_bytes(&decoded);
let image = Arc::new(decoded);
let (used, evictions) = with_cache(|cache| {
cache.insert(key, Arc::clone(&image), bytes);
(cache.used_bytes, cache.evictions)
});
STORED_BYTES.store(used, Ordering::Relaxed);
EVICTIONS.store(evictions, Ordering::Relaxed);
Ok(image)
}
#[cfg(all(feature = "image", not(alloc_frugal)))]
pub fn cached_from_path(path: impl AsRef<std::path::Path>) -> Result<Arc<Vec<u8>>, std::io::Error> {
let path = path.as_ref();
let metadata = std::fs::metadata(path)?;
let stamp = SourceStamp {
len: metadata.len(),
modified_nanos: metadata
.modified()
.ok()
.and_then(|time| time.duration_since(std::time::UNIX_EPOCH).ok())
.map(|since| since.as_nanos())
.unwrap_or(0),
};
let path_key = path_key(path);
FILE_REQUESTS.fetch_add(1, Ordering::Relaxed);
if let Some(bytes) = with_cache(|cache| cache.get_source(&path_key, stamp)) {
FILE_HITS.fetch_add(1, Ordering::Relaxed);
return Ok(bytes);
}
FILE_MISSES.fetch_add(1, Ordering::Relaxed);
let bytes = Arc::new(std::fs::read(path)?);
let stored = Arc::clone(&bytes);
let (used, evictions) = with_cache(|cache| {
cache.insert_source(path_key, stored, stamp);
(cache.used_bytes, cache.evictions)
});
STORED_BYTES.store(used, Ordering::Relaxed);
EVICTIONS.store(evictions, Ordering::Relaxed);
Ok(bytes)
}
#[cfg(all(feature = "image", not(alloc_frugal)))]
fn path_key(path: &std::path::Path) -> alloc::vec::Vec<u8> {
path.as_os_str()
.to_string_lossy()
.as_bytes()
.iter()
.map(|byte| if *byte == b'\\' { b'/' } else { *byte })
.collect()
}
#[cfg(all(feature = "image", not(alloc_frugal)))]
pub fn decode_file_to_rgba8_cached(
path: impl AsRef<std::path::Path>,
) -> Result<Arc<DecodedImage>, String> {
let bytes = cached_from_path(path.as_ref()).map_err(|error| error.to_string())?;
decode_to_rgba8_cached(&bytes)
}
#[cfg(all(feature = "image", not(alloc_frugal)))]
pub fn file_rgba8_or_none(path: impl AsRef<std::path::Path>) -> Option<Arc<Vec<u8>>> {
let path = path.as_ref();
let bytes = match cached_from_path(path) {
Ok(bytes) => bytes,
Err(error) => {
log::warn!("image {path:?} could not be read ({error}); nothing is drawn for it");
return None;
}
};
pixels_rgba8_or_none(&bytes)
}
#[cfg(all(feature = "image", not(alloc_frugal)))]
pub fn pixels_rgba8_or_none(data: &[u8]) -> Option<Arc<Vec<u8>>> {
REQUESTS.fetch_add(1, Ordering::Relaxed);
let key = content_key(data, PIXELS_TAG_RGBA8);
if let Some(pixels) = with_cache(|cache| cache.get_pixels(key)) {
HITS.fetch_add(1, Ordering::Relaxed);
return Some(pixels);
}
MISSES.fetch_add(1, Ordering::Relaxed);
match crate::image::decoder::decode_to_rgba8(data) {
Ok(decoded) => match decoded.data {
crate::image::format::ImageData::Rgba8(pixels) => {
let bytes = pixels.len();
let shared = Arc::new(pixels);
let (used, evictions) = with_cache(|cache| {
cache.insert_pixels(key, Arc::clone(&shared), bytes);
(cache.used_bytes, cache.evictions)
});
STORED_BYTES.store(used, Ordering::Relaxed);
EVICTIONS.store(evictions, Ordering::Relaxed);
Some(shared)
}
_ => None,
},
Err(reason) => {
log::warn!("image bytes could not be decoded ({reason}); nothing is drawn for them");
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::image::format::{DecodedImage, ImageData, ImageFormat};
const PNG_1X1_RED: &[u8] = &[
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44,
0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1f,
0x15, 0xc4, 0x89, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9c, 0x63, 0xf8,
0xcf, 0xc0, 0xf0, 0x1f, 0x00, 0x05, 0x00, 0x01, 0xff, 0x89, 0x99, 0x3d, 0x1d, 0x00, 0x00,
0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
];
fn png_1x1() -> Vec<u8> {
PNG_1X1_RED.to_vec()
}
#[test]
fn the_key_depends_on_the_bytes_and_the_format() {
let a = content_key(b"hello", FORMAT_TAG_RGBA8);
let b = content_key(b"hello", FORMAT_TAG_RGBA8);
let c = content_key(b"hellp", FORMAT_TAG_RGBA8);
assert_eq!(a, b, "the same request must key the same");
assert_ne!(a, c, "a one-byte change must change the key");
assert_ne!(
content_key(b"hello", FORMAT_TAG_RGBA8),
content_key(b"hello", 2),
"the format is part of the request"
);
assert_ne!(content_key(b"ab", FORMAT_TAG_RGBA8), content_key(b"ab\0", FORMAT_TAG_RGBA8));
}
#[cfg(all(feature = "image", not(alloc_frugal)))]
#[test]
fn the_same_bytes_decode_once() {
let _guard = stats_test_guard();
clear();
let before = stats();
let png = png_1x1();
let first = decode_to_rgba8_cached(&png).expect("the PNG must decode");
let second = decode_to_rgba8_cached(&png).expect("and the cached one");
let third = decode_to_rgba8_cached(&png).expect("and again");
assert!(Arc::ptr_eq(&first, &second), "a hit must hand back the same allocation");
assert!(Arc::ptr_eq(&second, &third));
let after = stats();
assert_eq!(after.requests - before.requests, 3, "three requests");
assert_eq!(after.misses - before.misses, 1, "but one decode");
assert_eq!(after.hits - before.hits, 2, "and two saves");
assert_eq!(after.avoided(), after.hits);
assert_eq!(after.hits + after.misses, after.requests, "the books must balance");
}
#[cfg(all(feature = "image", not(alloc_frugal)))]
#[test]
fn the_pixel_vector_is_shared_between_requests() {
let _guard = stats_test_guard();
clear();
let png = png_1x1();
let before = stats();
let a = pixels_rgba8_or_none(&png).expect("decodes");
let b = pixels_rgba8_or_none(&png).expect("hits");
assert!(Arc::ptr_eq(&a, &b), "the second request must not re-allocate the pixels");
assert_eq!(a.len(), 4, "one RGBA pixel");
let after = stats();
assert_eq!(after.requests - before.requests, 2);
assert_eq!(after.misses - before.misses, 1);
assert_eq!(after.hits - before.hits, 1);
let mid = stats();
let _ = decode_to_rgba8_cached(&png).expect("decodes");
assert_eq!(
stats().misses - mid.misses,
1,
"a different request shape is a different entry"
);
}
#[cfg(all(feature = "image", not(alloc_frugal)))]
#[test]
fn a_decode_failure_is_not_cached() {
let _guard = stats_test_guard();
clear();
let before = stats();
assert!(decode_to_rgba8_cached(b"not an image at all").is_err());
assert!(decode_to_rgba8_cached(b"not an image at all").is_err());
let after = stats();
assert_eq!(
after.misses - before.misses,
2,
"a transient read failure must not become a permanent one"
);
assert_eq!(after.hits - before.hits, 0, "there was nothing to hit");
}
#[cfg(all(feature = "image", not(alloc_frugal)))]
#[test]
fn the_budget_is_enforced_by_eviction() {
let image = |tag: u8, side: usize| {
Arc::new(DecodedImage::new(
ImageFormat::Rgba8,
ImageData::Rgba8(vec![tag; side * side * 4]),
side as u32,
side as u32,
))
};
let mut cache = DecodeCache::new(1000);
cache.insert(1, image(1, 10), 400);
cache.insert(2, image(2, 10), 400);
assert_eq!(cache.used_bytes, 800);
assert!(cache.get(1).is_some(), "touching 1 makes 2 the victim");
let evictions_before = cache.evictions;
cache.insert(3, image(3, 10), 400);
assert_eq!(cache.evictions, evictions_before + 1, "exactly one victim was dropped");
assert!(cache.used_bytes <= 1000, "the budget is a ceiling, not a target");
assert!(cache.get(1).is_some(), "the recently used entry survives");
assert!(cache.get(2).is_none(), "the least recently used one was dropped");
}
#[cfg(all(feature = "image", not(alloc_frugal)))]
#[test]
fn an_oversized_image_is_not_stored() {
let mut cache = DecodeCache::new(100);
let huge =
Arc::new(DecodedImage::new(ImageFormat::Rgba8, ImageData::Rgba8(vec![0; 400]), 10, 10));
cache.insert(1, huge, 400);
assert_eq!(cache.used_bytes, 0, "storing it would evict everything for nothing");
assert!(cache.get(1).is_none());
}
#[test]
fn the_counters_describe_an_empty_cache_honestly() {
let stats = DecodeCacheStats::default();
assert_eq!(stats.hit_rate(), 0.0, "no requests is not a hit rate of zero-proud");
assert_eq!(stats.avoided(), 0);
let half = DecodeCacheStats { requests: 8, hits: 4, ..Default::default() };
assert!((half.hit_rate() - 0.5).abs() < f32::EPSILON);
}
fn write_temp(name_hint: &str, bytes: &[u8]) -> std::path::PathBuf {
let mut path = std::env::temp_dir();
path.push(format!(
"rw_cache_test_{}_{}_{}.png",
name_hint,
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0)
));
std::fs::write(&path, bytes).expect("the temp file must be writable");
path
}
#[cfg(all(feature = "image", not(alloc_frugal)))]
#[test]
fn the_second_read_of_one_file_does_not_read_it() {
let _guard = stats_test_guard();
clear();
let png = png_1x1();
let path = write_temp("read_once", &png);
let before = stats();
let first = cached_from_path(&path).expect("the file reads");
let second = cached_from_path(&path).expect("and the cached one");
let third = cached_from_path(&path).expect("and again");
let after = stats();
assert_eq!(&*first, &png[..], "the bytes are the file's");
assert!(Arc::ptr_eq(&first, &second), "a hit hands back the same allocation");
assert!(Arc::ptr_eq(&second, &third));
assert_eq!(after.file_requests - before.file_requests, 3);
assert_eq!(
after.file_hits - before.file_hits,
2,
"the second and third reads were avoided"
);
assert_eq!(after.file_misses - before.file_misses, 1, "exactly one real read");
let _ = std::fs::remove_file(&path);
}
#[cfg(all(feature = "image", not(alloc_frugal)))]
#[test]
fn a_changed_file_is_not_served_from_stale_bytes() {
let _guard = stats_test_guard();
clear();
let path = write_temp("changed", b"first version");
let first = cached_from_path(&path).expect("reads");
assert_eq!(&*first, b"first version");
std::thread::sleep(std::time::Duration::from_millis(1100));
std::fs::write(&path, b"a second, much longer version").expect("rewrite");
let second = cached_from_path(&path).expect("re-reads");
assert_eq!(
&*second, b"a second, much longer version",
"a changed file must be re-read, not served from the old bytes"
);
assert!(!Arc::ptr_eq(&first, &second));
let _ = std::fs::remove_file(&path);
}
#[cfg(all(feature = "image", not(alloc_frugal)))]
#[test]
fn one_file_reads_and_decodes_once_for_many_requests() {
let _guard = stats_test_guard();
clear();
let png = png_1x1();
let path = write_temp("compose", &png);
let before = stats();
for _ in 0..12 {
let pixels = file_rgba8_or_none(&path).expect("twelve controls on one icon");
assert_eq!(pixels.len(), 4, "a 1x1 RGBA image is four bytes");
}
let after = stats();
assert_eq!(after.file_misses - before.file_misses, 1, "the file was read once");
assert_eq!(after.file_hits - before.file_hits, 11, "and the other eleven were stats");
assert_eq!(after.misses - before.misses, 1, "and decoded once");
assert_eq!(after.hits - before.hits, 11);
let _ = std::fs::remove_file(&path);
}
#[cfg(all(feature = "image", not(alloc_frugal)))]
#[test]
fn a_missing_file_errors_and_is_not_remembered() {
let _guard = stats_test_guard();
clear();
let mut path = std::env::temp_dir();
path.push(format!("rw_cache_test_missing_{}.png", std::process::id()));
let _ = std::fs::remove_file(&path);
assert!(cached_from_path(&path).is_err(), "a missing file is an error");
std::fs::write(&path, png_1x1()).expect("write");
assert!(
cached_from_path(&path).is_ok(),
"a file that appears later must be readable; the earlier failure was not an entry"
);
let _ = std::fs::remove_file(&path);
}
#[cfg(all(feature = "image", not(alloc_frugal)))]
#[test]
fn the_file_layer_is_inside_the_same_budget() {
let mut cache = DecodeCache::new(100);
let stamp = SourceStamp { len: 80, modified_nanos: 1 };
cache.insert_source(b"a".to_vec(), Arc::new(vec![0u8; 80]), stamp);
assert_eq!(cache.used_bytes, 80, "the file bytes are on the budget's books");
let stamp2 = SourceStamp { len: 80, modified_nanos: 2 };
cache.insert_source(b"b".to_vec(), Arc::new(vec![1u8; 80]), stamp2);
assert!(cache.used_bytes <= 100, "the budget is a ceiling across all three maps");
assert_eq!(cache.evictions, 1);
assert!(cache.get_source(b"a", stamp).is_none(), "the older file was the victim");
assert!(cache.get_source(b"b", stamp2).is_some());
}
}