mirror of
https://github.com/lexmount/moli.git
synced 2026-11-02 00:06:25 +00:00
fix(renderer): release retired child content surfaces
Backport the shared iframe realm-retention fix and its GC regressions.
Adapt mode coverage to main's Mock and OnDemand policies.
Validated with cargo fmt --all, full all-feature Clippy, and
20,081 passing nextest tests (17 existing skips).
(cherry picked from commit 1b40c6f53b)
This commit is contained in:
@@ -33,8 +33,11 @@ struct ChildWindowProxyFacadeContextHandle(DomHandle);
|
||||
#[derive(Default)]
|
||||
pub(in crate::native_bridge::context_host) struct ChildWindowProxyRecords {
|
||||
records: HashMap<DomHandle, ChildWindowProxyRecord>,
|
||||
detached_content_window_wrappers: HashMap<DomHandle, v8::Global<v8::Object>>,
|
||||
detached_content_document_wrappers: HashMap<DomHandle, v8::Global<v8::Object>>,
|
||||
// Live Windows are rooted by `records`; detached projections are traced
|
||||
// through the iframe wrapper's private slots. These identity lookups must
|
||||
// not keep a retired child realm alive after its last author reference.
|
||||
detached_content_window_wrappers: HashMap<DomHandle, v8::Weak<v8::Object>>,
|
||||
detached_content_document_wrappers: HashMap<DomHandle, v8::Weak<v8::Object>>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -360,7 +363,7 @@ impl ChildWindowProxyRecords {
|
||||
) -> Option<v8::Local<'s, v8::Object>> {
|
||||
self.detached_content_document_wrappers
|
||||
.get(&handle)
|
||||
.map(|wrapper| v8::Local::new(scope, wrapper))
|
||||
.and_then(|wrapper| wrapper.to_local(scope))
|
||||
}
|
||||
|
||||
pub(in crate::native_bridge::context_host) fn set_detached_content_document(
|
||||
@@ -370,7 +373,7 @@ impl ChildWindowProxyRecords {
|
||||
document: v8::Local<'_, v8::Object>,
|
||||
) {
|
||||
self.detached_content_document_wrappers
|
||||
.insert(handle, v8::Global::new(scope, document));
|
||||
.insert(handle, v8::Weak::new(scope, document));
|
||||
}
|
||||
|
||||
pub(in crate::native_bridge::context_host) fn detached_content_window<'s>(
|
||||
@@ -380,7 +383,7 @@ impl ChildWindowProxyRecords {
|
||||
) -> Option<v8::Local<'s, v8::Object>> {
|
||||
self.detached_content_window_wrappers
|
||||
.get(&handle)
|
||||
.map(|wrapper| v8::Local::new(scope, wrapper))
|
||||
.and_then(|wrapper| wrapper.to_local(scope))
|
||||
}
|
||||
|
||||
pub(in crate::native_bridge::context_host) fn set_detached_content_window(
|
||||
@@ -390,7 +393,7 @@ impl ChildWindowProxyRecords {
|
||||
window: v8::Local<'_, v8::Object>,
|
||||
) {
|
||||
self.detached_content_window_wrappers
|
||||
.insert(handle, v8::Global::new(scope, window));
|
||||
.insert(handle, v8::Weak::new(scope, window));
|
||||
}
|
||||
|
||||
pub(in crate::native_bridge::context_host) fn clear_detached_content_surfaces(
|
||||
|
||||
@@ -82,12 +82,27 @@ impl ScriptVm {
|
||||
if execution_context_retired {
|
||||
self.cancel_history_traversals_for_retiring_window(execution_context_owner);
|
||||
}
|
||||
if execution_context_retired
|
||||
&& let Some(execution_context_id) = self
|
||||
if execution_context_retired {
|
||||
if let Some(execution_context_id) = self
|
||||
.child_frame_realm_store
|
||||
.context_id_for_local_window_id(retired_owner.local_window_id)
|
||||
{
|
||||
self.destroy_child_default_context(execution_context_id);
|
||||
{
|
||||
self.destroy_child_default_context(execution_context_id);
|
||||
} else {
|
||||
// A synchronous contentDocument/contentWindow getter can
|
||||
// expose a realm before its owner task promotes it. Move
|
||||
// wrappers into that realm's traced graph before retiring
|
||||
// the execution binding below discards strong cache entries.
|
||||
let pending_context = self
|
||||
.prebootstrapped_child_default_contexts
|
||||
.borrow()
|
||||
.values()
|
||||
.find(|context| context.local_window_id == retired_owner.local_window_id)
|
||||
.map(|context| context.context.clone());
|
||||
if let Some(context) = pending_context {
|
||||
self.clear_context_wrapper_cache_for_context_ptr(&context, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
let retired_timer_count = if execution_context_retired {
|
||||
self.document_runtime
|
||||
|
||||
@@ -3,6 +3,7 @@ use super::*;
|
||||
use crate::frame_owner_model::FrameDocumentTaskOwner;
|
||||
|
||||
mod child_documents;
|
||||
mod child_realm_retention;
|
||||
mod child_script_execution;
|
||||
mod main_document_lifecycle;
|
||||
mod page_runtime;
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
use super::*;
|
||||
use moli_page_types::LayoutPolicy;
|
||||
|
||||
fn collected_native_context_count(vm: &mut StandaloneScriptVmHarness) -> usize {
|
||||
// Two collections also process weak handles cleared by the first pass.
|
||||
// No JS WeakRef is dereferenced in the collecting task.
|
||||
for _ in 0..2 {
|
||||
vm.renderer_document_isolate
|
||||
.with_renderer_document_isolate_mut(|isolate| isolate.low_memory_notification());
|
||||
}
|
||||
vm.renderer_document_isolate_heap_usage()
|
||||
.expect("native context inventory")
|
||||
.number_of_native_contexts
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retired_child_content_surfaces_do_not_root_unreachable_realms() {
|
||||
for policy in [LayoutPolicy::Mock, LayoutPolicy::OnDemand] {
|
||||
let mut vm = new_parsed_test_vm(
|
||||
"https://child-content-retention.test/",
|
||||
"<!doctype html><body></body>",
|
||||
);
|
||||
vm.set_layout_policy(policy);
|
||||
let baseline = collected_native_context_count(&mut vm);
|
||||
for _ in 0..4 {
|
||||
vm.eval(
|
||||
r#"
|
||||
(() => {
|
||||
const frame = document.createElement('iframe');
|
||||
document.body.append(frame);
|
||||
const child = frame.contentDocument;
|
||||
child.body.textContent = 'retire this child';
|
||||
if (frame.contentWindow.document !== child) throw new Error('identity');
|
||||
frame.remove();
|
||||
return 'removed';
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("create, expose and remove child content surfaces");
|
||||
vm.prune_stale_child_default_execution_contexts();
|
||||
}
|
||||
assert_eq!(vm.child_frame_realm_store.len(), 0, "{policy:?}");
|
||||
assert_eq!(
|
||||
collected_native_context_count(&mut vm),
|
||||
baseline,
|
||||
"{policy:?}: retiring the frame must release caches that root otherwise unreachable child realms"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retained_child_content_surfaces_preserve_author_state_until_released() {
|
||||
for policy in [LayoutPolicy::Mock, LayoutPolicy::OnDemand] {
|
||||
let mut vm = new_parsed_test_vm(
|
||||
"https://retained-child-content.test/",
|
||||
"<!doctype html><body></body>",
|
||||
);
|
||||
vm.set_layout_policy(policy);
|
||||
let baseline = collected_native_context_count(&mut vm);
|
||||
vm.eval(
|
||||
r#"
|
||||
(() => {
|
||||
const frame = document.createElement('iframe');
|
||||
document.body.append(frame);
|
||||
const child = frame.contentDocument;
|
||||
child.body.innerHTML = '<p id="kept">before</p>';
|
||||
child.authorMarker = 42;
|
||||
globalThis.savedChildDocument = child;
|
||||
globalThis.savedChildWindow = frame.contentWindow;
|
||||
savedChildWindow.authorMarker = 43;
|
||||
globalThis.savedChildNode = child.getElementById('kept');
|
||||
frame.remove();
|
||||
return 'retained';
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("retain author references across child detachment");
|
||||
vm.prune_stale_child_default_execution_contexts();
|
||||
assert!(
|
||||
collected_native_context_count(&mut vm) > baseline,
|
||||
"{policy:?}: an author reference must retain its realm"
|
||||
);
|
||||
assert_eq!(
|
||||
vm.eval(
|
||||
r#"
|
||||
savedChildNode.textContent = 'after';
|
||||
JSON.stringify([
|
||||
savedChildDocument.authorMarker,
|
||||
savedChildWindow.authorMarker,
|
||||
savedChildDocument.getElementById('kept') === savedChildNode,
|
||||
savedChildNode.ownerDocument === savedChildDocument,
|
||||
savedChildDocument.body.textContent
|
||||
])
|
||||
"#,
|
||||
)
|
||||
.expect("retained author state remains observable"),
|
||||
r#"[42,43,true,true,"after"]"#,
|
||||
"{policy:?}"
|
||||
);
|
||||
vm.eval("savedChildDocument = savedChildWindow = savedChildNode = undefined; 'released'")
|
||||
.expect("release the last author references");
|
||||
assert_eq!(
|
||||
collected_native_context_count(&mut vm),
|
||||
baseline,
|
||||
"{policy:?}: caches must not outlive the last author reference"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn promoted_child_content_surfaces_follow_author_reachability() {
|
||||
for policy in [LayoutPolicy::Mock, LayoutPolicy::OnDemand] {
|
||||
let mut vm = new_parsed_test_vm(
|
||||
"https://promoted-child-content.test/",
|
||||
"<!doctype html><body></body>",
|
||||
);
|
||||
vm.set_layout_policy(policy);
|
||||
let baseline = collected_native_context_count(&mut vm);
|
||||
vm.eval(
|
||||
r#"
|
||||
(() => {
|
||||
globalThis.savedFrame = document.createElement('iframe');
|
||||
document.body.append(savedFrame);
|
||||
const child = savedFrame.contentDocument;
|
||||
child.authorMarker = 42;
|
||||
child.body.innerHTML = '<p id="kept">before</p>';
|
||||
globalThis.savedChildNode = child.getElementById('kept');
|
||||
savedChildNode.authorMarker = 43;
|
||||
return 'live';
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("expose live child surfaces");
|
||||
assert!(vm.run_child_realm_materialization_body_for_test().unwrap());
|
||||
assert_eq!(vm.child_frame_realm_store.len(), 1);
|
||||
assert!(collected_native_context_count(&mut vm) > baseline);
|
||||
assert_eq!(
|
||||
vm.eval(
|
||||
"savedFrame.contentDocument === savedChildNode.ownerDocument && \
|
||||
savedFrame.contentWindow.document === savedChildNode.ownerDocument",
|
||||
)
|
||||
.unwrap(),
|
||||
"true",
|
||||
"{policy:?}: live registry owns the surfaces across GC"
|
||||
);
|
||||
vm.eval("savedFrame.remove(); savedFrame = undefined; 'removed'")
|
||||
.unwrap();
|
||||
vm.prune_stale_child_default_execution_contexts();
|
||||
assert_eq!(vm.child_frame_realm_store.len(), 0);
|
||||
assert!(collected_native_context_count(&mut vm) > baseline);
|
||||
assert_eq!(
|
||||
vm.eval(
|
||||
r#"
|
||||
savedChildNode.textContent = 'after';
|
||||
JSON.stringify([
|
||||
savedChildNode.authorMarker,
|
||||
savedChildNode.ownerDocument.authorMarker,
|
||||
savedChildNode.ownerDocument.getElementById('kept') === savedChildNode,
|
||||
savedChildNode.ownerDocument.body.textContent
|
||||
])
|
||||
"#,
|
||||
)
|
||||
.unwrap(),
|
||||
r#"[43,42,true,"after"]"#,
|
||||
"{policy:?}: retaining only a Node preserves its Document and identity"
|
||||
);
|
||||
vm.eval("savedChildNode = undefined; 'released'").unwrap();
|
||||
assert_eq!(
|
||||
collected_native_context_count(&mut vm),
|
||||
baseline,
|
||||
"{policy:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detached_iframe_projection_keeps_its_cached_surfaces_across_gc() {
|
||||
let mut vm = new_storage_test_vm("https://detached-content-cache.test/");
|
||||
vm.eval(
|
||||
r#"
|
||||
(() => {
|
||||
const detached = document.implementation.createHTMLDocument('');
|
||||
globalThis.savedDetachedFrame = detached.createElement('iframe');
|
||||
detached.body.append(savedDetachedFrame);
|
||||
const child = savedDetachedFrame.contentDocument;
|
||||
const win = savedDetachedFrame.contentWindow;
|
||||
child.authorMarker = 42;
|
||||
win.authorMarker = 43;
|
||||
child.body.innerHTML = '<p>kept</p>';
|
||||
return 'cached';
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
collected_native_context_count(&mut vm);
|
||||
assert_eq!(
|
||||
vm.eval(
|
||||
r#"
|
||||
JSON.stringify([
|
||||
savedDetachedFrame.contentDocument.authorMarker,
|
||||
savedDetachedFrame.contentWindow.authorMarker,
|
||||
savedDetachedFrame.contentWindow.document === savedDetachedFrame.contentDocument,
|
||||
savedDetachedFrame.contentDocument.body.textContent
|
||||
])
|
||||
"#,
|
||||
)
|
||||
.unwrap(),
|
||||
r#"[42,43,true,"kept"]"#
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user