mirror of
https://github.com/encounter/objdiff.git
synced 2025-12-09 13:37:55 +00:00
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.
This commit is contained in:
@@ -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<Box<CheckUpdateResult>> {
|
||||
update_status(status, "Fetching latest release".to_string(), 0, 1, &cancel)?;
|
||||
fn run_check_update(context: &JobContext, cancel: Receiver<()>) -> Result<Box<CheckUpdateResult>> {
|
||||
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)))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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<RwLock<JobStatus>>;
|
||||
#[derive(Clone)]
|
||||
pub struct JobContext {
|
||||
pub status: Arc<RwLock<JobStatus>>,
|
||||
pub egui: egui::Context,
|
||||
}
|
||||
|
||||
pub struct JobState {
|
||||
pub id: usize,
|
||||
pub kind: Job,
|
||||
pub handle: Option<JoinHandle<JobResult>>,
|
||||
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<JobResult> + Send + 'static,
|
||||
run: impl FnOnce(JobContext, Receiver<()>) -> Result<JobResult> + 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(())
|
||||
}
|
||||
|
||||
@@ -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<Box<ObjDiffResult>> {
|
||||
@@ -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)))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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<Box<UpdateResult>> {
|
||||
fn run_update(status: &JobContext, cancel: Receiver<()>) -> Result<Box<UpdateResult>> {
|
||||
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<Box<UpdateR
|
||||
Ok(Box::from(UpdateResult { exe_path: target_file }))
|
||||
}
|
||||
|
||||
pub fn start_update() -> 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)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user