M12.8: the app no longer freezes every 10 s on Windows: the system memory is read in the background

Every 10 seconds the UI thread re-read the system's memory for the cache budgets, and on
Windows that starts PowerShell (`Get-CimInstance Win32_OperatingSystem`) and waits for it:
about a second, sometimes two, during which the whole app froze. The reading now runs on a
background thread (`sysinfo::MemoryWatch`); `Session::memory_tick` takes the last reading and
starts the next one, never waiting. Console programs started for it no longer open a console
window (`CREATE_NO_WINDOW`).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
bflatasticandClaude Opus 5.5 committed 2026-10-05 07:05:17 +07:00
1 parent 4b37443481
commit 2d4a0902ea
4 files changed
+77 -5

No files matched your search

+3
View File
@@ -373,6 +373,8 @@ pub struct Session {
/// The last physical-memory reading (Settings ▸ Memory & CPU budgets); `None` = unknown
/// (headless sessions don't query it, see [`Session::memory_tick`]).
pub sys_memory: Option<sysinfo::SysMemory>,
/// Reads the system's memory in the background for [`Session::memory_tick`].
pub memory_watch: sysinfo::MemoryWatch,
/// Switches Affect Nested Comps as last applied (a change empties the layer cache).
pub applied_nested_switches: Option<bool>,
/// Runs renders and analyses off the UI thread where there are no threads (the web app's
@@ -465,6 +467,7 @@ impl Default for Session {
plugin_loader: None,
script_ui: scriptui::ScriptUi::default(),
sys_memory: None,
memory_watch: Default::default(),
applied_nested_switches: None,
offload: None,
offloaded: vec![],
+5 -3
View File
@@ -75,11 +75,13 @@ impl Session {
self.footage.set_cache_budget(b.media);
}
/// Re-read the system's memory (desktop frontends call this every few seconds) and re-apply
/// the cache budgets when the reading changes them. Returns the effective budgets.
/// Take the latest reading of the system's memory and start the next one in the background
/// (desktop frontends call this every few seconds; it never waits for the system), then
/// re-apply the cache budgets when the reading changes them. Returns the effective budgets.
pub fn memory_tick(&mut self) -> crate::prefs::CacheBudgets {
let before = self.prefs.cache_budgets(self.sys_memory);
self.sys_memory = crate::sysinfo::memory().or(self.sys_memory);
self.sys_memory = self.memory_watch.latest().or(self.sys_memory);
self.memory_watch.poll();
let after = self.prefs.cache_budgets(self.sys_memory);
if after != before {
self.apply_cache_budgets();
+47 -2
View File
@@ -4,7 +4,11 @@
//! Pure Rust and dependency-free: the values come from the operating system's own reporting
//! (`/proc/meminfo` on Linux, `sysctl`/`vm_stat` on macOS, `wmic`/PowerShell on Windows). Every
//! query may fail (sandboxes, the web): callers treat `None` as "unknown" and keep their
//! configured budgets.
//! configured budgets. The periodic reading behind the cache budgets runs on a background
//! thread ([`MemoryWatch`]): on Windows it starts PowerShell, which takes about a second.
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, PoisonError};
use serde::Serialize;
@@ -23,10 +27,51 @@ impl SysMemory {
}
}
/// The system's memory, read in the background: [`MemoryWatch::poll`] starts a reading (unless
/// one is running) and [`MemoryWatch::latest`] returns the last one without waiting.
#[derive(Clone, Default)]
pub struct MemoryWatch {
latest: Arc<Mutex<Option<SysMemory>>>,
busy: Arc<AtomicBool>,
}
impl MemoryWatch {
/// Start a new reading in the background (no threads on the web: read inline, it is
/// unknown there anyway).
pub fn poll(&self) {
if self.busy.swap(true, Ordering::AcqRel) {
return;
}
let (latest, busy) = (self.latest.clone(), self.busy.clone());
let read = move || {
if let Some(m) = memory() {
*latest.lock().unwrap_or_else(PoisonError::into_inner) = Some(m);
}
busy.store(false, Ordering::Release);
};
#[cfg(not(target_arch = "wasm32"))]
if std::thread::Builder::new().name("ec-memory".into()).spawn(read).is_err() {
self.busy.store(false, Ordering::Release);
}
#[cfg(target_arch = "wasm32")]
read();
}
/// The last reading (`None` until one finished).
pub fn latest(&self) -> Option<SysMemory> {
*self.latest.lock().unwrap_or_else(PoisonError::into_inner)
}
}
#[cfg(not(target_arch = "wasm32"))]
#[allow(dead_code)]
fn run(cmd: &str, args: &[&str]) -> Option<String> {
let out = std::process::Command::new(cmd).args(args).output().ok()?;
let mut c = std::process::Command::new(cmd);
c.args(args);
// A console program started from the windowed app would flash a console window.
#[cfg(target_os = "windows")]
std::os::windows::process::CommandExt::creation_flags(&mut c, 0x0800_0000); // CREATE_NO_WINDOW
let out = c.output().ok()?;
out.status.success().then(|| String::from_utf8_lossy(&out.stdout).to_string())
}
+22
View File
@@ -464,3 +464,25 @@ fn open_recent_increment_and_revert() {
assert_eq!(autosave::increment_path("/x/v1.ecproj", |p| p.ends_with("v1 2.ecproj")), "/x/v1 3.ecproj");
let _ = std::fs::remove_dir_all(d);
}
#[test]
fn memory_tick_never_waits_for_the_system() {
let mut s = Session::default();
let t0 = std::time::Instant::now();
s.memory_tick();
s.memory_tick();
// Reading the system's memory starts PowerShell on Windows (about a second): the UI thread
// only takes the last reading.
assert!(t0.elapsed() < std::time::Duration::from_millis(200), "{:?}", t0.elapsed());
if crate::sysinfo::memory().is_some() {
let mut got = None;
for _ in 0..200 {
got = s.memory_watch.latest();
if got.is_some() {
break;
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
assert!(got.is_some_and(|m| m.total > 0), "the reading arrives in the background");
}
}