diff --git a/CHANGELOG.md b/CHANGELOG.md index 496cc159..0e305d40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. +## [Unreleased] + +### Removed + +- **Perf telemetry**: Remove the obsolete `perf` Cargo feature, `--enable-perf`, `--perf-log`, and `--perf-append` flags, and high-frequency telemetry sidecars. Runtime diagnostics remain available through the existing logging flags and presets. Doctor reports and debug bundles no longer advertise perf paths; the debug-bundle manifest schema is now `claude-rs-debug-bundle/v2`. + ## [0.14.9] - 2026-10-01 [Changes][v0.14.9] ### Features diff --git a/Cargo.toml b/Cargo.toml index 4276ea64..23d508d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,9 +66,6 @@ uuid = { version = "1.23.2", features = ["v4"] } which = "8.0.2" zip = { version = "8.6.0", default-features = false, features = ["deflate", "time"] } -[features] -perf = [] - [profile.dev] debug = "line-tables-only" incremental = false diff --git a/agent-sdk/src/bridge/logger.ts b/agent-sdk/src/bridge/logger.ts index 70efb407..efbef4f7 100644 --- a/agent-sdk/src/bridge/logger.ts +++ b/agent-sdk/src/bridge/logger.ts @@ -12,7 +12,6 @@ export const LOG_TARGETS = { APP_NETWORK: "app.network", APP_INPUT: "app.input", APP_PASTE: "app.paste", - APP_PERF: "app.perf", APP_RENDER: "app.render", APP_SESSION: "app.session", APP_TOOL: "app.tool", diff --git a/docs/src/architecture.md b/docs/src/architecture.md index 363e15c3..89bae3d6 100644 --- a/docs/src/architecture.md +++ b/docs/src/architecture.md @@ -14,7 +14,7 @@ Important Rust areas: | Area | Responsibility | | --- | --- | -| `src/main.rs` | Process entrypoint, runtime setup, logging/perf setup, and exit behavior. | +| `src/main.rs` | Process entrypoint, runtime setup, logging setup, and exit behavior. | | `src/lib.rs` | CLI arguments, subcommands, and diagnostics presets. | | `src/agent/` | Bridge process resolution, NDJSON client, wire types, and bridge error handling. | | `src/app/` | App state, lifecycle, sessions, config, permissions, input, slash commands, plugins, MCP, usage, and trust. | diff --git a/docs/src/diagnostics.md b/docs/src/diagnostics.md index a75f10c8..5023e9fe 100644 --- a/docs/src/diagnostics.md +++ b/docs/src/diagnostics.md @@ -77,7 +77,7 @@ Available presets: | --- | --- | | `runtime` | Debugging general app, bridge, session, tool, permission, network, and update flow. | | `session` | Debugging session startup, permission, and command flow. | -| `render` | Debugging rendering, cache, input, paste, and perf-adjacent UI behavior. | +| `render` | Debugging rendering, cache, input, and paste behavior. | | `bridge` | Debugging Agent SDK bridge lifecycle, protocol, SDK, permission, and MCP behavior. | | `full` | Capturing the broadest diagnostic trace. | @@ -123,7 +123,7 @@ claude-rs logs --path claude-rs logs --latest ``` -`claude-rs logs` prints the runtime log directory, legacy log path, perf log directory, latest discovered log, and common follow-up commands. `--path` prints only the default runtime log directory for scripts. `--latest` prints only the latest runtime log path, falling back to the legacy shared log when no timestamped runtime log exists. +`claude-rs logs` prints the runtime log directory, legacy log path, latest discovered log, and common follow-up commands. `--path` prints only the default runtime log directory for scripts. `--latest` prints only the latest runtime log path, falling back to the legacy shared log when no timestamped runtime log exists. To inspect recent log output safely: @@ -143,6 +143,8 @@ claude-rs logs --bundle --yes Without `--yes`, an interactive terminal is prompted before the bundle is written. Use `--output ` to choose the ZIP path. +The manifest uses schema `claude-rs-debug-bundle/v2`. Diagnostics paths describe the runtime log directory and legacy log path. + The bundle includes: - `manifest.json` @@ -185,28 +187,6 @@ CLAUDE_RS_AGENT_BRIDGE_RUNTIME=/path/to/bun Release npm installs use the root package launcher to start the native platform binary and point it at the bundled bridge script. The native binary resolves the private `claude-rs-bridge-bun` executable from the installed platform package. In `doctor --json`, the runtime checks are reported as `bridge_runtime`, `bridge_runtime_version`, and `bridge_script`. -## Perf Telemetry - -Perf telemetry is a separate JSON-lines sidecar intended for high-frequency render and layout samples. It requires a binary built with the `perf` feature. - -From source: - -```bash -cargo run --features perf -- --enable-perf -cargo run --features perf -- --perf-log claude-rs-perf.log -``` - -For an already-built perf-enabled binary: - -```bash -claude-rs --enable-perf -claude-rs --perf-log claude-rs-perf.log -``` - -If the binary was not built with `--features perf`, perf flags are rejected at startup. - -When `--perf-log` is omitted, default perf telemetry uses timestamped JSON-lines files under the sibling `logs/perf/` directory. - ## Useful Issue Reports Include: diff --git a/docs/src/usage.md b/docs/src/usage.md index e280a0ee..95bb80ee 100644 --- a/docs/src/usage.md +++ b/docs/src/usage.md @@ -57,11 +57,8 @@ The installed `claude-rs --help` command exposes these options: | `--log-file ` | Write tracing diagnostics to a specific file. | | `--log-filter ` | Use explicit tracing filter directives. | | `--log-append` | Append to the active log file instead of resetting it on startup. | -| `--enable-perf` | Enable perf telemetry when the binary was built with the `perf` feature. | -| `--perf-log ` | Write high-frequency perf telemetry to a specific JSON-lines file. | -| `--perf-append` | Append to the perf log instead of truncating it. | -See [Diagnostics](diagnostics.md) before enabling verbose logs or perf telemetry. +See [Diagnostics](diagnostics.md) before enabling verbose logs. ## Core UI diff --git a/src/app/connect/mod.rs b/src/app/connect/mod.rs index 3786b377..59e5f69f 100644 --- a/src/app/connect/mod.rs +++ b/src/app/connect/mod.rs @@ -81,50 +81,6 @@ pub fn create_app(cli: &Cli) -> App { let (event_tx, event_rx) = mpsc::channel(CLIENT_EVENT_QUEUE_CAPACITY); let (file_index_event_tx, file_index_event_rx) = super::file_index::event_channel(); - let perf_path = match crate::logging::resolve_perf_path(cli) { - Ok(path) => path, - Err(err) => { - tracing::warn!( - target: crate::logging::targets::APP_PERF, - event_name = "perf_telemetry_unavailable", - message = "failed to resolve perf telemetry sidecar path", - outcome = "failure", - telemetry_channel = "perf_sidecar", - perf_schema = "claude-rs-perf/v1", - perf_append = cli.perf_append, - error = %err, - ); - None - } - }; - let perf = perf_path.as_deref().and_then(|path| { - let logger = crate::perf::PerfLogger::open(path, cli.perf_append); - if logger.is_some() { - tracing::info!( - target: crate::logging::targets::APP_PERF, - event_name = "perf_telemetry_enabled", - message = "perf telemetry sidecar enabled", - outcome = "success", - telemetry_channel = "perf_sidecar", - perf_schema = "claude-rs-perf/v1", - perf_log = %path.display(), - perf_append = cli.perf_append, - ); - } else { - tracing::warn!( - target: crate::logging::targets::APP_PERF, - event_name = "perf_telemetry_unavailable", - message = "failed to enable perf telemetry sidecar", - outcome = "failure", - telemetry_channel = "perf_sidecar", - perf_schema = "claude-rs-perf/v1", - perf_log = %path.display(), - perf_append = cli.perf_append, - ); - } - logger - }); - let loaded_settings = match settings::load_global_settings(env!("CARGO_PKG_VERSION")) { Ok(loaded) => loaded, Err(err) => { @@ -213,13 +169,10 @@ pub fn create_app(cli: &Cli) -> App { usage: super::UsageState::default(), mcp: super::McpState::default(), notifications: super::notify::NotificationManager::new(), - perf, render_cache_budget: RenderCacheBudget::default(), history_retention: HistoryRetentionPolicy::default(), history_retention_stats: HistoryRetentionStats::default(), cache_metrics: CacheMetrics::default(), - fps_ema: None, - last_frame_at: None, startup: StartupState::new( cli.bridge_script.clone(), match &cli.command { diff --git a/src/app/connect/tests.rs b/src/app/connect/tests.rs index 41dfe15a..6b1f2a1d 100644 --- a/src/app/connect/tests.rs +++ b/src/app/connect/tests.rs @@ -34,9 +34,6 @@ fn create_app_defers_file_index_and_routes_untrusted_cwd_to_trust_surface() { log_file: None, log_filter: None, log_append: false, - enable_perf: false, - perf_log: None, - perf_append: false, }; let app = super::create_app(&cli); diff --git a/src/app/events/streaming.rs b/src/app/events/streaming.rs index 033e3f30..b55e8df8 100644 --- a/src/app/events/streaming.rs +++ b/src/app/events/streaming.rs @@ -64,24 +64,14 @@ pub(super) fn append_agent_stream_text( blocks.push(new_text_block(chunk.to_owned(), source_message_uuid)); } - let split_count = split_tail_text_block(blocks); - if split_count > 0 { - crate::perf::mark_with("text_block_split_count", "count", split_count); - } - - if let Some(MessageBlock::Text(block)) = blocks.last() { - crate::perf::mark_with("text_block_active_tail_bytes", "bytes", block.text.len()); - } - let text_block_count = blocks.iter().filter(|b| matches!(b, MessageBlock::Text(..))).count(); - crate::perf::mark_with("text_block_frozen_count", "count", text_block_count.saturating_sub(1)); + split_tail_text_block(blocks); } fn new_text_block(text: String, source_message_uuid: Option<&str>) -> MessageBlock { MessageBlock::Text(TextBlock::new(text).with_source_message_uuid(source_message_uuid)) } -fn split_tail_text_block(blocks: &mut Vec) -> usize { - let mut split_count = 0usize; +fn split_tail_text_block(blocks: &mut Vec) { loop { let Some(tail_idx) = blocks.len().checked_sub(1) else { break; @@ -115,9 +105,7 @@ fn split_tail_text_block(blocks: &mut Vec) -> usize { .with_source_message_uuids(source_message_uuids.clone()), ); blocks.insert(tail_idx, completed_text_block(completed, split, source_message_uuids)); - split_count += 1; } - split_count } fn completed_text_block( diff --git a/src/app/events/tool_calls.rs b/src/app/events/tool_calls.rs index a99d083b..c73f9fcf 100644 --- a/src/app/events/tool_calls.rs +++ b/src/app/events/tool_calls.rs @@ -220,8 +220,6 @@ fn update_existing_tool_call(app: &mut App, mi: usize, bi: usize, tool_info: &To if changed { existing.invalidate_render_cache(); layout_dirty = true; - } else { - crate::perf::mark("tool_update_noop_skips"); } } if layout_dirty { diff --git a/src/app/events/tool_updates.rs b/src/app/events/tool_updates.rs index 8919ca5f..e3a87298 100644 --- a/src/app/events/tool_updates.rs +++ b/src/app/events/tool_updates.rs @@ -123,8 +123,6 @@ fn apply_tool_call_update_to_indexed_block( tc.invalidate_render_cache(); app.sync_render_cache_slot(mi, bi); out.layout_dirty_idx = Some(mi); - } else { - crate::perf::mark("tool_update_noop_skips"); } } diff --git a/src/app/mod.rs b/src/app/mod.rs index 1be06554..43b59634 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -308,20 +308,7 @@ async fn run_tui_loop( events::reconcile_terminal_size(app, width, height); terminal_runtime.apply_surface_rebuilds(app)?; if app.surface_dirty.active_surface_needs_draw(app.terminal_lifecycle) { - if let Some(ref mut perf) = app.perf { - perf.next_frame(); - } - if app.perf.is_some() { - app.mark_frame_presented(Instant::now()); - } - #[allow(clippy::drop_non_drop)] - { - let timer = app.perf.as_ref().map(|p| p.start("frame_total")); - let draw_timer = app.perf.as_ref().map(|p| p.start("frame::terminal_draw")); - terminal_runtime.draw_active_surface(app)?; - drop(draw_timer); - drop(timer); - } + terminal_runtime.draw_active_surface(app)?; } if app.shutdown_requested() { diff --git a/src/app/state/app.rs b/src/app/state/app.rs index 1335d260..fd3a7e0e 100644 --- a/src/app/state/app.rs +++ b/src/app/state/app.rs @@ -97,10 +97,6 @@ pub struct App { /// Central notification manager (bell + desktop toast when unfocused). pub(crate) notifications: notify::NotificationManager, - /// Performance logger. Present only when built with `--features perf`. - /// Taken out (`Option::take`) during render, used, then put back to avoid - /// borrow conflicts with `&mut App`. - pub(crate) perf: Option, /// Global in-memory budget for rendered block and message caches. pub(crate) render_cache_budget: RenderCacheBudget, /// Byte budget for source conversation history retained in memory. @@ -109,10 +105,6 @@ pub struct App { pub(crate) history_retention_stats: HistoryRetentionStats, /// Cross-cutting cache metrics accumulator (enforcement counts, watermarks, rate limits). pub(crate) cache_metrics: CacheMetrics, - /// Smoothed frames-per-second (EMA of presented frame cadence). - pub(crate) fps_ema: Option, - /// Timestamp of the previous presented frame. - pub(crate) last_frame_at: Option, /// Bootstrap sequencing state resolved from CLI flags at launch. pub(crate) startup: StartupState, /// Owned bridge-process task and its explicit shutdown signal. @@ -274,13 +266,10 @@ impl App { usage: UsageState::default(), mcp: McpState::default(), notifications: notify::NotificationManager::new(), - perf: None, render_cache_budget: RenderCacheBudget::default(), history_retention: HistoryRetentionPolicy::default(), history_retention_stats: HistoryRetentionStats::default(), cache_metrics: CacheMetrics::default(), - fps_ema: None, - last_frame_at: None, startup: StartupState::default(), bridge_task: None, } diff --git a/src/app/state/history_retention.rs b/src/app/state/history_retention.rs index 40ef9faa..b6fc8e42 100644 --- a/src/app/state/history_retention.rs +++ b/src/app/state/history_retention.rs @@ -581,12 +581,6 @@ impl super::App { stats.total_dropped_messages = self.history_retention_stats.total_dropped_messages; stats.total_dropped_bytes = self.history_retention_stats.total_dropped_bytes; - crate::perf::mark_with("history::bytes_before", "bytes", stats.total_before_bytes); - crate::perf::mark_with("history::bytes_after", "bytes", stats.total_after_bytes); - crate::perf::mark_with("history::dropped_messages", "count", stats.dropped_messages); - crate::perf::mark_with("history::dropped_bytes", "bytes", stats.dropped_bytes); - crate::perf::mark_with("history::total_dropped", "count", stats.total_dropped_messages); - stats } diff --git a/src/app/state/repaint.rs b/src/app/state/repaint.rs index 95233e1f..174dbc4b 100644 --- a/src/app/state/repaint.rs +++ b/src/app/state/repaint.rs @@ -50,27 +50,6 @@ impl App { } } - /// Mark one presented frame at `now`, updating smoothed FPS. - pub fn mark_frame_presented(&mut self, now: Instant) { - let Some(prev) = self.last_frame_at.replace(now) else { - return; - }; - let dt = now.saturating_duration_since(prev).as_secs_f32(); - if dt <= f32::EPSILON { - return; - } - let fps = (1.0 / dt).clamp(0.0, 240.0); - self.fps_ema = Some(match self.fps_ema { - Some(current) => current * 0.9 + fps * 0.1, - None => fps, - }); - } - - #[must_use] - pub fn frame_fps(&self) -> Option { - self.fps_ema - } - pub fn invalidate_layout(&mut self, _level: LayoutInvalidation) { self.chat_render.clear_measurements(); self.chat_render.invalidate_live_anchor(); diff --git a/src/app/state/tool_call_info.rs b/src/app/state/tool_call_info.rs index 34ed8c03..5f9fc837 100644 --- a/src/app/state/tool_call_info.rs +++ b/src/app/state/tool_call_info.rs @@ -157,9 +157,7 @@ impl ToolCallInfo { /// Invalidate cached rendered lines for this tool call. pub fn invalidate_render_cache(&mut self) { - crate::perf::mark("tc_invalidations_requested"); self.cache.invalidate(); - crate::perf::mark("tc_invalidations_applied"); } pub fn set_raw_input(&mut self, raw_input: Option) -> bool { diff --git a/src/cli/config.rs b/src/cli/config.rs index d83cea8b..2ea15e91 100644 --- a/src/cli/config.rs +++ b/src/cli/config.rs @@ -824,9 +824,6 @@ mod tests { log_file: None, log_filter: None, log_append: false, - enable_perf: false, - perf_log: None, - perf_append: false, } } } diff --git a/src/cli/doctor.rs b/src/cli/doctor.rs index ef5d65b1..a06f03ac 100644 --- a/src/cli/doctor.rs +++ b/src/cli/doctor.rs @@ -289,7 +289,7 @@ impl DoctorSection { "config_settings" | "config_local_settings" | "config_preferences" | "config_paths" => { Self::Configuration } - "runtime_log_dir" | "legacy_log_path" | "perf_log_dir" => Self::Logs, + "runtime_log_dir" | "legacy_log_path" => Self::Logs, "npm_root_package" | "npm_platform_package" => Self::Npm, "claude_credentials" => Self::Credentials, _ => Self::Overview, @@ -560,12 +560,6 @@ fn log_path_checks() -> Vec { crate::logging::default_legacy_log_path(), false, ), - result_path_check( - "perf_log_dir", - "Perf log directory", - crate::logging::default_perf_log_dir(), - false, - ), ] } diff --git a/src/cli/logs.rs b/src/cli/logs.rs index 5d1cafc9..9150c2b2 100644 --- a/src/cli/logs.rs +++ b/src/cli/logs.rs @@ -16,7 +16,7 @@ use zip::CompressionMethod; use zip::ZipWriter; use zip::write::SimpleFileOptions; -const BUNDLE_SCHEMA: &str = "claude-rs-debug-bundle/v1"; +const BUNDLE_SCHEMA: &str = "claude-rs-debug-bundle/v2"; const BUNDLE_RUNTIME_LOG_LIMIT: usize = 5; pub fn run( @@ -68,7 +68,6 @@ fn write_summary( writeln!(stdout, "{}", style.heading("Locations"))?; write_path_row(stdout, style, "Runtime logs", &paths.runtime_dir)?; write_path_row(stdout, style, "Legacy log", &paths.legacy_log_path)?; - write_path_row(stdout, style, "Perf telemetry", &paths.perf_dir)?; writeln!(stdout)?; writeln!(stdout, "{}", style.heading("Latest"))?; @@ -287,7 +286,6 @@ struct BundleManifest { diagnostics_root: String, runtime_log_dir: String, legacy_log_path: String, - perf_log_dir: String, included_files: Vec, skipped: Vec<&'static str>, redaction: &'static str, @@ -388,7 +386,6 @@ fn write_bundle_zip( diagnostics_root: paths.root_dir.display().to_string(), runtime_log_dir: paths.runtime_dir.display().to_string(), legacy_log_path: paths.legacy_log_path.display().to_string(), - perf_log_dir: paths.perf_dir.display().to_string(), included_files, skipped: vec![ "full config files", @@ -413,7 +410,6 @@ fn write_bundle_zip( "diagnostics_root": paths.root_dir.display().to_string(), "runtime_log_dir": paths.runtime_dir.display().to_string(), "legacy_log_path": paths.legacy_log_path.display().to_string(), - "perf_log_dir": paths.perf_dir.display().to_string(), "config_files": "excluded", }), )?; @@ -588,16 +584,13 @@ mod tests { let dir = tempdir().expect("tempdir"); let root = dir.path().join("logs"); let runtime_dir = root.join("runtime"); - let perf_dir = root.join("perf"); fs::create_dir_all(&runtime_dir).expect("runtime dir"); - fs::create_dir_all(&perf_dir).expect("perf dir"); fs::write(runtime_dir.join("claude-rs-20260614T075924Z-p1-rabc.log"), "runtime") .expect("runtime log"); let paths = crate::logging::DiagnosticsPaths { root_dir: root.clone(), runtime_dir, legacy_log_path: root.join("claude-rs.log"), - perf_dir, }; let mut stdout = Vec::new(); @@ -610,6 +603,7 @@ mod tests { assert!(output.contains("Locations")); assert!(output.contains("[DIR]")); assert!(output.contains("[MISS] Legacy log")); + assert!(!output.contains("Perf telemetry")); assert!(output.contains("Latest")); assert!(output.contains("[FOUND] Latest log")); assert!(output.contains("Commands")); @@ -698,7 +692,6 @@ mod tests { let dir = tempdir().expect("tempdir"); let root = dir.path().join("logs"); let runtime_dir = root.join("runtime"); - let perf_dir = root.join("perf"); fs::create_dir_all(&runtime_dir).expect("runtime dir"); let runtime_log = runtime_dir.join("claude-rs-20260614T075924Z-p1-rabc.log"); fs::write( @@ -709,12 +702,8 @@ mod tests { let legacy_log_path = root.join("claude-rs.log"); fs::write(&legacy_log_path, "accessToken=secret-token").expect("write legacy log"); let output_path = dir.path().join("bundle.zip"); - let paths = crate::logging::DiagnosticsPaths { - root_dir: root, - runtime_dir, - legacy_log_path, - perf_dir, - }; + let paths = + crate::logging::DiagnosticsPaths { root_dir: root, runtime_dir, legacy_log_path }; let plan = BundlePlan { output_path: output_path.clone(), runtime_logs: vec![runtime_log], @@ -734,6 +723,29 @@ mod tests { assert!(names.contains(&"paths.json".to_owned())); assert!(names.contains(&"logs/bridge-diagnostics.jsonl".to_owned())); + for name in ["manifest.json", "paths.json"] { + let json: serde_json::Value = + serde_json::from_reader(archive.by_name(name).expect("JSON entry")) + .expect("bundle JSON"); + assert!(json.get("perf_log_dir").is_none(), "{name} contains removed perf path"); + assert_eq!(json["diagnostics_root"], paths.root_dir.display().to_string()); + assert_eq!(json["runtime_log_dir"], paths.runtime_dir.display().to_string()); + assert_eq!(json["legacy_log_path"], paths.legacy_log_path.display().to_string()); + if name == "manifest.json" { + assert_eq!(json["schema"], "claude-rs-debug-bundle/v2"); + } + } + let doctor: serde_json::Value = + serde_json::from_reader(archive.by_name("doctor.json").expect("doctor entry")) + .expect("doctor JSON"); + assert!( + doctor["checks"] + .as_array() + .expect("doctor checks") + .iter() + .all(|check| check["id"] != "perf_log_dir") + ); + let mut bridge = String::new(); archive .by_name("logs/bridge-diagnostics.jsonl") @@ -749,7 +761,6 @@ mod tests { let dir = tempdir().expect("tempdir"); let root = dir.path().join("logs"); let runtime_dir = root.join("runtime"); - let perf_dir = root.join("perf"); fs::create_dir_all(&runtime_dir).expect("runtime dir"); fs::create_dir_all(&root).expect("root dir"); let crash_path = root.join(crate::failure::LAST_CRASH_FILE_NAME); @@ -763,7 +774,6 @@ mod tests { root_dir: root, runtime_dir, legacy_log_path: dir.path().join("claude-rs.log"), - perf_dir, }; let plan = BundlePlan { output_path: output_path.clone(), @@ -804,9 +814,6 @@ mod tests { log_file: None, log_filter: None, log_append: false, - enable_perf: false, - perf_log: None, - perf_append: false, } } } diff --git a/src/cli/mod.rs b/src/cli/mod.rs index d2bedfb0..588208a9 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -65,6 +65,12 @@ mod tests { assert_eq!(result, Some(0)); assert!(!stdout.is_empty()); assert!(stderr.is_empty()); + let report: serde_json::Value = serde_json::from_slice(&stdout).expect("doctor JSON"); + let checks = report["checks"].as_array().expect("doctor checks"); + for id in ["runtime_log_dir", "legacy_log_path"] { + assert!(checks.iter().any(|check| check["id"] == id), "missing {id}"); + } + assert!(checks.iter().all(|check| check["id"] != "perf_log_dir")); } #[test] @@ -113,9 +119,6 @@ mod tests { log_file: None, log_filter: None, log_append: false, - enable_perf: false, - perf_log: None, - perf_append: false, } } } diff --git a/src/lib.rs b/src/lib.rs index b9fc7a5b..960cd2e4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,7 +9,6 @@ pub mod error; pub mod failure; pub mod install_method; pub mod logging; -pub mod perf; pub mod ui; use clap::{Args, Parser, Subcommand, ValueEnum}; @@ -33,14 +32,12 @@ impl DiagnosticsPreset { Self::Session => { "info,bridge.lifecycle=debug,bridge.protocol=debug,app.session=debug,app.permission=debug,app.command=debug" } - Self::Render => { - "info,app.render=trace,app.cache=debug,app.input=debug,app.paste=debug,app.perf=info" - } + Self::Render => "info,app.render=trace,app.cache=debug,app.input=debug,app.paste=debug", Self::Bridge => { "info,bridge.lifecycle=debug,bridge.protocol=debug,bridge.sdk=debug,bridge.permission=debug,bridge.mcp=debug" } Self::Full => { - "info,app.render=trace,app.perf=info,bridge.lifecycle=debug,bridge.protocol=debug,bridge.sdk=debug,bridge.permission=debug,bridge.mcp=debug,app.session=debug,app.tool=debug,app.command=debug,app.permission=debug,app.network=debug,app.update=debug,app.cache=debug,app.input=debug,app.paste=debug,app.config=debug,app.auth=debug,app.file_index=debug" + "info,app.render=trace,bridge.lifecycle=debug,bridge.protocol=debug,bridge.sdk=debug,bridge.permission=debug,bridge.mcp=debug,app.session=debug,app.tool=debug,app.command=debug,app.permission=debug,app.network=debug,app.update=debug,app.cache=debug,app.input=debug,app.paste=debug,app.config=debug,app.auth=debug,app.file_index=debug" } } } @@ -97,19 +94,6 @@ pub struct Cli { /// Without `--log-file`, appends to the legacy shared default log for compatibility. #[arg(long)] pub log_append: bool, - - /// Enable perf telemetry using a default sidecar path when `--perf-log` is omitted. - /// Requires a binary built with `--features perf`. - #[arg(long)] - pub enable_perf: bool, - - /// Write high-frequency perf telemetry to a sidecar JSON file (requires `--features perf` build). - #[arg(long, value_name = "PATH")] - pub perf_log: Option, - - /// Append to `--perf-log` instead of truncating on startup. - #[arg(long)] - pub perf_append: bool, } #[derive(Subcommand, Debug, PartialEq, Eq)] @@ -245,6 +229,30 @@ mod tests { assert!(Cli::try_parse_from(["claude-rs", "--resume", "abc-123"]).is_err()); } + #[test] + fn cli_rejects_removed_perf_flags() { + let cases: &[&[&str]] = &[ + &["claude-rs", "--enable-perf"], + &["claude-rs", "--perf-log", "perf.jsonl"], + &["claude-rs", "--perf-append"], + ]; + for args in cases { + let error = Cli::try_parse_from(args.iter().copied()).expect_err("removed flag"); + assert_eq!(error.kind(), clap::error::ErrorKind::UnknownArgument); + } + } + + #[test] + fn cli_help_omits_perf_flags_and_retains_runtime_logging() { + let help = Cli::command().render_long_help().to_string(); + for flag in ["--enable-perf", "--perf-log", "--perf-append"] { + assert!(!help.contains(flag), "help still advertises {flag}"); + } + for flag in ["--enable-logs", "--log-file", "--log-append", "--diagnostics-preset"] { + assert!(help.contains(flag), "runtime logging flag {flag} missing"); + } + } + #[test] fn cli_doctor_defaults_to_human_output() { let cli = Cli::try_parse_from(["claude-rs", "doctor"]).expect("parse"); diff --git a/src/logging.rs b/src/logging.rs index d8413c70..d3280a5d 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -29,7 +29,6 @@ pub mod targets { pub const APP_LIFECYCLE: &str = "app.lifecycle"; pub const APP_NETWORK: &str = "app.network"; pub const APP_PASTE: &str = "app.paste"; - pub const APP_PERF: &str = "app.perf"; pub const APP_PERMISSION: &str = "app.permission"; pub const APP_RENDER: &str = "app.render"; pub const APP_SESSION: &str = "app.session"; @@ -48,11 +47,8 @@ const BRIDGE_LINE_PREVIEW_LIMIT: usize = 240; const DEFAULT_LOG_DIR: &str = "claude-code-rust"; const DEFAULT_LOG_FILE_NAME: &str = "claude-rs.log"; const DEFAULT_RUNTIME_LOG_SUBDIR: &str = "runtime"; -const DEFAULT_PERF_LOG_SUBDIR: &str = "perf"; const DEFAULT_RUNTIME_LOG_PREFIX: &str = "claude-rs"; -const DEFAULT_PERF_LOG_PREFIX: &str = "claude-rs-perf"; const RUNTIME_LOG_EXTENSION: &str = "log"; -const PERF_LOG_EXTENSION: &str = "jsonl"; const LOG_ROTATION_MAX_BYTES: u64 = 10 * 1024 * 1024; const LOG_ROTATION_MAX_FILES: usize = 5; const LOG_RETENTION_MAX_BYTES: u64 = 256 * 1024 * 1024; @@ -67,7 +63,6 @@ pub struct DiagnosticsPaths { pub root_dir: PathBuf, pub runtime_dir: PathBuf, pub legacy_log_path: PathBuf, - pub perf_dir: PathBuf, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -433,34 +428,15 @@ pub fn default_legacy_log_path() -> anyhow::Result { Ok(base_dir.join(DEFAULT_LOG_FILE_NAME)) } -pub fn resolve_perf_path(cli: &Cli) -> anyhow::Result> { - if let Some(path) = cli.perf_log.clone() { - return Ok(Some(path)); - } - if !perf_enabled_without_explicit_path(cli) { - return Ok(None); - } - Ok(Some(generated_log_path( - &default_perf_log_dir()?, - DEFAULT_PERF_LOG_PREFIX, - PERF_LOG_EXTENSION, - ))) -} - pub fn default_runtime_log_dir() -> anyhow::Result { Ok(default_diagnostics_dir()?.join(DEFAULT_RUNTIME_LOG_SUBDIR)) } -pub fn default_perf_log_dir() -> anyhow::Result { - Ok(default_diagnostics_dir()?.join(DEFAULT_PERF_LOG_SUBDIR)) -} - pub fn default_diagnostics_paths() -> anyhow::Result { let root_dir = default_diagnostics_dir()?; Ok(DiagnosticsPaths { runtime_dir: root_dir.join(DEFAULT_RUNTIME_LOG_SUBDIR), legacy_log_path: root_dir.join(DEFAULT_LOG_FILE_NAME), - perf_dir: root_dir.join(DEFAULT_PERF_LOG_SUBDIR), root_dir, }) } @@ -546,10 +522,6 @@ fn short_run_id() -> String { uuid::Uuid::new_v4().simple().to_string().chars().take(8).collect() } -fn perf_enabled_without_explicit_path(cli: &Cli) -> bool { - cli.enable_perf || cli.perf_append -} - pub fn default_diagnostics_dir() -> anyhow::Result { if let Some(dir) = dirs::data_local_dir() { return Ok(dir.join(DEFAULT_LOG_DIR).join("logs")); @@ -1010,7 +982,6 @@ impl BridgeDiagnosticRecord { targets::APP_INPUT => emit_for_level!(targets::APP_INPUT), targets::APP_PERMISSION => emit_for_level!(targets::APP_PERMISSION), targets::APP_PASTE => emit_for_level!(targets::APP_PASTE), - targets::APP_PERF => emit_for_level!(targets::APP_PERF), targets::APP_RENDER => emit_for_level!(targets::APP_RENDER), targets::APP_SESSION => emit_for_level!(targets::APP_SESSION), targets::APP_TOOL => emit_for_level!(targets::APP_TOOL), @@ -1033,7 +1004,7 @@ mod tests { LogRetentionPolicy, LogRetentionTarget, RollingFileWriter, baseline_field_allowed, clear_rotated_files, enforce_log_retention_at, generated_log_file_name, is_managed_log_file, list_managed_runtime_logs_in, preview_text, resolve_log_path, - resolve_perf_path, rotated_log_path, + rotated_log_path, }; use crate::{Cli, DiagnosticsPreset}; use std::fs; @@ -1166,9 +1137,6 @@ mod tests { log_file: None, log_filter: None, log_append: false, - enable_perf: false, - perf_log: None, - perf_append: false, }; let resolved = resolve_log_path(&cli).expect("resolve succeeds").expect("path exists"); @@ -1189,9 +1157,6 @@ mod tests { log_file: Some(PathBuf::from("custom.log")), log_filter: None, log_append: false, - enable_perf: false, - perf_log: None, - perf_append: false, }; let resolved = resolve_log_path(&cli).expect("resolve succeeds").expect("path exists"); @@ -1213,9 +1178,6 @@ mod tests { log_file: None, log_filter: Some("app.render=trace".to_owned()), log_append: false, - enable_perf: false, - perf_log: None, - perf_append: false, }; let resolved = resolve_log_path(&cli).expect("resolve succeeds").expect("path exists"); @@ -1246,9 +1208,6 @@ mod tests { log_file: None, log_filter: None, log_append: false, - enable_perf: false, - perf_log: None, - perf_append: false, }; let resolved = resolve_log_path(&cli).expect("resolve succeeds").expect("path exists"); @@ -1268,9 +1227,6 @@ mod tests { log_file: None, log_filter: None, log_append: false, - enable_perf: false, - perf_log: None, - perf_append: false, }; let resolved = resolve_log_path(&cli).expect("resolve succeeds").expect("path exists"); @@ -1290,9 +1246,6 @@ mod tests { log_file: None, log_filter: None, log_append: true, - enable_perf: false, - perf_log: None, - perf_append: false, }; let resolved = resolve_log_path(&cli).expect("resolve succeeds").expect("path exists"); @@ -1303,34 +1256,6 @@ mod tests { assert!(path.ends_with("claude-code-rust/logs/claude-rs.log")); } - #[test] - fn resolve_perf_path_uses_default_when_enable_perf_is_set() { - let cli = Cli { - command: None, - no_update_check: false, - dir: None, - bridge_script: None, - enable_logs: false, - diagnostics_preset: None, - log_file: None, - log_filter: None, - log_append: false, - enable_perf: true, - perf_log: None, - perf_append: false, - }; - - let resolved = resolve_perf_path(&cli).expect("resolve succeeds").expect("path exists"); - let path = resolved.to_string_lossy().replace('\\', "/"); - assert!(path.contains("claude-code-rust/logs/perf/claude-rs-perf-")); - assert!( - resolved - .extension() - .and_then(|extension| extension.to_str()) - .is_some_and(|extension| extension.eq_ignore_ascii_case("jsonl")) - ); - } - #[test] fn generated_default_log_name_is_timestamped_and_filesystem_safe() { let name = generated_log_file_name( diff --git a/src/main.rs b/src/main.rs index f6018ad0..a233dff6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -57,15 +57,6 @@ fn run() -> anyhow::Result { } let _logging = claude_code_rust::logging::LoggingRuntime::init(&cli)?; - let perf_path = claude_code_rust::logging::resolve_perf_path(&cli)?; - - #[cfg(not(feature = "perf"))] - if perf_path.is_some() { - return Err(anyhow::anyhow!( - "perf telemetry requires a binary built with `--features perf`" - )); - } - { let startup_bootstrap_span = info_span!( target: claude_code_rust::logging::targets::APP_LIFECYCLE, @@ -74,7 +65,6 @@ fn run() -> anyhow::Result { cli.command, Some(claude_code_rust::Command::Resume { .. }) ), - perf_telemetry_requested = perf_path.is_some(), explicit_bridge_script = cli.bridge_script.is_some(), ); let _entered = startup_bootstrap_span.enter(); diff --git a/src/perf.rs b/src/perf.rs deleted file mode 100644 index 98a57f75..00000000 --- a/src/perf.rs +++ /dev/null @@ -1,342 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright 2025 Simon Peter Rothgang - -//! Explicit high-frequency performance telemetry sidecar. -//! -//! This module is intentionally separate from the main structured runtime logs. -//! Use it only for hot-path timings and counters where writing every sample into -//! the normal operational log stream would create unacceptable noise. -//! -//! What belongs here: -//! -//! - render-frame timing -//! - layout/cache timing and counters -//! - terminal/render hot-path counters -//! - other explicit perf-mode samples -//! -//! What does not belong here: -//! -//! - session or bridge lifecycle -//! - tool, permission, or auth lifecycle -//! - user-facing state changes -//! - raw payloads or content previews -//! -//! Gated behind `--features perf`. When the feature is disabled, all types -//! become zero-size and all methods are no-ops that the compiler eliminates. -//! -//! # Usage -//! -//! ```bash -//! cargo run --features perf -- --perf-log performance.log -//! # Writes JSON lines: -//! # {"schema":"claude-rs-perf/v1","kind":"duration","run_id":"...","frame":1234,"ts_ms":1739599900793,"metric":"chat::render","duration_ms":2.345,"extra":{"key":"msgs","value":42}} -//! ``` - -#[cfg(feature = "perf")] -mod enabled { - use serde::Serialize; - use std::cell::RefCell; - use std::fs::{File, OpenOptions}; - use std::io::{BufWriter, Write}; - use std::path::Path; - use std::time::{Instant, SystemTime, UNIX_EPOCH}; - - const PERF_SCHEMA: &str = "claude-rs-perf/v1"; - - // Thread-local file handle so Timer::drop can log without borrowing PerfLogger. - thread_local! { - pub(crate) static LOG_FILE: RefCell>> = const { RefCell::new(None) }; - static FRAME_COUNTER: RefCell = const { RefCell::new(0) }; - static RUN_ID: RefCell = const { RefCell::new(String::new()) }; - } - - pub struct PerfLogger { - _private: (), - } - - #[derive(Serialize)] - struct PerfExtraField { - key: &'static str, - value: usize, - } - - #[derive(Serialize)] - struct PerfSample<'a> { - schema: &'static str, - kind: &'static str, - run_id: &'a str, - frame: u64, - ts_ms: u128, - metric: &'a str, - duration_ms: Option, - extra: Option, - } - - #[derive(Serialize)] - struct PerfRunStarted<'a> { - schema: &'static str, - kind: &'static str, - run_id: &'a str, - ts_ms: u128, - pid: u32, - version: &'a str, - append: bool, - } - - fn unix_ms() -> u128 { - SystemTime::now().duration_since(UNIX_EPOCH).map_or(0, |duration| duration.as_millis()) - } - - fn write_json_line(file: &mut BufWriter, value: &T) { - if serde_json::to_writer(&mut *file, value).is_ok() { - let _ = writeln!(file); - } - } - - pub(crate) fn write_entry(name: &'static str, ms: f64, extra: Option<(&'static str, usize)>) { - let frame = FRAME_COUNTER.with(|c| *c.borrow()); - let ts_ms = unix_ms(); - LOG_FILE.with(|f| { - if let Some(ref mut file) = *f.borrow_mut() { - RUN_ID.with(|run| { - let run_id = run.borrow(); - let sample = PerfSample { - schema: PERF_SCHEMA, - kind: if ms == 0.0 { "mark" } else { "duration" }, - run_id: run_id.as_str(), - frame, - ts_ms, - metric: name, - duration_ms: (ms != 0.0).then_some(ms), - extra: extra.map(|(key, value)| PerfExtraField { key, value }), - }; - write_json_line(file, &sample); - }); - } - }); - } - - #[allow(clippy::unused_self)] - impl PerfLogger { - /// Open (or create) the log file. Returns `None` on I/O error. - pub fn open(path: &Path, append: bool) -> Option { - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent).ok()?; - } - let mut options = OpenOptions::new(); - options.create(true).write(true); - if append { - options.append(true); - } else { - options.truncate(true); - } - let file = options.open(path).ok()?; - let mut writer = BufWriter::new(file); - let run_id = uuid::Uuid::new_v4().to_string(); - let ts_ms = unix_ms(); - let started = PerfRunStarted { - schema: PERF_SCHEMA, - kind: "run_started", - run_id: run_id.as_str(), - ts_ms, - pid: std::process::id(), - version: env!("CARGO_PKG_VERSION"), - append, - }; - write_json_line(&mut writer, &started); - let _ = writer.flush(); - LOG_FILE.with(|f| *f.borrow_mut() = Some(writer)); - RUN_ID.with(|r| *r.borrow_mut() = run_id); - FRAME_COUNTER.with(|c| *c.borrow_mut() = 0); - Some(Self { _private: () }) - } - - /// Increment the frame counter. Call once at the start of each render frame. - pub fn next_frame(&mut self) { - let frame = FRAME_COUNTER.with(|c| { - let mut value = c.borrow_mut(); - *value += 1; - *value - }); - if frame.is_multiple_of(240) { - LOG_FILE.with(|f| { - if let Some(ref mut file) = *f.borrow_mut() { - let _ = file.flush(); - } - }); - } - } - - /// Start a named timer. Logs duration on drop. - #[must_use] - pub fn start(&self, name: &'static str) -> Timer { - Timer { name, start: Instant::now(), extra: None } - } - - /// Start a named timer with an extra numeric field (e.g. message count). - #[must_use] - pub fn start_with( - &self, - name: &'static str, - extra_name: &'static str, - extra_val: usize, - ) -> Timer { - Timer { name, start: Instant::now(), extra: Some((extra_name, extra_val)) } - } - - /// Log an instant marker for the current frame (`ms = 0`). - pub fn mark(&self, name: &'static str) { - write_entry(name, 0.0, None); - } - - /// Log an instant marker with an extra numeric field (`ms = 0`). - pub fn mark_with(&self, name: &'static str, extra_name: &'static str, extra_val: usize) { - write_entry(name, 0.0, Some((extra_name, extra_val))); - } - } - - pub struct Timer { - pub(crate) name: &'static str, - pub(crate) start: Instant, - pub(crate) extra: Option<(&'static str, usize)>, - } - - #[allow(clippy::unused_self)] - impl Timer { - /// Manually stop and log. Useful when you need to end timing before scope exit. - pub fn stop(self) { - // Drop impl handles logging - } - } - - impl Drop for Timer { - fn drop(&mut self) { - let ms = self.start.elapsed().as_secs_f64() * 1000.0; - write_entry(self.name, ms, self.extra); - } - } -} - -#[cfg(not(feature = "perf"))] -mod disabled { - use std::path::Path; - - pub struct PerfLogger; - pub struct Timer; - - #[allow(clippy::unused_self)] - impl PerfLogger { - #[inline] - pub fn open(_path: &Path, _append: bool) -> Option { - None - } - #[inline] - pub fn next_frame(&mut self) {} - #[inline] - #[must_use] - pub fn start(&self, _name: &'static str) -> Timer { - Timer - } - #[inline] - #[must_use] - pub fn start_with( - &self, - _name: &'static str, - _extra_name: &'static str, - _extra_val: usize, - ) -> Timer { - Timer - } - #[inline] - pub fn mark(&self, _name: &'static str) {} - #[inline] - pub fn mark_with(&self, _name: &'static str, _extra_name: &'static str, _extra_val: usize) { - } - } - - #[allow(clippy::unused_self)] - impl Timer { - #[inline] - pub fn stop(self) {} - } -} - -/// Start a timer without needing a `PerfLogger` reference. -/// Uses the thread-local log file directly. Returns `None` (and is a no-op) -/// when the `perf` feature is disabled or no logger has been opened. -#[cfg(feature = "perf")] -#[must_use] -#[inline] -pub fn start(name: &'static str) -> Option { - // Only create a timer if the log file is actually open - enabled::LOG_FILE.with(|f| { - if f.borrow().is_some() { - Some(Timer { name, start: std::time::Instant::now(), extra: None }) - } else { - None - } - }) -} - -#[cfg(feature = "perf")] -#[must_use] -#[inline] -pub fn start_with(name: &'static str, extra_name: &'static str, extra_val: usize) -> Option { - enabled::LOG_FILE.with(|f| { - if f.borrow().is_some() { - Some(Timer { - name, - start: std::time::Instant::now(), - extra: Some((extra_name, extra_val)), - }) - } else { - None - } - }) -} - -#[cfg(not(feature = "perf"))] -#[must_use] -#[inline] -pub fn start(_name: &'static str) -> Option { - None -} - -#[cfg(not(feature = "perf"))] -#[must_use] -#[inline] -pub fn start_with( - _name: &'static str, - _extra_name: &'static str, - _extra_val: usize, -) -> Option { - None -} - -/// Write an instant marker for the current frame (`ms = 0`). -#[cfg(feature = "perf")] -#[inline] -pub fn mark(name: &'static str) { - enabled::write_entry(name, 0.0, None); -} - -#[cfg(not(feature = "perf"))] -#[inline] -pub fn mark(_name: &'static str) {} - -/// Write an instant marker with one numeric field (`ms = 0`). -#[cfg(feature = "perf")] -#[inline] -pub fn mark_with(name: &'static str, extra_name: &'static str, extra_val: usize) { - enabled::write_entry(name, 0.0, Some((extra_name, extra_val))); -} - -#[cfg(not(feature = "perf"))] -#[inline] -pub fn mark_with(_name: &'static str, _extra_name: &'static str, _extra_val: usize) {} - -#[cfg(feature = "perf")] -pub use enabled::{PerfLogger, Timer}; - -#[cfg(not(feature = "perf"))] -pub use disabled::{PerfLogger, Timer}; diff --git a/src/ui/message.rs b/src/ui/message.rs index 91fe9e70..bb53d4e6 100644 --- a/src/ui/message.rs +++ b/src/ui/message.rs @@ -101,13 +101,9 @@ pub(super) fn render_text_cached( if cache.height_at(width).is_some() && let Some(cached_lines) = cache.get() { - crate::perf::mark_with("msg::cache_hit", "lines", cached_lines.len()); out.extend_from_slice(cached_lines); return; } - crate::perf::mark("msg::cache_miss"); - - let _t = crate::perf::start("msg::render_text"); let mut preprocessed = preprocess_markdown(text); if preserve_newlines { @@ -118,10 +114,7 @@ pub(super) fn render_text_cached( // Store in the full block cache with wrapped height. // For streaming messages this will be invalidated on the next chunk, // but for completed messages it persists. - let h = { - let _t = crate::perf::start_with("msg::wrap_height", "lines", fresh.len()); - wrap_markdown_lines_to_physical_rows(&fresh, width).len() - }; + let h = wrap_markdown_lines_to_physical_rows(&fresh, width).len(); cache.store(fresh); cache.set_height(h, width); if let Some(stored) = cache.get() { diff --git a/src/ui/tool_call/mod.rs b/src/ui/tool_call/mod.rs index cce07c3b..8e69a2e9 100644 --- a/src/ui/tool_call/mod.rs +++ b/src/ui/tool_call/mod.rs @@ -104,11 +104,8 @@ pub fn render_tool_call_cached( let cached_body = if body_depends_on_width { tc.cache.get_for_width(width) } else { tc.cache.get() }; if let Some(cached_body) = cached_body { - crate::perf::mark_with("tc::cache_hit_body", "lines", cached_body.len()); out.extend_from_slice(cached_body); } else { - crate::perf::mark("tc::cache_miss_body"); - let _t = crate::perf::start("tc::render_body"); let body = standard::render_tool_call_body(tc, width); if body_depends_on_width { tc.cache.store_for_width(body, width);