feat: buffer tracing logs before console init

This commit is contained in:
Ryan Walters
2025-09-01 17:22:22 -05:00
parent b1b03b0e9c
commit c79ba0d824
6 changed files with 184 additions and 14 deletions

View File

@@ -25,8 +25,8 @@ pub struct App {
impl App { impl App {
/// Initializes SDL subsystems, creates the game window, and sets up the game state. /// Initializes SDL subsystems, creates the game window, and sets up the game state.
/// ///
/// Performs comprehensive initialization including video/audio subsystems, platform-specific /// Performs comprehensive initialization including video/audio subsystems,
/// console setup, window creation with proper scaling, and canvas configuration. All SDL /// window creation with proper scaling, and canvas configuration. All SDL
/// resources are leaked to maintain 'static lifetimes required by the game architecture. /// resources are leaked to maintain 'static lifetimes required by the game architecture.
/// ///
/// # Errors /// # Errors
@@ -44,9 +44,6 @@ impl App {
let event_pump: &'static mut EventPump = let event_pump: &'static mut EventPump =
Box::leak(Box::new(sdl_context.event_pump().map_err(|e| GameError::Sdl(e.to_string()))?)); Box::leak(Box::new(sdl_context.event_pump().map_err(|e| GameError::Sdl(e.to_string()))?));
// Initialize platform-specific console
get_platform().init_console()?;
let window = video_subsystem let window = video_subsystem
.window( .window(
"Pac-Man", "Pac-Man",

View File

@@ -2,8 +2,6 @@
use crate::{app::App, constants::LOOP_TIME}; use crate::{app::App, constants::LOOP_TIME};
use tracing::info; use tracing::info;
use tracing_error::ErrorLayer;
use tracing_subscriber::layer::SubscriberExt;
mod app; mod app;
mod asset; mod asset;
@@ -23,14 +21,22 @@ mod texture;
/// This function initializes SDL, the window, the game state, and then enters /// This function initializes SDL, the window, the game state, and then enters
/// the main game loop. /// the main game loop.
pub fn main() { pub fn main() {
// Setup tracing // Setup buffered tracing subscriber that will buffer logs until console is ready
let subscriber = tracing_subscriber::fmt() let switchable_writer = platform::tracing_buffer::setup_switchable_subscriber();
.with_ansi(cfg!(not(target_os = "emscripten")))
.with_max_level(tracing::Level::DEBUG)
.finish()
.with(ErrorLayer::default());
tracing::subscriber::set_global_default(subscriber).expect("Could not set global default"); // Log early to show buffering is working
tracing::debug!("Tracing subscriber initialized with buffering - logs will be buffered until console is ready");
// Initialize platform-specific console
tracing::debug!("Starting console initialization...");
platform::get_platform().init_console().expect("Could not initialize console");
tracing::debug!("Console initialization completed");
// Now that console is initialized, flush buffered logs and switch to direct output
tracing::debug!("Switching to direct logging mode and flushing buffer...");
if let Err(e) = switchable_writer.switch_to_direct_mode() {
tracing::warn!("Failed to flush buffered logs to console: {}", e);
}
let mut app = App::new().expect("Could not create app"); let mut app = App::new().expect("Could not create app");

View File

@@ -0,0 +1,55 @@
//! Buffered writer for tracing logs that can store logs before console attachment.
use parking_lot::Mutex;
use std::io::{self, Write};
use std::sync::Arc;
/// A thread-safe buffered writer that stores logs in memory until flushed.
#[derive(Clone)]
pub struct BufferedWriter {
buffer: Arc<Mutex<Vec<u8>>>,
}
impl BufferedWriter {
/// Creates a new buffered writer.
pub fn new() -> Self {
Self {
buffer: Arc::new(Mutex::new(Vec::new())),
}
}
/// Flushes all buffered content to the provided writer and clears the buffer.
pub fn flush_to<W: Write>(&self, mut writer: W) -> io::Result<()> {
let mut buffer = self.buffer.lock();
if !buffer.is_empty() {
writer.write_all(&buffer)?;
writer.flush()?;
buffer.clear();
}
Ok(())
}
/// Returns the current buffer size in bytes.
pub fn buffer_size(&self) -> usize {
self.buffer.lock().len()
}
}
impl Write for BufferedWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let mut buffer = self.buffer.lock();
buffer.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
// For buffered writer, flush is a no-op since we're storing in memory
Ok(())
}
}
impl Default for BufferedWriter {
fn default() -> Self {
Self::new()
}
}

View File

@@ -10,6 +10,8 @@ mod desktop;
#[cfg(target_os = "emscripten")] #[cfg(target_os = "emscripten")]
mod emscripten; mod emscripten;
pub mod buffered_writer;
pub mod tracing_buffer;
/// Cross-platform abstraction layer providing unified APIs for platform-specific operations. /// Cross-platform abstraction layer providing unified APIs for platform-specific operations.
pub trait CommonPlatform { pub trait CommonPlatform {
/// Platform-specific sleep function (required due to Emscripten's non-standard sleep requirements). /// Platform-specific sleep function (required due to Emscripten's non-standard sleep requirements).

View File

@@ -0,0 +1,91 @@
//! Buffered tracing setup for handling logs before console attachment.
use crate::platform::buffered_writer::BufferedWriter;
use std::io;
use tracing::Level;
use tracing_error::ErrorLayer;
use tracing_subscriber::fmt::MakeWriter;
use tracing_subscriber::layer::SubscriberExt;
/// A writer that can switch between buffering and direct output.
#[derive(Clone, Default)]
pub struct SwitchableWriter {
buffered_writer: BufferedWriter,
direct_mode: std::sync::Arc<parking_lot::Mutex<bool>>,
}
impl SwitchableWriter {
pub fn switch_to_direct_mode(&self) -> io::Result<()> {
// Get buffer size before flushing for debug logging
let buffer_size = self.buffered_writer.buffer_size();
// First flush any buffered content
self.buffered_writer.flush_to(io::stdout())?;
// Switch to direct mode
*self.direct_mode.lock() = true;
// Log how much was buffered (this will now go directly to stdout)
tracing::debug!("Flushed {} bytes of buffered logs to console", buffer_size);
Ok(())
}
}
impl io::Write for SwitchableWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
if *self.direct_mode.lock() {
io::stdout().write(buf)
} else {
self.buffered_writer.clone().write(buf)
}
}
fn flush(&mut self) -> io::Result<()> {
if *self.direct_mode.lock() {
io::stdout().flush()
} else {
// For buffered mode, flush is a no-op
Ok(())
}
}
}
/// A make writer that uses the switchable writer.
#[derive(Clone)]
pub struct SwitchableMakeWriter {
writer: SwitchableWriter,
}
impl SwitchableMakeWriter {
pub fn new(writer: SwitchableWriter) -> Self {
Self { writer }
}
}
impl<'a> MakeWriter<'a> for SwitchableMakeWriter {
type Writer = SwitchableWriter;
fn make_writer(&'a self) -> Self::Writer {
self.writer.clone()
}
}
/// Sets up a switchable tracing subscriber that can transition from buffered to direct output.
///
/// Returns the switchable writer that can be used to control the behavior.
pub fn setup_switchable_subscriber() -> SwitchableWriter {
let switchable_writer = SwitchableWriter::default();
let make_writer = SwitchableMakeWriter::new(switchable_writer.clone());
let _subscriber = tracing_subscriber::fmt()
.with_ansi(cfg!(not(target_os = "emscripten")))
.with_max_level(Level::DEBUG)
.with_writer(make_writer)
.finish()
.with(ErrorLayer::default());
tracing::subscriber::set_global_default(_subscriber).expect("Could not set global default switchable subscriber");
switchable_writer
}

19
tests/tracing_buffer.rs Normal file
View File

@@ -0,0 +1,19 @@
use pacman::platform::tracing_buffer::SwitchableWriter;
use std::io::Write;
#[test]
fn test_switchable_writer_buffering() {
let mut writer = SwitchableWriter::default();
// Write some data while in buffered mode
writer.write_all(b"Hello, ").unwrap();
writer.write_all(b"world!").unwrap();
writer.write_all(b"This is buffered content.\n").unwrap();
// Switch to direct mode (this should flush to stdout and show buffer size)
// In a real test we can't easily capture stdout, so we'll just verify it doesn't panic
writer.switch_to_direct_mode().unwrap();
// Write more data in direct mode
writer.write_all(b"Direct output after flush\n").unwrap();
}