diff --git a/Cargo.lock b/Cargo.lock index a18539c..01a56ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2681,6 +2681,7 @@ dependencies = [ "object", "path-slash", "png", + "pollster", "ppc750cl", "rabbitizer", "reqwest", diff --git a/Cargo.toml b/Cargo.toml index 691e86a..34f8dd3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ memmap2 = "0.9.0" notify = "6.1.1" object = { version = "0.32.1", features = ["read_core", "std", "elf"], default-features = false } png = "0.17.10" +pollster = "0.3.0" ppc750cl = { git = "https://github.com/encounter/ppc750cl", rev = "4a2bbbc6f84dcb76255ab6f3595a8d4a0ce96618" } rabbitizer = "1.8.0" rfd = { version = "0.12.1" } #, default-features = false, features = ['xdg-portal'] diff --git a/src/app.rs b/src/app.rs index b0e49d7..3a5b6f9 100644 --- a/src/app.rs +++ b/src/app.rs @@ -7,7 +7,6 @@ use std::{ atomic::{AtomicBool, Ordering}, Arc, Mutex, RwLock, }, - time::Duration, }; use filetime::FileTime; @@ -29,7 +28,9 @@ use crate::{ config_ui, diff_options_window, project_window, ConfigViewState, DEFAULT_WATCH_PATTERNS, }, data_diff::data_diff_ui, + debug::debug_window, demangle::{demangle_window, DemangleViewState}, + frame_history::FrameHistory, function_diff::function_diff_ui, jobs::jobs_ui, symbol_diff::{symbol_diff_ui, DiffViewState, View}, @@ -42,10 +43,12 @@ pub struct ViewState { pub config_state: ConfigViewState, pub demangle_state: DemangleViewState, pub diff_state: DiffViewState, + pub frame_history: FrameHistory, pub show_appearance_config: bool, pub show_demangle: bool, pub show_project_config: bool, pub show_diff_options: bool, + pub show_debug: bool, } /// The configuration for a single object file. @@ -257,7 +260,7 @@ impl App { log::info!("Job {} finished", job.id); match result { JobResult::None => { - if let Some(err) = &job.status.read().unwrap().error { + if let Some(err) = &job.context.status.read().unwrap().error { log::error!("{:?}", err); } } @@ -278,12 +281,12 @@ impl App { } else { anyhow::Error::msg("Thread panicked") }; - let result = job.status.write(); + let result = job.context.status.write(); if let Ok(mut guard) = result { guard.error = Some(err); } else { drop(result); - job.status = Arc::new(RwLock::new(JobStatus { + job.context.status = Arc::new(RwLock::new(JobStatus { title: "Error".to_string(), progress_percent: 0.0, progress_items: None, @@ -298,14 +301,14 @@ impl App { jobs.clear_finished(); diff_state.pre_update(jobs, &self.config); - config_state.pre_update(jobs); + config_state.pre_update(jobs, &self.config); debug_assert!(jobs.results.is_empty()); } - fn post_update(&mut self) { + fn post_update(&mut self, ctx: &egui::Context) { let ViewState { jobs, diff_state, config_state, .. } = &mut self.view_state; - config_state.post_update(jobs, &self.config); - diff_state.post_update(jobs, &self.config); + config_state.post_update(ctx, jobs, &self.config); + diff_state.post_update(&self.config); let Ok(mut config) = self.config.write() else { return; @@ -335,7 +338,7 @@ impl App { if let Some(project_dir) = &config.project_dir { match build_globset(&config.watch_patterns).map_err(anyhow::Error::new).and_then( |globset| { - create_watcher(self.modified.clone(), project_dir, globset) + create_watcher(ctx.clone(), self.modified.clone(), project_dir, globset) .map_err(anyhow::Error::new) }, ) { @@ -374,7 +377,7 @@ impl App { // Don't clear `queue_build` if a build is running. A file may have been modified during // the build, so we'll start another build after the current one finishes. if config.queue_build && config.selected_obj.is_some() && !jobs.is_running(Job::ObjDiff) { - jobs.push(start_build(ObjDiffConfig::from_config(config))); + jobs.push(start_build(ctx, ObjDiffConfig::from_config(config))); config.queue_build = false; config.queue_reload = false; } else if config.queue_reload && !jobs.is_running(Job::ObjDiff) { @@ -382,7 +385,7 @@ impl App { // Don't build, just reload the current files diff_config.build_base = false; diff_config.build_target = false; - jobs.push(start_build(diff_config)); + jobs.push(start_build(ctx, diff_config)); config.queue_reload = false; } } @@ -404,18 +407,27 @@ impl eframe::App for App { let ViewState { jobs, - show_appearance_config, - demangle_state, - show_demangle, - diff_state, config_state, + demangle_state, + diff_state, + frame_history, + show_appearance_config, + show_demangle, show_project_config, show_diff_options, + show_debug, } = view_state; + frame_history.on_new_frame(ctx.input(|i| i.time), frame.info().cpu_usage); + egui::TopBottomPanel::top("top_panel").show(ctx, |ui| { egui::menu::bar(ui, |ui| { ui.menu_button("File", |ui| { + #[cfg(debug_assertions)] + if ui.button("Debug…").clicked() { + *show_debug = !*show_debug; + ui.close_menu(); + } if ui.button("Project…").clicked() { *show_project_config = !*show_project_config; ui.close_menu(); @@ -511,16 +523,9 @@ impl eframe::App for App { appearance_window(ctx, show_appearance_config, appearance); demangle_window(ctx, show_demangle, demangle_state, appearance); diff_options_window(ctx, config, show_diff_options, appearance); + debug_window(ctx, show_debug, frame_history, appearance); - self.post_update(); - - // Windows + request_repaint_after breaks dialogs: - // https://github.com/emilk/egui/issues/2003 - if cfg!(windows) || self.view_state.jobs.any_running() { - ctx.request_repaint(); - } else { - ctx.request_repaint_after(Duration::from_millis(100)); - } + self.post_update(ctx); } /// Called by the frame work to save state before shutdown. @@ -533,6 +538,7 @@ impl eframe::App for App { } fn create_watcher( + ctx: egui::Context, modified: Arc, project_dir: &Path, patterns: GlobSet, @@ -552,7 +558,9 @@ fn create_watcher( continue; }; if patterns.is_match(path) { + log::info!("File modified: {}", path.display()); modified.store(true, Ordering::Relaxed); + ctx.request_repaint(); } } } diff --git a/src/jobs/check_update.rs b/src/jobs/check_update.rs index 2b29b2b..89bd288 100644 --- a/src/jobs/check_update.rs +++ b/src/jobs/check_update.rs @@ -4,7 +4,7 @@ use anyhow::{Context, Result}; use self_update::{cargo_crate_version, update::Release}; use crate::{ - jobs::{start_job, update_status, Job, JobResult, JobState, JobStatusRef}, + jobs::{start_job, update_status, Job, JobContext, JobResult, JobState}, update::{build_updater, BIN_NAME}, }; @@ -14,20 +14,20 @@ pub struct CheckUpdateResult { pub found_binary: bool, } -fn run_check_update(status: &JobStatusRef, cancel: Receiver<()>) -> Result> { - update_status(status, "Fetching latest release".to_string(), 0, 1, &cancel)?; +fn run_check_update(context: &JobContext, cancel: Receiver<()>) -> Result> { + update_status(context, "Fetching latest release".to_string(), 0, 1, &cancel)?; let updater = build_updater().context("Failed to create release updater")?; let latest_release = updater.get_latest_release()?; let update_available = self_update::version::bump_is_greater(cargo_crate_version!(), &latest_release.version)?; let found_binary = latest_release.assets.iter().any(|a| a.name == BIN_NAME); - update_status(status, "Complete".to_string(), 1, 1, &cancel)?; + update_status(context, "Complete".to_string(), 1, 1, &cancel)?; Ok(Box::new(CheckUpdateResult { update_available, latest_release, found_binary })) } -pub fn start_check_update() -> JobState { - start_job("Check for updates", Job::CheckUpdate, move |status, cancel| { - run_check_update(status, cancel).map(|result| JobResult::CheckUpdate(Some(result))) +pub fn start_check_update(ctx: &egui::Context) -> JobState { + start_job(ctx, "Check for updates", Job::CheckUpdate, move |context, cancel| { + run_check_update(&context, cancel).map(|result| JobResult::CheckUpdate(Some(result))) }) } diff --git a/src/jobs/mod.rs b/src/jobs/mod.rs index 654e207..34ba02e 100644 --- a/src/jobs/mod.rs +++ b/src/jobs/mod.rs @@ -48,6 +48,7 @@ impl JobQueue { } /// Returns whether any job is running. + #[allow(dead_code)] pub fn any_running(&self) -> bool { self.jobs.iter().any(|job| { if let Some(handle) = &job.handle { @@ -81,7 +82,7 @@ impl JobQueue { self.jobs.retain(|job| { !(job.should_remove && job.handle.is_none() - && job.status.read().unwrap().error.is_none()) + && job.context.status.read().unwrap().error.is_none()) }); } @@ -89,13 +90,17 @@ impl JobQueue { pub fn remove(&mut self, id: usize) { self.jobs.retain(|job| job.id != id); } } -pub type JobStatusRef = Arc>; +#[derive(Clone)] +pub struct JobContext { + pub status: Arc>, + pub egui: egui::Context, +} pub struct JobState { pub id: usize, pub kind: Job, pub handle: Option>, - pub status: JobStatusRef, + pub context: JobContext, pub cancel: Sender<()>, pub should_remove: bool, } @@ -124,9 +129,10 @@ fn should_cancel(rx: &Receiver<()>) -> bool { } fn start_job( + ctx: &egui::Context, title: &str, kind: Job, - run: impl FnOnce(&JobStatusRef, Receiver<()>) -> Result + Send + 'static, + run: impl FnOnce(JobContext, Receiver<()>) -> Result + Send + 'static, ) -> JobState { let status = Arc::new(RwLock::new(JobStatus { title: title.to_string(), @@ -135,10 +141,11 @@ fn start_job( status: String::new(), error: None, })); - let status_clone = status.clone(); + let context = JobContext { status: status.clone(), egui: ctx.clone() }; + let context_inner = JobContext { status: status.clone(), egui: ctx.clone() }; let (tx, rx) = std::sync::mpsc::channel(); let handle = std::thread::spawn(move || { - return match run(&status, rx) { + return match run(context_inner, rx) { Ok(state) => state, Err(e) => { if let Ok(mut w) = status.write() { @@ -150,24 +157,18 @@ fn start_job( }); let id = JOB_ID.fetch_add(1, Ordering::Relaxed); log::info!("Started job {}", id); - JobState { - id, - kind, - handle: Some(handle), - status: status_clone, - cancel: tx, - should_remove: true, - } + JobState { id, kind, handle: Some(handle), context, cancel: tx, should_remove: true } } fn update_status( - status: &JobStatusRef, + context: &JobContext, str: String, count: u32, total: u32, cancel: &Receiver<()>, ) -> Result<()> { - let mut w = status.write().map_err(|_| anyhow::Error::msg("Failed to lock job status"))?; + let mut w = + context.status.write().map_err(|_| anyhow::Error::msg("Failed to lock job status"))?; w.progress_items = Some([count, total]); w.progress_percent = count as f32 / total as f32; if should_cancel(cancel) { @@ -176,5 +177,7 @@ fn update_status( } else { w.status = str; } + drop(w); + context.egui.request_repaint(); Ok(()) } diff --git a/src/jobs/objdiff.rs b/src/jobs/objdiff.rs index 9133dad..eda9051 100644 --- a/src/jobs/objdiff.rs +++ b/src/jobs/objdiff.rs @@ -11,7 +11,7 @@ use time::OffsetDateTime; use crate::{ app::{AppConfig, ObjectConfig}, diff::{diff_objs, DiffAlg, DiffObjConfig}, - jobs::{start_job, update_status, Job, JobResult, JobState, JobStatusRef}, + jobs::{start_job, update_status, Job, JobContext, JobResult, JobState}, obj::{elf, ObjInfo}, }; @@ -102,7 +102,7 @@ fn run_make(cwd: &Path, arg: &Path, config: &ObjDiffConfig) -> BuildStatus { } fn run_build( - status: &JobStatusRef, + context: &JobContext, cancel: Receiver<()>, config: ObjDiffConfig, ) -> Result> { @@ -142,7 +142,7 @@ fn run_build( let first_status = match target_path_rel { Some(target_path_rel) if config.build_target => { update_status( - status, + context, format!("Building target {}", target_path_rel.display()), 0, total, @@ -156,7 +156,7 @@ fn run_build( let second_status = match base_path_rel { Some(base_path_rel) if config.build_base => { update_status( - status, + context, format!("Building base {}", base_path_rel.display()), 0, total, @@ -173,7 +173,7 @@ fn run_build( match &obj_config.target_path { Some(target_path) if first_status.success => { update_status( - status, + context, format!("Loading target {}", target_path_rel.unwrap().display()), 2, total, @@ -189,7 +189,7 @@ fn run_build( let mut second_obj = match &obj_config.base_path { Some(base_path) if second_status.success => { update_status( - status, + context, format!("Loading base {}", base_path_rel.unwrap().display()), 3, total, @@ -203,16 +203,16 @@ fn run_build( _ => None, }; - update_status(status, "Performing diff".to_string(), 4, total, &cancel)?; + update_status(context, "Performing diff".to_string(), 4, total, &cancel)?; let diff_config = DiffObjConfig { code_alg: config.code_alg, data_alg: config.data_alg }; diff_objs(&diff_config, first_obj.as_mut(), second_obj.as_mut())?; - update_status(status, "Complete".to_string(), total, total, &cancel)?; + update_status(context, "Complete".to_string(), total, total, &cancel)?; Ok(Box::new(ObjDiffResult { first_status, second_status, first_obj, second_obj, time })) } -pub fn start_build(config: ObjDiffConfig) -> JobState { - start_job("Object diff", Job::ObjDiff, move |status, cancel| { - run_build(status, cancel, config).map(|result| JobResult::ObjDiff(Some(result))) +pub fn start_build(ctx: &egui::Context, config: ObjDiffConfig) -> JobState { + start_job(ctx, "Object diff", Job::ObjDiff, move |context, cancel| { + run_build(&context, cancel, config).map(|result| JobResult::ObjDiff(Some(result))) }) } diff --git a/src/jobs/update.rs b/src/jobs/update.rs index 3c76c17..bb72de4 100644 --- a/src/jobs/update.rs +++ b/src/jobs/update.rs @@ -9,7 +9,7 @@ use anyhow::{Context, Result}; use const_format::formatcp; use crate::{ - jobs::{start_job, update_status, Job, JobResult, JobState, JobStatusRef}, + jobs::{start_job, update_status, Job, JobContext, JobResult, JobState}, update::{build_updater, BIN_NAME}, }; @@ -17,7 +17,7 @@ pub struct UpdateResult { pub exe_path: PathBuf, } -fn run_update(status: &JobStatusRef, cancel: Receiver<()>) -> Result> { +fn run_update(status: &JobContext, cancel: Receiver<()>) -> Result> { update_status(status, "Fetching latest release".to_string(), 0, 3, &cancel)?; let updater = build_updater().context("Failed to create release updater")?; let latest_release = updater.get_latest_release()?; @@ -53,8 +53,8 @@ fn run_update(status: &JobStatusRef, cancel: Receiver<()>) -> Result JobState { - start_job("Update app", Job::Update, move |status, cancel| { - run_update(status, cancel).map(JobResult::Update) +pub fn start_update(ctx: &egui::Context) -> JobState { + start_job(ctx, "Update app", Job::Update, move |context, cancel| { + run_update(&context, cancel).map(JobResult::Update) }) } diff --git a/src/views/config.rs b/src/views/config.rs index 1bec40f..7ed8f47 100644 --- a/src/views/config.rs +++ b/src/views/config.rs @@ -26,7 +26,10 @@ use crate::{ Job, JobQueue, JobResult, }, update::RELEASE_URL, - views::appearance::Appearance, + views::{ + appearance::Appearance, + file::{FileDialogResult, FileDialogState}, + }, }; #[derive(Default)] @@ -45,10 +48,11 @@ pub struct ConfigViewState { pub filter_incomplete: bool, #[cfg(feature = "wsl")] pub available_wsl_distros: Option>, + pub file_dialog_state: FileDialogState, } impl ConfigViewState { - pub fn pre_update(&mut self, jobs: &mut JobQueue) { + pub fn pre_update(&mut self, jobs: &mut JobQueue, config: &AppConfigRef) { jobs.results.retain_mut(|result| { if let JobResult::CheckUpdate(result) = result { self.check_update = take(result); @@ -60,9 +64,52 @@ impl ConfigViewState { self.build_running = jobs.is_running(Job::ObjDiff); self.check_update_running = jobs.is_running(Job::CheckUpdate); self.update_running = jobs.is_running(Job::Update); + + // Check async file dialog results + match self.file_dialog_state.poll() { + FileDialogResult::None => {} + FileDialogResult::ProjectDir(path) => { + let mut guard = config.write().unwrap(); + guard.set_project_dir(path.to_path_buf()); + } + FileDialogResult::TargetDir(path) => { + let mut guard = config.write().unwrap(); + guard.set_target_obj_dir(path.to_path_buf()); + } + FileDialogResult::BaseDir(path) => { + let mut guard = config.write().unwrap(); + guard.set_base_obj_dir(path.to_path_buf()); + } + FileDialogResult::Object(path) => { + let mut guard = config.write().unwrap(); + if let (Some(base_dir), Some(target_dir)) = + (&guard.base_obj_dir, &guard.target_obj_dir) + { + if let Ok(obj_path) = path.strip_prefix(base_dir) { + let target_path = target_dir.join(obj_path); + guard.set_selected_obj(ObjectConfig { + name: obj_path.display().to_string(), + target_path: Some(target_path), + base_path: Some(path), + reverse_fn_order: None, + complete: None, + }); + } else if let Ok(obj_path) = path.strip_prefix(target_dir) { + let base_path = base_dir.join(obj_path); + guard.set_selected_obj(ObjectConfig { + name: obj_path.display().to_string(), + target_path: Some(path), + base_path: Some(base_path), + reverse_fn_order: None, + complete: None, + }); + } + } + } + } } - pub fn post_update(&mut self, jobs: &mut JobQueue, config: &AppConfigRef) { + pub fn post_update(&mut self, ctx: &egui::Context, jobs: &mut JobQueue, config: &AppConfigRef) { if self.queue_build { self.queue_build = false; if let Ok(mut config) = config.write() { @@ -72,12 +119,12 @@ impl ConfigViewState { if self.queue_check_update { self.queue_check_update = false; - jobs.push_once(Job::CheckUpdate, start_check_update); + jobs.push_once(Job::CheckUpdate, || start_check_update(ctx)); } if self.queue_update { self.queue_update = false; - jobs.push_once(Job::Update, start_update); + jobs.push_once(Job::Update, || start_update(ctx)); } } } @@ -210,33 +257,19 @@ pub fn config_ui( let mut new_selected_obj = selected_obj.clone(); if objects.is_empty() { - if let (Some(base_dir), Some(target_dir)) = (base_obj_dir, target_obj_dir) { + if let (Some(_base_dir), Some(target_dir)) = (base_obj_dir, target_obj_dir) { if ui.button("Select object").clicked() { - if let Some(path) = rfd::FileDialog::new() - .set_directory(&target_dir) - .add_filter("Object file", &["o", "elf"]) - .pick_file() - { - if let Ok(obj_path) = path.strip_prefix(&base_dir) { - let target_path = target_dir.join(obj_path); - new_selected_obj = Some(ObjectConfig { - name: obj_path.display().to_string(), - target_path: Some(target_path), - base_path: Some(path), - reverse_fn_order: None, - complete: None, - }); - } else if let Ok(obj_path) = path.strip_prefix(&target_dir) { - let base_path = base_dir.join(obj_path); - new_selected_obj = Some(ObjectConfig { - name: obj_path.display().to_string(), - target_path: Some(path), - base_path: Some(base_path), - reverse_fn_order: None, - complete: None, - }); - } - } + state.file_dialog_state.queue( + || { + Box::pin( + rfd::AsyncFileDialog::new() + .set_directory(&target_dir) + .add_filter("Object file", &["o", "elf"]) + .pick_file(), + ) + }, + FileDialogResult::Object, + ); } if let Some(obj) = selected_obj { ui.label( @@ -579,9 +612,10 @@ fn split_obj_config_ui( true, ); if response.clicked() { - if let Some(path) = rfd::FileDialog::new().pick_folder() { - config.set_project_dir(path); - } + state.file_dialog_state.queue( + || Box::pin(rfd::AsyncFileDialog::new().pick_folder()), + FileDialogResult::ProjectDir, + ); } ui.separator(); @@ -640,9 +674,10 @@ fn split_obj_config_ui( config.project_config_info.is_none(), ); if response.clicked() { - if let Some(path) = rfd::FileDialog::new().set_directory(&project_dir).pick_folder() { - config.set_target_obj_dir(path); - } + state.file_dialog_state.queue( + || Box::pin(rfd::AsyncFileDialog::new().set_directory(&project_dir).pick_folder()), + FileDialogResult::TargetDir, + ); } ui.checkbox(&mut config.build_target, "Build target objects").on_hover_ui(|ui| { let mut job = LayoutJob::default(); @@ -690,9 +725,10 @@ fn split_obj_config_ui( config.project_config_info.is_none(), ); if response.clicked() { - if let Some(path) = rfd::FileDialog::new().set_directory(&project_dir).pick_folder() { - config.set_base_obj_dir(path); - } + state.file_dialog_state.queue( + || Box::pin(rfd::AsyncFileDialog::new().set_directory(&project_dir).pick_folder()), + FileDialogResult::BaseDir, + ); } ui.checkbox(&mut config.build_base, "Build base objects").on_hover_ui(|ui| { let mut job = LayoutJob::default(); diff --git a/src/views/debug.rs b/src/views/debug.rs new file mode 100644 index 0000000..43df4f0 --- /dev/null +++ b/src/views/debug.rs @@ -0,0 +1,17 @@ +use crate::views::{appearance::Appearance, frame_history::FrameHistory}; + +pub fn debug_window( + ctx: &egui::Context, + show: &mut bool, + frame_history: &mut FrameHistory, + appearance: &Appearance, +) { + egui::Window::new("Debug").open(show).show(ctx, |ui| { + debug_ui(ui, frame_history, appearance); + }); +} + +fn debug_ui(ui: &mut egui::Ui, frame_history: &mut FrameHistory, _appearance: &Appearance) { + ui.label(format!("Repainting the UI each frame. FPS: {:.1}", frame_history.fps())); + frame_history.ui(ui); +} diff --git a/src/views/file.rs b/src/views/file.rs new file mode 100644 index 0000000..37d3de6 --- /dev/null +++ b/src/views/file.rs @@ -0,0 +1,51 @@ +use std::{future::Future, path::PathBuf, pin::Pin, thread::JoinHandle}; + +use pollster::FutureExt; +use rfd::FileHandle; + +#[derive(Default)] +pub enum FileDialogResult { + #[default] + None, + ProjectDir(PathBuf), + TargetDir(PathBuf), + BaseDir(PathBuf), + Object(PathBuf), +} + +#[derive(Default)] +pub struct FileDialogState { + thread: Option>, +} + +impl FileDialogState { + pub fn queue(&mut self, init: InitCb, result_cb: ResultCb) + where + InitCb: FnOnce() -> Pin> + Send>>, + ResultCb: FnOnce(PathBuf) -> FileDialogResult + Send + 'static, + { + if self.thread.is_some() { + return; + } + let future = init(); + self.thread = Some(std::thread::spawn(move || { + if let Some(handle) = future.block_on() { + result_cb(PathBuf::from(handle)) + } else { + FileDialogResult::None + } + })); + } + + pub fn poll(&mut self) -> FileDialogResult { + if let Some(thread) = &mut self.thread { + if thread.is_finished() { + self.thread.take().unwrap().join().unwrap_or(FileDialogResult::None) + } else { + FileDialogResult::None + } + } else { + FileDialogResult::None + } + } +} diff --git a/src/views/frame_history.rs b/src/views/frame_history.rs new file mode 100644 index 0000000..82015d0 --- /dev/null +++ b/src/views/frame_history.rs @@ -0,0 +1,142 @@ +// From https://github.com/emilk/egui/blob/e037489ac20a9e419715ae75d205a8baa117c3cf/crates/egui_demo_app/src/frame_history.rs +// Copyright (c) 2018-2021 Emil Ernerfeldt +// +// Permission is hereby granted, free of charge, to any +// person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the +// Software without restriction, including without +// limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software +// is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice +// shall be included in all copies or substantial portions +// of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +use egui::util::History; + +pub struct FrameHistory { + frame_times: History, +} + +impl Default for FrameHistory { + fn default() -> Self { + let max_age: f32 = 1.0; + let max_len = (max_age * 300.0).round() as usize; + Self { frame_times: History::new(0..max_len, max_age) } + } +} + +impl FrameHistory { + // Called first + pub fn on_new_frame(&mut self, now: f64, previous_frame_time: Option) { + let previous_frame_time = previous_frame_time.unwrap_or_default(); + if let Some(latest) = self.frame_times.latest_mut() { + *latest = previous_frame_time; // rewrite history now that we know + } + self.frame_times.add(now, previous_frame_time); // projected + } + + pub fn mean_frame_time(&self) -> f32 { self.frame_times.average().unwrap_or_default() } + + pub fn fps(&self) -> f32 { 1.0 / self.frame_times.mean_time_interval().unwrap_or_default() } + + pub fn ui(&mut self, ui: &mut egui::Ui) { + ui.label(format!("Mean CPU usage: {:.2} ms / frame", 1e3 * self.mean_frame_time())) + .on_hover_text( + "Includes egui layout and tessellation time.\n\ + Does not include GPU usage, nor overhead for sending data to GPU.", + ); + egui::warn_if_debug_build(ui); + + if !cfg!(target_arch = "wasm32") { + egui::CollapsingHeader::new("📊 CPU usage history").default_open(false).show( + ui, + |ui| { + self.graph(ui); + }, + ); + } + } + + fn graph(&mut self, ui: &mut egui::Ui) -> egui::Response { + use egui::*; + + ui.label("egui CPU usage history"); + + let history = &self.frame_times; + + // TODO(emilk): we should not use `slider_width` as default graph width. + let height = ui.spacing().slider_width; + let size = vec2(ui.available_size_before_wrap().x, height); + let (rect, response) = ui.allocate_at_least(size, Sense::hover()); + let style = ui.style().noninteractive(); + + let graph_top_cpu_usage = 0.010; + let graph_rect = Rect::from_x_y_ranges(history.max_age()..=0.0, graph_top_cpu_usage..=0.0); + let to_screen = emath::RectTransform::from_to(graph_rect, rect); + + let mut shapes = Vec::with_capacity(3 + 2 * history.len()); + shapes.push(Shape::Rect(epaint::RectShape::new( + rect, + style.rounding, + ui.visuals().extreme_bg_color, + ui.style().noninteractive().bg_stroke, + ))); + + let rect = rect.shrink(4.0); + let color = ui.visuals().text_color(); + let line_stroke = Stroke::new(1.0, color); + + if let Some(pointer_pos) = response.hover_pos() { + let y = pointer_pos.y; + shapes.push(Shape::line_segment( + [pos2(rect.left(), y), pos2(rect.right(), y)], + line_stroke, + )); + let cpu_usage = to_screen.inverse().transform_pos(pointer_pos).y; + let text = format!("{:.1} ms", 1e3 * cpu_usage); + shapes.push(ui.fonts(|f| { + Shape::text( + f, + pos2(rect.left(), y), + egui::Align2::LEFT_BOTTOM, + text, + TextStyle::Monospace.resolve(ui.style()), + color, + ) + })); + } + + let circle_color = color; + let radius = 2.0; + let right_side_time = ui.input(|i| i.time); // Time at right side of screen + + for (time, cpu_usage) in history.iter() { + let age = (right_side_time - time) as f32; + let pos = to_screen.transform_pos_clamped(Pos2::new(age, cpu_usage)); + + shapes.push(Shape::line_segment([pos2(pos.x, rect.bottom()), pos], line_stroke)); + + if cpu_usage < graph_top_cpu_usage { + shapes.push(Shape::circle_filled(pos, radius, circle_color)); + } + } + + ui.painter().extend(shapes); + + response + } +} diff --git a/src/views/jobs.rs b/src/views/jobs.rs index 501c093..fc1f61e 100644 --- a/src/views/jobs.rs +++ b/src/views/jobs.rs @@ -7,7 +7,7 @@ pub fn jobs_ui(ui: &mut egui::Ui, jobs: &mut JobQueue, appearance: &Appearance) let mut remove_job: Option = None; for job in jobs.iter_mut() { - let Ok(status) = job.status.read() else { + let Ok(status) = job.context.status.read() else { continue; }; ui.group(|ui| { diff --git a/src/views/mod.rs b/src/views/mod.rs index daa49a5..08f5c1c 100644 --- a/src/views/mod.rs +++ b/src/views/mod.rs @@ -3,7 +3,10 @@ use egui::{text::LayoutJob, Color32, FontId, TextFormat}; pub(crate) mod appearance; pub(crate) mod config; pub(crate) mod data_diff; +pub(crate) mod debug; pub(crate) mod demangle; +pub(crate) mod file; +pub(crate) mod frame_history; pub(crate) mod function_diff; pub(crate) mod jobs; pub(crate) mod symbol_diff; diff --git a/src/views/symbol_diff.rs b/src/views/symbol_diff.rs index 39f521b..791b15b 100644 --- a/src/views/symbol_diff.rs +++ b/src/views/symbol_diff.rs @@ -73,7 +73,7 @@ impl DiffViewState { } } - pub fn post_update(&mut self, _jobs: &mut JobQueue, config: &AppConfigRef) { + pub fn post_update(&mut self, config: &AppConfigRef) { if self.queue_build { self.queue_build = false; if let Ok(mut config) = config.write() {