Skip to main content

cljrs_value/
shared.rs

1//! Cross-isolate shared mutable state: `SharedValue`, `SharedAtom` (Phase B3).
2//!
3//! The isolate model is share-nothing for GC-heap data.  But some state
4//! genuinely needs to be visible across isolates — global configuration, shared
5//! counters, published results.  Phase B3 adds an *explicit*, *honest* escape
6//! hatch: `shared-atom`.
7//!
8//! ## Two-tier mutable references (per the ADR)
9//!
10//! | Primitive | Backing | `Send`? | Use case |
11//! |-----------|---------|---------|----------|
12//! | `atom`      | `GcPtr<Atom>` (`Mutex<Value>`) | `!Send` | isolate-local, fast |
13//! | `shared-atom` | `Arc<SharedAtom>` (`ArcSwap<SharedValue>`) | ✓ | cross-isolate, lock-free CAS |
14//!
15//! A value stored in a `shared-atom` must be **promotable** — it must be
16//! representable as a `SharedValue`.  The promotion cost is paid once on
17//! publish; reads (`deref`) are an atomic load.
18//!
19//! ## `SharedValue`
20//!
21//! Covers only the "plain data" subset of `Value`:
22//! - Scalars (stored inline, no allocation)
23//! - Strings (`Arc<str>`, immutable, refcounted)
24//! - Keywords / symbols (`StaticGcPtr<T>`, interned, program-lifetime)
25//! - Large byte buffers (`Arc<ByteBlob>`, the BEAM off-heap-binary trick)
26//!
27//! Closures, native resources, and isolate-bound GC objects are not
28//! promotable.  This restriction is enforced at publish time via `promote`.
29//!
30//! ## Memory accounting
31//!
32//! Shared allocations live outside every isolate heap, so each one owns a
33//! governor [`MemoryCharge`] (`MemoryClass::SharedValue`): one per
34//! [`ByteBlob`] and one per stored [`SharedSlot`].  Cloning the `Arc` does
35//! not add a charge; dropping the last reference returns it.
36
37use std::ops::Deref;
38use std::sync::{Arc, Mutex};
39
40use arc_swap::ArcSwap;
41use cljrs_gc::{GcPtr, MemoryCharge, MemoryClass, MemoryLimitExceeded, StaticGcPtr, governor};
42
43use crate::intern::{intern_keyword, intern_symbol};
44use crate::keyword::Keyword;
45use crate::symbol::Symbol;
46use crate::value::Value;
47
48// ── PromoteError ─────────────────────────────────────────────────────────────
49
50/// Returned when a `Value` cannot be promoted to [`SharedValue`].
51#[derive(Debug, Clone)]
52pub struct PromoteError {
53    pub type_name: &'static str,
54    /// Set when the value is promotable but the governor refused its shared
55    /// allocation (strict mode).
56    pub memory: Option<MemoryLimitExceeded>,
57}
58
59impl PromoteError {
60    pub fn not_promotable(type_name: &'static str) -> Self {
61        Self {
62            type_name,
63            memory: None,
64        }
65    }
66
67    /// The governor refused the shared allocation for a `type_name` value.
68    pub fn memory_limit(type_name: &'static str, error: MemoryLimitExceeded) -> Self {
69        Self {
70            type_name,
71            memory: Some(error),
72        }
73    }
74}
75
76impl PromoteError {
77    /// The `ValueError` a builtin reports for this failure: the memory
78    /// refusal itself, or `Other` with the message.
79    pub fn into_value_error(self) -> crate::error::ValueError {
80        match self.memory {
81            Some(limit) => crate::error::ValueError::MemoryLimitExceeded(limit),
82            None => crate::error::ValueError::Other(self.to_string()),
83        }
84    }
85}
86
87impl std::fmt::Display for PromoteError {
88    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89        if let Some(limit) = &self.memory {
90            return limit.fmt(f);
91        }
92        write!(
93            f,
94            "value of type '{}' cannot be promoted to a shared-atom: \
95             only scalars, strings, keywords, symbols, and byte-blobs are supported",
96            self.type_name
97        )
98    }
99}
100
101impl std::error::Error for PromoteError {}
102
103// ── SharedValue ───────────────────────────────────────────────────────────────
104
105/// A `Send + Sync` value representation for cross-isolate sharing.
106///
107/// Covers a carefully chosen subset of `Value`: scalars (stored inline),
108/// immutable strings and byte buffers (refcounted via `Arc`), and interned
109/// keywords/symbols (program-lifetime `StaticGcPtr`).  Anything that holds
110/// isolate-local `GcPtr`s is excluded.
111///
112/// Cycles are impossible — `SharedValue` contains no `SharedValue` references
113/// — so plain `Arc` refcounting is sufficient and cycle-free.
114#[derive(Debug, Clone)]
115pub enum SharedValue {
116    Nil,
117    Bool(bool),
118    Long(i64),
119    Double(f64),
120    Char(char),
121    Uuid(u128),
122    /// Immutable interned string slice.
123    Str(Arc<str>),
124    /// Keyword interned into the global static arena.
125    Keyword(StaticGcPtr<Keyword>),
126    /// Symbol interned into the global static arena.
127    Symbol(StaticGcPtr<Symbol>),
128    /// Large refcounted byte buffer (the BEAM off-heap-binary trick).
129    /// Shared without copy across the isolate boundary; freed when the last
130    /// reference is dropped.
131    ByteBlob(Arc<ByteBlob>),
132}
133
134// SAFETY: all variants are either value types or `Arc`/`StaticGcPtr` wrappers
135// that are themselves `Send + Sync`.
136unsafe impl Send for SharedValue {}
137unsafe impl Sync for SharedValue {}
138
139impl SharedValue {
140    pub fn type_name(&self) -> &'static str {
141        match self {
142            SharedValue::Nil => "nil",
143            SharedValue::Bool(_) => "boolean",
144            SharedValue::Long(_) => "long",
145            SharedValue::Double(_) => "double",
146            SharedValue::Char(_) => "char",
147            SharedValue::Uuid(_) => "uuid",
148            SharedValue::Str(_) => "string",
149            SharedValue::Keyword(_) => "keyword",
150            SharedValue::Symbol(_) => "symbol",
151            SharedValue::ByteBlob(_) => "byte-blob",
152        }
153    }
154}
155
156// ── Shared allocations ────────────────────────────────────────────────────────
157
158/// Bytes of an `Arc<T>` allocation: two reference counts plus `T`.
159const fn arc_bytes<T>() -> usize {
160    2 * std::mem::size_of::<usize>() + std::mem::size_of::<T>()
161}
162
163/// Reserve `bytes` of shared-value memory for an infallible constructor.
164/// A strict-mode refusal raises the thread's runtime fault and charges the
165/// critical reserve (or past it, as a counted overrun) instead, like
166/// `GcPtr::new`; the returned charge always covers `bytes`.
167fn reserve(bytes: usize) -> MemoryCharge {
168    governor().reserve_infallible(MemoryClass::SharedValue, bytes)
169}
170
171/// Immutable byte buffer shared across isolates by `Arc` (`Value::ByteBlob`).
172///
173/// Owns one governor charge for its buffer and `Arc` allocation, however many
174/// isolates hold a reference.
175pub struct ByteBlob {
176    bytes: Box<[u8]>,
177    charge: MemoryCharge,
178}
179
180impl ByteBlob {
181    /// Take ownership of `bytes` and charge them to the governor.
182    /// Infallible: a strict-mode refusal raises the thread's runtime fault
183    /// (see [`Self::try_new`]).
184    pub fn new(bytes: impl Into<Box<[u8]>>) -> Self {
185        let bytes = bytes.into();
186        let charge = reserve(arc_bytes::<ByteBlob>() + bytes.len());
187        Self { bytes, charge }
188    }
189
190    /// Take ownership of `bytes` if the governor admits their charge.  On
191    /// refusal the buffer is dropped and the error returned.
192    pub fn try_new(bytes: impl Into<Box<[u8]>>) -> Result<Self, MemoryLimitExceeded> {
193        let bytes = bytes.into();
194        let charge = governor().reserve_shared(
195            MemoryClass::SharedValue,
196            arc_bytes::<ByteBlob>() + bytes.len(),
197        )?;
198        Ok(Self { bytes, charge })
199    }
200
201    /// Reserve a `len`-byte blob's charge before building its buffer, so a
202    /// refused blob never allocates.  `fill` builds the buffer.
203    pub fn try_with(
204        len: usize,
205        fill: impl FnOnce() -> Box<[u8]>,
206    ) -> Result<Self, MemoryLimitExceeded> {
207        let charge =
208            governor().reserve_shared(MemoryClass::SharedValue, arc_bytes::<ByteBlob>() + len)?;
209        Ok(Self {
210            bytes: fill(),
211            charge,
212        })
213    }
214
215    /// The buffer contents.
216    pub fn as_bytes(&self) -> &[u8] {
217        &self.bytes
218    }
219
220    /// Bytes charged to the governor for this blob.
221    pub fn charged_bytes(&self) -> usize {
222        self.charge.bytes()
223    }
224}
225
226impl Deref for ByteBlob {
227    type Target = [u8];
228    fn deref(&self) -> &[u8] {
229        &self.bytes
230    }
231}
232
233impl AsRef<[u8]> for ByteBlob {
234    fn as_ref(&self) -> &[u8] {
235        &self.bytes
236    }
237}
238
239impl From<Vec<u8>> for ByteBlob {
240    fn from(bytes: Vec<u8>) -> Self {
241        Self::new(bytes)
242    }
243}
244
245impl From<&[u8]> for ByteBlob {
246    fn from(bytes: &[u8]) -> Self {
247        Self::new(bytes)
248    }
249}
250
251impl std::fmt::Debug for ByteBlob {
252    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
253        write!(f, "ByteBlob({} bytes)", self.bytes.len())
254    }
255}
256
257/// One value stored in a [`SharedAtom`].  Each `reset!`/`swap!` stores a new
258/// slot; the slot owns the governor charge for its `Arc` allocation and any
259/// string it owns.  Byte-blob payloads are charged by the blob.
260pub struct SharedSlot {
261    value: SharedValue,
262    charge: MemoryCharge,
263}
264
265impl SharedSlot {
266    /// Wrap `value` and charge its storage to the governor.  Infallible: a
267    /// strict-mode refusal raises the thread's runtime fault (see
268    /// [`Self::try_new`]).
269    pub fn new(value: SharedValue) -> Self {
270        let charge = reserve(Self::bytes_for(&value));
271        Self { value, charge }
272    }
273
274    /// Wrap `value` if the governor admits its storage charge.
275    pub fn try_new(value: SharedValue) -> Result<Self, MemoryLimitExceeded> {
276        let charge =
277            governor().reserve_shared(MemoryClass::SharedValue, Self::bytes_for(&value))?;
278        Ok(Self { value, charge })
279    }
280
281    fn bytes_for(value: &SharedValue) -> usize {
282        let payload = match value {
283            SharedValue::Str(s) => s.len(),
284            _ => 0,
285        };
286        arc_bytes::<SharedSlot>() + payload
287    }
288
289    /// The stored value.
290    pub fn value(&self) -> &SharedValue {
291        &self.value
292    }
293
294    /// Bytes charged to the governor for this slot.
295    pub fn charged_bytes(&self) -> usize {
296        self.charge.bytes()
297    }
298}
299
300impl Deref for SharedSlot {
301    type Target = SharedValue;
302    fn deref(&self) -> &SharedValue {
303        &self.value
304    }
305}
306
307impl AsRef<SharedValue> for SharedSlot {
308    fn as_ref(&self) -> &SharedValue {
309        &self.value
310    }
311}
312
313impl std::fmt::Debug for SharedSlot {
314    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
315        self.value.fmt(f)
316    }
317}
318
319// ── promote / demote ──────────────────────────────────────────────────────────
320
321/// Promote a `Value` to a [`SharedValue`] for cross-isolate publishing.
322///
323/// The promotion cost is paid once (at `reset!`/`swap!` time); reads of the
324/// resulting `SharedAtom` are lock-free atomic loads.
325///
326/// Returns [`PromoteError`] for values that hold isolate-local state.
327pub fn promote(value: &Value) -> Result<SharedValue, PromoteError> {
328    match value {
329        Value::Nil => Ok(SharedValue::Nil),
330        Value::Bool(b) => Ok(SharedValue::Bool(*b)),
331        Value::Long(n) => Ok(SharedValue::Long(*n)),
332        Value::Double(d) => Ok(SharedValue::Double(*d)),
333        Value::Char(c) => Ok(SharedValue::Char(*c)),
334        Value::Uuid(u) => Ok(SharedValue::Uuid(u.get().0)),
335        Value::Str(s) => Ok(SharedValue::Str(Arc::from(s.get().as_str()))),
336        Value::Keyword(kw) => {
337            let kw = kw.get();
338            let ptr = intern_keyword(kw.namespace.as_deref(), &kw.name);
339            Ok(SharedValue::Keyword(ptr))
340        }
341        Value::Symbol(sym) => {
342            let sym = sym.get();
343            let ptr = intern_symbol(sym.namespace.as_deref(), &sym.name, sym.version.as_deref());
344            Ok(SharedValue::Symbol(ptr))
345        }
346        Value::ByteArray(arr) => {
347            let bytes = arr.get().lock().unwrap();
348            // Reserve before copying, so a refused promotion builds nothing.
349            let blob = ByteBlob::try_with(bytes.len(), || bytes.iter().map(|&b| b as u8).collect())
350                .map_err(|e| PromoteError::memory_limit("byte-array", e))?;
351            Ok(SharedValue::ByteBlob(Arc::new(blob)))
352        }
353        Value::ByteBlob(blob) => Ok(SharedValue::ByteBlob(blob.clone())),
354        other => Err(PromoteError::not_promotable(other.type_name())),
355    }
356}
357
358/// Demote a [`SharedValue`] back into an isolate-local `Value`.
359///
360/// Always succeeds.  Keywords and symbols are cloned from their interned
361/// static-arena allocation into a fresh `GcPtr` on the calling isolate's heap.
362/// Map lookups compare by namespace/name content (not pointer identity) so
363/// equality is preserved across the round-trip.
364pub fn demote(sv: &SharedValue) -> Value {
365    match sv {
366        SharedValue::Nil => Value::Nil,
367        SharedValue::Bool(b) => Value::Bool(*b),
368        SharedValue::Long(n) => Value::Long(*n),
369        SharedValue::Double(d) => Value::Double(*d),
370        SharedValue::Char(c) => Value::Char(*c),
371        SharedValue::Uuid(u) => Value::uuid(*u),
372        SharedValue::Str(s) => Value::Str(GcPtr::new(s.as_ref().to_owned())),
373        SharedValue::Keyword(kw) => Value::Keyword(GcPtr::new(kw.get().clone())),
374        SharedValue::Symbol(sym) => Value::Symbol(GcPtr::new(sym.get().clone())),
375        SharedValue::ByteBlob(blob) => Value::ByteBlob(blob.clone()),
376    }
377}
378
379// ── SharedAtom ────────────────────────────────────────────────────────────────
380
381/// A cross-isolate mutable reference backed by a lock-free `ArcSwap`.
382///
383/// `reset!` and `swap!` promote the new value and perform an atomic pointer
384/// swap — no locking, O(1) for simple resets, O(retries) for CAS-retry
385/// `swap!` under write contention.  `deref` is a single atomic load plus an
386/// `Arc` reference-count bump.
387///
388/// Wrapped in `Value::SharedAtom(Arc<SharedAtom>)`.  The `Arc` gives the atom
389/// identity (pointer equality) and allows any number of isolates to hold a
390/// reference.
391///
392/// Each stored value is a [`SharedSlot`] that carries its own governor
393/// charge; a replaced slot returns its charge when the last reader drops it.
394#[derive(Debug)]
395pub struct SharedAtom {
396    pub cell: Arc<ArcSwap<SharedSlot>>,
397    pub meta: Mutex<Option<SharedValue>>,
398}
399
400impl SharedAtom {
401    pub fn new(val: SharedValue) -> Self {
402        Self {
403            cell: Arc::new(ArcSwap::new(Arc::new(SharedSlot::new(val)))),
404            meta: Mutex::new(None),
405        }
406    }
407
408    /// Fallible [`Self::new`]: refuse if the governor cannot admit the slot.
409    pub fn try_new(val: SharedValue) -> Result<Self, MemoryLimitExceeded> {
410        Ok(Self {
411            cell: Arc::new(ArcSwap::new(Arc::new(SharedSlot::try_new(val)?))),
412            meta: Mutex::new(None),
413        })
414    }
415
416    /// Load the current value.  Lock-free atomic load + refcount bump.
417    pub fn deref_val(&self) -> Arc<SharedSlot> {
418        self.cell.load_full()
419    }
420
421    /// Atomically replace the value and return the new `Arc`.
422    pub fn reset(&self, val: SharedValue) -> Arc<SharedSlot> {
423        let arc = Arc::new(SharedSlot::new(val));
424        self.cell.store(arc.clone());
425        arc
426    }
427
428    /// Fallible [`Self::reset`]: the slot is reserved before it is
429    /// published, so a refusal leaves the current value unchanged.
430    pub fn try_reset(&self, val: SharedValue) -> Result<Arc<SharedSlot>, MemoryLimitExceeded> {
431        let arc = Arc::new(SharedSlot::try_new(val)?);
432        self.cell.store(arc.clone());
433        Ok(arc)
434    }
435
436    /// CAS-retry swap: apply `f` to the current value and store the result.
437    /// Returns the new value.  Retries automatically if another writer races.
438    pub fn swap<F>(&self, mut f: F) -> Arc<SharedSlot>
439    where
440        F: FnMut(&SharedValue) -> SharedValue,
441    {
442        self.cell.rcu(|old| Arc::new(SharedSlot::new(f(old))))
443    }
444
445    /// Single lock-free compare-and-set.  Atomically stores `new` iff the cell
446    /// still holds `current` (by `Arc` identity).  Returns `true` on success.
447    ///
448    /// This is the primitive used by Clojure-level `compare-and-set!` and by the
449    /// retry loop behind `swap!`: a caller that needs to run arbitrary
450    /// (interpreter) code between the load and the store cannot use the closure
451    /// form of [`swap`], so it loads via [`deref_val`](Self::deref_val), computes
452    /// the next value, then commits with this method, retrying on contention.
453    pub fn compare_and_set(&self, current: &Arc<SharedSlot>, new: SharedValue) -> bool {
454        let prev = self
455            .cell
456            .compare_and_swap(current, Arc::new(SharedSlot::new(new)));
457        // The swap committed iff the value we replaced is the one we expected.
458        std::ptr::eq(Arc::as_ptr(current), Arc::as_ptr(&prev))
459    }
460
461    /// Fallible [`Self::compare_and_set`]: the new slot is reserved before
462    /// the swap, so a refusal leaves the current value unchanged.
463    pub fn try_compare_and_set(
464        &self,
465        current: &Arc<SharedSlot>,
466        new: SharedValue,
467    ) -> Result<bool, MemoryLimitExceeded> {
468        let slot = Arc::new(SharedSlot::try_new(new)?);
469        let prev = self.cell.compare_and_swap(current, slot);
470        Ok(std::ptr::eq(Arc::as_ptr(current), Arc::as_ptr(&prev)))
471    }
472}
473
474// ── Tests ─────────────────────────────────────────────────────────────────────
475
476#[cfg(test)]
477mod tests {
478    use std::sync::Arc;
479
480    use crate::keyword::Keyword;
481    use crate::value::Value;
482    use cljrs_gc::GcPtr;
483
484    use super::*;
485
486    fn kw(name: &str) -> Value {
487        Value::Keyword(GcPtr::new(Keyword::simple(name)))
488    }
489
490    #[test]
491    fn promote_scalars() {
492        assert!(matches!(promote(&Value::Nil), Ok(SharedValue::Nil)));
493        assert!(matches!(
494            promote(&Value::Bool(true)),
495            Ok(SharedValue::Bool(true))
496        ));
497        assert!(matches!(
498            promote(&Value::Long(42)),
499            Ok(SharedValue::Long(42))
500        ));
501    }
502
503    #[test]
504    fn promote_keyword_interns() {
505        let v = kw("foo");
506        let sv = promote(&v).unwrap();
507        let sv2 = promote(&kw("foo")).unwrap();
508        if let (SharedValue::Keyword(a), SharedValue::Keyword(b)) = (sv, sv2) {
509            assert!(
510                cljrs_gc::StaticGcPtr::ptr_eq(&a, &b),
511                "same keyword should intern to same StaticGcPtr"
512            );
513        } else {
514            panic!("expected SharedValue::Keyword");
515        }
516    }
517
518    #[test]
519    fn demote_roundtrip_long() {
520        let sv = SharedValue::Long(99);
521        assert!(matches!(demote(&sv), Value::Long(99)));
522    }
523
524    #[test]
525    fn demote_roundtrip_keyword() {
526        let sv = promote(&kw("test")).unwrap();
527        let v = demote(&sv);
528        if let Value::Keyword(kw_ptr) = v {
529            assert_eq!(kw_ptr.get().name.as_ref(), "test");
530        } else {
531            panic!("expected Value::Keyword");
532        }
533    }
534
535    #[test]
536    fn promote_non_promotable_returns_err() {
537        let atom = Value::Atom(GcPtr::new(crate::types::Atom::new(Value::Nil)));
538        assert!(promote(&atom).is_err());
539    }
540
541    #[test]
542    fn promote_byte_blob() {
543        let arr: Vec<i8> = vec![1, 2, 3];
544        let v = Value::ByteArray(GcPtr::new(std::sync::Mutex::new(arr)));
545        let sv = promote(&v).unwrap();
546        assert!(matches!(sv, SharedValue::ByteBlob(_)));
547    }
548
549    #[test]
550    fn shared_atom_reset_and_deref() {
551        let atom = SharedAtom::new(SharedValue::Long(0));
552        atom.reset(SharedValue::Long(42));
553        let val = atom.deref_val();
554        assert!(matches!(val.value(), SharedValue::Long(42)));
555    }
556
557    #[test]
558    fn shared_atom_swap() {
559        let atom = SharedAtom::new(SharedValue::Long(1));
560        atom.swap(|old| {
561            if let SharedValue::Long(n) = old {
562                SharedValue::Long(n + 1)
563            } else {
564                SharedValue::Long(0)
565            }
566        });
567        let val = atom.deref_val();
568        assert!(matches!(val.value(), SharedValue::Long(2)));
569    }
570
571    #[test]
572    fn shared_atom_compare_and_set() {
573        let atom = SharedAtom::new(SharedValue::Long(1));
574        let cur = atom.deref_val();
575        // Stale expectation succeeds while no one races us.
576        assert!(atom.compare_and_set(&cur, SharedValue::Long(2)));
577        assert!(matches!(atom.deref_val().value(), SharedValue::Long(2)));
578        // `cur` is now stale: a second CAS against it must fail and not write.
579        assert!(!atom.compare_and_set(&cur, SharedValue::Long(99)));
580        assert!(matches!(atom.deref_val().value(), SharedValue::Long(2)));
581    }
582
583    #[test]
584    fn shared_atom_is_send_sync() {
585        fn assert_send_sync<T: Send + Sync>() {}
586        assert_send_sync::<SharedAtom>();
587        assert_send_sync::<Arc<SharedAtom>>();
588    }
589
590    #[test]
591    fn byte_blob_shared_across_clone() {
592        let blob = Arc::new(ByteBlob::new(vec![10u8, 20, 30]));
593        let v1 = Value::ByteBlob(blob.clone());
594        let v2 = Value::ByteBlob(blob.clone());
595        // Same underlying buffer
596        if let (Value::ByteBlob(a), Value::ByteBlob(b)) = (&v1, &v2) {
597            assert!(Arc::ptr_eq(a, b));
598        }
599    }
600
601    // Other tests charge the process governor concurrently, so these tests
602    // check each value's own charge rather than the class total.
603
604    #[test]
605    fn byte_blob_holds_one_charge() {
606        let blob = Arc::new(ByteBlob::new(vec![0u8; 4096]));
607        assert!(blob.charged_bytes() >= 4096);
608        let clone = blob.clone();
609        assert_eq!(clone.charged_bytes(), blob.charged_bytes());
610        assert_eq!(&clone[..3], &[0, 0, 0]);
611    }
612
613    #[test]
614    fn shared_slot_charges_string_payload() {
615        let short = SharedSlot::new(SharedValue::Long(1));
616        let long = SharedSlot::new(SharedValue::Str(Arc::from("x".repeat(1000))));
617        assert!(short.charged_bytes() > 0);
618        assert_eq!(long.charged_bytes(), short.charged_bytes() + 1000);
619    }
620
621    #[test]
622    fn shared_value_class_reports_live_charges() {
623        let blob = ByteBlob::new(vec![1u8; 1 << 20]);
624        // Concurrent tests only add their own small charges, and this blob
625        // stays alive across the read.
626        assert!(governor().class_bytes(MemoryClass::SharedValue) >= blob.charged_bytes());
627    }
628}