From 75ceda4bef7400c570108f10bfd1694787726c92 Mon Sep 17 00:00:00 2001 From: Tony Bierman Date: Mon, 4 May 2026 08:24:25 -0500 Subject: [PATCH 1/2] feat: per-frame tracing buffer that dumps on panic Replaces the always-on debug-mode file log with a per-frame in-memory buffer that captures tracing events emitted during run_rewrite_passes and dumps them to a $TMPDIR/masonry-*-panic.log file only when the process panics. This eliminates per-log-line I/O on the hot path while preserving full diagnostics for crashes. Closes #1556. Co-Authored-By: Claude Opus 4.7 --- masonry/examples/panic_log.rs | 197 +++++++++++++++ masonry_core/src/app/mod.rs | 2 + masonry_core/src/app/panic_log_buffer.rs | 293 +++++++++++++++++++++++ masonry_core/src/app/render_root.rs | 4 + masonry_core/src/app/tracing_backend.rs | 76 +++--- 5 files changed, 533 insertions(+), 39 deletions(-) create mode 100644 masonry/examples/panic_log.rs create mode 100644 masonry_core/src/app/panic_log_buffer.rs diff --git a/masonry/examples/panic_log.rs b/masonry/examples/panic_log.rs new file mode 100644 index 000000000..1aa452327 --- /dev/null +++ b/masonry/examples/panic_log.rs @@ -0,0 +1,197 @@ +// Copyright 2025 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +//! Demonstrates the panic-log buffering: a widget panics inside its `layout` +//! method (which runs during the layout pass, inside `run_rewrite_passes`). +//! The panic-buffer tracing subscriber flushes that frame's events to a file +//! in `$TMPDIR` named `masonry-{ts}-{pid}-panic.log`. + +#![cfg_attr(not(test), windows_subsystem = "windows")] + +use masonry::accesskit::{Node, Role}; +use masonry::core::{ + AccessCtx, AccessEvent, ChildrenIds, ErasedAction, EventCtx, LayoutCtx, MeasureCtx, NewWidget, + NoAction, PaintCtx, PointerEvent, PropertiesMut, PropertiesRef, RegisterCtx, TextEvent, Widget, + WidgetId, +}; +use masonry::imaging::Painter; +use masonry::kurbo::{Axis, Size}; +use masonry::layout::LenReq; +use masonry::theme::default_property_set; +use masonry_winit::app::{AppDriver, DriverCtx, NewWindow, WindowId}; +use masonry_winit::winit::window::Window; +use tracing::{Span, info, trace_span}; + +struct Driver; + +impl AppDriver for Driver { + fn on_action( + &mut self, + _window_id: WindowId, + _ctx: &mut DriverCtx<'_, '_>, + _widget_id: WidgetId, + _action: ErasedAction, + ) { + } +} + +/// A leaf widget that emits a tracing event and then panics in `layout`, +/// which runs during the layout pass inside `run_rewrite_passes`. +struct PanicWidget; + +impl Widget for PanicWidget { + type Action = NoAction; + + fn on_pointer_event( + &mut self, + _ctx: &mut EventCtx<'_>, + _props: &mut PropertiesMut<'_>, + _event: &PointerEvent, + ) { + } + + fn on_text_event( + &mut self, + _ctx: &mut EventCtx<'_>, + _props: &mut PropertiesMut<'_>, + _event: &TextEvent, + ) { + } + + fn on_access_event( + &mut self, + _ctx: &mut EventCtx<'_>, + _props: &mut PropertiesMut<'_>, + _event: &AccessEvent, + ) { + } + + fn register_children(&mut self, _ctx: &mut RegisterCtx<'_>) {} + + fn measure( + &mut self, + _ctx: &mut MeasureCtx<'_>, + _props: &PropertiesRef<'_>, + _axis: Axis, + _len_req: LenReq, + _cross_length: Option, + ) -> f64 { + 0.0 + } + + fn layout(&mut self, _ctx: &mut LayoutCtx<'_>, _props: &PropertiesRef<'_>, _size: Size) { + info!("PanicWidget::layout called — about to simulate a widget bug"); + panic!("simulated widget bug during layout pass"); + } + + fn paint( + &mut self, + _ctx: &mut PaintCtx<'_>, + _props: &PropertiesRef<'_>, + _painter: &mut Painter<'_>, + ) { + } + + fn accessibility_role(&self) -> Role { + Role::GenericContainer + } + + fn accessibility( + &mut self, + _ctx: &mut AccessCtx<'_>, + _props: &PropertiesRef<'_>, + _node: &mut Node, + ) { + } + + fn children_ids(&self) -> ChildrenIds { + ChildrenIds::new() + } + + fn make_trace_span(&self, id: WidgetId) -> Span { + trace_span!("PanicWidget", id = id.trace()) + } +} + +fn main() { + let window_attributes = Window::default_attributes().with_title("Panic-log demo"); + + masonry_winit::app::run( + vec![NewWindow::new( + window_attributes, + NewWidget::new(PanicWidget).erased(), + )], + Driver, + default_property_set(), + ) + .unwrap(); +} + +// --- MARK: TESTS + +#[cfg(test)] +mod tests { + use std::panic::{AssertUnwindSafe, catch_unwind}; + + use masonry_testing::TestHarness; + + use super::*; + + /// Drives the panic widget through the layout pass synchronously and + /// asserts that the panic-buffer tracing layer wrote a panic log file + /// containing the in-pass tracing event. + /// + /// The harness installs the panic hook indirectly via + /// `try_init_test_tracing`. `AssertUnwindSafe` is sound because the + /// harness is dropped (not reused) before any state is read back. + #[test] + fn panic_log_file_is_written() { + // Snapshot existing `masonry-*-panic.log` files before the run so we + // can identify the new one by set difference. Filename-only matching + // (e.g. by PID suffix) is unreliable: PIDs are recycled and a stale + // file from a prior process with the same PID would collide. + let pid_suffix = format!("-{}-panic.log", std::process::id()); + let snapshot = panic_log_paths(&pid_suffix); + + let result = catch_unwind(AssertUnwindSafe(|| { + let mut harness = + TestHarness::create(default_property_set(), NewWidget::new(PanicWidget)); + harness.render(); + })); + assert!( + result.is_err(), + "expected the layout-pass panic to propagate" + ); + + let new_paths: Vec<_> = panic_log_paths(&pid_suffix) + .into_iter() + .filter(|p| !snapshot.contains(p)) + .collect(); + assert_eq!( + new_paths.len(), + 1, + "expected exactly one new masonry-*{pid_suffix} file in $TMPDIR; got {new_paths:?}" + ); + + let log_path = &new_paths[0]; + let contents = std::fs::read_to_string(log_path).unwrap(); + + // Remove the file before asserting on its contents, so a failing + // assertion doesn't leave a stale artifact in $TMPDIR. + let _ = std::fs::remove_file(log_path); + + assert!( + contents.contains("about to simulate a widget bug"), + "panic log should contain the buffered tracing event; got:\n{contents}" + ); + } + + fn panic_log_paths(pid_suffix: &str) -> std::collections::HashSet { + std::fs::read_dir(std::env::temp_dir()) + .unwrap() + .filter_map(Result::ok) + .filter(|e| e.file_name().to_string_lossy().ends_with(pid_suffix)) + .map(|e| e.path()) + .collect() + } +} diff --git a/masonry_core/src/app/mod.rs b/masonry_core/src/app/mod.rs index 0bb460e93..9fc81ec3c 100644 --- a/masonry_core/src/app/mod.rs +++ b/masonry_core/src/app/mod.rs @@ -4,6 +4,8 @@ //! Types needed for running a Masonry app. mod layer_stack; +#[cfg(not(target_arch = "wasm32"))] +mod panic_log_buffer; mod render_root; mod tracing_backend; mod visual_layers; diff --git a/masonry_core/src/app/panic_log_buffer.rs b/masonry_core/src/app/panic_log_buffer.rs new file mode 100644 index 000000000..b95aa6c31 --- /dev/null +++ b/masonry_core/src/app/panic_log_buffer.rs @@ -0,0 +1,293 @@ +// Copyright 2025 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +//! A tracing buffer that records events during rewrite passes and dumps them +//! to a file only when the process panics. +//! +//! # Usage +//! +//! 1. At subscriber setup time, call [`register_panic_hook`] once and register +//! a `tracing_subscriber::fmt::layer().with_writer(BufferWriter)`. +//! 2. In [`RenderRoot::run_rewrite_passes`](crate::app::RenderRoot), call +//! [`start_frame_recording()`] and hold the returned +//! [`FrameRecordingGuard`] for the duration of the call. +//! 3. If a panic occurs while a guard is live, the panic hook writes the +//! buffer to the log path chosen during step 1. +//! +//! Only the *most recent* panic before process exit is preserved on disk: +//! each call to the panic hook truncates the existing log. This is fine for +//! the common case (panic crashes the process); callers using +//! `std::panic::catch_unwind` should be aware that earlier panics' buffers +//! will be overwritten. + +use std::collections::VecDeque; +use std::fs::File; +use std::io::{self, BufWriter, Write}; +use std::path::PathBuf; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Mutex, OnceLock, TryLockError}; + +/// Fast-path flag: `true` while a frame recording is active. +/// +/// `Relaxed` is sufficient *for correctness of buffer accesses*: the [`Mutex`] +/// in [`EVENT_BUFFER`] provides the happens-before relationship. Both the +/// recording flip in [`FrameRecordingGuard::drop`] and the verifying load in +/// [`BufferWriter::write`] happen *under* the mutex, so once drop returns no +/// subsequent writer can push into the buffer for the just-ended frame. +/// +/// The fast-path load in [`BufferWriter::write`] is *not* under the mutex; it +/// is a best-effort optimization to skip the slow path when no frame is +/// recording. On weak memory architectures a cross-thread writer may briefly +/// observe a stale `false` just after [`start_frame_recording`] flips the +/// flag, dropping events emitted in that window. This is acceptable because +/// rewrite passes run on the UI thread and cross-thread tracing during a +/// pass is rare. +static IS_RECORDING: AtomicBool = AtomicBool::new(false); + +/// Buffered event lines for the current frame. Bounded by both +/// [`MAX_BUFFER_LINES`] (entry count) and [`MAX_BUFFER_BYTES`] (total length); +/// when either cap is exceeded, the oldest entries are evicted. +static EVENT_BUFFER: OnceLock> = OnceLock::new(); + +/// In-memory event buffer with a running byte total so eviction can enforce +/// [`MAX_BUFFER_BYTES`] without re-summing. +struct EventBuffer { + lines: VecDeque, + bytes: usize, +} + +impl EventBuffer { + fn new() -> Self { + Self { + lines: VecDeque::new(), + bytes: 0, + } + } + + fn clear(&mut self) { + self.lines.clear(); + self.bytes = 0; + } + + fn push(&mut self, line: String) { + self.bytes += line.len(); + self.lines.push_back(line); + while self.lines.len() > MAX_BUFFER_LINES || self.bytes > MAX_BUFFER_BYTES { + let Some(removed) = self.lines.pop_front() else { + // Single line larger than MAX_BUFFER_BYTES: the loop popped + // everything including the line we just pushed, leaving the + // buffer empty. `bytes` is now stale, so reset it. + self.bytes = 0; + break; + }; + self.bytes = self.bytes.saturating_sub(removed.len()); + } + } +} + +/// Path to write panic logs to; set once on first call to +/// [`register_panic_hook`]. +static LOG_PATH: OnceLock = OnceLock::new(); + +/// Guards one-time installation of the panic hook (and the path/closure +/// allocations that go with it). Subsequent [`register_panic_hook`] calls are +/// pure no-ops. +static HOOK_INSTALLED: OnceLock<()> = OnceLock::new(); + +/// Maximum number of event lines retained per frame. +const MAX_BUFFER_LINES: usize = 10_000; + +/// Maximum total bytes (sum of line lengths) retained per frame. A second +/// guard alongside [`MAX_BUFFER_LINES`] so a misbehaving widget that emits very +/// long log lines cannot balloon RSS within a single frame. +const MAX_BUFFER_BYTES: usize = 8 * 1024 * 1024; + +// --- MARK: PANIC HOOK + +/// Registers a panic hook that flushes buffered rewrite-pass events to a +/// timestamped file in `$TMPDIR` if the process panics. +/// +/// Safe to call multiple times (e.g. in tests): only the first call computes +/// the path and installs the hook; subsequent calls are no-ops. +/// +/// Pair this with a `tracing_subscriber::fmt::layer().with_writer(BufferWriter)` +/// registered on the global subscriber. +pub(crate) fn register_panic_hook() { + EVENT_BUFFER.get_or_init(|| Mutex::new(EventBuffer::new())); + + HOOK_INSTALLED.get_or_init(|| { + let id = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis(); + let pid = std::process::id(); + let path = std::env::temp_dir().join(format!("masonry-{id:016}-{pid}-panic.log")); + // Sole writer; inside HOOK_INSTALLED's initializer. + let _ = LOG_PATH.set(path); + + // Run the previous hook first so the panic message is emitted before + // our footer, matching the order users expect. + let previous_hook = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |info| { + previous_hook(info); + #[allow(clippy::print_stderr, reason = "Crash diagnostic pointer")] + match flush_buffer_to_log() { + Ok(Some(path)) => { + eprintln!("→ Pre-panic trace written to {}", path.display()); + } + Ok(None) => {} + Err(err) => { + eprintln!("→ Failed to write pre-panic trace: {err}"); + } + } + })); + }); +} + +/// Flushes the current event buffer to [`LOG_PATH`]. +/// +/// - `Ok(Some(path))`: a non-empty buffer was written successfully. +/// - `Ok(None)`: nothing to do (no path registered, no buffer, lock would +/// block, or the buffer is empty). +/// - `Err(_)`: the I/O write itself failed. +fn flush_buffer_to_log() -> io::Result> { + let Some(path) = LOG_PATH.get() else { + return Ok(None); + }; + let Some(lock) = EVENT_BUFFER.get() else { + return Ok(None); + }; + + // `try_lock` (not `lock`) so the panic hook never deadlocks against + // itself when a panic originates inside [`BufferWriter::write`] while it + // holds the buffer mutex. + let events = match lock.try_lock() { + Ok(guard) => guard, + Err(TryLockError::Poisoned(poisoned)) => poisoned.into_inner(), + Err(TryLockError::WouldBlock) => return Ok(None), + }; + + if events.lines.is_empty() { + return Ok(None); + } + + let mut writer = BufWriter::new(File::create(path)?); + for line in &events.lines { + writeln!(writer, "{line}")?; + } + writer.flush()?; + Ok(Some(path.clone())) +} + +// --- MARK: FRAME GUARD + +/// RAII guard returned by [`start_frame_recording`]. +/// +/// While this guard is live, [`BufferWriter`] will capture tracing events +/// into the in-memory buffer. On drop the recording stops and the buffer is +/// cleared. If a panic occurs before the guard is dropped, the panic hook +/// registered by [`register_panic_hook`] writes the buffer to disk. +/// +/// Nesting is not supported: a second `start_frame_recording` call while a +/// guard is live shares the same buffer, and dropping the inner guard clears +/// it out from under the outer. +pub(crate) struct FrameRecordingGuard(()); + +impl Drop for FrameRecordingGuard { + fn drop(&mut self) { + // Take the lock first, then flip the flag *under* the lock. Any + // writer that re-checks IS_RECORDING under the lock after we drop + // will see `false` and skip; any writer already past its re-check + // has already pushed and will be cleared below. + if let Some(lock) = EVENT_BUFFER.get() { + let mut events = match lock.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + }; + IS_RECORDING.store(false, Ordering::Relaxed); + events.clear(); + } else { + IS_RECORDING.store(false, Ordering::Relaxed); + } + } +} + +/// Starts buffering tracing events for the duration of one +/// `run_rewrite_passes` invocation. +/// +/// In release builds this is a no-op: the per-frame fmt layer is not +/// registered, so there is nothing to record. The returned guard is still +/// held by the caller, which keeps the call site free of `cfg` gates. +/// +/// Use `let _guard = start_frame_recording();` — a bare `let _ = ...` would +/// drop the guard immediately. +pub(crate) fn start_frame_recording() -> FrameRecordingGuard { + #[cfg(debug_assertions)] + { + // Clear any leftover entries from a frame that was unwound past via + // `catch_unwind`, so the next panic dump only contains this frame's + // events. + if let Some(lock) = EVENT_BUFFER.get() { + let mut events = match lock.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + }; + events.clear(); + IS_RECORDING.store(true, Ordering::Relaxed); + } + } + FrameRecordingGuard(()) +} + +// --- MARK: BUFFER WRITER + +/// A [`tracing_subscriber::fmt::MakeWriter`] that directs formatted events +/// into the in-memory buffer when a frame recording is active. +/// +/// Use as `.with_writer(BufferWriter)` on a `fmt::layer()`. Writes are +/// no-ops when [`IS_RECORDING`] is `false`. When [`MAX_BUFFER_LINES`] or +/// [`MAX_BUFFER_BYTES`] is exceeded, the oldest entries are evicted. +pub(crate) struct BufferWriter; + +impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for BufferWriter { + type Writer = Self; + + fn make_writer(&'a self) -> Self::Writer { + Self + } +} + +impl Write for BufferWriter { + fn write(&mut self, buf: &[u8]) -> io::Result { + // Fast path: avoid touching the mutex when no frame is recording. + if !IS_RECORDING.load(Ordering::Relaxed) { + return Ok(buf.len()); + } + let Ok(line) = std::str::from_utf8(buf) else { + return Ok(buf.len()); + }; + let line = line.trim_end(); + if line.is_empty() { + return Ok(buf.len()); + } + let Some(lock) = EVENT_BUFFER.get() else { + return Ok(buf.len()); + }; + let mut events = match lock.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + }; + // Re-check under the lock: FrameRecordingGuard::drop flips this flag + // while holding the same mutex, so a `false` here means recording + // ended before we acquired the lock. + if !IS_RECORDING.load(Ordering::Relaxed) { + return Ok(buf.len()); + } + events.push(line.to_owned()); + Ok(buf.len()) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} diff --git a/masonry_core/src/app/render_root.rs b/masonry_core/src/app/render_root.rs index 729bb4ef5..669b485cd 100644 --- a/masonry_core/src/app/render_root.rs +++ b/masonry_core/src/app/render_root.rs @@ -791,6 +791,10 @@ impl RenderRoot { /// /// See the [passes documentation](crate::doc::pass_system) for details. pub(crate) fn run_rewrite_passes(&mut self) { + // Capture this frame's events for the panic-log dump. + #[cfg(not(target_arch = "wasm32"))] + let _guard = crate::app::panic_log_buffer::start_frame_recording(); + const REWRITE_PASSES_MAX: usize = 4; for _ in 0..REWRITE_PASSES_MAX { diff --git a/masonry_core/src/app/tracing_backend.rs b/masonry_core/src/app/tracing_backend.rs index 510ef18fb..3ae379d0a 100644 --- a/masonry_core/src/app/tracing_backend.rs +++ b/masonry_core/src/app/tracing_backend.rs @@ -3,8 +3,15 @@ //! Configures a suitable default [`tracing`] implementation for a Masonry application. //! -//! This uses a custom log format specialised for GUI applications, -//! and will write all logs to a temporary file in debug mode. +//! This uses a custom log format specialised for GUI applications. +//! In debug mode, tracing events emitted during the per-frame rewrite passes +//! (the layout/compose/paint cycle driven by [`RenderRoot`]) are buffered in +//! memory and only written to a temporary file if the process panics. This +//! avoids file I/O on every log line during normal operation while preserving +//! full diagnostics for crashes. +//! +//! [`RenderRoot`]: crate::app::RenderRoot +//! //! This also uses a default filter, which can be overwritten using `RUST_LOG`. //! This will include all [`DEBUG`](tracing::Level::DEBUG) messages in debug mode, //! and all [`INFO`](tracing::Level::INFO) level messages in release mode. @@ -15,11 +22,6 @@ use std::error::Error; use std::fmt; -#[cfg(not(target_arch = "wasm32"))] -use std::fs::File; -#[cfg(not(target_arch = "wasm32"))] -use std::time::UNIX_EPOCH; - #[cfg(not(target_arch = "wasm32"))] use time::macros::format_description; use tracing::Subscriber; @@ -30,6 +32,16 @@ use tracing_subscriber::filter::LevelFilter; use tracing_subscriber::fmt::time::UtcTime; use tracing_subscriber::prelude::*; +#[cfg(not(target_arch = "wasm32"))] +use super::panic_log_buffer::BufferWriter; + +/// Install the panic-log hook once a subscriber has been set globally. +/// Compiles to nothing in release builds and on wasm. +fn install_panic_log_hook() { + #[cfg(all(not(target_arch = "wasm32"), debug_assertions))] + super::panic_log_buffer::register_panic_hook(); +} + #[cfg(not(target_arch = "wasm32"))] /// Get the tracing subscriber we wish to set-up for a non-web platform with the given `default_level`. /// @@ -63,36 +75,18 @@ fn default_tracing_subscriber_native( .with_target(false) .with_filter(env_filter); - // We skip the layer which stores to a file in `--release` mode for performance. - let log_file_layer = if cfg!(debug_assertions) { - // TODO - Replace with a more targeted subscriber. - // See https://github.com/linebender/xilem/issues/1556 - - let id = std::time::SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis(); - let tmp_path = std::env::temp_dir().join(format!("masonry-{id:016}-dense.log")); - // If modifying, also update the module level docs - let log_file_layer = tracing_subscriber::fmt::layer() - .with_timer(timer) - .with_writer(File::create(&tmp_path).unwrap()) - // TODO - For some reason, `.with_ansi(false)` still leaves some italics in the output. - .with_ansi(false); - // Note that this layer does not use the provided filter, and instead logs all events. - - #[allow(clippy::print_stderr, reason = "Can only use stderr")] - { - // We print this message to stderr (rather than through `tracing`), because: - // 1) Tracing hasn't been set up yet - // 2) The tracing logs could have been configured to eat this message, and we think this is still important to have visible. - // 3) This message is only sent in debug mode, so won't be exposed to end-users. - eprintln!("---"); - eprintln!("Writing full logs to {}", tmp_path.display()); - eprintln!("---"); - } - - Some(log_file_layer) + // Captures all events at all levels (no env filter), but only events + // emitted during `run_rewrite_passes` are retained. + let panic_buffer_layer = if cfg!(debug_assertions) { + // If you change the writer or formatting here, also update the + // module-level docs above which describe the buffering behaviour. + Some( + tracing_subscriber::fmt::layer() + .with_timer(timer) + // TODO - For some reason, `.with_ansi(false)` still leaves some italics in the output. + .with_ansi(false) + .with_writer(BufferWriter), + ) } else { None }; @@ -102,7 +96,7 @@ fn default_tracing_subscriber_native( let registry = tracing_subscriber::registry() .with(console_layer) - .with(log_file_layer); + .with(panic_buffer_layer); #[cfg(target_os = "android")] let registry = registry.with(android_trace_layer); @@ -178,7 +172,7 @@ fn verify_subscriber_has_not_been_set() -> Result<(), TracingSubscriberHasBeenSe } /// Initialise tracing with a default subscriber for a unit test. -/// This ignores most messages to limit noise (but will still log all messages to a file). +/// This ignores most messages to limit noise. pub fn try_init_test_tracing() -> Result<(), TracingSubscriberHasBeenSetError> { // For unit tests we want to suppress most messages. let default_level = LevelFilter::WARN; @@ -189,6 +183,8 @@ pub fn try_init_test_tracing() -> Result<(), TracingSubscriberHasBeenSetError> { // We may ignore potential errors here because we already checked that no subscriber has been set. let _ = tracing::subscriber::set_global_default(subscriber); + install_panic_log_hook(); + if let Some(err) = err { tracing::error!(err, "Logging init had recoverable error"); } @@ -213,6 +209,8 @@ pub fn try_init_tracing() -> Result<(), TracingSubscriberHasBeenSetError> { // We may ignore potential errors here because we already checked that no subscriber has been set. let _ = tracing::subscriber::set_global_default(subscriber); + install_panic_log_hook(); + if let Some(err) = err { tracing::error!("Initialising logging encountered recoverable error: {err}"); } From 3029db5288b4167dcd3a8c1d8ff2500547572508 Mon Sep 17 00:00:00 2001 From: Tony Bierman Date: Mon, 4 May 2026 08:44:00 -0500 Subject: [PATCH 2/2] fix: derive Debug on BufferWriter for android build AndroidTraceLayer's Layer impl requires Debug on the inner subscriber types, which transitively requires BufferWriter: Debug. Co-Authored-By: Claude Sonnet 4.6 --- masonry_core/src/app/panic_log_buffer.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/masonry_core/src/app/panic_log_buffer.rs b/masonry_core/src/app/panic_log_buffer.rs index b95aa6c31..e95acebea 100644 --- a/masonry_core/src/app/panic_log_buffer.rs +++ b/masonry_core/src/app/panic_log_buffer.rs @@ -247,6 +247,7 @@ pub(crate) fn start_frame_recording() -> FrameRecordingGuard { /// Use as `.with_writer(BufferWriter)` on a `fmt::layer()`. Writes are /// no-ops when [`IS_RECORDING`] is `false`. When [`MAX_BUFFER_LINES`] or /// [`MAX_BUFFER_BYTES`] is exceeded, the oldest entries are evicted. +#[derive(Debug)] pub(crate) struct BufferWriter; impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for BufferWriter {