Web: the layer and footage caches stay small enough for eight stacked clips

Cause: the browser build kept the desktop's cache budgets (1 GB layer
cache, 1 GB footage frame cache) inside the page's memory, which wasm32
caps at 4 GiB. FilmCraft's decoders keep up to 384 MB of decoded frames
per open movie beside them, so a comp of eight stacked 1080p H.264 clips
grew the page by about 55 MB/s while playing and ran it out of memory
after about 50 s ("unreachable": a failed allocation).

Fix: in the browser Prefs::cache_budgets keeps the Layer Cache within
512 MB and the Footage Frame Cache within 256 MB (WEB_LAYER_CACHE,
WEB_MEDIA_CACHE); smaller settings and the desktop are unchanged, and
the RAM preview keeps its budget. Docs (web, preferences) say so.

Tests: tests_m145::web_cache_budgets_leave_room_for_the_decoders: with
4 GB settings the web budgets are the caps, the desktop's are the
settings, and smaller settings stay as they are.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
daijroandClaude Opus 5.5 committed 2026-10-09 16:44:15 -05:00
1 parent 30aaddf7c2
commit 2ccc66e73b
4 files changed
+39 -2

No files matched your search

+19
View File
@@ -586,8 +586,18 @@ impl Prefs {
/// budgets, scaled down so together they leave **RAM Reserved for Other Applications** free,
/// and halved while the system is low on memory when **Reduce Cache Size When System Is Low
/// on Memory** is on. `mem` = the system's memory (`None` = unknown: configured budgets).
/// In the browser the layer and footage caches also stay within [`WEB_LAYER_CACHE`] and
/// [`WEB_MEDIA_CACHE`].
pub fn cache_budgets(&self, mem: Option<crate::sysinfo::SysMemory>) -> CacheBudgets {
self.cache_budgets_in(mem, cfg!(target_arch = "wasm32"))
}
/// [`Prefs::cache_budgets`], in the browser's memory (`web`) or not.
pub(crate) fn cache_budgets_in(&self, mem: Option<crate::sysinfo::SysMemory>, web: bool) -> CacheBudgets {
let (mut l, mut m, mut p) = (self.layer_cache_bytes() as u64, self.media_cache_bytes() as u64, self.preview_cache_bytes() as u64);
if web {
(l, m) = (l.min(WEB_LAYER_CACHE), m.min(WEB_MEDIA_CACHE));
}
let floor = 64u64 << 20;
let mut capped = false;
let mut reduced = false;
@@ -732,6 +742,15 @@ fn push_mru(list: &mut Vec<String>, v: &str, max: usize) {
list.truncate(max.max(1));
}
/// The most the layer cache holds in the browser. A wasm32 page addresses at most 4 GiB, which
/// these caches share with the app and with the decoders' own decoded frames (FilmCraft keeps
/// up to 384 MB per open movie): with the desktop's 1 GB defaults, eight stacked 1080p clips ran
/// the page out of memory a few seconds into playback.
pub const WEB_LAYER_CACHE: u64 = 512 << 20;
/// The most the footage frame cache holds in the browser (see [`WEB_LAYER_CACHE`]).
pub const WEB_MEDIA_CACHE: u64 = 256 << 20;
/// Effective cache budgets in bytes (see [`Prefs::cache_budgets`]).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
pub struct CacheBudgets {
+17
View File
@@ -202,6 +202,23 @@ fn cache_budgets_leave_reserved_ram_and_shrink_when_low() {
assert!(!p.cache_budgets(Some(low)).reduced);
}
/// Eight stacked 1080p clips ran the browser build out of its 4 GiB of memory: the footage and
/// layer caches kept their desktop sizes there, beside the decoders' own frames.
#[test]
fn web_cache_budgets_leave_room_for_the_decoders() {
use crate::prefs::{WEB_LAYER_CACHE, WEB_MEDIA_CACHE};
let mut p = crate::prefs::Prefs::default();
p.memory.layer_cache_mb = 4096;
p.memory.media_cache_mb = 4096;
let web = p.cache_budgets_in(None, true);
assert_eq!((web.layer as u64, web.media as u64), (WEB_LAYER_CACHE, WEB_MEDIA_CACHE));
assert_eq!(web.preview, p.cache_budgets_in(None, false).preview, "the RAM preview keeps its budget");
// Smaller settings stay as they are; the desktop is unchanged.
p.memory.media_cache_mb = 128;
assert_eq!(p.cache_budgets_in(None, true).media, 128 << 20);
assert_eq!(p.cache_budgets_in(None, false).layer, 4096 << 20);
}
#[test]
fn motion_path_span_follows_the_composition_setting() {
let mut p = crate::prefs::Prefs::default();
+2 -2
View File
@@ -71,8 +71,8 @@ Commands (CLI, MCP, control channel):
- `audio.outputDevice`: Default Output (device list from the desktop host)
- `audio.outputLeft`: Left (Audio Output Mapping)
- `audio.outputRight`: Right (Audio Output Mapping)
- `memory.layerCacheMb`: Layer Cache
- `memory.mediaCacheMb`: Footage Frame Cache
- `memory.layerCacheMb`: Layer Cache (in the browser at most 512 MB, see [web.md](web.md))
- `memory.mediaCacheMb`: Footage Frame Cache (in the browser at most 256 MB)
- `memory.previewCacheMb`: Preview (RAM) Cache
- `threeD.defaultRenderer`: Default 3D Renderer
- `labels.N.name` / `labels.N.color`: the 16 label names and colours, used by the Label menu,
+1
View File
@@ -61,6 +61,7 @@ device of their own), `?nosw` (don't register the service worker).
| config directory: settings, shortcut presets, recent projects, the recovery sentinel | `store::WebConfig` in browser storage |
| auto-save folder | `/EffectCraft Auto-Save/` in browser storage (`ConfigStore::files`) |
| media reads (`MediaPool`, `probe`) | `files::WebImporter`: bytes from the file table, handed to `MediaPool::add_bytes` / `probe_bytes` |
| Memory & CPU cache budgets, scaled to the system's RAM | the page's memory, at most 4 GiB (wasm32), which the caches share with the decoders' own decoded frames (FilmCraft keeps up to 384 MB per open movie): the Layer Cache stays within 512 MB and the Footage Frame Cache within 256 MB (`Prefs::cache_budgets`) |
| export writes (`effectcraft-export`) | the job's `sink` (`effectcraft_host::FileExporter { sink }`): files land in the table and download after the render; several files (an image sequence) download as one stored `.zip` |
| rfd file dialogs | `<input type=file>` for File ▸ Open / Import; drop files anywhere on the page (`.ecproj` opens, everything else imports); "Save As" / "Output To" pick a download name |
| Media Browser on the file system | browser storage and folders opened with the File System Access API (below) |