fix(browser): address activation and document review regressions

This commit is contained in:
kevin
2026-10-08 16:34:59 +08:00
committed by lanyue-llk
parent fba5a4253f
commit b731d64154
8 changed files with 223 additions and 112 deletions
+15 -14
View File
@@ -70,22 +70,23 @@ where
/// longer fit. Retained entries keep their original insertion order.
pub fn set_limits(&mut self, limits: ByteLimits) -> Vec<(K, V)> {
self.limits = limits;
let oversized = self
.insertion_order
.iter()
.filter(|key| {
self.entries
.get(*key)
.is_some_and(|entry| entry.byte_len > limits.max_entry_bytes)
})
.cloned()
.collect::<Vec<_>>();
let mut evicted = Vec::new();
for key in oversized {
if let Some(value) = self.remove(&key) {
evicted.push((key, value));
// Filter the order once. Calling remove for every oversized entry
// would rescan the whole queue on each removal when budgets shrink.
self.insertion_order.retain(|key| {
if self
.entries
.get(key)
.is_none_or(|entry| entry.byte_len <= limits.max_entry_bytes)
{
return true;
}
}
if let Some(entry) = self.entries.remove(key) {
self.used_bytes -= entry.byte_len;
evicted.push((key.clone(), entry.value));
}
false
});
while self.used_bytes > limits.max_total_bytes {
if let Some(entry) = self.pop_oldest() {
evicted.push(entry);
+16
View File
@@ -1,5 +1,21 @@
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));
+26 -1
View File
@@ -573,14 +573,39 @@ pub(crate) fn build_inline_fragments(
.iter()
.map(text_style_paint_outsets)
.collect::<Vec<_>>();
// Inline objects occupy positions between text units, not text bytes.
// Retain their logical offsets so spaces before an atomic box cannot be
// mistaken for line-end whitespace (including in bidi-reordered lines).
let atomic_offsets: HashMap<_, _> = layout
.inline_boxes()
.filter(|inline_box| {
context
.object(inline_box.id)
.is_some_and(|object| object.role == InlineObjectRole::Atomic)
})
.map(|inline_box| (inline_box.id, inline_box.index))
.collect();
for (line_index, line) in layout.lines().enumerate() {
let metrics = line.metrics();
let line_range = line.text_range();
let last_atomic_offset = line
.items()
.filter_map(|item| match item {
PositionedLayoutItem::InlineBox(positioned) => {
atomic_offsets.get(&positioned.id).copied()
}
PositionedLayoutItem::GlyphRun(_) => None,
})
.max()
.unwrap_or(line_range.start);
let trailing_start = overlapping_output_ranges(&context.text_units, &line_range)
.iter()
.rev()
.take_while(|unit| unit.control || unit.collapsible_whitespace)
.take_while(|unit| {
unit.output_range.start >= last_atomic_offset
&& (unit.control || unit.collapsible_whitespace)
})
.filter(|unit| unit.collapsible_whitespace)
.map(|unit| unit.output_range.start)
.last();
+41 -31
View File
@@ -773,37 +773,7 @@ where
let bottom =
last.final_layout.location.y - last_offset.y + last.final_layout.size.height;
let rect = LayoutRect::new(content.x, top, content.width, (bottom - top).max(0.0));
// A zero-area leading continuation follows escaped leading
// margins into the first block's flow position. It carries no
// line strut of its own and must not remain at the pre-collapse y.
for id in &self.boxes[run.preceding_inline.index()].fragments {
let existing = &mut self.fragments[id.index()];
if matches!(existing.kind, LayoutFragmentKind::InlineBox { .. })
&& existing.rect.width == 0.0
&& existing.rect.height == 0.0
{
let original = self.coordinate_spaces[existing.coordinate_space.index()]
.layout
.local_to_document_ignoring_css_transforms
.map_point(LayoutPoint::new(existing.rect.x, existing.rect.y));
let x = self.coordinate_spaces[parent.index() + 1]
.layout
.local_to_document_ignoring_css_transforms
.inverse()
.map(|inverse| inverse.map_point(original).x)
.unwrap_or(existing.rect.x);
let boundary = LayoutRect::new(x, top, 0.0, 0.0);
existing.rect = boundary;
existing.coordinate_space =
LayoutCoordinateSpaceId::from_index(parent.index() + 1);
existing.box_model = Some(LayoutFragmentBoxModel {
content: boundary,
padding: boundary,
border: boundary,
margin: boundary,
});
}
}
self.reposition_empty_inline_continuation(run.preceding_inline, parent, top);
let fragment = self.push_fragment(LayoutFragment {
id: LayoutFragmentId::from_index(0),
kind: LayoutFragmentKind::Box {
@@ -824,6 +794,46 @@ where
}
}
/// An empty leading continuation follows escaped margins into the first
/// block's flow position; it carries no independent line strut.
fn reposition_empty_inline_continuation(
&mut self,
inline: LayoutBoxId,
parent: LayoutBoxId,
top: f32,
) {
let coordinate_space = LayoutCoordinateSpaceId::from_index(parent.index() + 1);
let document_to_parent = self.coordinate_spaces[coordinate_space.index()]
.layout
.local_to_document_ignoring_css_transforms
.inverse();
for id in &self.boxes[inline.index()].fragments {
let fragment = &mut self.fragments[id.index()];
if !matches!(fragment.kind, LayoutFragmentKind::InlineBox { .. })
|| fragment.rect.width != 0.0
|| fragment.rect.height != 0.0
{
continue;
}
let original = self.coordinate_spaces[fragment.coordinate_space.index()]
.layout
.local_to_document_ignoring_css_transforms
.map_point(LayoutPoint::new(fragment.rect.x, fragment.rect.y));
let x = document_to_parent
.map(|inverse| inverse.map_point(original).x)
.unwrap_or(fragment.rect.x);
let boundary = LayoutRect::new(x, top, 0.0, 0.0);
fragment.rect = boundary;
fragment.coordinate_space = coordinate_space;
fragment.box_model = Some(LayoutFragmentBoxModel {
content: boundary,
padding: boundary,
border: boundary,
margin: boundary,
});
}
}
fn split_block_relative_inset_basis(&self, parent: LayoutBoxId) -> taffy::Size<Option<f32>> {
let content = self.boxes[parent.index()].content_box;
let Some(inputs) = self
+29 -30
View File
@@ -103,45 +103,44 @@ impl PreparedWorldLayout {
N: Copy + Debug + Eq + Hash,
{
assert!(self.feedback_invalidation_worklist.is_empty());
if viewport_changed {
for layout_box in &mut world.boxes {
layout_box.clear_layout_caches();
}
world.viewport_layout.cache.clear();
return world.boxes.len();
}
if changed_boxes.is_empty() {
return 0;
}
self.feedback_invalidation_marks
.resize(world.boxes.len(), false);
let mut invalidated = Vec::new();
if viewport_changed {
for index in 0..world.boxes.len() {
let id = LayoutBoxId::from_index(index);
self.feedback_invalidation_marks[index] = true;
invalidated.push(id);
world.boxes[index].clear_layout_caches();
self.feedback_invalidation_worklist
.extend(changed_boxes.iter().copied());
while let Some(id) = self.feedback_invalidation_worklist.pop() {
if self.feedback_invalidation_marks[id.index()] {
continue;
}
} else {
self.feedback_invalidation_marks[id.index()] = true;
invalidated.push(id);
world.boxes[id.index()].clear_layout_caches();
self.feedback_invalidation_worklist
.extend(changed_boxes.iter().copied());
while let Some(id) = self.feedback_invalidation_worklist.pop() {
if self.feedback_invalidation_marks[id.index()] {
continue;
}
self.feedback_invalidation_marks[id.index()] = true;
invalidated.push(id);
world.boxes[id.index()].clear_layout_caches();
self.feedback_invalidation_worklist
.extend(world.boxes[id.index()].layout_children.iter().copied());
}
.extend(world.boxes[id.index()].layout_children.iter().copied());
}
for changed in changed_boxes.iter().copied() {
let mut ancestor = world.boxes[changed.index()].layout_parent;
while let Some(id) = ancestor {
if !self.feedback_invalidation_marks[id.index()] {
self.feedback_invalidation_marks[id.index()] = true;
invalidated.push(id);
world.boxes[id.index()].clear_layout_caches();
}
ancestor = world.boxes[id.index()].layout_parent;
for changed in changed_boxes.iter().copied() {
let mut ancestor = world.boxes[changed.index()].layout_parent;
while let Some(id) = ancestor {
if !self.feedback_invalidation_marks[id.index()] {
self.feedback_invalidation_marks[id.index()] = true;
invalidated.push(id);
world.boxes[id.index()].clear_layout_caches();
}
ancestor = world.boxes[id.index()].layout_parent;
}
}
if viewport_changed || !changed_boxes.is_empty() {
world.viewport_layout.cache.clear();
}
world.viewport_layout.cache.clear();
for id in invalidated.iter().copied() {
self.feedback_invalidation_marks[id.index()] = false;
}
+47 -4
View File
@@ -5,9 +5,9 @@ use moli_layout::{
LayoutElementSemantics, LayoutEnvironment, LayoutError, LayoutFlushReason, LayoutFragmentKind,
LayoutNamespace, LayoutPaintedSurfaceHit, LayoutPassRequest, LayoutPassResult, LayoutPoint,
LayoutPosition, LayoutQuery, LayoutQueryAnswer, LayoutQueryBatch, LayoutRect,
LayoutScrollbarColors, LayoutScrollbarGutter, LayoutScrollbarPart, LayoutScrollbarWidth,
LayoutSource, LayoutSourceKind, LayoutStyleResolver, LayoutTransform2D, LayoutViewport,
PaintBrush, PaintCaptureRequest, PaintColor, PaintFragment, PaintShape,
LayoutReplacedKind, LayoutScrollbarColors, LayoutScrollbarGutter, LayoutScrollbarPart,
LayoutScrollbarWidth, LayoutSource, LayoutSourceKind, LayoutStyleResolver, LayoutTransform2D,
LayoutViewport, PaintBrush, PaintCaptureRequest, PaintColor, PaintFragment, PaintShape,
ResolvedLayoutElementStyles, ResolvedLayoutStyle, build_layout_pass,
};
use style::Atom;
@@ -91,7 +91,7 @@ impl LayoutSource for Source {
LayoutNamespace::Html,
self.0[node].local_name,
LayoutElementCategory::Generic,
None,
(self.0[node].local_name == "img").then_some(LayoutReplacedKind::Image),
)
})
}
@@ -2809,6 +2809,49 @@ fn inline_output_preserves_line_text_and_utf16_source_fragments() {
assert_eq!(scroll_geometry.scroll_containers.len(), 1);
}
#[test]
fn collapsed_space_before_atomic_inline_keeps_its_range_geometry() {
for local_name in ["span", "img"] {
for trailing_text in ["", " "] {
let source = Source(vec![
Node::element("root", vec![1, 2, 3]),
Node::text("before", "a "),
Node::html_element("atomic", local_name, vec![]),
Node::text("after", trailing_text),
]);
let mut styles = Styles::default();
styles
.0
.insert(0, fixed_size(LayoutDisplay::Block, 200.0, 80.0));
for node in [1, 3] {
styles
.0
.insert(node, resolved(LayoutDisplay::Inline, Style::default()));
}
let display = if local_name == "img" {
LayoutDisplay::Inline
} else {
LayoutDisplay::InlineBlock
};
styles.0.insert(2, fixed_size(display, 20.0, 20.0));
let output = build(&source, &mut styles);
let space = output.text_range_rects(1, 1..2);
assert_eq!(space.len(), 1, "{local_name}, {trailing_text:?}");
let space = space[0].bounding_rect();
assert!(
space.width > 0.0,
"space before an atomic inline is not trailing"
);
let atomic = output.client_rects_for_source(2)[0].bounding_rect();
assert_close(space.x + space.width, atomic.x);
assert!(
output.text_range_rects(3, 0..1).is_empty(),
"line-end space still collapses"
);
}
}
}
#[test]
fn inline_offset_metrics_use_fragment_bounds_and_keep_empty_sibling_anchors() {
let source = Source(vec![
+45 -30
View File
@@ -1391,24 +1391,39 @@ impl CapturedResponseBodyStore {
self.durable_sessions.insert(session, limits);
} else {
self.durable_sessions.remove(&session);
// Turning off durability revokes only old-document claims, not
// ordinary access to responses from the current document.
for request_id in self.retained_request_ids.clone() {
if let Some(body) = self.bodies.get_mut(&request_id)
&& !body.remove_session_visibility(session_id)
{
self.bodies.remove(&request_id);
}
if let Some(body) = self.buffered_bodies.get_mut(&request_id)
&& !body.remove_session_visibility(session_id)
{
self.buffered_bodies.remove(&request_id);
}
self.revoke_retained_visibility(session_id);
}
for (id, mut body) in self
.buffered_bodies
.set_limits(self.effective_durable_limits())
{
body.mark_evicted();
self.bodies.insert(id, body);
}
self.synchronize_durable_entries();
}
fn revoke_retained_visibility(&mut self, session_id: Option<&str>) {
// Revocation affects only old-document claims, not ordinary access to
// responses from the current document.
for request_id in &self.retained_request_ids {
if let Some(body) = self.bodies.get_mut(request_id)
&& !body.remove_session_visibility(session_id)
{
self.bodies.remove(request_id);
}
if let Some(body) = self.buffered_bodies.get_mut(request_id)
&& !body.remove_session_visibility(session_id)
{
self.buffered_bodies.remove(request_id);
}
}
}
fn effective_durable_limits(&self) -> ByteLimits {
// One target owns one payload store. Multiple clients cannot multiply
// its budget; each requested budget is an upper bound, not a reserve.
let limits = self.durable_sessions.values().fold(
self.durable_sessions.values().fold(
ByteLimits::new(
RESPONSE_BODY_BUFFER_MAX_TOTAL_BYTES,
RESPONSE_BODY_BUFFER_MAX_ENTRY_BYTES,
@@ -1419,26 +1434,26 @@ impl CapturedResponseBodyStore {
acc.max_entry_bytes.min(limits.max_entry_bytes),
)
},
);
for (id, mut body) in self.buffered_bodies.set_limits(limits) {
body.mark_evicted();
self.bodies.insert(id, body);
}
)
}
fn synchronize_durable_entries(&mut self) {
if self.durable_sessions.is_empty() {
self.durable_entry_order.clear();
self.retained_request_ids.clear();
} else {
let mut untracked = self
.bodies
.keys()
.chain(self.buffered_bodies.iter().map(|(id, _)| id))
.filter(|id| !self.durable_entry_order.contains(id))
.cloned()
.collect::<Vec<_>>();
untracked.sort();
self.durable_entry_order.extend(untracked);
self.trim_durable_entries();
return;
}
let tracked: HashSet<_> = self.durable_entry_order.iter().collect();
let mut untracked = self
.bodies
.keys()
.chain(self.buffered_bodies.iter().map(|(id, _)| id))
.filter(|id| !tracked.contains(id))
.cloned()
.collect::<Vec<_>>();
untracked.sort();
self.durable_entry_order.extend(untracked);
self.trim_durable_entries();
}
fn track_durable_entry(&mut self, request_id: &str) {
@@ -489,10 +489,12 @@ fn spawn_repeated_synchronous_xhr_server(
}
Err(error) => panic!("accept repeated synchronous XHR: {error}"),
};
// Accepted sockets inherit nonblocking mode on macOS.
// macOS can inherit O_NONBLOCK from the listening socket.
// This fixture reads the complete request synchronously; the
// socket timeout alone does not turn WouldBlock into a wait.
stream
.set_nonblocking(false)
.expect("use blocking synchronous XHR request I/O");
.expect("make accepted synchronous XHR stream blocking");
stream
.set_read_timeout(Some(Duration::from_secs(2)))
.expect("bound synchronous XHR request read");