diff --git a/changelog.d/10244-android-tls-pool.md b/changelog.d/10244-android-tls-pool.md new file mode 100644 index 0000000000..e1530f08bf --- /dev/null +++ b/changelog.d/10244-android-tls-pool.md @@ -0,0 +1 @@ +- Android runtimes share one pthread key across Perry's cached thread-local declarations, preventing the key exhaustion that aborted minimal UI apps when the timer pump started (#10219). Per-thread values keep independent initialization and cleanup, and accesses after teardown remain fallible. diff --git a/crates/perry-runtime/src/lib.rs b/crates/perry-runtime/src/lib.rs index 264f395c30..2382587e3f 100644 --- a/crates/perry-runtime/src/lib.rs +++ b/crates/perry-runtime/src/lib.rs @@ -198,6 +198,9 @@ pub mod timer; /// #7469: one `_tlv_get_addr` for the whole allocation hot path. #[doc(hidden)] pub mod tls_hot; +#[cfg(any(target_os = "android", all(test, unix)))] +#[doc(hidden)] +pub mod tls_os_pool; pub mod typed_feedback; pub mod typedarray; pub mod typedarray_half; diff --git a/crates/perry-runtime/src/tls_hot.rs b/crates/perry-runtime/src/tls_hot.rs index 7a1c7a7e18..49148ef3f5 100644 --- a/crates/perry-runtime/src/tls_hot.rs +++ b/crates/perry-runtime/src/tls_hot.rs @@ -100,6 +100,13 @@ //! every thread falls back to `_tlv_get_addr`, permanently and silently //! correctly. It cannot degrade into reading a wrong address. +// Android uses pooled storage with the same try_with failure semantics. Other +// platforms keep std's storage and exact AccessError type. +#[cfg(target_os = "android")] +pub use crate::tls_os_pool::AccessError; +#[cfg(not(target_os = "android"))] +pub use std::thread::AccessError; + use std::cell::{Cell, UnsafeCell}; /// How many generic [`HotKey`] slots one thread's cache can hold. @@ -263,6 +270,7 @@ impl HotTls { }; } +#[cfg(not(target_os = "android"))] thread_local! { /// `const`-initialised on purpose: a lazily-initialised `thread_local!` /// pays a "has this been initialised / has this been dropped" check on @@ -272,6 +280,12 @@ thread_local! { static HOT: UnsafeCell = const { UnsafeCell::new(HotTls::EMPTY) }; } +// Keep the cache in the pool too. It must outlive pooled value destructors, +// whose SlotGuard clears cached addresses while the pool is being torn down. +#[cfg(target_os = "android")] +static HOT: crate::tls_os_pool::LocalKey> = + crate::tls_os_pool::LocalKey::new(|| UnsafeCell::new(HotTls::EMPTY)); + /// Resolve every cached address for this thread. Cold: runs once per thread. /// /// Each `…_hot_addr()` touches its own `thread_local!` exactly as any other @@ -794,15 +808,15 @@ impl Drop for SlotGuard { /// A thread-local whose address is cached in this thread's [`HotTls`]. /// -/// Drop-in for `std::thread::LocalKey` at the call site: `with` and `try_with` -/// keep the same signatures, so converting a declaration converts every one of -/// its uses. +/// `with` and `try_with` accept the same closures as `std::thread::LocalKey`. +/// Android uses the pooled backend's [`AccessError`]; other platforms retain +/// std's exact error type. pub struct HotKey { slot: &'static SlotId, /// Resolves the owning `thread_local!` the ordinary way and returns the /// address of its *value*. Cold path only — never called once the slot is /// populated, so the indirect call never appears on a hot path. - resolve: fn() -> Result<*mut u8, std::thread::AccessError>, + resolve: fn() -> Result<*mut u8, AccessError>, /// Records the claimed index in this thread's teardown guard, if the value /// has one. Generated alongside the storage, so it knows the `GUARD` that /// `HotKey` deliberately does not. @@ -819,7 +833,7 @@ impl HotKey { #[doc(hidden)] pub const fn new( slot: &'static SlotId, - resolve: fn() -> Result<*mut u8, std::thread::AccessError>, + resolve: fn() -> Result<*mut u8, AccessError>, arm_guard: fn(u32), ) -> Self { Self { @@ -847,10 +861,14 @@ impl HotKey { /// As [`HotKey::with`], but reports rather than panics when this thread's /// value is being or has been destroyed. #[inline(always)] - pub fn try_with(&'static self, f: F) -> Result + pub fn try_with(&'static self, f: F) -> Result where F: FnOnce(&T) -> R, { + #[cfg(target_os = "android")] + if crate::tls_os_pool::is_destroyed() { + return Err(AccessError); + } let idx = self.slot.raw(); if (idx as usize) < HOT_SLOT_CAPACITY { let cell = hot().slot(idx); @@ -915,7 +933,7 @@ impl HotKey { /// resolve through the real `thread_local!` and publish it for this thread. #[cold] #[inline(never)] - fn resolve_and_cache(&'static self) -> Result<*mut u8, std::thread::AccessError> { + fn resolve_and_cache(&'static self) -> Result<*mut u8, AccessError> { // Claim before resolving storage: another provider can already have // published this declaration in the shared cache. Do not construct or // overwrite a second copy. The claim lock is released before any TLS @@ -1013,7 +1031,7 @@ macro_rules! __perry_thread_local_one { // a cached address could otherwise outlive the value. type Storage = $crate::tls_hot::HotCell<$t, { ::core::mem::needs_drop::<$t>() as usize }>; $crate::__perry_thread_local_storage!(Storage, $($init)+); - fn resolve() -> ::core::result::Result<*mut u8, ::std::thread::AccessError> { + fn resolve() -> ::core::result::Result<*mut u8, $crate::tls_hot::AccessError> { STORAGE.try_with(|cell| cell.value_addr()) } fn arm_guard(idx: u32) { @@ -1028,14 +1046,22 @@ macro_rules! __perry_thread_local_one { #[macro_export] macro_rules! __perry_thread_local_storage { ($storage:ty, const $init:block) => { + #[cfg(not(target_os = "android"))] ::std::thread_local! { static STORAGE: $storage = const { <$storage>::new($init) }; } + #[cfg(target_os = "android")] + static STORAGE: $crate::tls_os_pool::LocalKey<$storage> = + $crate::tls_os_pool::LocalKey::new(|| <$storage>::new($init)); }; ($storage:ty, expr ($init:expr)) => { + #[cfg(not(target_os = "android"))] ::std::thread_local! { static STORAGE: $storage = <$storage>::new($init); } + #[cfg(target_os = "android")] + static STORAGE: $crate::tls_os_pool::LocalKey<$storage> = + $crate::tls_os_pool::LocalKey::new(|| <$storage>::new($init)); }; } diff --git a/crates/perry-runtime/src/tls_hot/provider_tests.rs b/crates/perry-runtime/src/tls_hot/provider_tests.rs index eea3fd83f9..2038878079 100644 --- a/crates/perry-runtime/src/tls_hot/provider_tests.rs +++ b/crates/perry-runtime/src/tls_hot/provider_tests.rs @@ -21,10 +21,17 @@ fn provider_copies_share_storage_without_initializing_a_second_value() { } } type Storage = super::HotCell; + #[cfg(not(target_os = "android"))] thread_local! { static FIRST_STORAGE: Storage = Storage::new(Probe::new()); static SECOND_STORAGE: Storage = Storage::new(Probe::new()); } + #[cfg(target_os = "android")] + static FIRST_STORAGE: crate::tls_os_pool::LocalKey = + crate::tls_os_pool::LocalKey::new(|| Storage::new(Probe::new())); + #[cfg(target_os = "android")] + static SECOND_STORAGE: crate::tls_os_pool::LocalKey = + crate::tls_os_pool::LocalKey::new(|| Storage::new(Probe::new())); static FIRST_SLOT: super::SlotId = super::SlotId::named("provider-test::shared"); static SECOND_SLOT: super::SlotId = super::SlotId::named("provider-test::shared"); static FIRST: super::HotKey = super::HotKey::new( diff --git a/crates/perry-runtime/src/tls_os_pool.rs b/crates/perry-runtime/src/tls_os_pool.rs new file mode 100644 index 0000000000..8fdeda1931 --- /dev/null +++ b/crates/perry-runtime/src/tls_os_pool.rs @@ -0,0 +1,291 @@ +//! Android storage for `perry_thread_local!` (#10219). +//! +//! Rust's Android TLS backend consumes one of bionic's 128 pthread keys for +//! every declaration it touches, including const, non-Drop declarations. The +//! runtime alone can exhaust that process-wide pool while starting the UI pump. +//! This backend owns one pthread key and lazily allocates typed values behind +//! it. The existing HotKey cache still supplies the steady-state fast path. +//! +//! Entries and values have stable System-allocated addresses. No collection +//! borrow survives an initializer or destructor. Destructors run in reverse +//! initialization order and can access other live keys or initialize new keys; +//! destroyed keys cannot be resurrected. Non-Drop values, including HotTls, +//! remain alive until all user destructors have run. The pthread destructor +//! retains a tombstone through later POSIX destructor passes. +use std::alloc::{GlobalAlloc, Layout, System}; +use std::marker::PhantomData; +use std::ptr; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::{Mutex, OnceLock}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct AccessError; +impl std::fmt::Display for AccessError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("thread-local value is being or has been destroyed") + } +} +impl std::error::Error for AccessError {} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum State { + Vacant, + Initializing, + Ready, + Destroyed, +} +struct Entry { + state: State, + value: *mut u8, + destroy: unsafe fn(*mut u8), + next: *mut Entry, + drop_next: *mut Entry, +} +struct Pool { + entries: *mut Entry, + drops: *mut Entry, + slots: *mut *mut Entry, + capacity: usize, +} +static NEXT_ID: Mutex = Mutex::new(0); +static KEY: OnceLock = OnceLock::new(); +const DESTROYED: *mut libc::c_void = ptr::without_provenance_mut(1); + +// System allocation avoids recursing through a global allocator's own TLS. +unsafe fn alloc(value: T) -> *mut T { + let layout = Layout::new::(); + let out = if layout.size() == 0 { + ptr::NonNull::::dangling().as_ptr() + } else { + let out = System.alloc(layout).cast::(); + if out.is_null() { + std::alloc::handle_alloc_error(layout); + } + out + }; + out.write(value); + out +} +unsafe fn destroy(value: *mut u8) { + let value = value.cast::(); + ptr::drop_in_place(value); + if std::mem::size_of::() != 0 { + System.dealloc(value.cast(), Layout::new::()); + } +} +fn key() -> libc::pthread_key_t { + *KEY.get_or_init(|| { + let mut key = 0; + if unsafe { libc::pthread_key_create(&mut key, Some(drop_pool)) } != 0 { + panic!("cannot allocate the shared thread-local key"); + } + key + }) +} +/// HotKey must report teardown before consulting its cached HotTls pointer. +#[cfg(target_os = "android")] +pub(crate) fn is_destroyed() -> bool { + KEY.get() + .is_some_and(|key| unsafe { libc::pthread_getspecific(*key) } == DESTROYED) +} + +fn pool() -> Result<*mut Pool, AccessError> { + let key = key(); + let value = unsafe { libc::pthread_getspecific(key) }; + if value == DESTROYED { + return Err(AccessError); + } + if !value.is_null() { + return Ok(value.cast()); + } + let value = unsafe { + alloc(Pool { + entries: ptr::null_mut(), + drops: ptr::null_mut(), + slots: ptr::null_mut(), + capacity: 0, + }) + }; + if unsafe { libc::pthread_setspecific(key, value.cast()) } != 0 { + unsafe { + destroy::(value.cast()); + } + panic!("cannot publish the shared thread-local storage"); + } + Ok(value) +} +// The index table grows without moving any Entry or T. Its allocation also +// uses System so lookup remains safe inside a global allocator's TLS setup. +unsafe fn grow_index_table(pool: *mut Pool, index: usize) { + if index < (*pool).capacity { + return; + } + let capacity = index + .checked_add(1) + .and_then(usize::checked_next_power_of_two) + .expect("thread-local index table overflow") + .max(8); + let layout = Layout::array::<*mut Entry>(capacity).expect("thread-local index table layout"); + let slots = System.alloc(layout).cast::<*mut Entry>(); + if slots.is_null() { + std::alloc::handle_alloc_error(layout); + } + for offset in 0..capacity { + // GC_STORE_AUDIT(POINTER_FREE): an empty native metadata slot has no heap edge. + slots.add(offset).write(ptr::null_mut()); + } + if (*pool).capacity != 0 { + ptr::copy_nonoverlapping((*pool).slots, slots, (*pool).capacity); + System.dealloc( + (*pool).slots.cast(), + Layout::array::<*mut Entry>((*pool).capacity).unwrap(), + ); + } + (*pool).slots = slots; + (*pool).capacity = capacity; +} + +unsafe extern "C" fn drop_pool(value: *mut libc::c_void) { + let key = *KEY.get().unwrap(); + // Keep a tombstone through every POSIX destructor pass: another library's + // destructor must not resurrect storage that we have already released. + if value == DESTROYED { + libc::pthread_setspecific(key, DESTROYED); + return; + } + let result = std::panic::catch_unwind(|| { + let pool = value.cast::(); + if libc::pthread_setspecific(key, value) != 0 { + std::process::abort(); + } + while !(*pool).drops.is_null() { + let entry = (*pool).drops; + (*pool).drops = (*entry).drop_next; + (*entry).state = State::Destroyed; + ((*entry).destroy)((*entry).value); + } + // No caller may rediscover the pool once its final allocations are freed. + if libc::pthread_setspecific(key, DESTROYED) != 0 { + std::process::abort(); + } + // Values without Drop stay available to the destructors above. This + // includes the hot-pointer cache used to retire cached slot addresses. + let mut entry = (*pool).entries; + while !entry.is_null() { + let next = (*entry).next; + if (*entry).state == State::Ready { + ((*entry).destroy)((*entry).value); + } + destroy::(entry.cast()); + entry = next; + } + if (*pool).capacity != 0 { + System.dealloc( + (*pool).slots.cast(), + Layout::array::<*mut Entry>((*pool).capacity).unwrap(), + ); + } + destroy::(pool.cast()); + }); + if result.is_err() { + std::process::abort(); + } +} + +pub struct LocalKey { + index: AtomicUsize, + initialize: fn() -> T, + marker: PhantomData, +} +// Handles contain no T; each access resolves the calling thread's storage. +unsafe impl Sync for LocalKey {} +impl LocalKey { + pub const fn new(initialize: fn() -> T) -> Self { + Self { + index: AtomicUsize::new(usize::MAX), + initialize, + marker: PhantomData, + } + } + fn index(&self) -> usize { + let index = self.index.load(Ordering::Relaxed); + if index != usize::MAX { + return index; + } + // Claim once per declaration, not per racing first-touching thread. + // Release the lock before any allocation, initializer, or destructor. + let mut next = NEXT_ID.lock().unwrap_or_else(|error| error.into_inner()); + let index = self.index.load(Ordering::Relaxed); + if index != usize::MAX { + return index; + } + let index = *next; + *next = next + .checked_add(1) + .expect("thread-local declaration index overflow"); + self.index.store(index, Ordering::Relaxed); + index + } + pub fn with(&'static self, f: F) -> R + where + F: FnOnce(&T) -> R, + { + self.try_with(f) + .expect("cannot access destroyed thread-local storage") + } + pub fn try_with(&'static self, f: F) -> Result + where + F: FnOnce(&T) -> R, + { + let pool = pool()?; + let index = self.index(); + unsafe { + grow_index_table(pool, index); + let mut entry = *(*pool).slots.add(index); + if entry.is_null() { + entry = alloc(Entry { + state: State::Vacant, + value: ptr::null_mut(), + destroy: destroy::, + next: (*pool).entries, + drop_next: ptr::null_mut(), + }); + // GC_STORE_AUDIT(POINTER_FREE): links System-allocated TLS + // metadata, not a Perry heap reference. The values retain + // their existing type-specific root scanners. + (*pool).entries = entry; + (*pool).slots.add(index).write(entry); + } + match (*entry).state { + State::Destroyed => return Err(AccessError), + State::Initializing => panic!("recursive thread-local initialization"), + State::Ready => {} + State::Vacant => { + (*entry).state = State::Initializing; + struct Reset(*mut Entry); + impl Drop for Reset { + fn drop(&mut self) { + unsafe { + (*self.0).state = State::Vacant; + } + } + } + let reset = Reset(entry); + // Hold no reference or collection borrow across user code. + let value = alloc((self.initialize)()); + (*entry).value = value.cast(); + (*entry).state = State::Ready; + if std::mem::needs_drop::() { + (*entry).drop_next = (*pool).drops; + (*pool).drops = entry; + } + std::mem::forget(reset); + } + } + Ok(f(&*(*entry).value.cast::())) + } + } +} + +#[cfg(test)] +mod tests; diff --git a/crates/perry-runtime/src/tls_os_pool/tests.rs b/crates/perry-runtime/src/tls_os_pool/tests.rs new file mode 100644 index 0000000000..50b7a66d46 --- /dev/null +++ b/crates/perry-runtime/src/tls_os_pool/tests.rs @@ -0,0 +1,127 @@ +use super::*; +use std::cell::Cell; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Mutex; +static ORDER: Mutex> = Mutex::new(Vec::new()); +struct Probe(u8); +static FIRST: LocalKey = LocalKey::new(|| Probe(1)); +static SECOND: LocalKey = LocalKey::new(|| Probe(2)); +static THIRD: LocalKey = LocalKey::new(|| Probe(3)); +static PLAIN: LocalKey> = LocalKey::new(|| Cell::new(99)); +impl Drop for Probe { + fn drop(&mut self) { + PLAIN.with(|v| assert_eq!(v.get(), 99)); + match self.0 { + 2 => { + assert!(SECOND.try_with(|_| ()).is_err()); + FIRST.with(|v| assert_eq!(v.0, 1)); + } + 1 => { + assert!(SECOND.try_with(|_| ()).is_err()); + THIRD.with(|v| assert_eq!(v.0, 3)); + } + 3 => assert!(FIRST.try_with(|_| ()).is_err()), + _ => unreachable!(), + } + ORDER.lock().unwrap().push(self.0); + } +} +#[test] +fn cleanup_supports_other_values_new_initializers_and_destroyed_errors() { + std::thread::spawn(|| { + PLAIN.with(|_| ()); + FIRST.with(|_| ()); + SECOND.with(|_| ()); + }) + .join() + .unwrap(); + assert_eq!(*ORDER.lock().unwrap(), [2, 1, 3]); +} +#[test] +fn initializer_unwind_leaves_the_key_retryable() { + static ATTEMPTS: AtomicUsize = AtomicUsize::new(0); + static RETRY: LocalKey = LocalKey::new(|| { + assert_ne!( + ATTEMPTS.fetch_add(1, Ordering::SeqCst), + 0, + "first attempt fails" + ); + 17 + }); + std::thread::spawn(|| { + assert!(std::panic::catch_unwind(|| RETRY.with(|_| ())).is_err()); + RETRY.with(|value| assert_eq!(*value, 17)); + }) + .join() + .unwrap(); + assert_eq!(ATTEMPTS.load(Ordering::SeqCst), 2); +} +#[test] +fn recursive_initializer_is_rejected_without_poisoning_other_keys() { + static RECURSIVE: LocalKey = LocalKey::new(|| RECURSIVE.with(|_| 1)); + std::thread::spawn(|| { + assert!(std::panic::catch_unwind(|| RECURSIVE.with(|_| ())).is_err()); + PLAIN.with(|v| assert_eq!(v.get(), 99)); + }) + .join() + .unwrap(); +} +#[test] +fn aligned_values_are_released_at_worker_exit() { + static DROPS: AtomicUsize = AtomicUsize::new(0); + #[repr(align(4096))] + struct Aligned(u8); + impl Drop for Aligned { + fn drop(&mut self) { + assert_eq!(self.0, 41); + DROPS.fetch_add(1, Ordering::SeqCst); + } + } + static ALIGNED: LocalKey = LocalKey::new(|| Aligned(41)); + for _ in 0..32 { + std::thread::spawn(|| ALIGNED.with(|v| assert_eq!((v as *const _ as usize) % 4096, 0))) + .join() + .unwrap(); + } + assert_eq!(DROPS.load(Ordering::SeqCst), 32); +} + +#[test] +fn hundreds_of_keys_stay_isolated_across_worker_turnover() { + static KEYS: [LocalKey>; 512] = [const { LocalKey::new(|| Cell::new(0)) }; 512]; + KEYS[0].with(|first| { + first.set(1234); + for key in KEYS.iter().skip(1) { + key.with(|_| ()); + } + assert_eq!( + first.get(), + 1234, + "growing the index table moved a borrowed value" + ); + }); + for (i, key) in KEYS.iter().enumerate() { + key.with(|v| v.set(i + 1)); + } + let workers: Vec<_> = (0..8) + .map(|worker| { + std::thread::spawn(move || { + for (i, key) in KEYS.iter().enumerate() { + key.with(|v| { + assert_eq!(v.get(), 0); + v.set(worker + i); + }); + } + for (i, key) in KEYS.iter().enumerate() { + key.with(|v| assert_eq!(v.get(), worker + i)); + } + }) + }) + .collect(); + for worker in workers { + worker.join().unwrap(); + } + for (i, key) in KEYS.iter().enumerate() { + key.with(|v| assert_eq!(v.get(), i + 1)); + } +} diff --git a/crates/perry-runtime/tests/android_tls_pool.rs b/crates/perry-runtime/tests/android_tls_pool.rs new file mode 100644 index 0000000000..e5ffd0a5dd --- /dev/null +++ b/crates/perry-runtime/tests/android_tls_pool.rs @@ -0,0 +1,135 @@ +//! #10219: more live Perry declarations than Android's entire pthread key +//! budget. The same binary checks thread isolation and real value destruction. +#![cfg(unix)] +#![recursion_limit = "512"] + +use std::cell::RefCell; +use std::sync::atomic::{AtomicUsize, Ordering}; +static DROPS: AtomicUsize = AtomicUsize::new(0); +struct Probe(usize); +impl Drop for Probe { + fn drop(&mut self) { + DROPS.fetch_add(1, Ordering::SeqCst); + } +} +macro_rules! declarations { + ($($key:ident),+ $(,)?) => { + perry_runtime::perry_thread_local! { + $(static $key: RefCell> = RefCell::new(Vec::new());)+ + } + fn visit(mut f: impl FnMut(usize, &'static perry_runtime::tls_hot::HotKey>>)) { + for (i, key) in [$(&$key),+].into_iter().enumerate() { f(i, key); } + } + }; +} +declarations!( + KEY_000, KEY_001, KEY_002, KEY_003, KEY_004, KEY_005, KEY_006, KEY_007, KEY_008, KEY_009, + KEY_010, KEY_011, KEY_012, KEY_013, KEY_014, KEY_015, KEY_016, KEY_017, KEY_018, KEY_019, + KEY_020, KEY_021, KEY_022, KEY_023, KEY_024, KEY_025, KEY_026, KEY_027, KEY_028, KEY_029, + KEY_030, KEY_031, KEY_032, KEY_033, KEY_034, KEY_035, KEY_036, KEY_037, KEY_038, KEY_039, + KEY_040, KEY_041, KEY_042, KEY_043, KEY_044, KEY_045, KEY_046, KEY_047, KEY_048, KEY_049, + KEY_050, KEY_051, KEY_052, KEY_053, KEY_054, KEY_055, KEY_056, KEY_057, KEY_058, KEY_059, + KEY_060, KEY_061, KEY_062, KEY_063, KEY_064, KEY_065, KEY_066, KEY_067, KEY_068, KEY_069, + KEY_070, KEY_071, KEY_072, KEY_073, KEY_074, KEY_075, KEY_076, KEY_077, KEY_078, KEY_079, + KEY_080, KEY_081, KEY_082, KEY_083, KEY_084, KEY_085, KEY_086, KEY_087, KEY_088, KEY_089, + KEY_090, KEY_091, KEY_092, KEY_093, KEY_094, KEY_095, KEY_096, KEY_097, KEY_098, KEY_099, + KEY_100, KEY_101, KEY_102, KEY_103, KEY_104, KEY_105, KEY_106, KEY_107, KEY_108, KEY_109, + KEY_110, KEY_111, KEY_112, KEY_113, KEY_114, KEY_115, KEY_116, KEY_117, KEY_118, KEY_119, + KEY_120, KEY_121, KEY_122, KEY_123, KEY_124, KEY_125, KEY_126, KEY_127, KEY_128, KEY_129, + KEY_130, KEY_131, KEY_132, KEY_133, KEY_134, KEY_135, KEY_136, KEY_137, KEY_138, KEY_139, + KEY_140, KEY_141, KEY_142, KEY_143, KEY_144, KEY_145, KEY_146, KEY_147, KEY_148, KEY_149, + KEY_150, KEY_151, KEY_152, KEY_153, KEY_154, KEY_155, KEY_156, KEY_157, KEY_158, KEY_159, + KEY_160, KEY_161, KEY_162, KEY_163, KEY_164, KEY_165, KEY_166, KEY_167, KEY_168, KEY_169, + KEY_170, KEY_171, KEY_172, KEY_173, KEY_174, KEY_175, KEY_176, KEY_177, KEY_178, KEY_179, + KEY_180, KEY_181, KEY_182, KEY_183, KEY_184, KEY_185, KEY_186, KEY_187, KEY_188, KEY_189, + KEY_190, KEY_191, +); + +#[test] +fn hundreds_of_perry_declarations_initialize_and_drop_on_multiple_threads() { + visit(|i, key| key.with(|value| value.borrow_mut().push(Probe(1000 + i)))); + let barrier = std::sync::Arc::new(std::sync::Barrier::new(8)); + let workers: Vec<_> = (0..8) + .map(|worker| { + let barrier = barrier.clone(); + std::thread::spawn(move || { + visit(|i, key| { + key.with(|value| { + assert!( + value.borrow().is_empty(), + "a worker inherited another thread's storage" + ); + value.borrow_mut().push(Probe(worker * 1000 + i)); + }) + }); + barrier.wait(); + visit(|i, key| { + key.with(|value| assert_eq!(value.borrow()[0].0, worker * 1000 + i)) + }); + }) + }) + .collect(); + for worker in workers { + worker.join().unwrap(); + } + assert_eq!( + DROPS.load(Ordering::SeqCst), + 192 * 8, + "worker values must be destroyed exactly once" + ); + visit(|i, key| key.with(|value| assert_eq!(value.borrow()[0].0, 1000 + i))); +} + +// A destructor owned by another library can run after Perry's entire pool. +// Keep try_with fallible then, including across repeated POSIX destructor passes. +#[cfg(target_os = "android")] +#[test] +fn later_pthread_destructors_cannot_resurrect_the_hot_cache() { + use std::sync::atomic::AtomicU8; + static OBSERVED: AtomicU8 = AtomicU8::new(0); + perry_runtime::perry_thread_local! { + static LATE_PROBE: RefCell> = RefCell::new(Vec::new()); + } + struct Callback { + key: libc::pthread_key_t, + round: u8, + } + unsafe extern "C" fn callback(raw: *mut libc::c_void) { + let state = &mut *raw.cast::(); + state.round += 1; + if state.round < 3 { + assert_eq!(libc::pthread_setspecific(state.key, raw), 0); + return; + } + let observed = match std::panic::catch_unwind(|| LATE_PROBE.try_with(|_| ())) { + Ok(Err(_)) => 1, + Ok(Ok(_)) => 2, + Err(_) => 3, + }; + OBSERVED.store(observed, Ordering::SeqCst); + drop(Box::from_raw(raw.cast::())); + } + // By the third callback pass the pool has been destroyed, regardless of + // which key the platform visits first within each pass. + let key = std::thread::spawn(|| { + LATE_PROBE.with(|value| value.borrow_mut().push(1)); + let mut key = 0; + assert_eq!( + unsafe { libc::pthread_key_create(&mut key, Some(callback)) }, + 0 + ); + let state = Box::into_raw(Box::new(Callback { key, round: 0 })); + assert_eq!(unsafe { libc::pthread_setspecific(key, state.cast()) }, 0); + key + }) + .join() + .unwrap(); + unsafe { + libc::pthread_key_delete(key); + } + assert_eq!( + OBSERVED.load(Ordering::SeqCst), + 1, + "1=AccessError, 2=resurrected value, 3=panic, 0=callback never ran" + ); +}