-
Notifications
You must be signed in to change notification settings - Fork 0
Description
1. Overview
This document outlines the complete architectural plan and implementation roadmap for creating a robust, decoupled logging service and a feature-rich, interactive Command-Line Interface (CLI) for whip_ui.
This plan is the final synthesis of our design discussions. It incorporates an analysis of the existing codebase, details the new architecture to be built, plans for the safe removal of obsolete code, and lays the groundwork for future diagnostic features. It is designed to be followed in incremental, testable steps, with a specific focus on delivering a functional, text-based CLI for logging and control before integrating an advanced UI.
2. Core Philosophy & Design Principles
The architecture of this system is guided by several core principles essential for building a maintainable, performant, and reliable developer tool.
-
Hybrid Push/Pull Logging Architecture: The system will use a hybrid model to achieve both high performance and maximum flexibility.
- Reasoning: A pure "push" system (where every consumer gets every log) can be inefficient, while a pure "pull" system (where consumers poll for logs) can be complex to implement performantly. This hybrid model gets the best of both.
- Push (Performance): A global, performant
tracing_subscriber::EnvFilteracts as the first gate for all log events. This is a "push" mechanism that runs before any significant processing. Its primary purpose is to discard high-volume logs (liketrace!) as early as possible, ensuring the application has near-zero logging overhead during normal operation or in release builds. - Pull (Flexibility): All logs that pass the global filter are processed and placed into a central, in-memory
CentralLogStore. This store acts as a temporary, circular database of recent log activity. Any consumer (like the CLI) can then access this central store and "pull" the data it needs, applying its own secondary filters for display. This allows the CLI to showinfologs while another future tool could simultaneously show onlydiagnosticslogs, all from the same data source.
-
Strict Separation of Concerns (Service vs. Consumer): The
loggingservice is a foundational, backend component, completely independent of any log consumer.- Reasoning: This is the most critical principle for long-term maintainability. By decoupling the "what" (the log data) from the "how" (the display), we can add, remove, or change consumers without ever touching the core logging pipeline.
- The Logging Service: Manages the global filter, processes incoming logs (e.g., for deduplication), and populates the
CentralLogStore. It has no knowledge of how the logs will be displayed. - Log Consumers: The
climodule is one such consumer. It reads from theCentralLogStoreand is solely responsible for presentation and user interaction. This separation means a future in-game GUI console, a network log streamer, or a file logger could be added as new consumers without altering the existing CLI.
-
Concurrency Safety & Performance: A desktop application must remain responsive at all times.
- Reasoning: A diagnostic tool must not interfere with the performance of the application it's diagnosing.
- The Command-Line Interface will run in its own dedicated thread to prevent its I/O operations (reading user input, drawing to the terminal) from blocking the main application's render loop.
- The
CentralLogStorewill be shared between threads safely using anArc<Mutex<...>>. Consumers will be designed to lock the mutex, clone the data they need, and unlock it as quickly as possible to minimize contention. - Communication from the CLI thread to the main Bevy
Appwill use boundedcrossbeam-channels to prevent unbounded memory growth if the main thread is busy.
-
Graceful Degradation & Fallbacks: The diagnostic tools must be the most reliable part of the application.
- Reasoning: A debugger that crashes when the main application has a problem is useless.
- The CLI will be built with a simple, robust, text-only renderer first. The advanced, layout-driven UI will be an enhancement that can gracefully fall back to the basic renderer if its assets (e.g., layout files) fail to load or are invalid.
- The CLI thread will use a
Dropguard to ensure that, even in the event of a panic, the user's terminal is restored to a usable state.
-
Scoped Error Handling: The system's response to errors will be appropriate to the error's severity.
- Fail-Fast on Core Initialization: The application will terminate immediately if a critical, non-recoverable error occurs during core setup (e.g., failure to enter terminal raw mode). This prevents the application from running in a broken state.
- Graceful In-Loop Error Handling: Recoverable errors that occur during operation (e.g., a user typing an invalid command into the CLI) will be handled gracefully, displaying a status message to the user without crashing the application.
3. Phased Implementation Plan
Phase 0: Prerequisite Work (Already Completed)
- Objective: To establish a sane, semantic baseline for all log messages across the application, reducing noise from per-frame events.
- Actionable Steps (Completed):
- Log Level Restructuring: All logging statements in the codebase were updated to use appropriate levels (
ERROR,WARN,INFO,DEBUG,TRACE). - Specific Changes Made:
rendering_systemlogs moved totrace!,debug_shape_visibility_systemlogs moved todebug!, etc.
- Log Level Restructuring: All logging statements in the codebase were updated to use appropriate levels (
Phase 1: Project Setup & New Architecture Foundation
-
Objective: Prepare the project structure for the new architecture and add all necessary dependencies.
-
Actionable Steps:
- Update
whip_ui/Cargo.tomlwith the following dependencies.[dependencies] # ... existing dependencies crossterm = "0.27" ratatui = { version = "0.26", features = ["crossterm"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter", "reload", "registry", "fmt"] } anyhow = "1.0" crossbeam-channel = "0.5" # serde, serde_json, thiserror are already present
- Create the decoupled directory and file structure:
whip_ui/src/ ├── logging/ │ ├── mod.rs │ └── structs.rs ├── cli/ │ ├── mod.rs │ ├── plugin.rs │ ├── renderer.rs │ ├── error.rs │ ├── commands.rs │ └── session.rs // For the terminal Drop guard └── ... (other modules)
- Update
-
✅ Testable Outcome: The project compiles successfully with the new dependencies and module structure.
Phase 2: Core Logging Service & Central Store
-
Objective: Build the independent logging infrastructure, including the central in-memory log store, processing layers, and a formal configuration structure. This will replace Bevy's default
LogPlugin. -
Actionable Steps:
- Define Core Data Structures and Config in
logging/structs.rs:use bevy_ecs::prelude::Resource; use std::sync::{Arc, Mutex}; use std::collections::VecDeque; use tracing::{Level, span}; use std::time::SystemTime; use tracing_subscriber::{reload, EnvFilter, Registry}; #[derive(Resource, Clone)] pub struct LoggingConfig { pub central_store_capacity: usize, pub default_global_filter: String, } #[derive(Clone, Debug)] pub struct ProcessedLog { pub id: u64, // A unique, incrementing ID for each log pub timestamp: SystemTime, pub level: Level, pub target: String, // e.g., "whip_ui::assets::loaders" pub message: String, pub fields: serde_json::Value, // Store structured fields as a JSON Value } #[derive(Resource)] pub struct LogReloadHandle(pub reload::Handle<EnvFilter, Registry>); #[derive(Resource, Clone)] pub struct CentralLogStore(pub Arc<Mutex<VecDeque<ProcessedLog>>>);
- Create the Processing & Storage Layers: These will be new
tracing::Layerimplementations within theloggingmodule.DeduplicationLayer: Atracing::Layerthat performs hash-based deduplication. It will store aHashMap<u64, Instant>to track the last time a log hash was seen. It will be configurable to only drop events if the same hash appeared within a specifiedDuration.StoreLayer: Atracing::Layerthat:
a. Collects all metadata from atracing::Eventand itsspan.
b. Constructs aProcessedLogstruct.
c. Locks theCentralLogStore, pushes the new log, and enforces the maximum buffer size fromLoggingConfig.
- Initialize the Service in
whip_ui_example/src/main.rs:- Remove the existing
LogPlugin. - Create the
LoggingConfiginstance:let config = LoggingConfig { central_store_capacity: 10_000, default_global_filter: "info,wgpu=error".to_string() }; - Create the
CentralLogStoreusing the config. - Create the
reload_handlefor the globalEnvFilter. - Assemble the
tracingpipeline:Global EnvFilter->DeduplicationLayer->StoreLayer. - Conditionally add a console output layer:
let subscriber = Registry::default() .with(filter_layer) .with(DeduplicationLayer::new()) .with(StoreLayer::new(log_store.clone())); // Only add the console formatter in debug builds #[cfg(debug_assertions)] let subscriber = subscriber.with(tracing_subscriber::fmt::layer()); tracing::subscriber::set_global_default(subscriber).expect("Failed to set global default subscriber");
- Insert
config,LogReloadHandle(reload_handle), andlog_store.clone()as Bevy resources.
- Remove the existing
- Define Core Data Structures and Config in
-
✅ Testable Outcome: The application runs. Logs are processed and stored in the central buffer. In debug builds, logs also appear on the console. In release builds, they do not.
Phase 3: Cleanup of Obsolete Code
-
Objective: To safely remove code and components that have been made redundant by the new architecture, using unit tests to prevent regressions.
-
Actionable Steps:
- Identify Obsolete Components:
DebugRingBuffer,StateChangeTracker,gui_framework/diagnostics.rs, and feature flagsdebug_loggingandtrace_logging. - Plan for Removal:
- Delete
DebugRingBuffer: Its functionality is entirely replaced by theCentralLogStore. Delete thegui_framework/debugmodule and its plugin registration. - Refactor
StateChangeTrackerwith a Testing Safety Net:
a. Write Unit Tests: Create a new test module for theDeduplicationLayer. Write unit tests that specifically replicate the known inputs and expected outputs of the oldStateChangeTracker's hashing and comparison logic.
b. Implement and Verify: Implement the time-windowed deduplication logic in theDeduplicationLayer. Ensure all new unit tests pass, proving the new implementation is a correct replacement.
c. Delete: Once verified, delete theStateChangeTrackerstruct and its associated systems. - Delete
gui_framework/diagnostics.rs: TheUiDiagnosticsPluginand its related systems are fully superseded by thetracing-based approach planned in Phase 6. Delete the file and remove its registration. - Remove Feature Flags: Clean up
Cargo.tomland the codebase by removing thedebug_loggingandtrace_loggingfeature flags and any associated#[cfg(...)]attributes.
- Delete
- Identify Obsolete Components:
-
✅ Testable Outcome: The application compiles and runs correctly. The unit tests for the
DeduplicationLayerpass, confirming the refactor was successful. The old components are gone.
Phase 4: Foundational CLI (Text-Mode UI)
-
Objective: To create a fully functional, interactive CLI with a simple, text-based renderer, built on a formal
Renderertrait to ensure future extensibility. This phase delivers the Minimum Viable Product. -
Actionable Steps:
- Define the
TerminalRendererTrait incli/renderer.rs: This creates a formal contract for any UI renderer.// Represents all state needed for the renderer to draw a single frame. pub struct CliFrameState<'a> { pub logs: &'a [ProcessedLog], pub input_buffer: &'a str, pub status_message: &'a str, // ... other state like scroll position, filter text, etc. } // The formal contract for any CLI renderer. pub trait TerminalRenderer { fn draw(&mut self, state: &CliFrameState) -> anyhow::Result<()>; fn resize(&mut self, new_width: u16, new_height: u16); }
- Implement a
BasicTerminalRendererincli/renderer.rs: This simple renderer will implement theTerminalRenderertrait. It will usecrosstermdirectly for line-by-line output and will not depend on Taffy or asset loading. - Define Commands in
cli/commands.rs: Create theCliCommandenum and aCommandParserthat handles the/command <subcommand> <value>syntax. - Implement
TerminalSessionGuard incli/session.rs: Create the struct with aDropimplementation to guaranteecrossterm::terminal::disable_raw_mode()is always called, preventing a broken terminal on panic or exit. - Implement
CliPluginandcli_main_loopincli/plugin.rs:- The
CliPlugin'sbuildfunction will:
a. Create a boundedcrossbeam-channel(e.g.,bounded(32)) forCliCommands.
b. Insert aCliCommandReceiverresource into the BevyApp.
c. Spawn thecli_main_loopthread, passing it theCentralLogStoreand the command sender. - The
cli_main_loopwill:
a. Instantiate theTerminalSessionguard.
b. Create an instance of theBasicTerminalRenderer.
c. Loop:
i. Handle user input and parse commands using theCommandParser. Send commands intended for Bevy over the channel.
ii. Lock theCentralLogStore, clone log data, and immediately unlock.
iii. Apply its local display filter to the cloned data.
iv. Create aCliFrameStateand pass it torenderer.draw().
- The
- Implement
handle_cli_commands_system: This Bevy system checks theCliCommandReceiverchannel for commands (likeExitAppor changing the global log filter) and executes them.
- Define the
-
✅ Testable Outcome: A text-only CLI appears immediately on launch. Logs are visible and scrollable. Commands like
/level debugand/exitare functional. The application exits cleanly, restoring the terminal to a usable state.
Phase 5: Advanced UI Integration (ratatui + Taffy)
-
Objective: To replace the basic text-mode renderer with a sophisticated renderer that uses Taffy for layout computation and
ratatuifor rendering TUI widgets, with robust error handling for asset loading. -
Actionable Steps:
- Implement the
RatatuiTaffyRendererincli/renderer.rs:- This new renderer will implement the
TerminalRenderertrait. - It will be responsible for interpreting a
UiDefinition, using Taffy to compute a layout, and then usingratatuito draw the UI withcrossterm.
- This new renderer will implement the
- Update
CliPluginincli/plugin.rs:- Modify the plugin to create a second bounded
crossbeam-channelfor sending the loaded layout from Bevy to the CLI thread. This channel will transport aResult<UiDefinition, String>. - The plugin will now use the
AssetServerto request the loading ofassets/cli/layout/main.json.
- Modify the plugin to create a second bounded
- Update
assets/systems.rs(ui_asset_loaded_system):- Implement logic to detect when the CLI's layout asset has finished loading.
- On success, send
Ok(cloned_definition)over the layout channel. - On failure (e.g., asset not found), send
Err("Failed to load layout asset: ...".to_string()).
- Modify
cli_main_loop:- The loop will now start by performing a non-blocking check on the layout channel.
- If it receives
Ok(definition), it will replace itsBasicTerminalRendererwith a newRatatuiTaffyRendererinitialized with the definition. - If it receives
Err(message), it will display the error in its status bar and continue to use theBasicTerminalRenderer, ensuring the CLI remains functional even if the layout is broken.
- Implement the
-
✅ Testable Outcome: The CLI now renders using the JSON-defined layout, drawn by
ratatui. If the layout fails to load, the CLI gracefully falls back to the basic text-mode UI and displays an error message.
Phase 6: Diagnostics Integration
-
Objective: To integrate Bevy and Vulkan diagnostic information into the logging pipeline, demonstrating the system's extensibility.
-
Actionable Steps:
- Create
diagnostics/mod.rsanddiagnostics/systems.rs: - Define Diagnostic Emitters: Create functions that emit structured
tracing::event!s with a specific target like"diagnostics::frame_time".// Example emitter tracing::event!( target: "diagnostics::frame_time", Level::INFO, fps = 1.0 / time.delta_seconds_f64(), delta_ms = time.delta_seconds_f32() * 1000.0 );
- Create Bevy Systems: Create systems that gather data (e.g., from
bevy_diagnostic::Diagnosticsorbevy::time::Time) and call the emitters. - Add Systems to App: Register these new diagnostic systems in the
WhipUiPlugin. - Update CLI: The CLI can now filter for these diagnostics using its local display filter (e.g., show only logs where
targetstarts with"diagnostics"). No changes are needed to the core logging service.
- Create
-
✅ Testable Outcome: Frame rate and other diagnostic data appear as structured logs in the CLI when the appropriate display filter is set.
Phase 7: Final Validation and Documentation
-
Objective: Verify the entire system's correctness, performance, and usability, and provide clear documentation for future developers.
-
Actionable Steps:
- [ ] Functional Validation: Confirm all CLI commands work. Confirm the two-level filtering works as intended. Confirm the application exits cleanly and restores the terminal state. Confirm the unit tests for the
DeduplicationLayerare in place and passing. - [ ] Performance Validation: Manually verify that the application's main render loop remains responsive even when the CLI is actively processing and displaying a large number of logs.
- [ ] Documentation (
LOGGING_GUIDE.md): Create a comprehensive guide documenting the hybrid push/pull architecture, the two levels of filtering, CLI usage, the command system, and the service-consumer model.
- [ ] Functional Validation: Confirm all CLI commands work. Confirm the two-level filtering works as intended. Confirm the application exits cleanly and restores the terminal state. Confirm the unit tests for the
-
✅ Testable Outcome: All validation checklist items are checked. The
LOGGING_GUIDE.mdis complete and committed to the repository.