mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 13:08:07 -06:00
Merge pull request #807 from cdinhoo/fix/nonblocking-expression-diagnostics
Performance: reduce UI stalls while dragging layers
This commit is contained in:
@@ -261,6 +261,13 @@ type Identity = (u64, Option<Arc<Project>>);
|
||||
/// Content keys of (project revision, comp), computed once per revision.
|
||||
type ContentKeys = Arc<Mutex<HashMap<(u64, u64), u128>>>;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[derive(Default)]
|
||||
struct ContentKeyJobs {
|
||||
running: bool,
|
||||
pending: VecDeque<(Arc<Project>, u64, u64)>,
|
||||
}
|
||||
|
||||
fn content_key(keys: &ContentKeys, project: &Project, revision: u64, comp: u64) -> u128 {
|
||||
if let Some(k) = keys.lock().ok().and_then(|m| m.get(&(revision, comp)).copied()) {
|
||||
return k;
|
||||
@@ -417,6 +424,8 @@ pub struct Frames {
|
||||
/// Render time of the last viewer (urgent) frame in ms, for the Info panel / perf readout.
|
||||
pub last_ms: Arc<Mutex<f64>>,
|
||||
content_keys: ContentKeys,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
content_key_jobs: Arc<Mutex<ContentKeyJobs>>,
|
||||
gpu_retired: Arc<AtomicBool>,
|
||||
/// [`comp_content`] identities by (revision, comp), with the project that keeps an identity
|
||||
/// found by [`Frames::identity`] in another project.
|
||||
@@ -451,6 +460,8 @@ impl Default for Frames {
|
||||
ctx: None,
|
||||
last_ms: Arc::new(Mutex::new(0.0)),
|
||||
content_keys: Arc::default(),
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
content_key_jobs: Arc::default(),
|
||||
gpu_retired: Arc::default(),
|
||||
identities: Mutex::default(),
|
||||
bases: Mutex::default(),
|
||||
@@ -726,7 +737,8 @@ impl Frames {
|
||||
}
|
||||
|
||||
/// Frames of the series of `base` in the disk cache but not in RAM — the blue cache bar.
|
||||
/// `opts` are the viewer's render options (part of the disk key).
|
||||
/// `opts` are the viewer's render options (part of the disk key). Native queries return
|
||||
/// no blue frames until the content key is ready; hashing never blocks timeline painting.
|
||||
pub fn disk_frames(&self, src: &RenderSource, base: &FrameKey, frames: i64, opts: &RenderOpts) -> Vec<i64> {
|
||||
let remote = self.remote.as_ref().filter(|r| r.disk());
|
||||
let contains = |k: u128| match (&src.disk, remote) {
|
||||
@@ -737,14 +749,64 @@ impl Frames {
|
||||
if src.disk.is_none() && remote.is_none() {
|
||||
return vec![];
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let content = {
|
||||
let known = self.content_keys.lock().unwrap_or_else(std::sync::PoisonError::into_inner).get(&(base.revision, base.comp)).copied();
|
||||
let Some(content) = known else {
|
||||
self.request_content_key(src.project.clone(), base.revision, base.comp);
|
||||
return vec![];
|
||||
};
|
||||
content
|
||||
};
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let content = content_key(&self.content_keys, &src.project, base.revision, base.comp);
|
||||
let ram: HashSet<i64> = self.cached_frames(base).into_iter().collect();
|
||||
let oh = opts_hash(opts);
|
||||
(0..frames.clamp(0, 100_000))
|
||||
.filter(|f| !ram.contains(f))
|
||||
.filter(|f| contains(frame_disk_key(&self.content_keys, &src.project, &FrameKey { frame: *f, ..*base }, oh)))
|
||||
.filter(|f| contains(disk_cache::frame_key(content, *f, base.scale, base.view ^ oh)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn request_content_key(&self, project: Arc<Project>, revision: u64, comp: u64) {
|
||||
let mut jobs = self.content_key_jobs.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
// Coalesce rapid edits. One worker hashes at most one revision at a time; retained
|
||||
// snapshots are bounded even with multiple timelines open.
|
||||
jobs.pending.retain(|(_, _, c)| *c != comp);
|
||||
if jobs.pending.len() >= 8 {
|
||||
jobs.pending.pop_front();
|
||||
}
|
||||
jobs.pending.push_back((project, revision, comp));
|
||||
if jobs.running {
|
||||
return;
|
||||
}
|
||||
jobs.running = true;
|
||||
drop(jobs);
|
||||
let (jobs, keys, ctx) = (self.content_key_jobs.clone(), self.content_keys.clone(), self.ctx.clone());
|
||||
let work = move || loop {
|
||||
let next = {
|
||||
let mut jobs = jobs.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let next = jobs.pending.pop_back();
|
||||
if next.is_none() {
|
||||
jobs.running = false;
|
||||
}
|
||||
next
|
||||
};
|
||||
let Some((project, revision, comp)) = next else { return };
|
||||
if std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| content_key(&keys, &project, revision, comp))).is_err() {
|
||||
log::error!("could not compute a preview disk-cache key; the cache indicator will retry");
|
||||
}
|
||||
if let Some(ctx) = &ctx {
|
||||
ctx.request_repaint();
|
||||
}
|
||||
};
|
||||
match &self.pool {
|
||||
Some(pool) => pool.spawn(work),
|
||||
None => rayon::spawn(work),
|
||||
}
|
||||
}
|
||||
|
||||
/// Change the RAM preview cache budget (Settings ▸ Memory & CPU), evicting the oldest
|
||||
/// frames when it shrinks.
|
||||
pub fn set_budget(&self, bytes: usize) {
|
||||
@@ -1294,6 +1356,67 @@ impl Queue {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn disk_indicator_does_not_hash_on_ui_thread_and_coalesces_edits() {
|
||||
use std::sync::mpsc;
|
||||
use std::time::{Duration, Instant};
|
||||
struct Disk(u128);
|
||||
impl RemoteFrames for Disk {
|
||||
fn slots(&self) -> usize {
|
||||
1
|
||||
}
|
||||
fn disk(&self) -> bool {
|
||||
true
|
||||
}
|
||||
fn disk_contains(&self, key: u128) -> bool {
|
||||
self.0 == key
|
||||
}
|
||||
fn start(&self, _: RemoteJob, done: RemoteDone) {
|
||||
done(Err("test has no renderer".into()));
|
||||
}
|
||||
}
|
||||
let mut s = effectcraft_engine::Session::default();
|
||||
s.execute("comp.new", serde_json::json!({"width":32,"height":18,"duration":1})).unwrap();
|
||||
let cid = s.active_comp_id().unwrap();
|
||||
let opts = RenderOpts::default();
|
||||
let stored = disk_cache::frame_key(disk_cache::comp_content_key(&s.project, cid), 2, 1000, opts_hash(&opts));
|
||||
let mut frames = Frames { pool: Some(rayon::ThreadPoolBuilder::new().num_threads(1).build().unwrap()), ..Default::default() };
|
||||
frames.set_remote(Some(Arc::new(Disk(stored))));
|
||||
let (entered, started) = mpsc::channel();
|
||||
let (release, wait) = mpsc::channel();
|
||||
frames.pool.as_ref().unwrap().spawn(move || {
|
||||
entered.send(()).unwrap();
|
||||
wait.recv_timeout(Duration::from_secs(5)).unwrap();
|
||||
});
|
||||
started.recv_timeout(Duration::from_secs(3)).unwrap();
|
||||
let src = RenderSource {
|
||||
project: s.project.clone(),
|
||||
footage: s.footage.clone(),
|
||||
expr: None,
|
||||
layer_cache: Arc::default(),
|
||||
gpu: None,
|
||||
gpu_display: false,
|
||||
disk: None,
|
||||
};
|
||||
let mut base = key(1, 0, 0);
|
||||
base.comp = cid.0;
|
||||
for revision in 1..50 {
|
||||
base.revision = revision;
|
||||
assert!(frames.disk_frames(&src, &base, 30, &opts).is_empty());
|
||||
}
|
||||
assert!(frames.content_keys.lock().unwrap().is_empty(), "painting computed a content hash while the worker was blocked");
|
||||
assert_eq!(frames.content_key_jobs.lock().unwrap().pending.len(), 1);
|
||||
release.send(()).unwrap();
|
||||
let until = Instant::now() + Duration::from_secs(3);
|
||||
while !frames.content_keys.lock().unwrap().contains_key(&(base.revision, base.comp)) {
|
||||
assert!(Instant::now() < until);
|
||||
std::thread::sleep(Duration::from_millis(1));
|
||||
}
|
||||
assert_eq!(frames.disk_frames(&src, &base, 30, &opts), vec![2], "the blue indicator must return once its key is ready");
|
||||
assert_eq!(frames.content_keys.lock().unwrap().len(), 1, "superseded revisions were hashed");
|
||||
}
|
||||
|
||||
/// Frames larger than the texture limit are averaged down by a whole factor (#201: an
|
||||
/// 11000×2200 comp at Full resolution against an 8192 limit).
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user