Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
c70d8f4
chore: trace snapshot inputs in instrumented builds
Nov 6, 2026
44b66df
chore: verify snapshot boundaries and census instruction accounting
Nov 6, 2026
81a8c7f
chore: retain input coverage and replay counterexamples
Nov 6, 2026
07320d1
chore: preserve mapped-file input observations
Nov 6, 2026
d95b3fa
Merge remote-tracking branch 'origin/main' into snap22
Nov 6, 2026
ea632bc
fix: preserve production bytes around census crypto hooks
Nov 7, 2026
8da58ff
chore: retain native census shape publication diagnostics
Nov 7, 2026
2d10475
fix: stream census inputs without allocator re-entry
Nov 7, 2026
377449f
fix: restore census page policy after first-write tracking
Nov 7, 2026
3c391bf
fix: share the census writable bulk transition
Nov 7, 2026
a44cd98
feat: census funnels, per-read argv sites, clock inputs and pc checks…
Nov 7, 2026
29b6af1
Merge remote-tracking branch 'origin/main' into snapk2/census-prefix
Nov 9, 2026
34f82db
snap census K2a (WIP): host-value reads and scan, P-entry (a)(b)(c), …
Nov 9, 2026
aca3cc7
snap census K2a: fixture boundaries, SIGTRAP re-protect order, P-entr…
Nov 9, 2026
5e612dc
Merge remote-tracking branch 'origin/main' into snapk2/census-prefix
Nov 9, 2026
b690144
SNAPK2b WIP: compare_literal site, stdout_style_bit funnel, open-fram…
Nov 9, 2026
957e9da
fix(util): styleText colours by node getColorDepth (FORCE_COLOR="" co…
Nov 9, 2026
5b598e9
Merge branch fix/style-color-depth into snapk2b/census-sites
Nov 9, 2026
4afa548
snap census: K2b fixtures and checker; compare_literal compares strin…
Nov 9, 2026
3aec8ff
snap census: compare_literal hook from the compare lowering itself
Nov 9, 2026
428917a
Merge GitHub main into snapk2b/census-sites
Nov 9, 2026
71710bc
snap census: compare hook test reads the call line, not the declaration
Nov 9, 2026
5e114c7
snap census: env/argv read sites classify against their own parent
Nov 9, 2026
51ae33d
Merge main 9093179838 into snapk2b/census-sites
Nov 9, 2026
4aa757b
snap census: the walker lock opens an unwind funnel; a native read in…
Nov 10, 2026
a3e210e
snap census: a job_queue row names each pending job (kind, callback c…
Nov 10, 2026
ef3056c
Merge branch 'main' of https://github.com/PerryTS/perry into snap/cen…
Nov 11, 2026
49656e8
changelog: snap census fragments (env/argv sites, walker lock, pendin…
Nov 11, 2026
ab53399
changelog: number the PR fragments
Nov 11, 2026
69fcd17
snap census: rows for the nextTick queue, scheduled resolutions and t…
Nov 11, 2026
53381d3
SNAPK4: re-export only snap_pending_timers (SnapPendingTimer is used …
Nov 11, 2026
7aacb04
SNAPK4: changelog fragment
Nov 11, 2026
d2c841d
Merge main into snap/census-k4
Nov 11, 2026
0c876a1
changelog: number the PR fragment
Nov 11, 2026
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/14466-snap-census-queues-at-cut.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Name the nextTick queue, scheduled promise resolves and timers/immediates pending at the startup snapshot cut in the instrumented census, each with its owner init and callback.
39 changes: 39 additions & 0 deletions crates/perry-runtime/src/builtins/globals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -821,6 +821,45 @@ pub(crate) fn test_seed_queued_job_owner(async_id: u64, resource: f64) {
});
}

/// SNAPK4 census: the queued nextTick jobs in queue order, one
/// `(owner, code)` per job: the init that queued it
/// (`AsyncContextSnapshot::snap_init`, stamped at enqueue) and its callback's
/// code address. None while the queue is borrowed.
#[cfg(perry_gc_instruments)]
pub(crate) fn snap_pending_ticks() -> Option<Vec<(u32, usize)>> {
QUEUED_MICROTASKS.with(|q| {
q.try_borrow().ok().map(|q| {
q.iter()
.map(|job| {
(
job.context.snap_init,
crate::promise::snap_closure_code(
job.callback as *const crate::closure::ClosureHeader,
),
)
})
.collect()
})
})
}

/// SNAPK4 census tests: queue a nextTick job owned by `owner` directly.
#[cfg(all(test, perry_gc_instruments))]
pub(crate) fn test_push_tick(callback: i64, owner: u32) {
let mut context = crate::async_context::capture_context();
context.snap_init = owner;
QUEUED_MICROTASKS.with(|q| {
q.borrow_mut().push_back(QueuedMicrotask {
callback,
context,
async_id: 0,
trigger_async_id: 0,
resource: f64::from_bits(crate::value::TAG_UNDEFINED),
args: Vec::new(),
})
});
}

#[cfg(test)]
pub(crate) fn test_clear_queued_jobs() {
QUEUED_MICROTASKS.with(|q| q.borrow_mut().clear());
Expand Down
4 changes: 4 additions & 0 deletions crates/perry-runtime/src/builtins/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,10 @@ pub use globals::{
pub(crate) use globals::{
discard_cancelled_microtasks, drain_queued_microtasks_count, queued_microtasks_pending,
};
#[cfg(perry_gc_instruments)]
pub(crate) use globals::snap_pending_ticks;
#[cfg(all(test, perry_gc_instruments))]
pub(crate) use globals::test_push_tick;

/// #6943: inertness predicate for the `js_string_coerce`-as-property-key
/// rooting family. Crate-internal — callers use it to skip a
Expand Down
14 changes: 12 additions & 2 deletions crates/perry-runtime/src/promise/combinators.rs
Original file line number Diff line number Diff line change
Expand Up @@ -561,17 +561,27 @@ static KEEP_PROMISE_ALL_SETTLED_ITERABLE: extern "C" fn(f64) -> *mut Promise =
#[used(compiler)]
static KEEP_PROMISE_ANY_ITERABLE: extern "C" fn(f64) -> *mut Promise = js_promise_any_iterable;

/// One scheduled resolution: the promise and its value. The instrumented
/// cohort's entry also names the init that queued it (SNAPK4 census).
#[cfg(not(perry_gc_instruments))]
pub(in crate::promise) type ScheduledResolve = (*mut Promise, f64);
#[cfg(perry_gc_instruments)]
pub(in crate::promise) type ScheduledResolve = (*mut Promise, f64, u32);

// Queue for scheduled promise resolutions
crate::perry_thread_local! {
pub(in crate::promise) static SCHEDULED_RESOLVES: RefCell<Vec<(*mut Promise, f64)>> = const { RefCell::new(Vec::new()) };
pub(in crate::promise) static SCHEDULED_RESOLVES: RefCell<Vec<ScheduledResolve>> = const { RefCell::new(Vec::new()) };
}

/// Schedule a promise to be resolved with a value when microtasks run
/// This simulates an async operation completing
#[no_mangle]
pub extern "C" fn js_promise_schedule_resolve(promise: *mut Promise, value: f64) {
SCHEDULED_RESOLVES.with(|q| {
#[cfg(not(perry_gc_instruments))]
q.borrow_mut().push((promise, value));
#[cfg(perry_gc_instruments)]
q.borrow_mut().push((promise, value, crate::snap_census::job_owner()));
});
}

Expand All @@ -581,7 +591,7 @@ pub(super) fn process_scheduled_resolves() -> i32 {
loop {
let item = SCHEDULED_RESOLVES.with(|q| q.borrow_mut().pop());
match item {
Some((promise, value)) => {
Some((promise, value, ..)) => {
js_promise_resolve(promise, value);
count += 1;
}
Expand Down
41 changes: 30 additions & 11 deletions crates/perry-runtime/src/promise/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ mod rejection_loop_tests;
mod resolving_function_kind_tests;
#[cfg(all(test, perry_gc_instruments))]
mod snap_pending_job_tests;
#[cfg(all(test, perry_gc_instruments))]
mod snap_pending_queue_tests;
pub mod scanners;
mod settled_step;
pub use settled_step::js_async_step_resume_now;
Expand Down Expand Up @@ -761,19 +763,36 @@ pub(crate) fn snap_pending_job_list() -> Option<Vec<SnapPendingJob>> {
})
}

/// SNAPK3c/K4 census: a callback's code address (`JsFunctionInfo::code`), 0
/// for no callback.
#[cfg(perry_gc_instruments)]
fn snap_pending_job(task: &Task) -> SnapPendingJob {
fn code(callback: ClosurePtr) -> usize {
if callback.is_null() {
return 0;
}
let info = unsafe { (*callback).info };
if info.is_null() {
0
} else {
unsafe { (*info).code as usize }
}
pub(crate) fn snap_closure_code(callback: ClosurePtr) -> usize {
if callback.is_null() {
return 0;
}
let info = unsafe { (*callback).info };
if info.is_null() {
0
} else {
unsafe { (*info).code as usize }
}
}

/// SNAPK4 census: the scheduled resolutions in queue order, one
/// `(owner, code)` per entry: the init that queued it and 0 (a resolution runs
/// no callback; it settles its promise). None while the queue is borrowed.
#[cfg(perry_gc_instruments)]
pub(crate) fn snap_scheduled_resolves() -> Option<Vec<(u32, usize)>> {
combinators::SCHEDULED_RESOLVES.with(|q| {
q.try_borrow()
.ok()
.map(|q| q.iter().map(|r| (r.2, 0)).collect())
})
}

#[cfg(perry_gc_instruments)]
fn snap_pending_job(task: &Task) -> SnapPendingJob {
let code = snap_closure_code;
let (kind, callback, owner) = match task {
Task::Promise(promise, _, is_fulfilled, context) => {
let callback = if promise.is_null() {
Expand Down
9 changes: 6 additions & 3 deletions crates/perry-runtime/src/promise/scanners.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ pub fn scan_promise_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) {
// Scan SCHEDULED_RESOLVES entries
super::combinators::SCHEDULED_RESOLVES.with(|q| {
let mut q = q.borrow_mut();
for (promise_ptr, value) in q.iter_mut() {
for (promise_ptr, value, ..) in q.iter_mut() {
visitor.visit_raw_mut_ptr_slot(promise_ptr);
visitor.visit_nanbox_f64_slot(value);
}
Expand Down Expand Up @@ -505,7 +505,7 @@ fn scan_scheduled_resolves_step(
if !consume_root_work(remaining) {
return false;
}
let (promise_ptr, value) = &mut q[state.index];
let (promise_ptr, value, ..) = &mut q[state.index];
match state.slot {
0 => visitor.visit_raw_mut_ptr_slot(promise_ptr),
1 => visitor.visit_nanbox_f64_slot(value),
Expand Down Expand Up @@ -593,7 +593,10 @@ pub(crate) fn test_seed_promise_scanner_roots(
super::combinators::SCHEDULED_RESOLVES.with(|q| {
let mut q = q.borrow_mut();
q.clear();
#[cfg(not(perry_gc_instruments))]
q.push((promise_ptr, value));
#[cfg(perry_gc_instruments)]
q.push((promise_ptr, value, 0));
});
}

Expand Down Expand Up @@ -672,7 +675,7 @@ pub(crate) fn test_promise_scanner_snapshot() -> TestPromiseScannerSnapshot {
});
super::combinators::SCHEDULED_RESOLVES.with(|q| {
let q = q.borrow();
if let Some((promise_ptr, value)) = q.first() {
if let Some((promise_ptr, value, ..)) = q.first() {
snapshot.scheduled_promise_ptr = *promise_ptr as usize;
snapshot.scheduled_value_bits = value.to_bits();
}
Expand Down
53 changes: 53 additions & 0 deletions crates/perry-runtime/src/promise/snap_pending_queue_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//! SNAPK4: the census names each queued nextTick job (`snap_pending_ticks`)
//! and each scheduled resolution (`snap_scheduled_resolves`) by the init that
//! queued it and its callback's code address, read from the queues themselves.
use super::*;

extern "C" fn tick_a(
_closure: *const crate::closure::ClosureHeader,
_this: crate::closure::JsThis,
_v: f64,
) -> f64 {
1.0
}

extern "C" fn tick_b(
_closure: *const crate::closure::ClosureHeader,
_this: crate::closure::JsThis,
_v: f64,
) -> f64 {
2.0
}

#[test]
fn pending_ticks_name_owner_and_callback() {
let info_a = crate::fn_info!(tick_a, 1);
let info_b = crate::fn_info!(tick_b, 1);
let code_a = unsafe { (*info_a).code as usize };
let code_b = unsafe { (*info_b).code as usize };
assert_ne!(code_a, code_b);
let cb_a = crate::closure::js_closure_alloc(info_a, 0) as i64;
let cb_b = crate::closure::js_closure_alloc(info_b, 0) as i64;
crate::builtins::test_clear_queued_jobs();
assert_eq!(crate::builtins::snap_pending_ticks(), Some(Vec::new()), "none pending, no entry");
crate::builtins::test_push_tick(cb_a, 7);
crate::builtins::test_push_tick(cb_b, 9);
let ticks = crate::builtins::snap_pending_ticks().expect("queue not borrowed");
crate::builtins::test_clear_queued_jobs();
assert_eq!(ticks, vec![(7, code_a), (9, code_b)]);
}

#[test]
fn scheduled_resolves_name_owner() {
combinators::SCHEDULED_RESOLVES.with(|q| q.borrow_mut().clear());
assert_eq!(snap_scheduled_resolves(), Some(Vec::new()), "none pending, no entry");
let promise = js_promise_new();
combinators::SCHEDULED_RESOLVES.with(|q| {
let mut q = q.borrow_mut();
q.push((promise, 1.0, 9));
q.push((promise, 2.0, 12));
});
let resolves = snap_scheduled_resolves().expect("queue not borrowed");
combinators::SCHEDULED_RESOLVES.with(|q| q.borrow_mut().clear());
assert_eq!(resolves, vec![(9, 0), (12, 0)]);
}
66 changes: 66 additions & 0 deletions crates/perry-runtime/src/snap_census/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5395,6 +5395,71 @@ fn pending_jobs_field() -> String {
out.join(",")
}

/// SNAPK4: the first 64 entries of a queue row's list field, `+N` counting
/// the rest; `busy` while the store is being changed.
fn shown_field(entries: Option<Vec<String>>) -> (String, String) {
const SHOWN: usize = 64;
let Some(entries) = entries else {
return ("busy".to_owned(), "busy".to_owned());
};
let n = entries.len();
let mut out: Vec<String> = entries.into_iter().take(SHOWN).collect();
if n > SHOWN {
out.push(format!("+{}", n - SHOWN));
}
(n.to_string(), out.join(","))
}

/// SNAPK4 rows at a candidate cut for the run queues beside TASK_QUEUE, each
/// read from its own store, one row per non-empty store (SNAP_DESIGN
/// §2.10.21). The nextTick queue (`tick_queue`, `tick:owner:code`) and the
/// scheduled resolutions (`scheduled_resolves`, `resolve:owner:0`) are imaged
/// with the task queue by the same FIFO-prefix rule (`policy=image`). A timer,
/// immediate or native completion (`timer_queue`,
/// `kind:owner:code:delay_ms:repeat`) waits on the host clock or host I/O:
/// a blocker (`policy=STOP`).
fn k4_queue_rows(st: &mut State) {
let init = st.frames.last().map_or(0, |f| f.init);
let ticks = crate::builtins::snap_pending_ticks()
.map(|t| t.iter().map(|(o, c)| format!("tick:{o}:{c:x}")).collect());
if ticks.as_ref().is_none_or(|t: &Vec<String>| !t.is_empty()) {
let (n, list) = shown_field(ticks);
input_record::event(
st,
"tick_queue",
format!("init={init};pending={n};policy=image;ticks={list}"),
);
}
let resolves = crate::promise::snap_scheduled_resolves()
.map(|r| r.iter().map(|(o, c)| format!("resolve:{o}:{c:x}")).collect());
if resolves.as_ref().is_none_or(|r: &Vec<String>| !r.is_empty()) {
let (n, list) = shown_field(resolves);
input_record::event(
st,
"scheduled_resolves",
format!("init={init};pending={n};policy=image;resolves={list}"),
);
}
let timers = crate::timer::snap_pending_timers().map(|t| {
t.iter()
.map(|e| {
format!(
"{}:{}:{:x}:{}:{}",
e.kind, e.owner, e.code, e.delay_ms, e.repeat as u8
)
})
.collect()
});
if timers.as_ref().is_none_or(|t: &Vec<String>| !t.is_empty()) {
let (n, list) = shown_field(timers);
input_record::event(
st,
"timer_queue",
format!("init={init};pending={n};policy=STOP;timers={list}"),
);
}
}

/// SNAPK2b checks at a candidate cut, `init_enter` of the init just pushed
/// (DESIGN27 §1.1, §1.2). Open frames: every open ancestor resumes at its
/// flag test (R1: static dependency or eager entry import, before the
Expand Down Expand Up @@ -5449,5 +5514,6 @@ fn k2b_enter_rows(st: &mut State) {
);
}
}
k4_queue_rows(st);
}
}
2 changes: 2 additions & 0 deletions crates/perry-runtime/src/timer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ mod async_lifecycle;
mod mock;
mod phases;
mod store;
#[cfg(perry_gc_instruments)]
pub(crate) use store::snap_pending_timers;

use crate::promise::{js_promise_new, Promise};
use async_lifecycle::enqueue_destroy_ids;
Expand Down
Loading
Loading