fix(ui): stabilize themed cache keys with static byte slices and harden tests

The themed handle cache (introduced in cc430cdb) keys entries by (address, length) of &'static [u8] art. However, the art was declared as const; Rust may inline distinct copies of a const at each use site, giving the same logical icon different addresses. This broke cache lookups for wrapper functions (e.g., themed_arrow_down) that refer to the same icon from a different module than the cache test. Convert the ~30 themed-relevant byte slices (TRI_*, HOME, UNDO, REDO, CHECK, etc.) to static so each artwork has one stable address process-wide.

Also refactor themed_handle to drop the immutable borrow before building a miss handle, preventing a reentrant call from panicking on a second borrow_mut.

Adjust themed_cache_tests to use delta assertions and per-test cache removal for isolation, and add coverage for second-call cache hits, wrapper delegation, and the length component of the key.

Test: ui::icons::themed_cache_tests::themed_cache_returns_same_handle_for_same_bytes
Test: ui::icons::themed_cache_tests::themed_cache_caches_distinct_bytes_separately
Test: ui::icons::themed_cache_tests::themed_cache_second_call_is_a_cache_hit
Test: ui::icons::themed_cache_tests::themed_wrapper_populates_cache_for_underlying_bytes
Test: ui::icons::themed_cache_tests::themed_cache_key_includes_length
Test: ui::icons::themed_cache_tests::themed_cache_speedup_over_uncached_handle_creation
This commit is contained in:
Karim Jerbi committed 2026-08-30 00:36:04 +01:00
1 parent 7a523a0f44
commit 166d758820
1 file changed
+224 -76
+224 -76
View File
@@ -19,81 +19,80 @@ use iced::{
};
use rustc_hash::FxHashMap;
const TRI_DOWN: &[u8] = include_bytes!("../../assets/icons/ui/tri_down.svg");
const TRI_UP: &[u8] = include_bytes!("../../assets/icons/ui/tri_up.svg");
const TRI_RIGHT: &[u8] = include_bytes!("../../assets/icons/ui/tri_right.svg");
const TRI_LEFT: &[u8] = include_bytes!("../../assets/icons/ui/tri_left.svg");
const HOME: &[u8] = include_bytes!("../../assets/icons/ui/home.svg");
const UNDO: &[u8] = include_bytes!("../../assets/icons/ui/undo.svg");
const REDO: &[u8] = include_bytes!("../../assets/icons/ui/redo.svg");
static TRI_DOWN: &[u8] = include_bytes!("../../assets/icons/ui/tri_down.svg");
static TRI_UP: &[u8] = include_bytes!("../../assets/icons/ui/tri_up.svg");
static TRI_RIGHT: &[u8] = include_bytes!("../../assets/icons/ui/tri_right.svg");
static TRI_LEFT: &[u8] = include_bytes!("../../assets/icons/ui/tri_left.svg");
static HOME: &[u8] = include_bytes!("../../assets/icons/ui/home.svg");
static UNDO: &[u8] = include_bytes!("../../assets/icons/ui/undo.svg");
static REDO: &[u8] = include_bytes!("../../assets/icons/ui/redo.svg");
// OSNAP marker symbols. Rendered as SVG (not Unicode glyphs) so the snap menu
// shows the right shapes on the web build, whose bundled Fira Sans lacks the
// geometric glyphs and rendered them as tofu boxes. (#138)
const OSNAP_ENDPOINT: &[u8] = include_bytes!("../../assets/icons/osnap/endpoint.svg");
const OSNAP_MIDPOINT: &[u8] = include_bytes!("../../assets/icons/osnap/midpoint.svg");
const OSNAP_CENTER: &[u8] = include_bytes!("../../assets/icons/osnap/center.svg");
const OSNAP_NODE: &[u8] = include_bytes!("../../assets/icons/osnap/node.svg");
const OSNAP_QUADRANT: &[u8] = include_bytes!("../../assets/icons/osnap/quadrant.svg");
const OSNAP_INTERSECTION: &[u8] = include_bytes!("../../assets/icons/osnap/intersection.svg");
const OSNAP_EXTENSION: &[u8] = include_bytes!("../../assets/icons/osnap/extension.svg");
const OSNAP_INSERTION: &[u8] = include_bytes!("../../assets/icons/osnap/insertion.svg");
const OSNAP_PERPENDICULAR: &[u8] =
include_bytes!("../../assets/icons/osnap/perpendicular.svg");
const OSNAP_TANGENT: &[u8] = include_bytes!("../../assets/icons/osnap/tangent.svg");
const OSNAP_NEAREST: &[u8] = include_bytes!("../../assets/icons/osnap/nearest.svg");
const OSNAP_APPARENT: &[u8] = include_bytes!("../../assets/icons/osnap/apparent.svg");
const OSNAP_PARALLEL: &[u8] = include_bytes!("../../assets/icons/osnap/parallel.svg");
const OSNAP_GRID: &[u8] = include_bytes!("../../assets/icons/osnap/grid.svg");
static OSNAP_ENDPOINT: &[u8] = include_bytes!("../../assets/icons/osnap/endpoint.svg");
static OSNAP_MIDPOINT: &[u8] = include_bytes!("../../assets/icons/osnap/midpoint.svg");
static OSNAP_CENTER: &[u8] = include_bytes!("../../assets/icons/osnap/center.svg");
static OSNAP_NODE: &[u8] = include_bytes!("../../assets/icons/osnap/node.svg");
static OSNAP_QUADRANT: &[u8] = include_bytes!("../../assets/icons/osnap/quadrant.svg");
static OSNAP_INTERSECTION: &[u8] = include_bytes!("../../assets/icons/osnap/intersection.svg");
static OSNAP_EXTENSION: &[u8] = include_bytes!("../../assets/icons/osnap/extension.svg");
static OSNAP_INSERTION: &[u8] = include_bytes!("../../assets/icons/osnap/insertion.svg");
static OSNAP_PERPENDICULAR: &[u8] = include_bytes!("../../assets/icons/osnap/perpendicular.svg");
static OSNAP_TANGENT: &[u8] = include_bytes!("../../assets/icons/osnap/tangent.svg");
static OSNAP_NEAREST: &[u8] = include_bytes!("../../assets/icons/osnap/nearest.svg");
static OSNAP_APPARENT: &[u8] = include_bytes!("../../assets/icons/osnap/apparent.svg");
static OSNAP_PARALLEL: &[u8] = include_bytes!("../../assets/icons/osnap/parallel.svg");
static OSNAP_GRID: &[u8] = include_bytes!("../../assets/icons/osnap/grid.svg");
const LAY_ON: &[u8] = include_bytes!("../../assets/icons/layers/layon.svg");
const LAY_OFF: &[u8] = include_bytes!("../../assets/icons/layers/layoff.svg");
const LAY_FRZ: &[u8] = include_bytes!("../../assets/icons/layers/layfrz.svg");
const LAY_THW: &[u8] = include_bytes!("../../assets/icons/layers/laythw.svg");
const LAY_LCK: &[u8] = include_bytes!("../../assets/icons/layers/laylck.svg");
const LAY_ULK: &[u8] = include_bytes!("../../assets/icons/layers/layulk.svg");
static LAY_ON: &[u8] = include_bytes!("../../assets/icons/layers/layon.svg");
static LAY_OFF: &[u8] = include_bytes!("../../assets/icons/layers/layoff.svg");
static LAY_FRZ: &[u8] = include_bytes!("../../assets/icons/layers/layfrz.svg");
static LAY_THW: &[u8] = include_bytes!("../../assets/icons/layers/laythw.svg");
static LAY_LCK: &[u8] = include_bytes!("../../assets/icons/layers/laylck.svg");
static LAY_ULK: &[u8] = include_bytes!("../../assets/icons/layers/layulk.svg");
// Monochrome chrome glyphs (replace Unicode glyphs in buttons / menus / toolbars).
// All are black-on-transparent; recolour them at the call site with [`tinted`].
pub const CHECK: &[u8] = include_bytes!("../../assets/icons/ui/check.svg");
pub const CLOSE: &[u8] = include_bytes!("../../assets/icons/ui/close.svg");
pub const PLUS: &[u8] = include_bytes!("../../assets/icons/ui/plus.svg");
pub const MINUS: &[u8] = include_bytes!("../../assets/icons/ui/minus.svg");
pub const TRASH: &[u8] = include_bytes!("../../assets/icons/ui/trash.svg");
pub const COPY: &[u8] = include_bytes!("../../assets/icons/ui/copy.svg");
pub const MENU: &[u8] = include_bytes!("../../assets/icons/ui/menu.svg");
pub const MOVE: &[u8] = include_bytes!("../../assets/icons/ui/move.svg");
pub const RESIZE: &[u8] = include_bytes!("../../assets/icons/ui/resize.svg");
pub const PIN: &[u8] = include_bytes!("../../assets/icons/ui/pin.svg");
pub const SPLIT_V: &[u8] = include_bytes!("../../assets/icons/ui/split_v.svg");
pub const SPLIT_H: &[u8] = include_bytes!("../../assets/icons/ui/split_h.svg");
pub const GRID: &[u8] = include_bytes!("../../assets/icons/ui/grid.svg");
pub const SNAP: &[u8] = include_bytes!("../../assets/icons/ui/snap.svg");
pub const DOC_NEW: &[u8] = include_bytes!("../../assets/icons/ui/doc_new.svg");
pub const FOLDER_OPEN: &[u8] = include_bytes!("../../assets/icons/ui/folder_open.svg");
pub const SAVE: &[u8] = include_bytes!("../../assets/icons/ui/save.svg");
pub const FILE_EXPORT: &[u8] = include_bytes!("../../assets/icons/ui/file_export.svg");
pub const PRINT: &[u8] = include_bytes!("../../assets/icons/ui/print.svg");
pub const HEART: &[u8] = include_bytes!("../../assets/icons/ui/heart.svg");
pub static CHECK: &[u8] = include_bytes!("../../assets/icons/ui/check.svg");
pub static CLOSE: &[u8] = include_bytes!("../../assets/icons/ui/close.svg");
pub static PLUS: &[u8] = include_bytes!("../../assets/icons/ui/plus.svg");
pub static MINUS: &[u8] = include_bytes!("../../assets/icons/ui/minus.svg");
pub static TRASH: &[u8] = include_bytes!("../../assets/icons/ui/trash.svg");
pub static COPY: &[u8] = include_bytes!("../../assets/icons/ui/copy.svg");
pub static MENU: &[u8] = include_bytes!("../../assets/icons/ui/menu.svg");
pub static MOVE: &[u8] = include_bytes!("../../assets/icons/ui/move.svg");
pub static RESIZE: &[u8] = include_bytes!("../../assets/icons/ui/resize.svg");
pub static PIN: &[u8] = include_bytes!("../../assets/icons/ui/pin.svg");
pub static SPLIT_V: &[u8] = include_bytes!("../../assets/icons/ui/split_v.svg");
pub static SPLIT_H: &[u8] = include_bytes!("../../assets/icons/ui/split_h.svg");
pub static GRID: &[u8] = include_bytes!("../../assets/icons/ui/grid.svg");
pub static SNAP: &[u8] = include_bytes!("../../assets/icons/ui/snap.svg");
pub static DOC_NEW: &[u8] = include_bytes!("../../assets/icons/ui/doc_new.svg");
pub static FOLDER_OPEN: &[u8] = include_bytes!("../../assets/icons/ui/folder_open.svg");
pub static SAVE: &[u8] = include_bytes!("../../assets/icons/ui/save.svg");
pub static FILE_EXPORT: &[u8] = include_bytes!("../../assets/icons/ui/file_export.svg");
pub static PRINT: &[u8] = include_bytes!("../../assets/icons/ui/print.svg");
pub static HEART: &[u8] = include_bytes!("../../assets/icons/ui/heart.svg");
#[cfg(target_arch = "wasm32")]
pub const GEAR: &[u8] = include_bytes!("../../assets/icons/ui/gear.svg");
pub const DOT: &[u8] = include_bytes!("../../assets/icons/ui/dot.svg");
pub const DIRTY_DOT: &[u8] = include_bytes!("../../assets/icons/ui/dirty_dot.svg");
pub const ARROW_LONG_RIGHT: &[u8] = include_bytes!("../../assets/icons/ui/arrow_long_right.svg");
pub static GEAR: &[u8] = include_bytes!("../../assets/icons/ui/gear.svg");
pub static DOT: &[u8] = include_bytes!("../../assets/icons/ui/dot.svg");
pub static DIRTY_DOT: &[u8] = include_bytes!("../../assets/icons/ui/dirty_dot.svg");
pub static ARROW_LONG_RIGHT: &[u8] = include_bytes!("../../assets/icons/ui/arrow_long_right.svg");
// ── Status-bar toggle icons (issue #216) ──────────────────────────────────
pub const ST_ORTHO: &[u8] = include_bytes!("../../assets/icons/status/ortho.svg");
pub const ST_POLAR: &[u8] = include_bytes!("../../assets/icons/status/polar.svg");
pub const ST_OSNAP: &[u8] = include_bytes!("../../assets/icons/status/osnap.svg");
pub const ST_OTRACK: &[u8] = include_bytes!("../../assets/icons/status/otrack.svg");
pub const ST_DYN: &[u8] = include_bytes!("../../assets/icons/status/dyn.svg");
pub const ST_LWT: &[u8] = include_bytes!("../../assets/icons/status/lwt.svg");
pub const ST_TRANSPARENCY: &[u8] = include_bytes!("../../assets/icons/status/transparency.svg");
pub const ST_ISOLATE: &[u8] = include_bytes!("../../assets/icons/status/isolate.svg");
pub const ST_QUICKPROPS: &[u8] = include_bytes!("../../assets/icons/status/quickprops.svg");
pub const ST_FILTER: &[u8] = include_bytes!("../../assets/icons/status/filter.svg");
pub const ST_SELCYCLE: &[u8] = include_bytes!("../../assets/icons/status/selcycle.svg");
pub const ST_CLEANSCREEN: &[u8] = include_bytes!("../../assets/icons/status/cleanscreen.svg");
pub static ST_ORTHO: &[u8] = include_bytes!("../../assets/icons/status/ortho.svg");
pub static ST_POLAR: &[u8] = include_bytes!("../../assets/icons/status/polar.svg");
pub static ST_OSNAP: &[u8] = include_bytes!("../../assets/icons/status/osnap.svg");
pub static ST_OTRACK: &[u8] = include_bytes!("../../assets/icons/status/otrack.svg");
pub static ST_DYN: &[u8] = include_bytes!("../../assets/icons/status/dyn.svg");
pub static ST_LWT: &[u8] = include_bytes!("../../assets/icons/status/lwt.svg");
pub static ST_TRANSPARENCY: &[u8] = include_bytes!("../../assets/icons/status/transparency.svg");
pub static ST_ISOLATE: &[u8] = include_bytes!("../../assets/icons/status/isolate.svg");
pub static ST_QUICKPROPS: &[u8] = include_bytes!("../../assets/icons/status/quickprops.svg");
pub static ST_FILTER: &[u8] = include_bytes!("../../assets/icons/status/filter.svg");
pub static ST_SELCYCLE: &[u8] = include_bytes!("../../assets/icons/status/selcycle.svg");
pub static ST_CLEANSCREEN: &[u8] = include_bytes!("../../assets/icons/status/cleanscreen.svg");
// Tool SVGs share a small source palette. These colours are semantic rather
// than literal: cyan is the accent, pale grey is foreground, yellow is warning,
@@ -151,17 +150,26 @@ thread_local! {
/// is bounded by construction and needs no eviction. Per-frame
/// cost: 9 ns/iter warm, 26 ns/iter cold (release profile, see
/// `themed_cache_speedup_over_uncached_handle_creation`).
///
/// **No borrow held during handle construction.** The cache is
/// read (immutable `borrow`), the `Ref` is dropped, and only then
/// is `svg::Handle::from_memory` invoked. The result is then
/// inserted under a fresh `borrow_mut`. This is defensive: a
/// re-entrant call into `themed*` during SVG parsing cannot
/// trigger a `RefCell` panic on the second `borrow_mut`, because
/// no `borrow_mut` is held while parsing runs. (The same
/// `thread_local!` + `RefCell` shape is used by `SEMANTIC_CACHE`.)
fn themed_handle(bytes: &'static [u8]) -> svg::Handle {
let key = (bytes.as_ptr() as usize, bytes.len());
if let Some(handle) = THEMED_CACHE.with(|cache| cache.borrow().get(&key).cloned()) {
return handle;
}
// Miss: build the handle with no cache borrow held, then insert.
let handle = svg::Handle::from_memory(bytes);
THEMED_CACHE.with(|cache| {
let mut cache = cache.borrow_mut();
if let Some(handle) = cache.get(&key) {
return handle.clone();
}
let handle = svg::Handle::from_memory(bytes);
cache.insert(key, handle.clone());
handle
})
cache.borrow_mut().insert(key, handle.clone());
});
handle
}
#[derive(Clone, Copy)]
@@ -733,17 +741,42 @@ mod themed_cache_tests {
THEMED_CACHE.with(|c| c.borrow().contains_key(&(ptr as usize, len)))
}
/// Returns a clone of the cached `svg::Handle` for `(ptr, len)`,
/// or `None` if no entry is cached. Test-only inspection helper.
fn themed_cache_get_handle_for_test(
ptr: *const u8,
len: usize,
) -> Option<svg::Handle> {
THEMED_CACHE.with(|c| c.borrow().get(&(ptr as usize, len)).cloned())
}
/// Removes the cache entry for `(ptr, len)`. Test-only helper so
/// individual tests can start from a known-empty cache regardless
/// of test execution order (tests share the `thread_local!` when
/// run on the same thread).
fn themed_cache_remove_for_test(ptr: *const u8, len: usize) {
THEMED_CACHE.with(|c| {
c.borrow_mut().remove(&(ptr as usize, len));
})
}
/// Two calls to the public `themed` function with the same
/// `&'static [u8]` must share a single cache entry: the second
/// call is a cache hit, not a fresh `Handle::from_memory`.
#[test]
fn themed_cache_returns_same_handle_for_same_bytes() {
themed_cache_remove_for_test(TRI_DOWN.as_ptr(), TRI_DOWN.len());
assert!(
!themed_cache_contains_for_test(TRI_DOWN.as_ptr(), TRI_DOWN.len()),
"precondition: TRI_DOWN must not be cached before the test"
);
let size_before = themed_cache_size_for_test();
let _: Element<'static, ()> = themed(TRI_DOWN, 16.0);
let _: Element<'static, ()> = themed(TRI_DOWN, 16.0);
assert_eq!(
themed_cache_size_for_test(),
1,
"two calls with the same bytes must share a single cache entry"
size_before + 1,
"two calls with the same bytes must share a single cache entry (delta +1)"
);
assert!(
themed_cache_contains_for_test(TRI_DOWN.as_ptr(), TRI_DOWN.len()),
@@ -755,17 +788,132 @@ mod themed_cache_tests {
/// entries (no false aliasing across different artwork).
#[test]
fn themed_cache_caches_distinct_bytes_separately() {
themed_cache_remove_for_test(TRI_DOWN.as_ptr(), TRI_DOWN.len());
themed_cache_remove_for_test(TRI_UP.as_ptr(), TRI_UP.len());
assert!(!themed_cache_contains_for_test(TRI_DOWN.as_ptr(), TRI_DOWN.len()));
assert!(!themed_cache_contains_for_test(TRI_UP.as_ptr(), TRI_UP.len()));
let size_before = themed_cache_size_for_test();
let _: Element<'static, ()> = themed(TRI_DOWN, 16.0);
let _: Element<'static, ()> = themed(TRI_UP, 16.0);
assert_eq!(
themed_cache_size_for_test(),
2,
"distinct bytes must produce distinct cache entries"
size_before + 2,
"distinct bytes must produce distinct cache entries (delta +2)"
);
assert!(themed_cache_contains_for_test(TRI_DOWN.as_ptr(), TRI_DOWN.len()));
assert!(themed_cache_contains_for_test(TRI_UP.as_ptr(), TRI_UP.len()));
}
/// The cached `svg::Handle` is a single source of truth: two
/// calls with the same bytes must return a handle derived from
/// the same cache entry, not a freshly-built handle.
///
/// **`svg::Handle` is opaque and has no `PartialEq` impl**, so
/// we cannot compare handles directly for pointer-identity. The
/// best verifiable proxy is: (a) the second call does not grow
/// the cache (a buggy `entry().or_insert_with` that always
/// inserted would still show size 1 for a single key, so this
/// rules out a double-insert on a single key), and (b) the
/// getter returns the cached handle across the two calls (the
/// cache survived and still resolves the key). Together these
/// confirm the cache is a single source — the test's identity
/// guarantee is the weaker "the cache is populated and reused"
/// rather than the stronger "the handle is pointer-identical",
/// which `svg::Handle` does not expose.
#[test]
fn themed_cache_second_call_is_a_cache_hit() {
themed_cache_remove_for_test(TRI_DOWN.as_ptr(), TRI_DOWN.len());
assert!(
!themed_cache_contains_for_test(TRI_DOWN.as_ptr(), TRI_DOWN.len()),
"precondition: TRI_DOWN must not be cached"
);
let size_before = themed_cache_size_for_test();
// Warm the cache with the first call (miss → build → insert).
let _: Element<'static, ()> = themed(TRI_DOWN, 16.0);
let cached_after_first =
themed_cache_get_handle_for_test(TRI_DOWN.as_ptr(), TRI_DOWN.len())
.expect("cache must hold the handle after the first call");
let size_after_first = themed_cache_size_for_test();
assert_eq!(
size_after_first,
size_before + 1,
"first call must insert exactly one entry"
);
// Second call: must be a cache hit, not a fresh build.
let _: Element<'static, ()> = themed(TRI_DOWN, 16.0);
let cached_after_second =
themed_cache_get_handle_for_test(TRI_DOWN.as_ptr(), TRI_DOWN.len())
.expect("cache must still hold the handle after the second call");
assert_eq!(
themed_cache_size_for_test(),
size_after_first,
"second call must not grow the cache (hit, not rebuild)"
);
let _ = (cached_after_first, cached_after_second);
}
/// Wrapper functions (e.g. `themed_arrow_down`) delegate to
/// the 9 `themed*` byte-taking functions and therefore inherit
/// the cache transparently. This end-to-end test confirms the
/// delegation: a wrapper call populates the cache for its
/// underlying static bytes. The cache entry is cleared first so
/// the assertion is independent of test execution order (tests
/// share the `thread_local!` when run on the same thread).
#[test]
fn themed_wrapper_populates_cache_for_underlying_bytes() {
themed_cache_remove_for_test(TRI_DOWN.as_ptr(), TRI_DOWN.len());
assert!(
!themed_cache_contains_for_test(TRI_DOWN.as_ptr(), TRI_DOWN.len()),
"precondition: TRI_DOWN must not be cached before the call"
);
let size_before = themed_cache_size_for_test();
// `themed_arrow_down` delegates to `themed(TRI_DOWN, size)`.
let _: Element<'static, ()> = themed_arrow_down(16.0);
assert_eq!(
themed_cache_size_for_test(),
size_before + 1,
"one wrapper call must populate exactly one cache entry (delta +1)"
);
assert!(
themed_cache_contains_for_test(TRI_DOWN.as_ptr(), TRI_DOWN.len()),
"the cache must hold an entry for the wrapper's underlying bytes (TRI_DOWN)"
);
}
/// Cache key includes length: a sub-slice of a static must not
/// alias the full static's entry. This guards the
/// `(address, length)` belt-and-braces key `src/ui/icons.rs:164`
/// — a pointer-only key would incorrectly hit on `&BYTES[1..]`.
#[test]
fn themed_cache_key_includes_length() {
// Ensure clean state for both keys.
themed_cache_remove_for_test(TRI_DOWN.as_ptr(), TRI_DOWN.len());
// Sub-slice of TRI_DOWN: same address region but different length.
// SAFETY: TRI_DOWN is at least 2 bytes (real SVG), so [1..] is valid.
let sub = &TRI_DOWN[1..];
themed_cache_remove_for_test(sub.as_ptr(), sub.len());
assert!(!themed_cache_contains_for_test(TRI_DOWN.as_ptr(), TRI_DOWN.len()));
assert!(!themed_cache_contains_for_test(sub.as_ptr(), sub.len()));
let size_before = themed_cache_size_for_test();
// Populate full static.
let _: Element<'static, ()> = themed(TRI_DOWN, 16.0);
assert!(themed_cache_contains_for_test(TRI_DOWN.as_ptr(), TRI_DOWN.len()));
assert!(
!themed_cache_contains_for_test(sub.as_ptr(), sub.len()),
"sub-slice must not hit the full static's entry"
);
assert_eq!(themed_cache_size_for_test(), size_before + 1);
// Populate sub-slice directly via the internal helper (bypasses
// the `themed` wrappers which only use the full statics).
let _ = super::themed_handle(sub);
assert!(themed_cache_contains_for_test(sub.as_ptr(), sub.len()));
assert_eq!(
themed_cache_size_for_test(),
size_before + 2,
"sub-slice must produce a distinct entry (length part of key)"
);
}
/// Before/after benchmark: the cached `themed_handle` path
/// (hashmap get + a `svg::Handle` clone) vs. the original
/// un-cached path (`svg::Handle::from_memory` per call, which