Skip to content
Merged
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
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,36 @@ cycle-accurate core later replaced.
keeps the evidence. What it did establish: restore costs ~114 µs with **no
framebuffer at all**, so its 8.3× asymmetry against `snapshot_core_into` is
per-section deserialization, not the payload.
||||||| parent of 0991b79a (feat(perf): tick_lat / tick_iv — the trigger is late, not the emulator (F15))
||||||| parent of 60507e12 (docs(perf): the run-ahead depth sweep — cost is the frames (F18))
**Run-ahead's cost is the frames, and depth 3 throttles itself (v2.3.3 F18).**
Sixteen captures, four per depth, in a **Latin square** (each depth in each
round-position exactly once — F13's correction: alternation balances drift
direction but does not buy exchangeability), every one validity-gated by F16
and **16/16 passing**. Emulation cost is **linear in depth** at the core's own
~4.3 ms per frame — increments +4.491 and +4.213 ms, equal within 6% — so with
snapshot + restore at ~136 µs (F19), run-ahead's cost is the emulated frames
and essentially nothing else: **the price of the feature, not overhead around
it**. Budget shares: 25.1% / 52.1% / **77.4%** at depths 0/1/2. Depth 3
measures like depth 0 because it **throttles itself** — `run_ahead_throttled`
is `true` in every one of its captures, the guard engaging at 85% of the frame
budget against depth 3's ~17.2 ms; correct behaviour that reads as a defect
without that column. It also retires an earlier cross-session artefact: the
+3.09/+4.20 ms increments that suggested a non-linear cost structure were
session noise, not structure.
**`tick_lat` / `tick_iv` — the trigger is late, not the emulator (v2.3.3
F15).** Two new independently-ranked series decomposing the produce interval,
whose variance tracks missed presents at r = 0.937. The display tick's channel
payload became a `CLOCK_MONOTONIC` timestamp instead of `()`, which is what
makes the cross-thread hop measurable. First verified capture (SMB,
`run_ahead = 1`, display-sync /2, window confirmed on screen by the F16 gate):
the winit→emu hop is **0.033-0.050 ms** — negligible — while `produced` p95
(24.635 ms) matches `tick_iv` p95 (24.578 ms) to within 0.06 ms. With `rlock`
at 0.000 and `tick_timeout` at 0 of 1494, **the produce tail is inherited
wholesale from the trigger interval**: the emulator is not late, it is asked
late. First positive location the campaign has produced rather than an
elimination. Also `crates/rustynes-frontend/src/clock.rs`, holding the
frontend's only `clock_gettime` call site.
**The unexplained between-session cadence spread is emulation budget margin
(v2.3.3 F17).** F14 left one quantity open and called it the largest
unexplained question in `docs/performance.md`: display cadence error varied
Expand Down
80 changes: 15 additions & 65 deletions crates/rustynes-frontend/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7080,78 +7080,28 @@ impl App {
///
/// The instrument that separates WORK from DESCHEDULING. `rwork` is wall
/// time, so a redraw that sat 27 ms off-CPU and one that computed for 27 ms
/// are identical to it — the same ambiguity `rtot` carried before F8 split
/// `rwait` out of it, one level down. Differencing this across the same
/// span gives the CPU time actually consumed: wall ≫ CPU means the thread
/// was descheduled, wall ≈ CPU means real computation.
#[cfg(all(not(target_arch = "wasm32"), unix, feature = "emu-thread"))]
/// are identical to it. Differencing this across the same span gives the CPU
/// time actually consumed: wall much greater than CPU means the thread was
/// descheduled, wall approximately equal to CPU means real computation.
///
/// Thread-local by definition — this is the WINIT thread's reading, which is
/// only meaningful because the span it brackets runs on that same thread.
/// Delegates to [`crate::clock`] (one `clock_gettime` site).
#[cfg(not(target_arch = "wasm32"))]
fn thread_cpu_now_ns() -> Option<u64> {
let mut ts = libc::timespec {
tv_sec: 0,
tv_nsec: 0,
};
// SAFETY: identical contract to `monotonic_now_ns` — `clock_gettime`
// writes only through the supplied pointer, which is a live,
// correctly-typed, stack-allocated `timespec` owned here for the call.
// `CLOCK_THREAD_CPUTIME_ID` is POSIX and available on Linux. The return
// value is checked.
#[allow(unsafe_code)]
let rc = unsafe { libc::clock_gettime(libc::CLOCK_THREAD_CPUTIME_ID, &raw mut ts) };
if rc != 0 {
return None;
}
u64::try_from(ts.tv_sec)
.ok()?
.checked_mul(1_000_000_000)?
.checked_add(u64::try_from(ts.tv_nsec).ok()?)
}

/// No thread-CPU clock available; the work/deschedule split is not reported.
#[cfg(all(not(target_arch = "wasm32"), not(all(unix, feature = "emu-thread"))))]
const fn thread_cpu_now_ns() -> Option<u64> {
None
crate::clock::thread_cpu_now_ns()
}

/// v2.3.3 — `CLOCK_MONOTONIC` in nanoseconds, or `None` where unavailable.
///
/// The anchor that lets the per-frame trace's produce/present rows be compared
/// to its compositor `scanout` rows. `wp_presentation` stamps presentations in
/// the clock named by its `clock_id` event (1 = `CLOCK_MONOTONIC`), and
/// `Instant` is that same clock on Linux — but opaque, so its absolute value
/// cannot be read. One reading taken at the trace origin converts the whole
/// series.
///
/// Read through `libc` rather than a new dependency: it is already a direct
/// dep behind the default-on `emu-thread` feature (for the thread's `rtprio`
/// call). With that feature off this returns `None` and the trace simply says
/// the two halves are unaligned, rather than aligning them wrongly.
#[cfg(all(not(target_arch = "wasm32"), unix, feature = "emu-thread"))]
/// to its compositor `scanout` rows. Delegates to [`crate::clock`], which owns
/// the single `clock_gettime` call site — F15 needed the same reading on the
/// emulation thread, and duplicating it would have duplicated an `unsafe`
/// block and its safety argument.
#[cfg(not(target_arch = "wasm32"))]
fn monotonic_now_ns() -> Option<u64> {
let mut ts = libc::timespec {
tv_sec: 0,
tv_nsec: 0,
};
// SAFETY: `clock_gettime` writes only through the supplied pointer, which
// is a live, correctly-typed, stack-allocated `timespec` owned here for the
// duration of the call. `CLOCK_MONOTONIC` is unconditionally available on
// Linux. The return value is checked; on failure `ts` is left as
// initialised above and the result discarded.
#[allow(unsafe_code)]
let rc = unsafe { libc::clock_gettime(libc::CLOCK_MONOTONIC, &raw mut ts) };
if rc != 0 {
return None;
}
u64::try_from(ts.tv_sec)
.ok()?
.checked_mul(1_000_000_000)?
.checked_add(u64::try_from(ts.tv_nsec).ok()?)
}

/// No monotonic anchor available: the trace records that the produce/present
/// and `scanout` halves are in different, unjoinable clock domains.
#[cfg(all(not(target_arch = "wasm32"), not(all(unix, feature = "emu-thread"))))]
const fn monotonic_now_ns() -> Option<u64> {
None
crate::clock::monotonic_now_ns()
}

/// v2.3.3 — fill the [`crate::perf::PerfView`] fields that live on the
Expand Down
91 changes: 91 additions & 0 deletions crates/rustynes-frontend/src/clock.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// SPDX-License-Identifier: GPL-3.0-or-later
//! v2.3.3 — the frontend's two absolute clock readings, in one place.
//!
//! `Instant` is the right type for durations and is what the rest of the
//! frontend uses. It is deliberately opaque, though, so its absolute value
//! cannot be read — and two of this campaign's instruments need exactly that:
//!
//! * **`monotonic_now_ns`** anchors the per-frame trace. `wp_presentation`
//! stamps presentations in the clock named by its `clock_id` event
//! (1 = `CLOCK_MONOTONIC`), which is the same clock `Instant` rides on Linux;
//! one absolute reading at the trace origin converts the whole series, and
//! without it the compositor rows and the process rows are unjoinable.
//! It is also the payload of the display tick, which is how the winit->emu
//! hop is measured across a thread boundary where no `Instant` can travel
//! meaningfully.
//! * **`thread_cpu_now_ns`** separates work from descheduling. Differenced
//! across a span, `wall - cpu` is time the thread was not running — the
//! distinction that showed the 9-32 ms `rwork` tail was a `cargo build`
//! competing for the host, not the frontend doing anything.
//!
//! **Why this module exists at all:** both readings were first written as
//! private associated functions on `App`, and F15 needed the monotonic one on
//! the emulation thread as well. Copying a `clock_gettime` call is copying an
//! `unsafe` block and its `// SAFETY:` justification, which is precisely what
//! the project's rule against scattering `unsafe` forbids. One site, two
//! callers.
//!
//! Read through `libc` rather than a new dependency: it is already a direct dep
//! behind the default-on `emu-thread` feature (for the thread's `rtprio` call).
//! With that feature off, or off Unix, both functions return `None` and every
//! consumer degrades to saying the measurement is unavailable rather than
//! substituting a wrong one.

/// `CLOCK_MONOTONIC` in nanoseconds, or `None` where unavailable.
#[cfg(all(not(target_arch = "wasm32"), unix, feature = "emu-thread"))]
#[must_use]
pub fn monotonic_now_ns() -> Option<u64> {
read(libc::CLOCK_MONOTONIC)
}

/// This thread's consumed CPU time in nanoseconds (`CLOCK_THREAD_CPUTIME_ID`),
/// or `None` where unavailable.
///
/// Thread-local by definition: the value belongs to whichever thread calls it,
/// which is what makes `wall - cpu` meaningful for that thread alone. Calling
/// it from a different thread than the one whose span is being measured yields
/// a number that looks plausible and means nothing.
#[cfg(all(not(target_arch = "wasm32"), unix, feature = "emu-thread"))]
#[must_use]
pub fn thread_cpu_now_ns() -> Option<u64> {
read(libc::CLOCK_THREAD_CPUTIME_ID)
}

/// The single `clock_gettime` call site.
#[cfg(all(not(target_arch = "wasm32"), unix, feature = "emu-thread"))]
fn read(clock: libc::clockid_t) -> Option<u64> {
let mut ts = libc::timespec {
tv_sec: 0,
tv_nsec: 0,
};
// SAFETY: `clock_gettime` writes only through the supplied pointer, which is
// a live, correctly-typed, stack-allocated `timespec` owned here for the
// duration of the call. Both `CLOCK_MONOTONIC` and `CLOCK_THREAD_CPUTIME_ID`
// are unconditionally available on Linux, and the parameter is private to
// this module so no caller can pass an arbitrary id. The return value is
// checked; on failure `ts` is left as initialised above and discarded.
#[allow(unsafe_code)]
let rc = unsafe { libc::clock_gettime(clock, &raw mut ts) };
if rc != 0 {
return None;
}
u64::try_from(ts.tv_sec)
.ok()?
.checked_mul(1_000_000_000)?
.checked_add(u64::try_from(ts.tv_nsec).ok()?)
}

/// No monotonic clock reachable: consumers record the measurement as
/// unavailable rather than substituting a wrong one.
#[cfg(not(all(not(target_arch = "wasm32"), unix, feature = "emu-thread")))]
#[must_use]
pub const fn monotonic_now_ns() -> Option<u64> {
None
}

/// No thread-CPU clock reachable; the work/deschedule split is not reported.
#[cfg(not(all(not(target_arch = "wasm32"), unix, feature = "emu-thread")))]
#[must_use]
pub const fn thread_cpu_now_ns() -> Option<u64> {
None
}
Loading