Skip to content
Open
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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,9 @@ Layout is an on-demand snapshot rather than continuously maintained state. The
first geometry request (a cold start) builds a working layout tree from the
current DOM/style, freezes its canonical geometry into an immutable,
DOM-independent `FrozenLayoutTree`, and retains only that latest tree. Ordinary
geometry reads may reuse it even if the page has changed. Screenshots always
geometry reads reuse it while layout inputs are unchanged. Input changes mark
the retained tree dirty without running layout; the next geometry request
rebuilds it once from the current inputs. Screenshots always
rebuild and replace the frozen tree. Each screencast subscription remembers
only an opaque visual-state token: an unchanged token suppresses the poll,
while a changed token triggers one fresh frame. Paint results are never reused.
Expand Down
2 changes: 1 addition & 1 deletion docs/README.de.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ Rechenintensive Browser-Operationen sind in Moli standardmäßig deaktiviert und
| `--image`, `--font`, `--audio`, `--video`, `--media`, `--text-track` | Eine bestimmte optionale Ressourcengruppe einzeln aktivieren |
| `--profile-dir`, `--http-cache-dir`, `--cookie-file` | Persistenz gezielt aktivieren, je nach Bedarf des Workloads |

Das Layoutergebnis ist ein bei Bedarf erzeugter Snapshot, kein dauerhaft gepflegter Zustand: Die erste Geometrieanfrage (Kaltstart) baut aus dem aktuellen DOM/Stil einen temporären Arbeitsbaum auf und friert dessen kanonische Geometrie in einen unveränderlichen, vom DOM unabhängigen `FrozenLayoutTree` ein — nur dieser jeweils neueste Baum wird vorgehalten. Normale Geometrieabfragen können ihn auch nach Seitenänderungen weiterverwenden; Screenshots und Screencasts dagegen bauen jedes Mal neu auf, ersetzen den eingefrorenen Baum und greifen nie auf alte Zeichenergebnisse zurück.
Das Layoutergebnis ist ein bei Bedarf erzeugter Snapshot, kein dauerhaft gepflegter Zustand: Die erste Geometrieanfrage (Kaltstart) baut aus dem aktuellen DOM/Stil einen temporären Arbeitsbaum auf und friert dessen kanonische Geometrie in einen unveränderlichen, vom DOM unabhängigen `FrozenLayoutTree` ein — nur dieser jeweils neueste Baum wird vorgehalten. Normale Geometrieabfragen verwenden ihn wieder, solange die Layouteingaben unverändert sind. Änderungen markieren den Baum als veraltet, ohne sofort ein Layout auszuführen; erst die nächste Geometrieanfrage baut ihn neu auf. Screenshots und Screencasts dagegen bauen jedes Mal neu auf, ersetzen den eingefrorenen Baum und greifen nie auf alte Zeichenergebnisse zurück.

## Architektur

Expand Down
2 changes: 1 addition & 1 deletion docs/README.es.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ En Moli, las operaciones caras del navegador hay que activarlas explícitamente:
| `--image`, `--font`, `--audio`, `--video`, `--media`, `--text-track` | Activa por separado una familia concreta de recursos opcionales |
| `--profile-dir`, `--http-cache-dir`, `--cookie-file` | Activa de forma selectiva la persistencia que necesite tu carga de trabajo |

El resultado del layout es una instantánea tomada bajo demanda, no un estado que se mantiene todo el tiempo: la primera petición de geometría (cold start) construye un árbol de trabajo temporal a partir del DOM y los estilos actuales, congela su geometría canónica en un `FrozenLayoutTree` inmutable e independiente del DOM, y se queda solo con ese árbol, el más reciente. Las lecturas de geometría normales pueden reutilizarlo aunque la página haya cambiado entretanto. Las capturas de pantalla siempre reconstruyen y sustituyen el árbol congelado. Cada suscripción de screencast solo conserva un token opaco del estado visual: un token sin cambios evita el trabajo, mientras que un cambio genera un frame nuevo. Los resultados de pintado nunca se reutilizan.
El resultado del layout es una instantánea tomada bajo demanda, no un estado que se mantiene todo el tiempo: la primera petición de geometría (cold start) construye un árbol de trabajo temporal a partir del DOM y los estilos actuales, congela su geometría canónica en un `FrozenLayoutTree` inmutable e independiente del DOM, y se queda solo con ese árbol, el más reciente. Las lecturas de geometría normales lo reutilizan mientras no cambien las entradas del layout. Los cambios marcan el árbol como desactualizado sin calcular el layout de inmediato; la siguiente petición de geometría lo reconstruye una sola vez. Las capturas de pantalla siempre reconstruyen y sustituyen el árbol congelado. Cada suscripción de screencast solo conserva un token opaco del estado visual: un token sin cambios evita el trabajo, mientras que un cambio genera un frame nuevo. Los resultados de pintado nunca se reutilizan.

## Arquitectura

Expand Down
2 changes: 1 addition & 1 deletion docs/README.fr.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ Dans Moli, les opérations coûteuses du navigateur ne sont jamais activées par
| `--image`, `--font`, `--audio`, `--video`, `--media`, `--text-track` | Active une famille spécifique de ressources optionnelles |
| `--profile-dir`, `--http-cache-dir`, `--cookie-file` | Active, au cas par cas, la persistance dont la charge de travail a besoin |

Le résultat de la mise en page est un instantané produit à la demande, pas un état maintenu en continu : la première requête de géométrie (« à froid ») construit un arbre de travail temporaire à partir du DOM et des styles courants, fige sa géométrie canonique dans un `FrozenLayoutTree` immuable et indépendant du DOM, puis ne conserve que ce dernier. Les lectures de géométrie ultérieures peuvent réutiliser cet arbre figé, même si la page a changé entre-temps. Les captures d'écran reconstruisent toujours et remplacent l'arbre figé. Chaque abonnement de screencast ne mémorise qu'un jeton opaque d'état visuel : un jeton inchangé supprime le travail, tandis qu'un changement produit une image fraîche. Les résultats de rendu ne sont jamais réutilisés.
Le résultat de la mise en page est un instantané produit à la demande, pas un état maintenu en continu : la première requête de géométrie (« à froid ») construit un arbre de travail temporaire à partir du DOM et des styles courants, fige sa géométrie canonique dans un `FrozenLayoutTree` immuable et indépendant du DOM, puis ne conserve que ce dernier. Les lectures de géométrie réutilisent cet arbre tant que les données de mise en page restent inchangées. Une modification marque l’arbre comme périmé sans recalcul immédiat ; la prochaine requête de géométrie le reconstruit une seule fois. Les captures d'écran reconstruisent toujours et remplacent l'arbre figé. Chaque abonnement de screencast ne mémorise qu'un jeton opaque d'état visuel : un jeton inchangé supprime le travail, tandis qu'un changement produit une image fraîche. Les résultats de rendu ne sont jamais réutilisés.

## Architecture

Expand Down
2 changes: 1 addition & 1 deletion docs/README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ Moli では、処理負荷の高い機能は明示的に有効にした場合だ
| `--image`、`--font`、`--audio`、`--video`、`--media`、`--text-track` | 指定した種類のデータを取得します。 |
| `--profile-dir`、`--http-cache-dir`、`--cookie-file` | 必要な保存機能だけを選んで有効にします。 |

レイアウト結果は常に更新される状態ではなく、要求を受けた時点の状態です。最初に位置・寸法を要求されたときは、現在の DOM とスタイルから一時的な作業用レイアウト木を構築し、その確定済みジオメトリを DOM から独立した不変の `FrozenLayoutTree` に凍結して、最新の木だけを保持します。その後はページが変化していても通常の位置・寸法取得で古い木を再利用する場合があります。一方、画面画像と連続画面配信は毎回再構築して凍結木を置き換え、古い描画結果を再利用しません。
レイアウト結果は常に更新される状態ではなく、要求を受けた時点の状態です。最初に位置・寸法を要求されたときは、現在の DOM とスタイルから一時的な作業用レイアウト木を構築し、その確定済みジオメトリを DOM から独立した不変の `FrozenLayoutTree` に凍結して、最新の木だけを保持します。レイアウトの入力が変わらない間は、通常の位置・寸法取得でこの木を再利用します。入力が変わってもすぐにはレイアウトを計算せず、木を古い状態としてマークし、次の位置・寸法要求で一度だけ再構築します。一方、画面画像と連続画面配信は毎回再構築して凍結木を置き換え、古い描画結果を再利用しません。

## 仕組み

Expand Down
2 changes: 1 addition & 1 deletion docs/README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ Moli 是 Lexmount 旗下的开源无头浏览器;Lexmount Browser 则是围绕
| `--image`、`--font`、`--audio`、`--video`、`--media`、`--text-track` | 单独启用某一类可选资源 |
| `--profile-dir`、`--http-cache-dir`、`--cookie-file` | 按工作负载需要,选择性开启持久化能力 |

布局结果是按需采样的一份快照,而不是持续维护的状态:第一次几何请求(冷启动)会根据当前 DOM/样式构建临时工作布局树,再把工作树丢弃后无法重算的几何冻结成一棵与 DOM 无关、不可变的 `FrozenLayoutTree`,长期只保留最新的这一棵树。在此之后,即便页面发生了变化,普通几何读取也可能复用旧树。截图始终重新构建并替换冻结树。每个屏幕串流订阅只记录一个不透明的视觉状态 token:状态未变时不产帧,状态变化时才新鲜渲染一帧;绘制结果不会被复用。
布局结果是按需采样的一份快照,而不是持续维护的状态:第一次几何请求(冷启动)会根据当前 DOM/样式构建临时工作布局树,再把工作树丢弃后无法重算的几何冻结成一棵与 DOM 无关、不可变的 `FrozenLayoutTree`,长期只保留最新的这一棵树。布局输入未变时,普通几何读取复用这棵树;输入变化只标记旧树失效,不立即执行布局,下一次几何请求才按当前输入重新构建一次。截图始终重新构建并替换冻结树。每个屏幕串流订阅只记录一个不透明的视觉状态 token:状态未变时不产帧,状态变化时才新鲜渲染一帧;绘制结果不会被复用。

## 架构

Expand Down
46 changes: 46 additions & 0 deletions moli-bounded-buffer/src/buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,21 @@ where
self.limits
}

/// Changes budgets, returning oversized and then oldest entries that no
/// longer fit. Retained entries keep their original insertion order.
pub fn set_limits(&mut self, limits: ByteLimits) -> Vec<(K, V)> {
Comment thread
lanyue-llk marked this conversation as resolved.
self.limits = limits;
let mut evicted = self.retain_entries(|_, entry| entry.byte_len <= limits.max_entry_bytes);
while self.used_bytes > limits.max_total_bytes {
if let Some(entry) = self.pop_oldest() {
evicted.push(entry);
} else {
break;
}
}
evicted
}

/// Returns the sum of the logical byte charges for retained entries.
pub fn used_bytes(&self) -> usize {
self.used_bytes
Expand Down Expand Up @@ -151,12 +166,43 @@ where
}

/// Removes all entries and releases all byte charges.
///
/// This does not return removed entries; use [`Self::retain`] to take ownership.
pub fn clear(&mut self) {
self.entries.clear();
self.insertion_order.clear();
self.used_bytes = 0;
}

/// Visits each value once in insertion order, returning removed entries in
/// that order and releasing their byte charges. Mutating a value does not
/// change its recorded charge; use [`Self::insert`] to replace its charge.
pub fn retain(&mut self, mut keep: impl FnMut(&K, &mut V) -> bool) -> Vec<(K, V)> {
self.retain_entries(|key, entry| keep(key, &mut entry.value))
}

fn retain_entries(
&mut self,
mut keep: impl FnMut(&K, &mut BufferedValue<V>) -> bool,
) -> Vec<(K, V)> {
let mut removed = Vec::new();
self.insertion_order.retain(|key| {
if self
.entries
.get_mut(key)
.is_some_and(|entry| keep(key, entry))
{
return true;
}
if let Some(entry) = self.entries.remove(key) {
self.used_bytes -= entry.byte_len;
removed.push((key.clone(), entry.value));
}
false
});
removed
}

fn pop_oldest(&mut self) -> Option<(K, V)> {
while let Some(key) = self.insertion_order.pop_front() {
let Some(entry) = self.entries.remove(&key) else {
Expand Down
73 changes: 73 additions & 0 deletions moli-bounded-buffer/src/tests.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,44 @@
use super::{BoundedByteBuffer, ByteLimits, InsertOutcome};

#[test]
fn shrinking_entry_limit_keeps_interleaved_survivors_in_fifo_order() {
let mut buffer = BoundedByteBuffer::new(ByteLimits::new(100, 20));
for (key, bytes) in [(0, 10), (1, 1), (2, 10), (3, 1), (4, 10)] {
buffer.insert(key, key, bytes);
}
assert_eq!(
buffer.set_limits(ByteLimits::new(100, 1)),
vec![(0, 0), (2, 2), (4, 4)]
);
assert_eq!(buffer.used_bytes(), 2);
assert_eq!(buffer.set_limits(ByteLimits::new(1, 1)), vec![(1, 1)]);
assert_eq!(buffer.get(&3), Some(&3));
assert_eq!(buffer.len(), 1);
}

#[test]
fn changing_limits_evicts_oversized_then_oldest_without_reordering_survivors() {
let mut buffer = BoundedByteBuffer::new(ByteLimits::new(20, 10));
buffer.insert("a", 1, 3);
buffer.insert("b", 2, 6);
buffer.insert("c", 3, 3);
assert_eq!(
buffer.set_limits(ByteLimits::new(4, 4)),
vec![("b", 2), ("a", 1)]
);
assert_eq!(buffer.used_bytes(), 3);
assert_eq!(buffer.get(&"c"), Some(&3));
assert!(buffer.set_limits(ByteLimits::new(10, 10)).is_empty());
assert_eq!(
buffer.insert("d", 4, 8),
InsertOutcome::Stored {
evicted: vec![("c", 3)]
}
);
assert_eq!(buffer.set_limits(ByteLimits::new(0, 0)), vec![("d", 4)]);
assert!(buffer.is_empty());
}

#[test]
fn accepts_entries_at_exact_limits() {
let mut buffer = BoundedByteBuffer::new(ByteLimits::new(4, 4));
Expand Down Expand Up @@ -115,3 +154,37 @@ fn remove_and_clear_return_all_byte_charges() {
assert!(buffer.is_empty());
assert_eq!(buffer.used_bytes(), 0);
}

#[test]
fn retain_visits_once_preserves_fifo_and_releases_only_removed_charges() {
let mut buffer = BoundedByteBuffer::new(ByteLimits::new(10, 10));
for key in 0..4 {
let _ = buffer.insert(key, key * 10, 2);
}
let mut visited = Vec::new();
let removed = buffer.retain(|key, value| {
visited.push(*key);
*value += 1;
key % 2 == 0
});
assert_eq!(visited, vec![0, 1, 2, 3]);
assert_eq!(removed, vec![(1, 11), (3, 31)]);
assert_eq!(buffer.used_bytes(), 4);
assert_eq!(buffer.get(&2), Some(&21));
assert_eq!(
buffer.insert(4, 40, 8),
InsertOutcome::Stored {
evicted: vec![(0, 1)]
}
);
assert!(buffer.retain(|_, _| true).is_empty());
assert_eq!(buffer.used_bytes(), 10);
assert_eq!(buffer.retain(|_, _| false), vec![(2, 21), (4, 40)]);
assert_eq!(buffer.used_bytes(), 0);
assert!(buffer.is_empty());
assert!(
buffer
.retain(|_, _| panic!("empty buffer must not visit"))
.is_empty()
);
}
14 changes: 14 additions & 0 deletions moli-cdp-smoke/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,20 @@ The suite covers real CDP-client workflows: `connect_over_cdp`, concurrent brows

## Chromium Behavior Evidence

The `network-durable` group was calibrated on 2026-09-30 against
`/usr/bin/chromium` 145.0.7632.116. Two independent browser WebSockets attach
to the same page with 128,000-byte and 12,000-byte durable budgets. After
three 8,000-byte responses and a cross-document navigation, the large client
retains all bodies and the small client retains only the newest. A second
case checks an 18,000-byte response followed by a 4,000-byte response, then
both cases disable, re-enable and disconnect the small client without
affecting the large client. Each case was observed once on Chromium before
running the same matrix against Moli. Repeated durable `Network.enable` is
accepted by this Chromium build. Chromium shares a collector between flattened
sessions under one browser WebSocket, so this oracle uses independent clients;
focused Rust tests also verify Moli's accounting for sessions on one connection.
Run with `uv run moli-cdp-smoke --group network-durable`.

Any smoke assertion described as Chromium-compatible must be verified against a
real Chromium binary. Do not infer observable behavior solely from the
specification, Chromium source code, an existing Moli test, or intuition.
Expand Down
6 changes: 6 additions & 0 deletions moli-cdp-smoke/moli_cdp_smoke/fixture.py
Original file line number Diff line number Diff line change
Expand Up @@ -1444,6 +1444,12 @@ def _dispatch(self) -> None:
self._send_text("fixture api body")
elif route == "/api-response-body-budget-small":
self._send_text("bounded response body remains readable")
elif route == "/api-durable-body":
size = int(query.get("size", ["0"])[0])
if not 0 <= size <= 128_000:
self.send_error(HTTPStatus.BAD_REQUEST)
else:
self._send_text("x" * size)
elif route == "/api-response-body-budget-oversize":
self._send_common(
HTTPStatus.OK,
Expand Down
Loading
Loading