Skip to content

Commit 74e8913

Browse files
committed
Repaint rework: more responsive, less energy
Previously, we repainted every frame on Windows at full refresh rate. This is an enormous waste, as the UI will be static most of the time. This was to work around a bug with `rfd` + `eframe`. On other platforms, we only repainted every frame when a job was running, which was better, but still not ideal. We also had a 100ms deadline, so we'd repaint at ~10fps minimum to catch new events (file watcher, jobs). This removes all repaint logic from the main loop and moves it into the individual places where we change state from another thread. For example, the file watcher thread will now immediately notify egui to repaint, rather than relying on the 100ms deadline we had previously. Jobs, when updating their status, also notify egui to repaint. For `rfd` file dialogs, this migrates to using the async API built on top of a polling thread + `pollster`. This interacts better with `eframe` on Windows. Overall, this should reduce repaints and improve responsiveness to file changes and background tasks.
1 parent 236e4d8 commit 74e8913

File tree

14 files changed

+366
-104
lines changed

14 files changed

+366
-104
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ memmap2 = "0.9.0"
4242
notify = "6.1.1"
4343
object = { version = "0.32.1", features = ["read_core", "std", "elf"], default-features = false }
4444
png = "0.17.10"
45+
pollster = "0.3.0"
4546
ppc750cl = { git = "https://github.com/encounter/ppc750cl", rev = "4a2bbbc6f84dcb76255ab6f3595a8d4a0ce96618" }
4647
rabbitizer = "1.8.0"
4748
rfd = { version = "0.12.1" } #, default-features = false, features = ['xdg-portal']

src/app.rs

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ use std::{
77
atomic::{AtomicBool, Ordering},
88
Arc, Mutex, RwLock,
99
},
10-
time::Duration,
1110
};
1211

1312
use filetime::FileTime;
@@ -29,7 +28,9 @@ use crate::{
2928
config_ui, diff_options_window, project_window, ConfigViewState, DEFAULT_WATCH_PATTERNS,
3029
},
3130
data_diff::data_diff_ui,
31+
debug::debug_window,
3232
demangle::{demangle_window, DemangleViewState},
33+
frame_history::FrameHistory,
3334
function_diff::function_diff_ui,
3435
jobs::jobs_ui,
3536
symbol_diff::{symbol_diff_ui, DiffViewState, View},
@@ -42,10 +43,12 @@ pub struct ViewState {
4243
pub config_state: ConfigViewState,
4344
pub demangle_state: DemangleViewState,
4445
pub diff_state: DiffViewState,
46+
pub frame_history: FrameHistory,
4547
pub show_appearance_config: bool,
4648
pub show_demangle: bool,
4749
pub show_project_config: bool,
4850
pub show_diff_options: bool,
51+
pub show_debug: bool,
4952
}
5053

5154
/// The configuration for a single object file.
@@ -257,7 +260,7 @@ impl App {
257260
log::info!("Job {} finished", job.id);
258261
match result {
259262
JobResult::None => {
260-
if let Some(err) = &job.status.read().unwrap().error {
263+
if let Some(err) = &job.context.status.read().unwrap().error {
261264
log::error!("{:?}", err);
262265
}
263266
}
@@ -278,12 +281,12 @@ impl App {
278281
} else {
279282
anyhow::Error::msg("Thread panicked")
280283
};
281-
let result = job.status.write();
284+
let result = job.context.status.write();
282285
if let Ok(mut guard) = result {
283286
guard.error = Some(err);
284287
} else {
285288
drop(result);
286-
job.status = Arc::new(RwLock::new(JobStatus {
289+
job.context.status = Arc::new(RwLock::new(JobStatus {
287290
title: "Error".to_string(),
288291
progress_percent: 0.0,
289292
progress_items: None,
@@ -298,14 +301,14 @@ impl App {
298301
jobs.clear_finished();
299302

300303
diff_state.pre_update(jobs, &self.config);
301-
config_state.pre_update(jobs);
304+
config_state.pre_update(jobs, &self.config);
302305
debug_assert!(jobs.results.is_empty());
303306
}
304307

305-
fn post_update(&mut self) {
308+
fn post_update(&mut self, ctx: &egui::Context) {
306309
let ViewState { jobs, diff_state, config_state, .. } = &mut self.view_state;
307-
config_state.post_update(jobs, &self.config);
308-
diff_state.post_update(jobs, &self.config);
310+
config_state.post_update(ctx, jobs, &self.config);
311+
diff_state.post_update(&self.config);
309312

310313
let Ok(mut config) = self.config.write() else {
311314
return;
@@ -335,7 +338,7 @@ impl App {
335338
if let Some(project_dir) = &config.project_dir {
336339
match build_globset(&config.watch_patterns).map_err(anyhow::Error::new).and_then(
337340
|globset| {
338-
create_watcher(self.modified.clone(), project_dir, globset)
341+
create_watcher(ctx.clone(), self.modified.clone(), project_dir, globset)
339342
.map_err(anyhow::Error::new)
340343
},
341344
) {
@@ -374,15 +377,15 @@ impl App {
374377
// Don't clear `queue_build` if a build is running. A file may have been modified during
375378
// the build, so we'll start another build after the current one finishes.
376379
if config.queue_build && config.selected_obj.is_some() && !jobs.is_running(Job::ObjDiff) {
377-
jobs.push(start_build(ObjDiffConfig::from_config(config)));
380+
jobs.push(start_build(ctx, ObjDiffConfig::from_config(config)));
378381
config.queue_build = false;
379382
config.queue_reload = false;
380383
} else if config.queue_reload && !jobs.is_running(Job::ObjDiff) {
381384
let mut diff_config = ObjDiffConfig::from_config(config);
382385
// Don't build, just reload the current files
383386
diff_config.build_base = false;
384387
diff_config.build_target = false;
385-
jobs.push(start_build(diff_config));
388+
jobs.push(start_build(ctx, diff_config));
386389
config.queue_reload = false;
387390
}
388391
}
@@ -404,18 +407,27 @@ impl eframe::App for App {
404407

405408
let ViewState {
406409
jobs,
407-
show_appearance_config,
410+
config_state,
408411
demangle_state,
409-
show_demangle,
410412
diff_state,
411-
config_state,
413+
frame_history,
414+
show_appearance_config,
415+
show_demangle,
412416
show_project_config,
413417
show_diff_options,
418+
show_debug,
414419
} = view_state;
415420

421+
frame_history.on_new_frame(ctx.input(|i| i.time), frame.info().cpu_usage);
422+
416423
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
417424
egui::menu::bar(ui, |ui| {
418425
ui.menu_button("File", |ui| {
426+
#[cfg(debug_assertions)]
427+
if ui.button("Debug…").clicked() {
428+
*show_debug = !*show_debug;
429+
ui.close_menu();
430+
}
419431
if ui.button("Project…").clicked() {
420432
*show_project_config = !*show_project_config;
421433
ui.close_menu();
@@ -511,16 +523,9 @@ impl eframe::App for App {
511523
appearance_window(ctx, show_appearance_config, appearance);
512524
demangle_window(ctx, show_demangle, demangle_state, appearance);
513525
diff_options_window(ctx, config, show_diff_options, appearance);
526+
debug_window(ctx, show_debug, frame_history, appearance);
514527

515-
self.post_update();
516-
517-
// Windows + request_repaint_after breaks dialogs:
518-
// https://github.com/emilk/egui/issues/2003
519-
if cfg!(windows) || self.view_state.jobs.any_running() {
520-
ctx.request_repaint();
521-
} else {
522-
ctx.request_repaint_after(Duration::from_millis(100));
523-
}
528+
self.post_update(ctx);
524529
}
525530

526531
/// Called by the frame work to save state before shutdown.
@@ -533,6 +538,7 @@ impl eframe::App for App {
533538
}
534539

535540
fn create_watcher(
541+
ctx: egui::Context,
536542
modified: Arc<AtomicBool>,
537543
project_dir: &Path,
538544
patterns: GlobSet,
@@ -552,7 +558,9 @@ fn create_watcher(
552558
continue;
553559
};
554560
if patterns.is_match(path) {
561+
log::info!("File modified: {}", path.display());
555562
modified.store(true, Ordering::Relaxed);
563+
ctx.request_repaint();
556564
}
557565
}
558566
}

src/jobs/check_update.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use anyhow::{Context, Result};
44
use self_update::{cargo_crate_version, update::Release};
55

66
use crate::{
7-
jobs::{start_job, update_status, Job, JobResult, JobState, JobStatusRef},
7+
jobs::{start_job, update_status, Job, JobContext, JobResult, JobState},
88
update::{build_updater, BIN_NAME},
99
};
1010

@@ -14,20 +14,20 @@ pub struct CheckUpdateResult {
1414
pub found_binary: bool,
1515
}
1616

17-
fn run_check_update(status: &JobStatusRef, cancel: Receiver<()>) -> Result<Box<CheckUpdateResult>> {
18-
update_status(status, "Fetching latest release".to_string(), 0, 1, &cancel)?;
17+
fn run_check_update(context: &JobContext, cancel: Receiver<()>) -> Result<Box<CheckUpdateResult>> {
18+
update_status(context, "Fetching latest release".to_string(), 0, 1, &cancel)?;
1919
let updater = build_updater().context("Failed to create release updater")?;
2020
let latest_release = updater.get_latest_release()?;
2121
let update_available =
2222
self_update::version::bump_is_greater(cargo_crate_version!(), &latest_release.version)?;
2323
let found_binary = latest_release.assets.iter().any(|a| a.name == BIN_NAME);
2424

25-
update_status(status, "Complete".to_string(), 1, 1, &cancel)?;
25+
update_status(context, "Complete".to_string(), 1, 1, &cancel)?;
2626
Ok(Box::new(CheckUpdateResult { update_available, latest_release, found_binary }))
2727
}
2828

29-
pub fn start_check_update() -> JobState {
30-
start_job("Check for updates", Job::CheckUpdate, move |status, cancel| {
31-
run_check_update(status, cancel).map(|result| JobResult::CheckUpdate(Some(result)))
29+
pub fn start_check_update(ctx: &egui::Context) -> JobState {
30+
start_job(ctx, "Check for updates", Job::CheckUpdate, move |context, cancel| {
31+
run_check_update(&context, cancel).map(|result| JobResult::CheckUpdate(Some(result)))
3232
})
3333
}

src/jobs/mod.rs

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ impl JobQueue {
4848
}
4949

5050
/// Returns whether any job is running.
51+
#[allow(dead_code)]
5152
pub fn any_running(&self) -> bool {
5253
self.jobs.iter().any(|job| {
5354
if let Some(handle) = &job.handle {
@@ -81,21 +82,25 @@ impl JobQueue {
8182
self.jobs.retain(|job| {
8283
!(job.should_remove
8384
&& job.handle.is_none()
84-
&& job.status.read().unwrap().error.is_none())
85+
&& job.context.status.read().unwrap().error.is_none())
8586
});
8687
}
8788

8889
/// Removes a job from the queue given its ID.
8990
pub fn remove(&mut self, id: usize) { self.jobs.retain(|job| job.id != id); }
9091
}
9192

92-
pub type JobStatusRef = Arc<RwLock<JobStatus>>;
93+
#[derive(Clone)]
94+
pub struct JobContext {
95+
pub status: Arc<RwLock<JobStatus>>,
96+
pub egui: egui::Context,
97+
}
9398

9499
pub struct JobState {
95100
pub id: usize,
96101
pub kind: Job,
97102
pub handle: Option<JoinHandle<JobResult>>,
98-
pub status: JobStatusRef,
103+
pub context: JobContext,
99104
pub cancel: Sender<()>,
100105
pub should_remove: bool,
101106
}
@@ -124,9 +129,10 @@ fn should_cancel(rx: &Receiver<()>) -> bool {
124129
}
125130

126131
fn start_job(
132+
ctx: &egui::Context,
127133
title: &str,
128134
kind: Job,
129-
run: impl FnOnce(&JobStatusRef, Receiver<()>) -> Result<JobResult> + Send + 'static,
135+
run: impl FnOnce(JobContext, Receiver<()>) -> Result<JobResult> + Send + 'static,
130136
) -> JobState {
131137
let status = Arc::new(RwLock::new(JobStatus {
132138
title: title.to_string(),
@@ -135,10 +141,11 @@ fn start_job(
135141
status: String::new(),
136142
error: None,
137143
}));
138-
let status_clone = status.clone();
144+
let context = JobContext { status: status.clone(), egui: ctx.clone() };
145+
let context_inner = JobContext { status: status.clone(), egui: ctx.clone() };
139146
let (tx, rx) = std::sync::mpsc::channel();
140147
let handle = std::thread::spawn(move || {
141-
return match run(&status, rx) {
148+
return match run(context_inner, rx) {
142149
Ok(state) => state,
143150
Err(e) => {
144151
if let Ok(mut w) = status.write() {
@@ -150,24 +157,18 @@ fn start_job(
150157
});
151158
let id = JOB_ID.fetch_add(1, Ordering::Relaxed);
152159
log::info!("Started job {}", id);
153-
JobState {
154-
id,
155-
kind,
156-
handle: Some(handle),
157-
status: status_clone,
158-
cancel: tx,
159-
should_remove: true,
160-
}
160+
JobState { id, kind, handle: Some(handle), context, cancel: tx, should_remove: true }
161161
}
162162

163163
fn update_status(
164-
status: &JobStatusRef,
164+
context: &JobContext,
165165
str: String,
166166
count: u32,
167167
total: u32,
168168
cancel: &Receiver<()>,
169169
) -> Result<()> {
170-
let mut w = status.write().map_err(|_| anyhow::Error::msg("Failed to lock job status"))?;
170+
let mut w =
171+
context.status.write().map_err(|_| anyhow::Error::msg("Failed to lock job status"))?;
171172
w.progress_items = Some([count, total]);
172173
w.progress_percent = count as f32 / total as f32;
173174
if should_cancel(cancel) {
@@ -176,5 +177,7 @@ fn update_status(
176177
} else {
177178
w.status = str;
178179
}
180+
drop(w);
181+
context.egui.request_repaint();
179182
Ok(())
180183
}

src/jobs/objdiff.rs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use time::OffsetDateTime;
1111
use crate::{
1212
app::{AppConfig, ObjectConfig},
1313
diff::{diff_objs, DiffAlg, DiffObjConfig},
14-
jobs::{start_job, update_status, Job, JobResult, JobState, JobStatusRef},
14+
jobs::{start_job, update_status, Job, JobContext, JobResult, JobState},
1515
obj::{elf, ObjInfo},
1616
};
1717

@@ -102,7 +102,7 @@ fn run_make(cwd: &Path, arg: &Path, config: &ObjDiffConfig) -> BuildStatus {
102102
}
103103

104104
fn run_build(
105-
status: &JobStatusRef,
105+
context: &JobContext,
106106
cancel: Receiver<()>,
107107
config: ObjDiffConfig,
108108
) -> Result<Box<ObjDiffResult>> {
@@ -142,7 +142,7 @@ fn run_build(
142142
let first_status = match target_path_rel {
143143
Some(target_path_rel) if config.build_target => {
144144
update_status(
145-
status,
145+
context,
146146
format!("Building target {}", target_path_rel.display()),
147147
0,
148148
total,
@@ -156,7 +156,7 @@ fn run_build(
156156
let second_status = match base_path_rel {
157157
Some(base_path_rel) if config.build_base => {
158158
update_status(
159-
status,
159+
context,
160160
format!("Building base {}", base_path_rel.display()),
161161
0,
162162
total,
@@ -173,7 +173,7 @@ fn run_build(
173173
match &obj_config.target_path {
174174
Some(target_path) if first_status.success => {
175175
update_status(
176-
status,
176+
context,
177177
format!("Loading target {}", target_path_rel.unwrap().display()),
178178
2,
179179
total,
@@ -189,7 +189,7 @@ fn run_build(
189189
let mut second_obj = match &obj_config.base_path {
190190
Some(base_path) if second_status.success => {
191191
update_status(
192-
status,
192+
context,
193193
format!("Loading base {}", base_path_rel.unwrap().display()),
194194
3,
195195
total,
@@ -203,16 +203,16 @@ fn run_build(
203203
_ => None,
204204
};
205205

206-
update_status(status, "Performing diff".to_string(), 4, total, &cancel)?;
206+
update_status(context, "Performing diff".to_string(), 4, total, &cancel)?;
207207
let diff_config = DiffObjConfig { code_alg: config.code_alg, data_alg: config.data_alg };
208208
diff_objs(&diff_config, first_obj.as_mut(), second_obj.as_mut())?;
209209

210-
update_status(status, "Complete".to_string(), total, total, &cancel)?;
210+
update_status(context, "Complete".to_string(), total, total, &cancel)?;
211211
Ok(Box::new(ObjDiffResult { first_status, second_status, first_obj, second_obj, time }))
212212
}
213213

214-
pub fn start_build(config: ObjDiffConfig) -> JobState {
215-
start_job("Object diff", Job::ObjDiff, move |status, cancel| {
216-
run_build(status, cancel, config).map(|result| JobResult::ObjDiff(Some(result)))
214+
pub fn start_build(ctx: &egui::Context, config: ObjDiffConfig) -> JobState {
215+
start_job(ctx, "Object diff", Job::ObjDiff, move |context, cancel| {
216+
run_build(&context, cancel, config).map(|result| JobResult::ObjDiff(Some(result)))
217217
})
218218
}

0 commit comments

Comments
 (0)