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
25 changes: 25 additions & 0 deletions book/src/guide/sandboxes.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,31 @@ print(f"Callback invocations: {result.callback_invocations}")
```
<!-- langtabs-end -->

### Trace Collection

In Rust, the sandbox also records line-level execution events in
`ExecuteResult::trace`. This is on by default and installs Python's
`sys.settrace` hook, whose cost scales with how much Python runs: `pass`
costs about 20% extra per execution, a small `json` + `string.Template`
render goes from 0.9 ms to 3.7 ms, and `sum(i * i for i in range(20_000))`
from 3.8 ms to 369 ms. Disable it unless you read the trace:

```rust
# extern crate eryx;
use eryx::Sandbox;

# fn main() -> Result<(), eryx::Error> {
let sandbox = Sandbox::embedded().with_trace_collection(false).build()?;
# Ok(())
# }
```

A `TraceHandler` set with `with_trace_handler` still receives events when
collection is disabled. Sessions created from the sandbox inherit the setting.

The Python bindings never collect traces (`ExecuteResult` does not expose
them), so no option is needed there.

## Error Handling

Sandbox execution can fail for various reasons. Eryx provides typed errors to help you handle them:
Expand Down
4 changes: 4 additions & 0 deletions crates/eryx-python/python/eryx/_eryx.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,10 @@ class Sandbox:

For sandboxes with custom packages, use `SandboxFactory` instead.

Execution trace collection (`sys.settrace`) is disabled for Python
sandboxes; `ExecuteResult` does not expose trace events, so there is no
per-line tracing overhead.

Example:
# Basic sandbox (stdlib only)
sandbox = Sandbox()
Expand Down
3 changes: 2 additions & 1 deletion crates/eryx-python/src/preinit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,8 @@ impl SandboxFactory {
// Use provided site_packages or fall back to the one from initialization
let site_packages_path = site_packages.or_else(|| self.site_packages_path.clone());

// Build sandbox from precompiled bytes
// Build sandbox from precompiled bytes. Trace collection (sys.settrace)
// is always off for Python sandboxes; see `Sandbox::new`.
// SAFETY: The precompiled bytes were created by PythonExecutor::precompile()
// from a valid WASM component, so they are safe to deserialize.
let mut builder = unsafe {
Expand Down
9 changes: 8 additions & 1 deletion crates/eryx-python/src/sandbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ impl Sandbox {
///
/// For sandboxes with custom packages, use `SandboxFactory` instead.
///
/// Execution trace collection (`sys.settrace`) is disabled for Python
/// sandboxes; `ExecuteResult` does not expose trace events, so there is
/// no per-line tracing overhead.
///
/// Args:
/// resource_limits: Optional resource limits for execution.
/// network: Optional network configuration. If provided, enables networking.
Expand Down Expand Up @@ -167,7 +171,10 @@ impl Sandbox {
})?,
);

// Build the eryx sandbox with embedded runtime
// Build the eryx sandbox with embedded runtime. Trace collection
// (sys.settrace) is always off: the Python ExecuteResult does not
// expose trace events, and the hook makes instruction-heavy scripts
// orders of magnitude slower.
let mut builder = eryx::Sandbox::embedded().with_trace_collection(false);

// Apply resource limits if provided
Expand Down
8 changes: 8 additions & 0 deletions crates/eryx/benches/execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
//!
//! Run with: `cargo bench --package eryx --features embedded`
//!
//! Trace collection (`sys.settrace`) is on by default, matching the library
//! default so historical numbers stay comparable. Set `ERYX_PROFILE_TRACE=0`
//! to benchmark with it disabled.
//!
//! ## Benchmark Groups
//!
//! - **sandbox_creation**: Measures time to create a new sandbox
Expand Down Expand Up @@ -121,7 +125,11 @@ impl TypedCallback for WorkCallback {
// ============================================================================

fn create_sandbox() -> Sandbox {
// Trace collection (sys.settrace) is on by default; `ERYX_PROFILE_TRACE=0`
// turns it off to measure without per-event trace overhead.
let collect_trace = !std::env::var("ERYX_PROFILE_TRACE").is_ok_and(|v| v.trim() == "0");
Sandbox::embedded()
.with_trace_collection(collect_trace)
.with_callback(NoopCallback)
.with_callback(EchoCallback)
.with_callback(WorkCallback)
Expand Down
11 changes: 10 additions & 1 deletion crates/eryx/examples/profile_execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
//!
//! Or with a specific iteration count:
//! samply record ./target/release/examples/profile_execution 5000
//!
//! Set `ERYX_PROFILE_TRACE=0` to disable trace collection (`sys.settrace`),
//! which is on by default and dominates anything heavier than `pass`.

use std::time::Instant;

Expand All @@ -23,7 +26,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {

rt.block_on(async {
eprintln!("Creating sandbox...");
let sandbox = Sandbox::embedded().build()?;
// Trace collection (sys.settrace) is on by default; `ERYX_PROFILE_TRACE=0`
// turns it off to measure without per-event trace overhead.
let collect_trace = !std::env::var("ERYX_PROFILE_TRACE").is_ok_and(|v| v.trim() == "0");
eprintln!(" trace collection: {collect_trace}");
let sandbox = Sandbox::embedded()
.with_trace_collection(collect_trace)
.build()?;

eprintln!("Creating session...");
let mut session = InProcessSession::new(&sandbox).await?;
Expand Down
6 changes: 5 additions & 1 deletion crates/eryx/examples/session_bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,13 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create sandbox using cache_dir (handles linking, pre-init, precompile, and mmap)
println!("Creating sandbox (cold - linking + compiling + caching)...");
let start = Instant::now();
// Trace collection (sys.settrace) is on by default; `ERYX_PROFILE_TRACE=0`
// turns it off to measure without per-event trace overhead.
let collect_trace = !std::env::var("ERYX_PROFILE_TRACE").is_ok_and(|v| v.trim() == "0");
println!(" trace collection: {collect_trace}");
// Start with embedded() which provides runtime+stdlib, then late-linking
// overrides the runtime when native extensions are added
let mut builder = Sandbox::embedded();
let mut builder = Sandbox::embedded().with_trace_collection(collect_trace);
for (name, bytes) in &extensions {
builder = builder.with_native_extension(name.clone(), bytes.clone());
}
Expand Down
24 changes: 21 additions & 3 deletions crates/eryx/src/sandbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1887,9 +1887,27 @@ impl<R, S> SandboxBuilder<R, S> {
/// Configure whether execution trace events are collected in the result.
///
/// Trace collection is enabled by default for backward compatibility. It
/// installs Python's `sys.settrace` hook, which can be expensive for
/// instruction-heavy workloads. A configured [`TraceHandler`] always keeps
/// tracing enabled regardless of this setting.
/// installs Python's `sys.settrace` hook, which fires on every line, call
/// and return in the guest, so its cost scales with the amount of Python
/// executed rather than with sandbox setup. Measured on a Ryzen 9 7950X
/// (wasmtime 48, fresh instance per call), enabled vs disabled:
///
/// | Workload | On | Off |
/// |---|---|---|
/// | `pass` | 1.28 ms | 1.08 ms |
/// | `json.loads` + `string.Template(...).substitute(...)` | 3.7 ms | 0.9 ms |
/// | `sum(i * i for i in range(20_000))` | 369 ms | 3.8 ms |
///
/// Disable it unless you read [`ExecuteResult::trace`]:
///
/// ```rust,ignore
/// let sandbox = Sandbox::embedded().with_trace_collection(false).build()?;
/// ```
///
/// This also applies to [`InProcessSession`](crate::session::InProcessSession)s
/// created from the sandbox. A configured [`TraceHandler`] always keeps
/// tracing enabled regardless of this setting; only the retention of
/// events in the result is controlled here.
#[must_use]
pub const fn with_trace_collection(mut self, enabled: bool) -> Self {
self.collect_trace = enabled;
Expand Down
Loading