Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/10244-android-tls-pool.md
Original file line number Diff line number Diff line change
@@ -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.
3 changes: 3 additions & 0 deletions crates/perry-runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
42 changes: 34 additions & 8 deletions crates/perry-runtime/src/tls_hot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -272,6 +280,12 @@ thread_local! {
static HOT: UnsafeCell<HotTls> = 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<UnsafeCell<HotTls>> =
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
Expand Down Expand Up @@ -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<T: 'static> {
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.
Expand All @@ -819,7 +833,7 @@ impl<T: 'static> HotKey<T> {
#[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 {
Expand Down Expand Up @@ -847,10 +861,14 @@ impl<T: 'static> HotKey<T> {
/// 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<F, R>(&'static self, f: F) -> Result<R, std::thread::AccessError>
pub fn try_with<F, R>(&'static self, f: F) -> Result<R, AccessError>
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);
Expand Down Expand Up @@ -915,7 +933,7 @@ impl<T: 'static> HotKey<T> {
/// 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
Expand Down Expand Up @@ -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) {
Expand All @@ -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));
};
}

Expand Down
7 changes: 7 additions & 0 deletions crates/perry-runtime/src/tls_hot/provider_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,17 @@ fn provider_copies_share_storage_without_initializing_a_second_value() {
}
}
type Storage = super::HotCell<Probe, 1>;
#[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<Storage> =
crate::tls_os_pool::LocalKey::new(|| Storage::new(Probe::new()));
#[cfg(target_os = "android")]
static SECOND_STORAGE: crate::tls_os_pool::LocalKey<Storage> =
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<Probe> = super::HotKey::new(
Expand Down
Loading
Loading