ElyShow: a layer comes in 16 MiB pieces; closing a deck drops its waiting layers (R2.20)
This commit is contained in:
6 files changed
+111
-22
No files matched your search
@@ -183,20 +183,37 @@ impl Doc {
|
||||
let head = worker.request(tag::SLIDE_LAYERS, &request)?;
|
||||
let mut unpack = Unpack::new(&head);
|
||||
let (width, height, count) = (unpack.u64()?, unpack.u64()?, unpack.u64()?);
|
||||
// One layer per reply keeps each frame small.
|
||||
// Layers arrive in pieces that keep each frame small.
|
||||
let mut whole = Pack::new().u64(width).u64(height).u64(count);
|
||||
for index in 0..count {
|
||||
let (json, painted) = (unpack.str()?, unpack.u64()?);
|
||||
whole = whole.str(&json).u64(painted);
|
||||
if painted != 0 {
|
||||
let id = worker.doc_id(self)?;
|
||||
let reply = worker.request(tag::SLIDE_LAYER, &Pack::new().u64(id).u64(index).bytes())?;
|
||||
whole = whole.blob(&Unpack::new(&reply).blob()?);
|
||||
whole = whole.blob(&self.slide_layer(worker, index)?);
|
||||
}
|
||||
}
|
||||
crate::show::layers(&whole.bytes())
|
||||
}
|
||||
|
||||
/// Layer `index` of the last layers read, piece by piece.
|
||||
fn slide_layer(&self, worker: &mut Worker, index: u64) -> io::Result<Vec<u8>> {
|
||||
let id = worker.doc_id(self)?;
|
||||
let mut pixels = Vec::new();
|
||||
loop {
|
||||
let ask = Pack::new().u64(id).u64(index).u64(pixels.len() as u64).bytes();
|
||||
let reply = worker.request(tag::SLIDE_LAYER, &ask)?;
|
||||
let mut unpack = Unpack::new(&reply);
|
||||
let (total, piece) = (unpack.u64()?, unpack.blob()?);
|
||||
pixels.extend_from_slice(&piece);
|
||||
if pixels.len() as u64 >= total {
|
||||
return Ok(pixels);
|
||||
}
|
||||
if piece.is_empty() {
|
||||
return Err(io::Error::other(format!("layer {index} stopped at byte {}", pixels.len())));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Saves to `url` as `format`; failure surfaces (LOK-09).
|
||||
pub fn save_as(&self, worker: &mut Worker, url: &str, format: &str) -> io::Result<()> {
|
||||
self.store(worker, url, format, "")
|
||||
|
||||
@@ -238,3 +238,34 @@ fn layers_larger_than_one_frame_arrive_one_by_one() {
|
||||
assert!(layers.list.len() * bytes > crate::ipc::MAX_FRAME, "{} layers of {bytes}", layers.list.len());
|
||||
assert!(layers.list.iter().all(|layer| layer.pixels.len() == bytes), "every layer arrives whole");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_layer_larger_than_one_frame_arrives_in_pieces() {
|
||||
if install().is_none() {
|
||||
eprintln!("SKIP: no pinned LOK build");
|
||||
return;
|
||||
}
|
||||
let (_dir, mut worker, doc) = open_deck();
|
||||
worker.run(&doc, "slide.size", &["10000", "120000", "true"]).expect("a tall slide");
|
||||
worker.run(&doc, "draw.shape", &["rectangle", "true"]).expect("draws");
|
||||
let layers = doc.slide_layers(&mut worker, 0, (1280, 15360)).expect("layers");
|
||||
let (w, h) = layers.size;
|
||||
let bytes = w as usize * h as usize * 4;
|
||||
assert!(bytes > crate::ipc::MAX_FRAME, "one layer of {bytes} bytes");
|
||||
assert!(layers.list.iter().all(|layer| layer.pixels.len() == bytes), "every layer arrives whole");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closing_a_deck_drops_its_waiting_layers() {
|
||||
if install().is_none() {
|
||||
eprintln!("SKIP: no pinned LOK build");
|
||||
return;
|
||||
}
|
||||
let (_dir, mut worker, doc) = open_deck();
|
||||
let id = worker.doc_id(&doc).expect("the id");
|
||||
let ask = crate::ipc::Pack::new().u64(id).u64(0).u64(1920).u64(1080).bytes();
|
||||
worker.request(crate::ipc::tag::SLIDE_LAYERS, &ask).expect("layers wait");
|
||||
worker.close_doc(doc).expect("closes");
|
||||
let late = crate::ipc::Pack::new().u64(id).u64(0).u64(0).bytes();
|
||||
assert!(worker.request(crate::ipc::tag::SLIDE_LAYER, &late).is_err(), "no layer outlives its deck");
|
||||
}
|
||||
@@ -347,6 +347,7 @@ fn close(id: u64) {
|
||||
}
|
||||
docs.remove(&id);
|
||||
revision::forget(id);
|
||||
slides::forget(id);
|
||||
log::info!("lok-worker: {} documents open", docs.len());
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
use std::{cell::RefCell, collections::HashMap, io};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
io,
|
||||
sync::{LazyLock, Mutex, MutexGuard},
|
||||
};
|
||||
|
||||
use super::with_doc;
|
||||
use crate::ipc::{Pack, Unpack};
|
||||
@@ -6,9 +10,20 @@ use crate::ipc::{Pack, Unpack};
|
||||
/// The most a slide's layers may take in memory.
|
||||
const LAYER_MEMORY: usize = 512 * 1024 * 1024;
|
||||
|
||||
// Rendered layers wait here; each leaves on its own request.
|
||||
thread_local! {
|
||||
static WAITING: RefCell<HashMap<u64, Vec<Option<Vec<u8>>>>> = RefCell::new(HashMap::new());
|
||||
/// The most pixel bytes one reply carries.
|
||||
const CHUNK: usize = 16 * 1024 * 1024;
|
||||
|
||||
/// Rendered layers by document; each leaves as it is read.
|
||||
type Waiting = HashMap<u64, Vec<Option<Vec<u8>>>>;
|
||||
|
||||
fn waiting() -> MutexGuard<'static, Waiting> {
|
||||
static WAITING: LazyLock<Mutex<Waiting>> = LazyLock::new(Mutex::default);
|
||||
WAITING.lock().expect("the layers lock")
|
||||
}
|
||||
|
||||
/// Drops document `id`'s waiting layers.
|
||||
pub(super) fn forget(id: u64) {
|
||||
waiting().remove(&id);
|
||||
}
|
||||
|
||||
/// Slide `part`'s layers, its pixels held back.
|
||||
@@ -38,23 +53,36 @@ pub(super) fn slide_layers(unpack: &mut Unpack) -> io::Result<Vec<u8>> {
|
||||
body = body.str(&layer.json).u64(u64::from(layer.pixels.is_some()));
|
||||
pixels.push(layer.pixels);
|
||||
}
|
||||
WAITING.with(|waiting| waiting.borrow_mut().insert(id, pixels));
|
||||
waiting().insert(id, pixels);
|
||||
Ok(body.bytes())
|
||||
})?
|
||||
}
|
||||
|
||||
/// Hands over layer `index`'s pixels once, then frees them.
|
||||
/// Hands over layer `index` from byte `from`; read layers free.
|
||||
pub(super) fn slide_layer(unpack: &mut Unpack) -> io::Result<Vec<u8>> {
|
||||
let (id, index) = (unpack.u64()?, unpack.u64()?);
|
||||
let taken = WAITING.with(|waiting| {
|
||||
let mut waiting = waiting.borrow_mut();
|
||||
let layers = waiting.get_mut(&id)?;
|
||||
let pixels = layers.get_mut(usize::try_from(index).ok()?)?.take();
|
||||
if layers.iter().all(Option::is_none) {
|
||||
waiting.remove(&id);
|
||||
}
|
||||
pixels
|
||||
});
|
||||
let pixels = taken.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, format!("no layer {index} waits")))?;
|
||||
Ok(Pack::new().blob(&pixels).bytes())
|
||||
let (id, index, from) = (unpack.u64()?, unpack.u64()?, unpack.u64()?);
|
||||
let mut waiting = waiting();
|
||||
let Some((total, bytes)) = piece(waiting.get_mut(&id), index, from) else {
|
||||
// A request off the batch ends it.
|
||||
waiting.remove(&id);
|
||||
let what = format!("no layer {index} from byte {from} waits");
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidInput, what));
|
||||
};
|
||||
if waiting.get(&id).is_some_and(|layers| layers.iter().all(Option::is_none)) {
|
||||
waiting.remove(&id);
|
||||
}
|
||||
Ok(Pack::new().u64(total).blob(&bytes).bytes())
|
||||
}
|
||||
|
||||
/// Up to `CHUNK` bytes of a layer and its length.
|
||||
fn piece(layers: Option<&mut Vec<Option<Vec<u8>>>>, index: u64, from: u64) -> Option<(u64, Vec<u8>)> {
|
||||
let slot = layers?.get_mut(usize::try_from(index).ok()?)?;
|
||||
let pixels = slot.as_ref()?;
|
||||
let from = usize::try_from(from).ok().filter(|from| *from < pixels.len() || *from == 0)?;
|
||||
let to = (from + CHUNK).min(pixels.len());
|
||||
let read = (pixels.len() as u64, pixels[from..to].to_vec());
|
||||
if to == pixels.len() {
|
||||
*slot = None;
|
||||
}
|
||||
Some(read)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
FAIL
|
||||
|
||||
crates/ely-lok/src/worker/slides.rs:59 — 单个图层仍整体装进一帧,逐层回复仍会触发 64 MiB 帧上限,违反 SHOW-09 的动画预览要求。具体复现:真实 worker 打开测试 DECK,执行 `slide.size(10000, 120000, true)`,得到界面允许的 10 × 120 cm 页面;按 `apps/ely-show/src/preview.rs:67` 的尺寸公式请求 `SLIDE_LAYERS(doc, 0, 1280, 15360)`,返回两个有像素的图层,每层 78,643,200 字节;首个 `SLIDE_LAYER` 返回 ERR:`a 78643204 byte frame is over the 67108864 byte cap`,客户端在 `client/doc.rs:193` 提前返回,预览失败。另以普通比例请求 6000 × 4500,内核实际返回 6000 × 3375,同样报 81,000,004 字节帧超限。修复方向:单层像素也按帧容量分块传输,计入长度前缀,并增加单层超过上限的回归用例。独立执行的 worker 构建、4 项 motion 测试、3 项 playlist 测试及注释检查均通过,现有新增测试覆盖的是总量超过上限、各单层仍小于上限的情况。复现日志:`/tmp/ely-r220-portrait.log`、`/tmp/ely-r220-probe.log`。
|
||||
|
||||
crates/ely-lok/src/worker/slides.rs:41 — 新增的 WAITING 暂存集合缺少文档关闭和取层失败时的清理,违反 LOK-10 的关闭资源释放要求。具体复现:上述合法长页面取首层失败后,客户端通过 `?` 退出,第二层的 78,643,200 字节仍留在 WAITING;发送 `CLOSE_DOC` 后,再按旧文档编号取第二层,仍得到同一超限错误,证明关闭后像素仍被持有。另以 1920 × 1080 请求图层后直接关闭文档,再发旧编号的 `SLIDE_LAYER`,实际返回 OK 和 8,294,404 字节,确认暂存数据跨越文档生命周期。`worker.rs:343` 的 close 仅清理文档、事件和 revision,WAITING 只在逐层取尽时移除;连续打开、预览失败、关闭会按每次遗留一层持续累积内存。修复方向:把暂存图层纳入文档生命周期,在关闭及取回失败或取消时释放整批数据,拒绝已关闭会话的取层请求,并覆盖失败后关闭的回归路径。复现日志:`/tmp/ely-r220-portrait.log`、`/tmp/ely-r220-probe.log`。
|
||||
@@ -0,0 +1,7 @@
|
||||
FAIL
|
||||
|
||||
apps/ely-pdf/src/edits.rs:102 — 编辑后的尺寸表只含前 100 页的实测值,其余页由 sizes() 填成第一页尺寸;这里将它与后台补读完整的 self.sizes 比较,混合纸张文档会进入 retire_tiles(),立即清空整屏旧图块。复现:生成 101 页 PDF,前 100 页为 612×792,第 101 页为 612×900;等尺寸补读与第一页绘制完成,在第一页添加便签。真实 worker 探针连续两次捕获 edited == 1 且 tiles.is_empty(),违反 R2.6。修复方向:保留已测量的尺寸,根据真实页面结构和几何变化决定失效,覆盖超过 100 页的混合尺寸文档。证据:/tmp/ely-r26-probes-repeat.log。
|
||||
|
||||
apps/ely-pdf/src/pages.rs:196 — stale 图块仍按当前布局的完整 TileKey 淘汰,scale 变化会让保留机制失效。复现:单页 PDF 使用默认适合宽度,点击便签工具,在页面点击并等右侧属性栏和图块稳定,再提交便签。refresh() 清掉 aim,属性栏收起,显示比例从 2.2026143 变为 2.72549;旧比例图块被 tiles.retain() 和 stale.retain() 淘汰,探针捕获 painting == true、tiles 为空、stale 为空的状态,页面继续闪白。加入布局稳定等待后仍复现,违反 R2.6。修复方向:处理编辑落地引起的布局变化,保留可绘制的旧页面图块直到新布局图块到达,并覆盖属性栏提交便签的实际路径。证据:/tmp/ely-r26-probes-repeat.log。
|
||||
|
||||
apps/ely-pdf/src/tests/input.rs:210 — 新增回归只观察缓存是否为空及 Arc 地址是否更换,无法验证旧图块实际进入绘制,也无法验证新画面的内容。隔离副本中将 pages.rs 的 Some(image) 绘制分支加上 !self.stale.contains(&key) 条件,对 stale 图块直接跳过绘制;日志确认该分支反复执行,原测试 an_edit_keeps_the_page_drawn_until_its_repaint_lands 仍通过。这个变异会在重绘期间显示纸张背景,直接破坏 R2.6。修复方向:验证重绘等待期间每帧的实际页面绘制,以及更新后批注或可视签名区域的像素;让跳过旧图块绘制的变异触发失败。基线应用和 worker 构建通过,ely-pdf 原有 115 项测试及注释检查通过;变异证据:/tmp/ely-r26-mutation.log。探针源码副本及独立 CARGO_TARGET_DIR 已在写入结论前删除。
|
||||
Reference in new issue
Block a user