From 8a0c8bc02111420080732645ceda78986629945b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 7 Aug 2026 03:50:13 +0200 Subject: [PATCH 1/8] perf(runtime): reach the hot-TLS cache without _tlv_get_addr on Apple aarch64 #7474 cached the addresses of the thread-locals on the allocation path, but `HOT` is itself a `thread_local!`, so every runtime function reading any hot field still paid one `_tlv_get_addr` call. Symbolicated on the pinned quiet host at 9938cbc1a that residue is 27.0% of `churn_alloc` self time, and the call-graph attribution is concentrated rather than diffuse: seven functions carry 98% of it and every one of them resolves `HOT`. Publish the cache's address into one `pthread_key_create` slot and read it back through `TPIDRRO_EL0`, which is how `pthread_getspecific` itself is implemented and what mimalloc (already linked here) does on this platform. The resolution becomes `mrs` plus two loads that LLVM can CSE across a function instead of an out-of-line call that clobbers caller-saved registers. The publishing thread reads its slot back through the direct path and compares it against what `pthread_setspecific` was handed; a mismatch disables the direct path process-wide and every thread falls back to `_tlv_get_addr`. There is no path on which a wrong address reaches the allocator. Claude-Session: https://claude.ai/code/session_019EHcmXKArA7m42SihYCcgH --- crates/perry-runtime/src/tls_hot.rs | 283 +++++++++++++++++++++++++++- 1 file changed, 280 insertions(+), 3 deletions(-) diff --git a/crates/perry-runtime/src/tls_hot.rs b/crates/perry-runtime/src/tls_hot.rs index dbc6aaa4c6..14342368fa 100644 --- a/crates/perry-runtime/src/tls_hot.rs +++ b/crates/perry-runtime/src/tls_hot.rs @@ -53,6 +53,35 @@ //! `js_inline_arena_state`. It is not an invitation to send one across //! threads, and the pointee types (`Cell`, `RefCell`, `UnsafeCell`) are all //! `!Sync`, so the compiler refuses that on its own. +//! +//! # Reaching the cache without a TLS resolution (#7469 structural half) +//! +//! Caching the addresses removed the *per-field* resolutions but left one: +//! `HOT` is itself a `thread_local!`, so every runtime function that reads any +//! hot field still paid one `_tlv_get_addr` call. Symbolicated on the pinned +//! quiet host at `9938cbc1a`, that residue was **27.0% of `churn_alloc`**, and +//! its call-graph attribution was not diffuse: **seven functions carried 98% +//! of it, and every one of the seven resolves `HOT`** — +//! `write_barrier_decoded_parent` (19.3%), `layout_forget_object` (18.9%), +//! `js_object_alloc_class_inline_keys` (18.5%), `arena_alloc` (14.9%), +//! `js_write_barrier_slot` (9.2%), `barrier_child_prologue` (8.8%) and +//! `typed_shape_layout_entry` (8.4%). Two of them resolve *nothing else*. +//! +//! So the structural fix is to make reaching `HOT` free rather than to thread a +//! context pointer through 2994 `.with()` sites. On Apple aarch64 the pthread +//! thread-specific-data array is directly addressable from `TPIDRRO_EL0` — +//! that is how `pthread_getspecific` itself is implemented, and what mimalloc +//! (already linked into this runtime) does on this platform. Publishing the +//! cache's address into one `pthread_key_create` slot turns the resolution +//! from an out-of-line call that clobbers caller-saved registers into `mrs` + +//! two loads that LLVM can CSE across a whole function. +//! +//! [`darwin_tsd`] carries the self-check that makes this safe to ship: the +//! publishing thread reads the slot back through the direct path and compares +//! it against what `pthread_setspecific` was handed. A mismatch — the shape a +//! future OS change would take — disables the direct path process-wide and +//! every thread falls back to `_tlv_get_addr`, permanently and silently +//! correctly. It cannot degrade into reading a wrong address. use std::cell::UnsafeCell; @@ -148,10 +177,11 @@ fn fill(slots: *mut HotTls) { } } -/// The per-thread address cache. One `_tlv_get_addr` for every thread-local it -/// covers. +/// Resolve this thread's cache through the ordinary TLS accessor, filling it +/// on first use. One `_tlv_get_addr` on Darwin; a `local-exec` fixed offset on +/// targets whose TLS model does not need an accessor call at all. #[inline(always)] -pub(crate) fn hot() -> &'static HotTls { +fn hot_via_tls() -> &'static HotTls { let slots = HOT.with(|cell| cell.get()); // SAFETY: `HOT` is const-init with no `Drop`, so its storage is valid for // the whole life of the thread — see the module docs on lifetime. @@ -163,6 +193,165 @@ pub(crate) fn hot() -> &'static HotTls { } } +/// Direct pthread thread-specific-data addressing, and the self-check that +/// keeps it honest. See the module docs for why this exists. +#[cfg(all( + target_vendor = "apple", + target_arch = "aarch64", + target_pointer_width = "64" +))] +pub(crate) mod darwin_tsd { + use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; + + /// `KEY`'s "there is no direct path" value — either the key has not been + /// created yet, or [`publish`]'s self-check rejected it. + pub(super) const NO_KEY: usize = usize::MAX; + + pub(super) static KEY: AtomicUsize = AtomicUsize::new(NO_KEY); + + /// Latched by [`disable`] so no later thread retries a path this process + /// has already proven wrong. + static DISABLED: AtomicBool = AtomicBool::new(false); + + /// Read thread-specific-data slot `slot` for the calling thread. + /// + /// `TPIDRRO_EL0` holds this thread's TSD base with the CPU number in the + /// low three bits. Masking those off and indexing is precisely what + /// `_os_tsd_get_direct` — and therefore `pthread_getspecific` — does, and + /// what mimalloc (already linked into this runtime) does on this platform. + /// + /// # Safety + /// `slot` must be a key returned by `pthread_key_create`, so that the index + /// lands inside the thread's TSD array. + #[inline(always)] + pub(super) unsafe fn get(slot: usize) -> *mut u8 { + let base: usize; + // `pure` + `nomem` is deliberate. Stripped of its CPU-number bits the + // register is a per-thread constant, so LLVM may CSE it across a whole + // function and hoist it out of loops. Without `pure` every reader + // re-issues the `mrs` and the change buys far less than the call it + // replaces. + core::arch::asm!( + "mrs {b}, tpidrro_el0", + b = out(reg) base, + options(nomem, nostack, preserves_flags, pure) + ); + // SAFETY: the caller guarantees `slot` is a live pthread key. + unsafe { *((base & !0b111) as *const *mut u8).add(slot) } + } + + /// Publish `value` as this thread's cache address, then prove the direct + /// read agrees with what `pthread_setspecific` was handed. + /// + /// The check is the whole reason this is safe to ship: if a future OS ever + /// moves the TSD array out from under `TPIDRRO_EL0`, the very first thread + /// notices here and the process reverts to `_tlv_get_addr` for good. There + /// is no path on which a mismatch turns into a wrong address being read on + /// the allocation hot path. + pub(super) fn publish(value: *mut u8) { + if DISABLED.load(Ordering::Relaxed) { + return; + } + let key = ensure_key(); + if key == NO_KEY { + return; + } + // SAFETY: `key` came from `pthread_key_create` below. + let rc = unsafe { libc::pthread_setspecific(key as libc::pthread_key_t, value.cast()) }; + // SAFETY: as above — and this is exactly the read `pthread_getspecific` + // would perform for the same key on this thread. + if rc != 0 || unsafe { get(key) } != value { + disable(key); + } + } + + fn ensure_key() -> usize { + static ONCE: std::sync::Once = std::sync::Once::new(); + ONCE.call_once(|| { + let mut key: libc::pthread_key_t = 0; + // No destructor: the cache is the `HOT` thread-local's own storage, + // owned by the TLS runtime. This key only borrows its address. + // SAFETY: `key` is a live local for the duration of the call. + if unsafe { libc::pthread_key_create(&mut key, None) } == 0 { + KEY.store(key as usize, Ordering::Release); + } else { + DISABLED.store(true, Ordering::Relaxed); + } + }); + KEY.load(Ordering::Acquire) + } + + #[cold] + fn disable(key: usize) { + DISABLED.store(true, Ordering::Relaxed); + KEY.store(NO_KEY, Ordering::Release); + // SAFETY: `key` came from `pthread_key_create`. + unsafe { + libc::pthread_setspecific(key as libc::pthread_key_t, std::ptr::null()); + } + } + + /// Whether the direct path is live for this process. + /// + /// A gate that does not assert this is measuring nothing: `false` means + /// every `hot()` is paying `_tlv_get_addr` again and the whole change is + /// inert. `tls_hot::tests::direct_tsd_path_is_live` is that assertion. + pub(crate) fn active() -> bool { + KEY.load(Ordering::Relaxed) != NO_KEY + } +} + +/// Resolve, fill and publish this thread's cache. Cold: once per thread. +#[cfg(all( + target_vendor = "apple", + target_arch = "aarch64", + target_pointer_width = "64" +))] +#[cold] +#[inline(never)] +fn hot_uncached() -> &'static HotTls { + let slots = hot_via_tls(); + darwin_tsd::publish(slots as *const HotTls as *mut u8); + slots +} + +/// The per-thread address cache. On Apple aarch64 this is an `mrs` plus two +/// loads with no call at all; elsewhere it is the single TLS access the field +/// cache already collapsed the whole allocation path down to. +#[cfg(all( + target_vendor = "apple", + target_arch = "aarch64", + target_pointer_width = "64" +))] +#[inline(always)] +pub(crate) fn hot() -> &'static HotTls { + let key = darwin_tsd::KEY.load(std::sync::atomic::Ordering::Relaxed); + if key != darwin_tsd::NO_KEY { + // SAFETY: `key` is a live pthread key, so the slot exists; it holds + // either null (this thread has not published yet) or the address this + // thread published from `HOT`. + let slots = unsafe { darwin_tsd::get(key) } as *mut HotTls; + if !slots.is_null() { + // SAFETY: published from `HOT`, which is const-init with no `Drop` + // — see the module docs on lifetime. + return unsafe { &*slots }; + } + } + hot_uncached() +} + +/// The per-thread address cache. One TLS access for every thread-local it +/// covers. +#[cfg(not(all( + target_vendor = "apple", + target_arch = "aarch64", + target_pointer_width = "64" +)))] +#[inline(always)] +pub(crate) fn hot() -> &'static HotTls { + hot_via_tls() +} + #[cfg(test)] mod tests { /// Every cached address must equal the address of the `thread_local!` it @@ -280,6 +469,94 @@ mod tests { } } + /// The direct thread-specific-data path must be live on this platform. + /// + /// This is the liveness assertion for #7469's structural half. Every other + /// test here passes identically whether `hot()` costs an `_tlv_get_addr` + /// call or three inline instructions, so without this one a silent fallback + /// would make the change inert and nothing would go red. + #[cfg(all( + target_vendor = "apple", + target_arch = "aarch64", + target_pointer_width = "64" + ))] + #[test] + fn direct_tsd_path_is_live() { + let expected = super::hot() as *const super::HotTls; + assert!( + super::darwin_tsd::active(), + "direct TSD path fell back to _tlv_get_addr — the #7469 structural \ + fix is inert on this run" + ); + let key = super::darwin_tsd::KEY.load(std::sync::atomic::Ordering::Relaxed); + // SAFETY: `active()` above proves the key came from pthread_key_create. + let direct = unsafe { super::darwin_tsd::get(key) } as *const super::HotTls; + assert_eq!( + direct, expected, + "direct TSD read disagreed with the published cache address" + ); + } + + /// The direct read must agree with `pthread_getspecific` for the same key. + /// + /// `get()` open-codes what libpthread does; this is the check that says so + /// against the real implementation rather than against our own belief about + /// it. If Apple ever moves the TSD array, this fails here rather than in + /// the allocator. + #[cfg(all( + target_vendor = "apple", + target_arch = "aarch64", + target_pointer_width = "64" + ))] + #[test] + fn direct_read_matches_pthread_getspecific() { + let mut key: libc::pthread_key_t = 0; + // SAFETY: `key` is a live local for the duration of the call. + assert_eq!(unsafe { libc::pthread_key_create(&mut key, None) }, 0); + let sentinel = 0x5eed_1234_usize as *mut libc::c_void; + // SAFETY: `key` was just created. + assert_eq!( + unsafe { libc::pthread_setspecific(key, sentinel) }, + 0, + "pthread_setspecific rejected a freshly created key" + ); + // SAFETY: as above. + let direct = unsafe { super::darwin_tsd::get(key as usize) }; + // SAFETY: as above. + let via_pthread = unsafe { libc::pthread_getspecific(key) }; + assert_eq!(direct as *mut libc::c_void, via_pthread); + assert_eq!(direct, sentinel.cast()); + // SAFETY: as above. + unsafe { + libc::pthread_setspecific(key, std::ptr::null()); + libc::pthread_key_delete(key); + } + } + + /// A thread that has not published yet must see null in its slot and take + /// the cold path, not another thread's cache. + #[cfg(all( + target_vendor = "apple", + target_arch = "aarch64", + target_pointer_width = "64" + ))] + #[test] + fn a_fresh_thread_publishes_its_own_slot() { + let mine = super::hot() as *const super::HotTls; + let theirs = std::thread::spawn(|| { + let addr = super::hot() as *const super::HotTls; + let key = super::darwin_tsd::KEY.load(std::sync::atomic::Ordering::Relaxed); + assert_ne!(key, super::darwin_tsd::NO_KEY); + // SAFETY: `key` is live; the assert above proves it was created. + let direct = unsafe { super::darwin_tsd::get(key) } as *const super::HotTls; + assert_eq!(direct, addr, "worker thread published the wrong address"); + addr as usize + }) + .join() + .expect("probe thread panicked"); + assert_ne!(mine as usize, theirs, "two threads shared one cache"); + } + /// The cache is per-thread: a second thread must resolve its own /// addresses, not inherit this one's. #[test] From 1ff10145203242cafd6f1b10f8e7b7978f45a04c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 7 Aug 2026 04:01:07 +0200 Subject: [PATCH 2/8] perf(runtime): cache the learned-inline-fields table address too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With `HOT` reachable without `_tlv_get_addr`, the residual on `churn_alloc` was 3.5% of self time and 100% of it attributed to one caller, `js_object_alloc_class_inline_keys` — which is `learned_inline_field_count`, run on every dynamic construct to right-size the inline slot count. The other thread-locals that function names statically (`MARK_SEEDS`, `WRITE_BARRIER_TRACE_COUNTERS`) sit behind cold gates and never resolve. Route it through the existing address cache, following the four-step contract in `tls_hot`: slot, provider next to the `thread_local!`, wiring in `fill`, and the pairing assertion that stands between a mis-wire and a well-typed reference to the wrong object. Claude-Session: https://claude.ai/code/session_019EHcmXKArA7m42SihYCcgH --- crates/perry-runtime/src/object/mod.rs | 4 ++- crates/perry-runtime/src/object/spill.rs | 43 ++++++++++++++++++------ crates/perry-runtime/src/tls_hot.rs | 19 +++++++++++ 3 files changed, 55 insertions(+), 11 deletions(-) diff --git a/crates/perry-runtime/src/object/mod.rs b/crates/perry-runtime/src/object/mod.rs index 35dc743670..fc95081794 100644 --- a/crates/perry-runtime/src/object/mod.rs +++ b/crates/perry-runtime/src/object/mod.rs @@ -101,7 +101,9 @@ mod regex_proto_thunks; // `object::*` modules reach these through `use super::*`, so re-export the // names they use (the rest stay internal to `spill`). mod spill; -pub(crate) use spill::{learned_inline_field_count, overflow_get, overflow_set}; +pub(crate) use spill::{ + learned_inline_field_count, learned_inline_fields_hot_addr, overflow_get, overflow_set, +}; #[cfg(test)] use spill::{object_spill_enabled, spill_capable_owner, spill_get, SPILL_MAX_FIELD_INDEX}; #[cfg(test)] diff --git a/crates/perry-runtime/src/object/spill.rs b/crates/perry-runtime/src/object/spill.rs index 5ec6852b43..dc3570737a 100644 --- a/crates/perry-runtime/src/object/spill.rs +++ b/crates/perry-runtime/src/object/spill.rs @@ -318,20 +318,43 @@ thread_local! { const { std::cell::UnsafeCell::new([(0u32, 0u32); LEARNED_INLINE_TABLE_SIZE]) }; } +type LearnedInlineTable = std::cell::UnsafeCell<[(u32, u32); LEARNED_INLINE_TABLE_SIZE]>; + +/// Address of this thread's `LEARNED_INLINE_FIELDS`. See `crate::tls_hot`. +pub(crate) fn learned_inline_fields_hot_addr() -> *mut u8 { + LEARNED_INLINE_FIELDS.with(|t| t as *const _ as *mut u8) +} + +/// `LEARNED_INLINE_FIELDS` without a TLS resolution. +/// +/// [`learned_inline_field_count`] runs on every dynamic construct, so this was +/// the last thread-local left on `churn_alloc`'s allocation path after #7469's +/// structural half: 100% of the residual `_tlv_get_addr` samples attributed to +/// `js_object_alloc_class_inline_keys`, which is this read. +#[inline(always)] +fn hot_learned_inline_fields() -> &'static LearnedInlineTable { + // SAFETY: paired with `learned_inline_fields_hot_addr` above, and asserted + // by `tls_hot::tests::cached_addresses_match_thread_locals`. + unsafe { &*(crate::tls_hot::hot().learned_inline_fields as *const LearnedInlineTable) } +} + #[inline] fn note_learned_inline_fields(class_id: u32, needed_fields: u32) { if class_id == 0 || needed_fields > LEARNED_INLINE_MAX_FIELDS { return; } let slot = (class_id as usize).wrapping_mul(0x9E37_79B1) % LEARNED_INLINE_TABLE_SIZE; - LEARNED_INLINE_FIELDS.with(|t| unsafe { + let t = hot_learned_inline_fields(); + // SAFETY: the table is this thread's own storage and the runtime is + // single-threaded per arena; `slot` is reduced modulo the table size. + unsafe { let e = &mut (*t.get())[slot]; if e.0 != class_id { *e = (class_id, needed_fields); } else if e.1 < needed_fields { e.1 = needed_fields; } - }); + } } /// Inline field count to pre-size a dynamic construct of `class_id` with — @@ -353,14 +376,14 @@ pub(crate) fn learned_inline_field_count(class_id: u32) -> u32 { return 0; } let slot = (class_id as usize).wrapping_mul(0x9E37_79B1) % LEARNED_INLINE_TABLE_SIZE; - LEARNED_INLINE_FIELDS.with(|t| unsafe { - let e = (*t.get())[slot]; - if e.0 == class_id { - e.1 - } else { - 0 - } - }) + let t = hot_learned_inline_fields(); + // SAFETY: as in `note_learned_inline_fields` above. + let e = unsafe { (*t.get())[slot] }; + if e.0 == class_id { + e.1 + } else { + 0 + } } /// accessed Vec — the common row-build pattern where an object's diff --git a/crates/perry-runtime/src/tls_hot.rs b/crates/perry-runtime/src/tls_hot.rs index 14342368fa..2d503b1d85 100644 --- a/crates/perry-runtime/src/tls_hot.rs +++ b/crates/perry-runtime/src/tls_hot.rs @@ -111,6 +111,8 @@ pub(crate) struct HotTls { pub(crate) per_object_layouts_nonempty: *mut u8, // gc/shape_install.rs pub(crate) shape_install_memo: *mut u8, + // object/spill.rs + pub(crate) learned_inline_fields: *mut u8, // gc/roots/temp_roots.rs pub(crate) temp_roots: *mut u8, } @@ -131,6 +133,7 @@ impl HotTls { shape_layouts: std::ptr::null_mut(), per_object_layouts_nonempty: std::ptr::null_mut(), shape_install_memo: std::ptr::null_mut(), + learned_inline_fields: std::ptr::null_mut(), temp_roots: std::ptr::null_mut(), }; } @@ -169,6 +172,7 @@ fn fill(slots: *mut HotTls) { (*slots).shape_layouts = crate::gc::shape_layouts_hot_addr(); (*slots).per_object_layouts_nonempty = crate::gc::per_object_layouts_nonempty_hot_addr(); (*slots).shape_install_memo = crate::gc::shape_install_memo_hot_addr(); + (*slots).learned_inline_fields = crate::object::learned_inline_fields_hot_addr(); // Last, and the field `hot()` tests: every other slot is already // written by the time this one is non-null, so a re-entrant call from // inside one of the providers above cannot observe a half-filled cache @@ -231,6 +235,15 @@ pub(crate) mod darwin_tsd { // function and hoist it out of loops. Without `pure` every reader // re-issues the `mrs` and the change buys far less than the call it // replaces. + // + // This grants LLVM exactly the freedom it already has over the thing + // being replaced: `@llvm.threadlocal.address` is `speculatable` and + // `memory(none)`, so a `thread_local!` read was already CSE-able and + // hoistable on the same terms. The one shape that is unsound under + // either is holding the result across a point where execution can + // resume on a *different* thread — an `.await` in a work-stealing + // executor. Nothing on the allocation path is `async`, and this is a + // pre-existing constraint on `hot()` rather than one introduced here. core::arch::asm!( "mrs {b}, tpidrro_el0", b = out(reg) base, @@ -427,6 +440,11 @@ mod tests { crate::gc::shape_install_memo_hot_addr(), "shape_install_memo" ); + assert_eq!( + hot.learned_inline_fields, + crate::object::learned_inline_fields_hot_addr(), + "learned_inline_fields" + ); assert_eq!( hot.temp_roots, crate::gc::temp_roots_hot_addr(), @@ -463,6 +481,7 @@ mod tests { hot.per_object_layouts_nonempty, ), ("shape_install_memo", hot.shape_install_memo), + ("learned_inline_fields", hot.learned_inline_fields), ("temp_roots", hot.temp_roots), ] { assert!(!ptr.is_null(), "{name} slot was left null by fill()"); From eefc83473861b93b9ad599132d7de6bf9f866afb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 7 Aug 2026 04:14:14 +0200 Subject: [PATCH 3/8] =?UTF-8?q?docs(changelog):=20#7469=20structural=20hal?= =?UTF-8?q?f=20=E2=80=94=20direct=20TSD=20hot-TLS=20resolution=20(#7565)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_019EHcmXKArA7m42SihYCcgH --- changelog.d/7565-tls-direct-tsd.md | 104 +++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 changelog.d/7565-tls-direct-tsd.md diff --git a/changelog.d/7565-tls-direct-tsd.md b/changelog.d/7565-tls-direct-tsd.md new file mode 100644 index 0000000000..b85f27f8d2 --- /dev/null +++ b/changelog.d/7565-tls-direct-tsd.md @@ -0,0 +1,104 @@ +### Allocation path: reaching the hot-TLS cache stops being a call (#7469) + +#7474 cached the *addresses* of the thread-locals on the allocation path in one +`const`-initialised thread-local, collapsing a dozen `_tlv_get_addr` calls per +object literal down to one. That one remained: `HOT` is itself a +`thread_local!`, so every runtime function reading any hot field still paid a +call into `libdyld`. It is now **1.1% of `churn_alloc` self time, from 27.0%**, +and the three allocation probes are 1.14x–1.18x faster. + +**Re-measured before scoping**, because three tickets in this campaign were +worked off stale headline numbers. Symbolicated `sample` on the pinned quiet +host at `9938cbc1a`, 923 leaf samples: `_tlv_get_addr` is 27.0% — the headline +had not drifted, but the *shape* had. The ticket opened saying "41 distinct +call-graph sites — this is diffuse, not one hot caller". It is not diffuse any +more. **Seven functions carry 98% of it, and every one of the seven resolves +`HOT`:** + +| caller | share of `_tlv_get_addr` | of total | +|---|--:|--:| +| `gc::barrier::write_barrier_decoded_parent` | 19.3% | 5.2% | +| `gc::layout_tables::layout_forget_object` | 18.9% | 5.1% | +| `js_object_alloc_class_inline_keys` | 18.5% | 5.0% | +| `arena::allocators::arena_alloc` | 14.9% | 4.0% | +| `js_write_barrier_slot` | 9.2% | 2.5% | +| `gc::barrier::barrier_child_prologue` | 8.8% | 2.4% | +| `gc::layout::typed_shape_layout_entry` | 8.4% | 2.3% | + +Two of the seven resolve *nothing else*. (Naming which thread-local each site +resolves needs a static census rather than a grep: on Mach-O there is no +`bl _tlv_get_addr` in the text at all — the call is indirect through the TLV +descriptor, so the census walks `adrp`/`add` pairs landing in `__thread_vars`.) + +That attribution chose the design. The lever is the accessor, not the call +graph: the ticket's "thread a context pointer through generated code" would +have to cross every runtime FFI boundary against 2994 `.with()` sites over 255 +`thread_local!` blocks, while making `hot()` free fixes all seven at once. + +On Apple aarch64 the pthread thread-specific-data array is directly addressable +from `TPIDRRO_EL0` — that is how `pthread_getspecific` itself is implemented, +and what mimalloc (already linked into this runtime) does on this platform. +`tls_hot` publishes the cache's address into one `pthread_key_create` slot and +reads it back inline: `mrs` plus two loads, no call, no caller-saved-register +clobber, and `pure`+`nomem` so LLVM CSEs it across a whole function. That is +exactly the freedom LLVM already had over `@llvm.threadlocal.address` +(`speculatable`, `memory(none)`), so it introduces no hazard the `thread_local!` +read did not already carry — written down at the site, along with the one shape +unsound under either (holding the result across an `.await` in a work-stealing +executor; nothing on the allocation path is `async`). Every other target keeps +the previous path unchanged. + +**It cannot silently read a wrong address.** The publishing thread reads its +slot back *through the direct path* and compares it against what +`pthread_setspecific` was handed; a mismatch — the shape a future OS change +would take — latches the direct path off process-wide and every thread reverts +to `_tlv_get_addr`. A fresh thread reads null and takes the cold path, which is +POSIX ("upon thread creation, the value NULL shall be associated with all +defined keys in the new thread"), not an implementation detail. + +**And the fast path is asserted live, not assumed.** Every other test in +`tls_hot` passes identically whether `hot()` costs a call or three +instructions, so a silent fallback would make the change inert with nothing +red. `direct_tsd_path_is_live` fails if the direct path was disabled; +`direct_read_matches_pthread_getspecific` checks the open-coded read against the +real libpthread implementation for the same key rather than against our belief +about it; `a_fresh_thread_publishes_its_own_slot` covers worker threads. +Statically, the nine `HOT` descriptor materialisations across those seven +functions are gone from the emitted binary, replaced by `TPIDRRO_EL0` reads +(whole-binary `mrs` count 104 → 482). + +A second commit takes the residue. With `HOT` free, `_tlv_get_addr` fell to +3.5% and **100% of what was left attributed to one caller** — +`js_object_alloc_class_inline_keys`, i.e. `learned_inline_field_count`, which +runs on every dynamic construct. `LEARNED_INLINE_FIELDS` joins the cache under +the same four-step contract. The other thread-locals those functions name +statically were deliberately left alone because the profile does not reach +them: `MARK_SEEDS` and `WRITE_BARRIER_TRACE_COUNTERS` sit behind cold gates, +and `ARENA_TOTAL_BYTES` / `OLD_GEN_IN_USE_BYTES` in `arena_alloc` only move +when a block is installed. + +Pinned quiet host, arms interleaved round by round so load drift hits both +equally, best-of-7 after a discarded warm-up: + +| probe | `main` | +direct TSD | +learned-inline | total | +|---|--:|--:|--:|--:| +| `churn_alloc` — object literal + push | 1.294 s | 1.134 s | **1.109 s** | **1.167x** | +| `churn` — literal + push + read back | 1.649 s | 1.411 s | **1.403 s** | **1.175x** | +| `push_cls` — `new Node(v,w)` + push | 1.263 s | 1.117 s | **1.104 s** | **1.144x** | + +Peak RSS flat (25.2 → 25.2–25.4 MB); program output byte-identical across all +three arms on all three probes. `cargo test -p perry-runtime`: 1811 passed, 0 +failed. All three probes under +`PERRY_GC_ZEAL=1 PERRY_GC_PROTECT_FROMSPACE=1 PERRY_GC_PROTECT_FROMSPACE_DEPTH=800` +exit 0 with 110 `[gc-fromspace-protect] mode=ProtectPages retired_set=#N` lines +each — the count is quoted rather than the exit code, because a run with zero +copying minors protects nothing. The GC ratchet reports exactly the ten gating +breaches #7559 already records on `main`, cell for cell, including +`05_closure_capture` +16.44% and `02_survivor_promotion` +2.77%; this change +moves none of them in either direction. + +**This closes the lever, not the ticket.** What is left of `_tlv_get_addr` is +`RuntimeHandleScope`, not the allocation path, so further thread-local work +here is worth at most ~1% — the ceiling in both directions. #7469's remaining +workstreams (codegen emitting the bump allocation inline; per-object footprint) +are untouched. From ebe4e248ade7fd9c27f597f55d3efad0c31346af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 7 Aug 2026 04:15:29 +0200 Subject: [PATCH 4/8] docs(engine-plan): _tlv_get_addr measured out at 1.1% (#7565) Claude-Session: https://claude.ai/code/session_019EHcmXKArA7m42SihYCcgH --- docs/engine-plan.md | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/docs/engine-plan.md b/docs/engine-plan.md index b068ae7ecb..17b8c2363f 100644 --- a/docs/engine-plan.md +++ b/docs/engine-plan.md @@ -64,7 +64,7 @@ and feedback bookkeeping and 7.7% is the allocation itself**: | group | share | ticket | |---|--:|---| | `gc::layout` side tables (`layout_forget_*` 14.5%, `layout_note_slot` 7.9%, `js_gc_init_typed_shape_layout` 7.7%, …) | **33.6%** | **#7510** (construction/death half of #5094) | -| `_tlv_get_addr` | 17.0% | #7469 structural half (partly the same thread-locals as #7510) | +| `_tlv_get_addr` | 17.0% → 27.0% → **1.1%** | closed by **#7565** (it grew as a *share* while everything round it shrank) | | write barriers | 16.1% | **#7511** — *correctness-first: a missed barrier is a use-after-free, not a slowdown* | | typed-feedback guards | 9.2% | repsel 3b | | array helpers | 6.2% | partly closed by #7501 | @@ -158,11 +158,21 @@ already be 2.2x better. Tracked in **#7478**. them — they close real use-after-frees. 2. **#7478 — the JSON tape's scan path**, where our optimized build is 2.2x slower than our unoptimized one. The 1350 ms idiomatic row is the floor. -3. **`_tlv_get_addr` — thread-local addressing, now 30.5% of `churn_alloc`** - and the largest single line in the profile. This is #7469's structural half - (a context pointer, or inlined bump-allocation in generated code); #7474's - hot-TLS cache took it from 34% to ~14–17%, and it has climbed back as a - *share* because everything around it shrank, not because it regressed. +3. ~~**`_tlv_get_addr` — thread-local addressing**~~ — **measured out (#7565).** + Re-measuring first is what decided the design: the 27.0% was real, but the + ticket's "41 distinct call-graph sites, this is diffuse" was not — **seven + functions carried 98% of it and every one resolved `tls_hot::HOT`**, two of + them resolving nothing else. So the lever was the accessor, not the call + graph. Publishing the address cache into a pthread TSD slot and reading it + inline off `TPIDRRO_EL0` (how `pthread_getspecific` itself works; what + mimalloc does here) took `_tlv_get_addr` to **1.1%** and bought + `churn_alloc` **1.167x**, `churn` **1.175x**, `push_cls` **1.144x** on the + pinned host, without touching a line of generated code — the ticket's + "thread a context pointer through generated code" would have crossed every + FFI boundary against 2994 `.with()` sites. What remains is + `RuntimeHandleScope`, not the allocation path, so **the ceiling on further + thread-local work here is ~1%**. #7469's other workstreams (codegen emitting + the bump allocation inline; per-object footprint) are untouched. **#7510 is effectively closed.** All three items were measured out rather than argued away: item 1 shipped (#7535, install now 1x per 20M From a7173b03580eca1df3e4bc0ca7e7de4e74a68bc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 7 Aug 2026 04:22:32 +0200 Subject: [PATCH 5/8] docs(changelog): record the same-session main-arm ratchet A/B (#7565) Claude-Session: https://claude.ai/code/session_019EHcmXKArA7m42SihYCcgH --- changelog.d/7565-tls-direct-tsd.md | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/changelog.d/7565-tls-direct-tsd.md b/changelog.d/7565-tls-direct-tsd.md index b85f27f8d2..38359f2ee8 100644 --- a/changelog.d/7565-tls-direct-tsd.md +++ b/changelog.d/7565-tls-direct-tsd.md @@ -92,10 +92,22 @@ failed. All three probes under `PERRY_GC_ZEAL=1 PERRY_GC_PROTECT_FROMSPACE=1 PERRY_GC_PROTECT_FROMSPACE_DEPTH=800` exit 0 with 110 `[gc-fromspace-protect] mode=ProtectPages retired_set=#N` lines each — the count is quoted rather than the exit code, because a run with zero -copying minors protects nothing. The GC ratchet reports exactly the ten gating -breaches #7559 already records on `main`, cell for cell, including -`05_closure_capture` +16.44% and `02_survivor_promotion` +2.77%; this change -moves none of them in either direction. +copying minors protects nothing. + +The GC ratchet was measured against a **same-session `main` build** rather than +argued from the filed numbers: both arms out of one target directory with the +identical `-p` set, back to back with the same harness. Both report the same ten +gating breaches with the same values, #7559's `05_closure_capture` +16.44% and +`02_survivor_promotion` +2.77% among them — this change moves none of them in +either direction. Cell by cell that is **156 cells over 12 probes with exactly +one semantic difference**, `12_large_live_set.heap_used_bytes` at +0.004% +(2,304 B), which is the single cell the ratchet's own `probe_override` excludes +as conservative-stack-scan noise (#7554/#7558, spread 9,072 B over 36 runs). +Every other retention and evacuation counter is bit-identical and every probe +checksum matches; `wall_ms` is faster on all twelve. The diff tool asserts it +compared a non-zero number of cells before printing a verdict, because the +metrics are `{samples, median, …}` dicts and a naive numeric read compares zero +of them and reports a vacuous "identical". **This closes the lever, not the ticket.** What is left of `_tlv_get_addr` is `RuntimeHandleScope`, not the allocation path, so further thread-local work From c72b35704813c5de0e330120d3fc92c53d50707b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 7 Aug 2026 05:11:02 +0200 Subject: [PATCH 6/8] fix(runtime): the TPIDRRO_EL0 read must not be `pure` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first version of this change marked the thread-pointer asm `options(pure, nomem)` so LLVM could CSE it across a function. `pure` promises the result depends only on the inputs, and this asm has none, so LLVM may compute it once and reuse the value anywhere in the function — including across a point where execution resumes on a *different* thread. `perry-stdlib`'s async bridge is exactly that shape: `hot()` is `#[inline(always)]` and LTO inlines it into futures tokio polls, so a hoisted thread pointer outlives the thread it was read on. Every `node:net` / `node:http` server aborted with tokio's "there is no reactor running", 5/5, against 5/5 clean on `main`. No unit test and no allocation benchmark reproduced it — it took the gap suite. The counter-argument that `@llvm.threadlocal.address` already has this freedom does not hold: on Darwin it lowers to a call through the TLV descriptor, which LLVM will not hoist across arbitrary code. Replacing the call with inline asm is what made the hoist possible. Claude-Session: https://claude.ai/code/session_019EHcmXKArA7m42SihYCcgH --- crates/perry-runtime/src/tls_hot.rs | 41 +++++++++++++++++++---------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/crates/perry-runtime/src/tls_hot.rs b/crates/perry-runtime/src/tls_hot.rs index 2d503b1d85..27b485895c 100644 --- a/crates/perry-runtime/src/tls_hot.rs +++ b/crates/perry-runtime/src/tls_hot.rs @@ -230,24 +230,37 @@ pub(crate) mod darwin_tsd { #[inline(always)] pub(super) unsafe fn get(slot: usize) -> *mut u8 { let base: usize; - // `pure` + `nomem` is deliberate. Stripped of its CPU-number bits the - // register is a per-thread constant, so LLVM may CSE it across a whole - // function and hoist it out of loops. Without `pure` every reader - // re-issues the `mrs` and the change buys far less than the call it - // replaces. + // **NOT `pure`. This is load-bearing and was learned the hard way.** // - // This grants LLVM exactly the freedom it already has over the thing - // being replaced: `@llvm.threadlocal.address` is `speculatable` and - // `memory(none)`, so a `thread_local!` read was already CSE-able and - // hoistable on the same terms. The one shape that is unsound under - // either is holding the result across a point where execution can - // resume on a *different* thread — an `.await` in a work-stealing - // executor. Nothing on the allocation path is `async`, and this is a - // pre-existing constraint on `hot()` rather than one introduced here. + // `options(pure, nomem)` is the obvious choice — masked of its + // CPU-number bits the register is a per-thread constant, so `pure` + // lets LLVM CSE the read across a whole function and hoist it out of + // loops, and it costs one `mrs` per reader to give that up. It is also + // **wrong**, and the first version of this change shipped it. + // + // `pure` promises the result depends only on the inputs, and this asm + // has none — so LLVM is free to compute it once and reuse the value + // anywhere in the function, *including across a point where execution + // resumes on a different thread*. `perry-stdlib`'s async bridge is + // exactly that shape: `hot()` is `#[inline(always)]` and LTO inlines it + // into futures that tokio polls, so a hoisted thread pointer outlives + // the thread it was read on. The observable was every `node:net` / + // `node:http` server aborting with tokio's "there is no reactor + // running" — five out of five runs, against five out of five clean on + // `main`, and it did not reproduce in any unit test or in the whole + // allocation-benchmark set. Deleting `pure` fixes it. + // + // The tempting counter-argument — that `@llvm.threadlocal.address` is + // already `speculatable` and `memory(none)`, so a `thread_local!` read + // had the same freedom — does not hold: on Darwin that intrinsic + // lowers to a *call* through the TLV descriptor, which LLVM will not + // hoist across arbitrary code. Replacing the call with inline asm is + // what made the hoist possible, so this is a constraint introduced + // here, not inherited. core::arch::asm!( "mrs {b}, tpidrro_el0", b = out(reg) base, - options(nomem, nostack, preserves_flags, pure) + options(nomem, nostack, preserves_flags) ); // SAFETY: the caller guarantees `slot` is a live pthread key. unsafe { *((base & !0b111) as *const *mut u8).add(slot) } From e620b867eab3b3082889d141ee87fc07899e5e76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 7 Aug 2026 05:18:24 +0200 Subject: [PATCH 7/8] docs(changelog): record the `pure` incident and the gap-suite triage (#7565) Claude-Session: https://claude.ai/code/session_019EHcmXKArA7m42SihYCcgH --- changelog.d/7565-tls-direct-tsd.md | 47 +++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/changelog.d/7565-tls-direct-tsd.md b/changelog.d/7565-tls-direct-tsd.md index 38359f2ee8..1cff8eef37 100644 --- a/changelog.d/7565-tls-direct-tsd.md +++ b/changelog.d/7565-tls-direct-tsd.md @@ -40,13 +40,31 @@ from `TPIDRRO_EL0` — that is how `pthread_getspecific` itself is implemented, and what mimalloc (already linked into this runtime) does on this platform. `tls_hot` publishes the cache's address into one `pthread_key_create` slot and reads it back inline: `mrs` plus two loads, no call, no caller-saved-register -clobber, and `pure`+`nomem` so LLVM CSEs it across a whole function. That is -exactly the freedom LLVM already had over `@llvm.threadlocal.address` -(`speculatable`, `memory(none)`), so it introduces no hazard the `thread_local!` -read did not already carry — written down at the site, along with the one shape -unsound under either (holding the result across an `.await` in a work-stealing -executor; nothing on the allocation path is `async`). Every other target keeps -the previous path unchanged. +clobber. Every other target keeps the previous path unchanged. + +**The asm must not be `pure`, and that was learned the hard way — recorded +here because the reasoning that produced the bug was persuasive.** Marking it +`options(pure, nomem)` is the obvious move: masked of its CPU-number bits the +register is a per-thread constant, so `pure` lets LLVM CSE the read across a +whole function. It is also wrong. `pure` promises the result depends only on +the inputs, and this asm has none, so LLVM may compute it once and reuse the +value anywhere in the function — *including across a point where execution +resumes on a different thread*. `perry-stdlib`'s async bridge is exactly that +shape: `hot()` is `#[inline(always)]` and LTO inlines it into futures tokio +polls, so a hoisted thread pointer outlives the thread it was read on. Every +`node:net` / `node:http` server aborted with tokio's "there is no reactor +running" — 5/5 against 5/5 clean on `main`. + +The tempting counter-argument, that `@llvm.threadlocal.address` is already +`speculatable` + `memory(none)` so a `thread_local!` read had the same freedom, +does **not** hold: on Darwin that intrinsic lowers to a *call* through the TLV +descriptor, which LLVM will not hoist across arbitrary code. Replacing the call +with inline asm is what made the hoist possible. Two further notes for whoever +meets this next: nothing cheaper than the gap suite found it — 1811 runtime +unit tests, 12 GC-ratchet probes, and the whole allocation benchmark set are +all green with the bug present — and dropping `pure` costs **nothing +measurable**, with all three probes landing on the same millisecond as the +`pure` build. **It cannot silently read a wrong address.** The publishing thread reads its slot back *through the direct path* and compares it against what @@ -109,6 +127,21 @@ compared a non-zero number of cells before printing a verdict, because the metrics are `{samples, median, …}` dicts and a naive numeric read compares zero of them and reports a vacuous "identical". +The gap suite (466/491 on this host) was what caught the `pure` bug, and it is +also how the rest of the divergence set was cleared: after the fix, five of the +six network aborts pass, and the sixth +(`test_gap_http_client_no_redirect_follow`) fails **byte-identically on +`main`**. The seventh crash, +`test_gap_gc_same_module_call_argument_rooting`, is a harness timeout rather +than a defect: standalone under its own `parity-env` +(`PERRY_GC_MOVING_LOOP_POLLS=1 PERRY_GC_ZEAL=1`) it exits 0 in 13.5 s with +output byte-identical to node, against the harness's 10 s cap. Worth recording +separately: **the gap gate cannot render a verdict on macOS at all** — +`run_gap_tests.sh` selects `test-parity/gap_snapshot.${platform}.json` for +non-Linux hosts and `gap_snapshot.macos.json` is not in the repo, so the run +ends in `FileNotFoundError` and its exit 1 is a missing baseline, not a +regression list. + **This closes the lever, not the ticket.** What is left of `_tlv_get_addr` is `RuntimeHandleScope`, not the allocation path, so further thread-local work here is worth at most ~1% — the ceiling in both directions. #7469's remaining From 66a4aa86a71f612c3e5d2850bdbe9c4ea8f319d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 7 Aug 2026 05:35:46 +0200 Subject: [PATCH 8/8] chore: bump version --- CLAUDE.md | 2 +- Cargo.lock | 152 ++++++++++++++++++++++++++--------------------------- Cargo.toml | 2 +- 3 files changed, 78 insertions(+), 78 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 59c9e2bede..28daecabd2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Perry is a native TypeScript compiler written in Rust that compiles TypeScript source code directly to native executables. It uses SWC for TypeScript parsing and LLVM for code generation. -**Current Version:** 0.5.1319 +**Current Version:** 0.5.1320 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index bf3f1c6496..26973e7c5c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5547,7 +5547,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "anyhow", "base64", @@ -5607,14 +5607,14 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "serde", ] [[package]] name = "perry-audio-miniaudio" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "cc", "libc", @@ -5622,7 +5622,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "anyhow", "inkwell", @@ -5639,7 +5639,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "anyhow", "perry-hir", @@ -5647,7 +5647,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "anyhow", "perry-hir", @@ -5655,7 +5655,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "anyhow", "perry-dispatch", @@ -5664,7 +5664,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "anyhow", "perry-hir", @@ -5672,7 +5672,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "anyhow", "base64", @@ -5684,7 +5684,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "anyhow", "perry-hir", @@ -5692,7 +5692,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "anyhow", "async-trait", @@ -5721,14 +5721,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "serde", "serde_json", @@ -5736,7 +5736,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1319" +version = "0.5.1320" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5747,7 +5747,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "anyhow", "clap", @@ -5762,7 +5762,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "block2", "objc2", @@ -5772,7 +5772,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "argon2", "perry-ffi", @@ -5780,7 +5780,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "perry-ffi", "reqwest", @@ -5789,7 +5789,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "bcrypt", "perry-ffi", @@ -5797,7 +5797,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "perry-ffi", "rusqlite", @@ -5805,7 +5805,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "perry-ffi", "scraper", @@ -5813,7 +5813,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "perry-ffi", "perry-runtime", @@ -5821,7 +5821,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "chrono", "cron", @@ -5831,7 +5831,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "chrono", "perry-ffi", @@ -5839,7 +5839,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "perry-ffi", "rust_decimal", @@ -5847,7 +5847,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "perry-ffi", "serde_json", @@ -5855,7 +5855,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "perry-ffi", "rand 0.10.1", @@ -5863,7 +5863,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "perry-ffi", "perry-runtime", @@ -5871,14 +5871,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "bytes", "http-body-util", @@ -5896,7 +5896,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "bytes", "lazy_static", @@ -5909,7 +5909,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "bytes", "h2", @@ -5933,7 +5933,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "lazy_static", "perry-ffi", @@ -5943,7 +5943,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "base64", "jsonwebtoken", @@ -5954,7 +5954,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "lru", "perry-ffi", @@ -5963,7 +5963,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "chrono", "perry-ffi", @@ -5971,7 +5971,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "bson", "futures-util", @@ -5983,7 +5983,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "chrono", "perry-ffi", @@ -5993,7 +5993,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "nanoid", "perry-ffi", @@ -6002,7 +6002,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "bytes", "perry-ffi", @@ -6015,7 +6015,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "const-oid 0.9.6", "der 0.7.10", @@ -6034,7 +6034,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "lettre", "perry-ffi", @@ -6044,7 +6044,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "perry-ffi", "printpdf", @@ -6052,7 +6052,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "perry-ffi", "sqlx", @@ -6061,7 +6061,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "governor", "perry-ffi", @@ -6069,7 +6069,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "fast_image_resize", "image", @@ -6079,14 +6079,14 @@ dependencies = [ [[package]] name = "perry-ext-slugify" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-streams" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "lazy_static", "perry-ffi", @@ -6095,7 +6095,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "perry-ffi", "perry-runtime", @@ -6104,7 +6104,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "perry-ffi", "uuid", @@ -6112,7 +6112,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "perry-ffi", "regex", @@ -6122,7 +6122,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "futures-util", "lazy_static", @@ -6135,7 +6135,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "brotli", "flate2", @@ -6145,7 +6145,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "dashmap", "once_cell", @@ -6154,7 +6154,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "anyhow", "perry-api-manifest", @@ -6172,7 +6172,7 @@ dependencies = [ [[package]] name = "perry-parser" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "anyhow", "perry-diagnostics", @@ -6184,7 +6184,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "anyhow", "base64", @@ -6226,14 +6226,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6328,14 +6328,14 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "anyhow", "perry-hir", @@ -6344,14 +6344,14 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "perry-ui-model", ] [[package]] name = "perry-ui-android" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "base64", "itoa", @@ -6368,7 +6368,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "rand 0.10.1", "serde", @@ -6378,7 +6378,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "base64", "cairo-rs 0.22.0", @@ -6401,7 +6401,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "base64", "block2", @@ -6417,7 +6417,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "base64", "block2", @@ -6432,7 +6432,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1319" +version = "0.5.1320" [[package]] name = "perry-ui-test" @@ -6443,11 +6443,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1319" +version = "0.5.1320" [[package]] name = "perry-ui-tvos" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "base64", "block2", @@ -6463,7 +6463,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "base64", "block2", @@ -6479,7 +6479,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "block2", "libc", @@ -6492,7 +6492,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "base64", "libc", @@ -6509,14 +6509,14 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "perry-ui-windows", ] [[package]] name = "perry-updater" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "anyhow", "base64", @@ -6532,7 +6532,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1319" +version = "0.5.1320" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index c29e72e799..56f86448ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -315,7 +315,7 @@ codegen-units = 16 codegen-units = 16 [workspace.package] -version = "0.5.1319" +version = "0.5.1320" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry"