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
4 changes: 2 additions & 2 deletions COMPATIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ descendant teardown, or fresh evidence on the other five targets.
Until 1.0, the four first-party clients use an exact Cargo requirement:

```toml
kernal-api = { version = "=0.1.22", features = ["..."] }
kernal-api = { version = "=0.1.23", features = ["..."] }

[profile.dev.package.kernal-api]
codegen-units = 1
Expand All @@ -107,7 +107,7 @@ codegen-units = 1
codegen-units = 1
```

The Python companion is likewise pinned with `kernal-api==0.1.22` when used by
The Python companion is likewise pinned with `kernal-api==0.1.23` when used by
first-party Python tooling. A source checkout may temporarily use a path patch
only on an explicit migration branch; release branches must resolve the exact
registry version. There is no `optional = true` legacy implementation behind
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "kernal-api"
version = "0.1.22"
version = "0.1.23"
build = "build.rs"
edition = "2021"
rust-version = "1.95.0"
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,10 @@ adds `kernal-api` a second time, as a build-dependency with only this feature:

```toml
[dependencies]
kernal-api = { version = "=0.1.22", features = ["window-icon"] }
kernal-api = { version = "=0.1.23", features = ["window-icon"] }

[build-dependencies]
kernal-api = { version = "=0.1.22", default-features = false, features = ["build-resources"] }
kernal-api = { version = "=0.1.23", default-features = false, features = ["build-resources"] }
```

Features enabled in the `[dependencies]` entry never reach the build script:
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/wasm-sketch/compiler-guest/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion benchmarks/wasm-sketch/compiler-guest/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ required-features = ["guest-proof"]
# Independently runnable Core compiler/cache and public-facade hash proofs for #13.
zccache-compiler = { git = "https://github.com/zackees/zccache", rev = "c6ddfa974a4920a127eac81773db6a5c56cd30a7", default-features = false }
zccache-hash = { git = "https://github.com/zackees/zccache", rev = "2543136ea8b648b295d2f7115a19656ff0854531", default-features = false }
kernal-api = { version = "=0.1.22", path = "../../..", default-features = false }
kernal-api = { version = "=0.1.23", path = "../../..", default-features = false }
base64 = "=0.22.1"
serde = { version = "=1.0.229", features = ["derive"] }
serde_json = "=1.0.151"
2 changes: 1 addition & 1 deletion benchmarks/wasm-sketch/component-guest/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion benchmarks/wasm-sketch/component-guest/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ compiler-artifact-output = []
zccache-compiler = { git = "https://github.com/zackees/zccache", rev = "c6ddfa974a4920a127eac81773db6a5c56cd30a7", default-features = false }
# Exact source-only migration fixture; not published-pin acceptance.
zccache-hash = { git = "https://github.com/zackees/zccache", rev = "2543136ea8b648b295d2f7115a19656ff0854531", default-features = false }
kernal-api = { version = "=0.1.22", path = "../../..", default-features = false, features = ["wasm-component-hash-experiment"] }
kernal-api = { version = "=0.1.23", path = "../../..", default-features = false, features = ["wasm-component-hash-experiment"] }
wit-bindgen = { version = "=0.58.0", default-features = false, features = ["macros", "realloc", "async", "std"] }

[profile.release]
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/wasm-sketch/component-tools/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion benchmarks/wasm-sketch/component-tools/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ execution-probe = ["engine-probe", "dep:kernal-api"]
anyhow = "=1.0.104"
wit-component = "=0.251.0"
wasmparser = "=0.251.0"
kernal-api = { version = "=0.1.22", path = "../../..", default-features = false, optional = true }
kernal-api = { version = "=0.1.23", path = "../../..", default-features = false, optional = true }
wasmtime = { version = "=45.0.0", optional = true, default-features = false, features = ["cranelift", "runtime", "component-model", "component-model-async"] }

[dev-dependencies]
Expand Down
2 changes: 1 addition & 1 deletion examples/wasm-tauri-screenshot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ the screenshot lifecycle starts. The map uses a deterministic standard hasher
and the channel reads are non-blocking after join: the closed profile therefore
adds neither ambient `random_get` nor `poll_oneoff` imports.

The source fixture uses an exact `=0.1.22` version plus a **migration-only local
The source fixture uses an exact `=0.1.23` version plus a **migration-only local
path**; it must switch to an actually published guest-capable release before
release acceptance. The packaged facade has separately passed a Wasm check,
but this is not evidence that the guest-capable package has been published.
Expand Down
2 changes: 1 addition & 1 deletion examples/wasm-tauri-screenshot/guest/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion examples/wasm-tauri-screenshot/guest/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@ proof-block-after-capture = []
[dependencies]
# Migration-only source fixture. Replace the path with the exact release pin
# after the guest-capable facade is published; this is not release acceptance.
kernal-api = { version = "=0.1.22", path = "../../..", default-features = false }
kernal-api = { version = "=0.1.23", path = "../../..", default-features = false }
dashmap = "6.1.0"
crossbeam-channel = "0.5.15"
2 changes: 1 addition & 1 deletion guests/threaded-smoke/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion guests/threaded-smoke/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ warnings = "deny"
[dependencies]
# Migration-only source pin for the public streaming proof. The generated
# dependency below remains solely for deliberate low-level ABI probes.
kernal-api = { version = "=0.1.22", path = "../..", default-features = false }
kernal-api = { version = "=0.1.23", path = "../..", default-features = false }
kernal-api-v1-bindings = { path = "../../src/wasm/generated/v1/guest" }
dashmap = "=6.1.0"
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "kernal-api"
version = "0.1.22"
version = "0.1.23"
description = "Async OS HAL, profiling, symbolization, and allocator instrumentation"
readme = "README.md"
requires-python = ">=3.10"
Expand Down
2 changes: 1 addition & 1 deletion python/kernal_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import sys
from dataclasses import dataclass

__version__ = "0.1.22"
__version__ = "0.1.23"
RUST_MSRV = "1.95.0"
SUPPORTED_SYSTEMS = frozenset({"Linux", "Darwin", "Windows"})
SUPPORTED_MACHINES = frozenset({"x86_64", "amd64", "aarch64", "arm64"})
Expand Down
2 changes: 1 addition & 1 deletion python/tests/test_compatibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@


def test_python_and_rust_versions_are_explicit() -> None:
assert kernal_api.__version__ == "0.1.22"
assert kernal_api.__version__ == "0.1.23"
assert kernal_api.RUST_MSRV == "1.95.0"


Expand Down
121 changes: 93 additions & 28 deletions src/crash/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,28 @@ pub enum CrashPolicy {
/// Environment opt-out checked before any crash state is created.
pub const NO_CRASH_HANDLER_ENV: &str = "KERNAL_API_NO_CRASH_HANDLER";

/// Sampling cadence for native crash snapshots. The default starts at 100 ms
/// and backs off exponentially to 1 s while captures are unchanged. A changed
/// capture resets the cadence. The sampler can wait longer than
/// `max_interval` when needed to preserve its one-percent CPU duty budget.
#[derive(Clone, Copy, Debug)]
pub struct CrashSamplerConfig {
/// Time before the first capture and between captures after a change.
pub interval: Duration,
/// Longest adaptive delay between unchanged captures, before the CPU
/// duty-cycle floor is applied.
pub max_interval: Duration,
}

impl Default for CrashSamplerConfig {
fn default() -> Self {
Self {
interval: Duration::from_millis(100),
max_interval: Duration::from_secs(1),
}
}
}

/// Local failure while arming crash capture.
#[derive(Debug, thiserror::Error)]
pub enum InstallError {
Expand All @@ -91,6 +113,9 @@ pub enum InstallError {
/// The all-thread sampler could not be started.
#[error("cannot start crash snapshot sampler: {0}")]
Sampler(#[source] io::Error),
/// The sampling intervals must be positive and ordered.
#[error("crash sampler intervals must be positive, with max_interval >= interval")]
InvalidSamplerConfig,
/// The platform SIGABRT predecessor chain could not be installed.
#[cfg(any(windows, target_os = "macos"))]
#[error("cannot chain the platform abort handler: {0}")]
Expand Down Expand Up @@ -284,9 +309,24 @@ impl Drop for TestSamplerDisabledGuard {

/// Arm native crash capture unless policy or environment opts out.
pub fn install(policy: CrashPolicy, metadata: CrashMetadata) -> Result<CrashGuard, InstallError> {
install_with_sampler_config(policy, metadata, CrashSamplerConfig::default())
}

/// Arm native crash capture with a caller-selected sampling cadence.
///
/// The first live registration sets the process-wide cadence until its final
/// guard is dropped. An opt-out policy does not validate or start a sampler.
pub fn install_with_sampler_config(
policy: CrashPolicy,
metadata: CrashMetadata,
config: CrashSamplerConfig,
) -> Result<CrashGuard, InstallError> {
if policy == CrashPolicy::Off || env_opted_out() {
return Ok(CrashGuard::inert());
}
if config.interval.is_zero() || config.max_interval < config.interval {
return Err(InstallError::InvalidSamplerConfig);
}

let pid = std::process::id();
match OWNER_PID.compare_exchange(0, pid, Ordering::AcqRel, Ordering::Acquire) {
Expand Down Expand Up @@ -326,7 +366,7 @@ pub fn install(policy: CrashPolicy, metadata: CrashMetadata) -> Result<CrashGuar
});
}

let (runtime, registration_id) = Runtime::new(metadata)?;
let (runtime, registration_id) = Runtime::new(metadata, config)?;
*weak = Arc::downgrade(&runtime);
Ok(CrashGuard {
runtime: Some(runtime),
Expand All @@ -352,7 +392,7 @@ struct Runtime {
}

impl Runtime {
fn new(metadata: CrashMetadata) -> Result<(Arc<Self>, u64), InstallError> {
fn new(metadata: CrashMetadata, config: CrashSamplerConfig) -> Result<(Arc<Self>, u64), InstallError> {
let (file, path, template) = spool::create_sink(&metadata).map_err(InstallError::Spool)?;
let shared = Arc::new(Shared::new(file, template));

Expand All @@ -376,7 +416,7 @@ impl Runtime {
let sampler = if sampler_disabled {
None
} else {
Some(match start_sampler(&shared, SAMPLE_INTERVAL) {
Some(match start_sampler(&shared, config) {
Ok(sampler) => sampler,
Err(error) => {
#[cfg(windows)]
Expand Down Expand Up @@ -485,14 +525,11 @@ impl Runtime {
}
}

/// How often the sampler refreshes the bounded pre-crash snapshot.
const SAMPLE_INTERVAL: Duration = Duration::from_millis(50);

fn start_sampler(shared: &Arc<Shared>, cadence: Duration) -> io::Result<JoinHandle<()>> {
fn start_sampler(shared: &Arc<Shared>, config: CrashSamplerConfig) -> io::Result<JoinHandle<()>> {
let sampler_state = Arc::clone(shared);
std::thread::Builder::new()
.name("rp-crash-sampler".into())
.spawn(move || sampler_loop(sampler_state, cadence))
.spawn(move || sampler_loop(sampler_state, config))
}

struct RegistrationState {
Expand Down Expand Up @@ -1155,13 +1192,25 @@ fn with_platform_fields<R>(
})
}

fn sampler_loop(shared: Arc<Shared>, cadence: Duration) {
fn sampler_loop(shared: Arc<Shared>, config: CrashSamplerConfig) {
let mut resolver = crate::snapshot::SessionResolver::for_crash(&crate::snapshot::SnapshotConfig::default());
let mut previous = None;
let mut cadence = config.interval;
while !shared.stop.load(Ordering::Acquire) {
if shared.wait_for_stop(cadence) {
break;
}
if !shared.reading.load(Ordering::Acquire) {
let sample = capture_sample();
let tick_start = std::time::Instant::now();
let sample = capture_sample(&mut resolver);
// Recheck after the allocating capture: the callback may have
// started while capture was in progress.
if let Some(sample) = sample {
cadence = if previous.as_ref() == Some(&sample) {
cadence.saturating_mul(2).min(config.max_interval)
} else {
config.interval
};
if !shared.reading.load(Ordering::Acquire) {
let _publish = match shared.publish.lock() {
Ok(publish) => publish,
Expand All @@ -1182,13 +1231,13 @@ fn sampler_loop(shared: Arc<Shared>, cadence: Duration) {
.sample_thread_count
.store(sample.threads.len(), Ordering::Release);
shared.sample_ready.store(true, Ordering::Release);
previous = Some(sample);
}
}
}
// Teardown signals the wait, so process exit never has to sit out a
// cadence tick it cannot interrupt.
if shared.wait_for_stop(cadence) {
break;
// Preserve a one-percent duty-cycle headroom even when snapshots
// differ on every tick. This bounds sampler CPU without dropping
// threads from the last complete pre-crash capture.
cadence = cadence.max(tick_start.elapsed().saturating_mul(100));
}
}
}
Expand All @@ -1197,18 +1246,10 @@ fn sampler_loop(shared: Arc<Shared>, cadence: Duration) {
any(windows, target_os = "linux", target_os = "macos"),
any(target_arch = "x86_64", target_arch = "aarch64")
))]
fn capture_sample() -> Option<CrashSample> {
use crate::snapshot::attribute::attribute;
use crate::snapshot::modules::enumerate_modules;
use crate::snapshot::{capture_and_resolve, SnapshotConfig};

let Ok(snapshot) = capture_and_resolve(&SnapshotConfig::default()) else {
fn capture_sample(resolver: &mut crate::snapshot::SessionResolver) -> Option<CrashSample> {
let Ok(attributed) = resolver.capture_attributed() else {
return None;
};
let Ok(loaded) = enumerate_modules() else {
return None;
};
let attributed = attribute(&snapshot, &loaded);
Some(CrashSample {
modules: attributed
.modules
Expand Down Expand Up @@ -1239,11 +1280,11 @@ fn capture_sample() -> Option<CrashSample> {
any(windows, target_os = "linux", target_os = "macos"),
any(target_arch = "x86_64", target_arch = "aarch64")
)))]
fn capture_sample() -> Option<CrashSample> {
fn capture_sample(_resolver: &mut crate::snapshot::SessionResolver) -> Option<CrashSample> {
None
}

#[derive(Default)]
#[derive(Default, PartialEq, Eq)]
struct CrashSample {
modules: Vec<CrashModule>,
threads: Vec<CrashThread>,
Expand Down Expand Up @@ -1591,7 +1632,10 @@ mod tests {
// it out could not pass.
shared.reading.store(true, Ordering::Release);
let cadence = Duration::from_secs(30);
let sampler = start_sampler(&shared, cadence).unwrap();
let sampler = start_sampler(&shared, CrashSamplerConfig {
interval: cadence,
max_interval: cadence,
}).unwrap();
// Not an assertion: this only gives the thread time to reach the wait
// so an uninterruptible sleep reproduces as a failure rather than a
// race against the loop's own stop check.
Expand All @@ -1611,4 +1655,25 @@ mod tests {
drop(shared);
let _ = std::fs::remove_file(&path);
}

#[test]
fn sampler_preserves_thread_frames_and_module_indices() {
let _state = process_state();
let mut resolver = crate::snapshot::SessionResolver::for_crash(
&crate::snapshot::SnapshotConfig::default(),
);
let Some(sample) = capture_sample(&mut resolver) else {
return; // Unsupported host/architecture has no native sampler.
};
assert!(!sample.threads.is_empty(), "sampler dropped every thread");
assert!(sample.threads.iter().all(|thread| !thread.frames.is_empty()));
assert!(sample.modules.iter().all(|module| !module.identity.is_empty()));
for thread in &sample.threads {
for frame in &thread.frames {
assert!(frame.module_index.is_none_or(|index| {
usize::try_from(index).is_ok_and(|index| index < sample.modules.len())
}));
}
}
}
}
Loading
Loading