Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 13 additions & 3 deletions apps/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,19 @@ app's own agent and `tools.json`, the system toolbox, `glance.publish` and
most 32, kept across closes and named by the file's name, size and
modification time (`src/covers.rs`). Slide drawings go before covers,
each oldest first, and the whole cache is cleared before a write would
fail with `storage_full:`. What the inspector shows beyond the
commands' answers (a box's anchor and autofit, a chart's and a table's
options, the deck's colour and font schemes and its footer, the engine's
fail with `storage_full:`. Playback (`deck.media`, `src/media.rs`) runs
no engine command: it writes a video or audio clip's bytes from the open
document's model, as the deck stores them and never decoded, to
`.cache/media/<SHA-256>.<ext>` for the app's `Video` widget (`ext` from
the deck's own name for the clip or its declared type, from SERVICE.md's
list, else `unsupported:`), staged and create-only, reused by hash, in a
media cache of 32 MiB and 8 clips (the one used longest ago goes first);
a document's clips go when it closes or is released, unless another open
document uses them, and before a clip would not fit, the render cache and
then the media cache are cleared (`storage_full:`). What the inspector
shows beyond the commands' answers (a box's anchor and autofit, a
chart's and a table's options, the deck's colour and font schemes and
its footer, the engine's
colour schemes with their colours) the service reads from the open
document's model, read-only (`src/reading.rs`, `docs::design_state`).
Errors start with a stable code
Expand Down
3 changes: 3 additions & 0 deletions apps/deck/host-service/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ zip = { version = "3", default-features = false, features = ["deflate"] }
# of the engine's `open_presentation`), since the engine's own sniff misses
# a package whose content-type list comes late, as LibreOffice writes them.
deckcraft-pptx = { workspace = true }
# Names a clip in the media cache by a hash of its bytes (src/media.rs);
# the workspace's sha2, as Mail's and the shell's.
sha2 = "0.10"

[dev-dependencies]
# Generates and checks the skill's references (tests/skill.rs).
Expand Down
412 changes: 412 additions & 0 deletions apps/deck/host-service/src/doc_tests.rs

Large diffs are not rendered by default.

40 changes: 32 additions & 8 deletions apps/deck/host-service/src/docs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@
//! address, can be reused); when its storage scope changes or goes (makepad's
//! `splash_storage::storage_for_heap` for that key, checked by every call's
//! [`sweep`]); and, for a caller with no isolate (a component's host call, a
//! test), after [`IDLE`] without a call. Its renders go with it, and any a
//! released document left behind go when the caller next opens a document
//! ([`cache::prune`]).
//! test), after [`IDLE`] without a call. Its renders go with it, and so do
//! the clips it handed out for playback that no other document open in the
//! same storage uses ([`media::forget`]); any a released document left
//! behind go when the caller next opens a document ([`cache::prune`],
//! [`media::prune`]).

use std::cell::{Cell, RefCell};
use std::collections::HashMap;
Expand All @@ -56,6 +58,7 @@ use crate::cache;
use crate::codes::{fail, invalid, too_large, Code};
use crate::gate::{self, HeldCache, Plan};
use crate::guard::Format;
use crate::media;
use crate::Ctx;

/// The most documents one caller (app and storage scope) keeps open.
Expand Down Expand Up @@ -120,6 +123,9 @@ pub(crate) struct OpenDoc {
/// What the session holds, as the door's ceilings count it, kept per
/// slide between calls ([`gate`]).
pub held: HeldCache,
/// The clips `deck.media` handed out for this document: storage-relative
/// files in the media cache ([`media`]).
pub clips: Vec<String>,
}

impl OpenDoc {
Expand Down Expand Up @@ -184,10 +190,17 @@ impl OpenDoc {
matches!(&self.binding, Binding::Isolate { heap, .. } if closed.contains(heap))
}

/// The document goes: its engine session, and its renders in the
/// service's cache.
/// The document goes: its engine session, its renders in the service's
/// cache, and the clips it handed out that no document still open in the
/// same storage uses.
fn release(self) {
cache::forget(&Area::new(self.owner.clone(), None, false), &self.handle);
let area = Area::new(self.owner.clone(), None, false);
cache::forget(&area, &self.handle);
if !self.clips.is_empty() {
if let Some(used) = clips_in_use(&self.owner) {
media::forget(&area, &self.clips, &used);
}
}
}
}

Expand Down Expand Up @@ -246,6 +259,13 @@ pub(crate) fn isolate_closed(heap: usize) {
}
}

/// The clips the documents open in the storage `owner` use, whichever app
/// opened them; `None` while a call holds the table (the clips then stay,
/// within the media cache's bounds, until a later open prunes them).
fn clips_in_use(owner: &Path) -> Option<Vec<String>> {
DOCS.with(|docs| docs.try_borrow().ok().map(|docs| docs.open.iter().filter(|d| d.owner == owner).flat_map(|d| d.clips.iter().cloned()).collect()))
}

/// Whose documents a call reaches: its area's root as the file system
/// spells it (two spellings of one folder are one scope).
pub(crate) fn owner(area: &Area) -> PathBuf {
Expand Down Expand Up @@ -365,8 +385,8 @@ fn room_for_one(cx: &Ctx, owner: &Path) -> Result<(), String> {
})
}

/// Add `doc` to the table, and clear the renders of documents no longer
/// open in its storage.
/// Add `doc` to the table, and clear the renders and clips of documents no
/// longer open in its storage.
fn admit(doc: OpenDoc, area: &Area) {
let (app, owner) = (doc.app.clone(), doc.owner.clone());
let open: Vec<String> = DOCS.with(|docs| {
Expand All @@ -375,6 +395,9 @@ fn admit(doc: OpenDoc, area: &Area) {
docs.open.iter().filter(|d| d.app == app && d.owner == owner).map(|d| d.handle.clone()).collect()
});
cache::prune(area, &open);
if let Some(used) = clips_in_use(&owner) {
media::prune(area, &used);
}
}

/// A fresh open document around `session` (which holds its deck).
Expand All @@ -392,6 +415,7 @@ fn new_doc(cx: &Ctx, owner: PathBuf, session: Session, file: Option<String>) ->
deck_rev: 0,
renders: HashMap::new(),
held: HeldCache::default(),
clips: Vec::new(),
};
// The service keeps the undo steps itself (crate::gate).
doc.st_mut()?.history.limit = usize::MAX;
Expand Down
14 changes: 9 additions & 5 deletions apps/deck/host-service/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@
//! name as `doc`; `close`, `state`, `save`, `slides`, `slide`, `hit`,
//! `view`, `layouts`, `text`, `format_state`, `slides_op`, `notes`,
//! `transition`, `insert`, `set_text`, `format`, `shape`, `picture`,
//! `table`, `chart`, `design`, `undo`, `redo`, `show` and `export` work on
//! it, and `themes`, `fonts`, `shapes` and `cover` need none. Who holds an
//! open document, on which thread, and what releases it: [`docs`]. How a
//! method's engine commands reach it, through the same door as `deck.run`,
//! as one undo step: [`gate`].
//! `table`, `chart`, `design`, `undo`, `redo`, `show`, `export` and
//! `media` work on it, and `themes`, `fonts`, `shapes` and `cover` need
//! none. Who holds an open document, on which thread, and what releases
//! it: [`docs`]. How a method's engine commands reach it, through the same
//! door as `deck.run`, as one undo step: [`gate`]. `media` runs no engine
//! command: it writes a clip's bytes, as the deck stores them, for the
//! app's player ([`media`]).
//!
//! The service does all file I/O itself and feeds the engine bytes, so the
//! engine never touches a path, and a link written inside a presentation
Expand Down Expand Up @@ -70,6 +72,7 @@ mod covers;
mod docs;
mod gate;
mod guard;
mod media;
mod objects;
mod pictures;
mod reading;
Expand Down Expand Up @@ -515,6 +518,7 @@ fn dispatch_in(method: &str, args: &Json, cx: &Ctx) -> Result<Json, String> {
"redo" => slides::history(args, cx, true),
"show" => reading::show(args, cx),
"export" => writing::export(args, cx),
"media" => media::media(args, cx),
other => Err(format!("deck.{other} is not a method of the deck service")),
}
}
Expand Down
Loading
Loading