Skip to content

compositing: Drop Painters when they have no WebViews#41144

Merged
mukilan merged 1 commit intoservo:mainfrom
mrobinson:cleanup-painters
Dec 9, 2025
Merged

compositing: Drop Painters when they have no WebViews#41144
mukilan merged 1 commit intoservo:mainfrom
mrobinson:cleanup-painters

Conversation

@mrobinson
Copy link
Copy Markdown
Member

@mrobinson mrobinson commented Dec 8, 2025

Remove Painters from the Compositor when their final WebView is
dropped. This ensures that their resources are properly released and
deinitialized, such as the WebRender instance and all of its resources.
Without this change, these things leak.

This change requires the ability to handle inexistant Painters during
message handling, as messages from other parts of the Constellation
for a Painter can arrive after it has been released. We don't really
need to do this for Compositor API calls because they are triggered
via the WebView. When a WebView is alive its Painter should always
also be alive, so the expect() call still makes sense in that case.

Part of this also involves removing the redundant RemoveWebView
message to the Compositor. This is handled now in the WebView Drop
implementation for both simplicity and necessity.

Testing: This should not change observable behavior, but does fix a memory leak
while closing windows.

@servo-highfive servo-highfive added the S-awaiting-review There is new code that needs to be reviewed. label Dec 8, 2025
@mrobinson mrobinson requested a review from mukilan December 8, 2025 16:38
@codecov-commenter
Copy link
Copy Markdown

❌ 5 Tests Failed:

Tests completed Failed Passed Skipped
815 5 810 0
View the top 3 failed test(s) by shortest run time
libservo::webview::test_create_webview_and_immediately_drop_webview_before_shutdown
Stack Traces | 0.144s run time
thread 'test_create_webview_and_immediately_drop_webview_before_shutdown' (41736) panicked at /root/.cargo.../webrender/src/render_api.rs:1264:23:
called `Result::unwrap()` on an `Err` value: RecvError
stack backtrace:
   0: __rustc::rust_begin_unwind
   1: core::panicking::panic_fmt
   2: core::result::unwrap_failed
   3: webrender::render_api::RenderApi::shut_down
   4: <compositing::painter::Painter as core::ops::drop::Drop>::drop
   5: core::ptr::drop_in_place<compositing::painter::Painter>
   6: alloc::rc::Rc<T,A>::drop_slow
   7: alloc::vec::Vec<T,A>::retain
   8: compositing::compositor::IOCompositor::remove_webview
   9: core::ptr::drop_in_place<core::cell::RefCell<servo::webview::WebViewInner>>
  10: alloc::rc::Rc<T,A>::drop_slow
  11: core::ops::function::FnOnce::call_once
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
libservo::webview::test_resize_webview_zero
Stack Traces | 0.196s run time
thread 'test_resize_webview_zero' (42879) panicked at /root/.cargo.../webrender/src/render_api.rs:1264:23:
called `Result::unwrap()` on an `Err` value: RecvError
stack backtrace:
   0: __rustc::rust_begin_unwind
   1: core::panicking::panic_fmt
   2: core::result::unwrap_failed
   3: webrender::render_api::RenderApi::shut_down
   4: <compositing::painter::Painter as core::ops::drop::Drop>::drop
   5: core::ptr::drop_in_place<compositing::painter::Painter>
   6: alloc::rc::Rc<T,A>::drop_slow
   7: alloc::vec::Vec<T,A>::retain
   8: compositing::compositor::IOCompositor::remove_webview
   9: core::ptr::drop_in_place<core::cell::RefCell<servo::webview::WebViewInner>>
  10: alloc::rc::Rc<T,A>::drop_slow
  11: core::ops::function::FnOnce::call_once
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
libservo::webview::test_negative_resize_to_request
Stack Traces | 0.224s run time
thread 'test_negative_resize_to_request' (42713) panicked at /root/.cargo.../webrender/src/render_api.rs:1264:23:
called `Result::unwrap()` on an `Err` value: RecvError
stack backtrace:
   0: __rustc::rust_begin_unwind
   1: core::panicking::panic_fmt
   2: core::result::unwrap_failed
   3: webrender::render_api::RenderApi::shut_down
   4: <compositing::painter::Painter as core::ops::drop::Drop>::drop
   5: core::ptr::drop_in_place<compositing::painter::Painter>
   6: alloc::rc::Rc<T,A>::drop_slow
   7: alloc::vec::Vec<T,A>::retain
   8: compositing::compositor::IOCompositor::remove_webview
   9: core::ptr::drop_in_place<core::cell::RefCell<servo::webview::WebViewInner>>
  10: alloc::rc::Rc<T,A>::drop_slow
  11: core::ops::function::FnOnce::call_once
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
libservo::webview::test_page_zoom
Stack Traces | 0.228s run time
thread 'test_page_zoom' (42728) panicked at /root/.cargo.../webrender/src/render_api.rs:1264:23:
called `Result::unwrap()` on an `Err` value: RecvError
stack backtrace:
   0: __rustc::rust_begin_unwind
   1: core::panicking::panic_fmt
   2: core::result::unwrap_failed
   3: webrender::render_api::RenderApi::shut_down
   4: <compositing::painter::Painter as core::ops::drop::Drop>::drop
   5: core::ptr::drop_in_place<compositing::painter::Painter>
   6: alloc::rc::Rc<T,A>::drop_slow
   7: alloc::vec::Vec<T,A>::retain
   8: compositing::compositor::IOCompositor::remove_webview
   9: core::ptr::drop_in_place<core::cell::RefCell<servo::webview::WebViewInner>>
  10: alloc::rc::Rc<T,A>::drop_slow
  11: core::ops::function::FnOnce::call_once
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
libservo::webview::test_alert_dialog
Stack Traces | 0.468s run time
thread 'test_alert_dialog' (41731) panicked at components/compositing/compositor.rs:263:14:
painter_id not found
stack backtrace:
   0: __rustc::rust_begin_unwind
   1: core::panicking::panic_fmt
   2: core::option::expect_failed
   3: compositing::compositor::IOCompositor::notify_input_event_handled
   4: servo::servo::ServoInner::handle_embedder_message
   5: servo::servo::ServoInner::spin_event_loop
   6: core::ptr::drop_in_place<servo::servo::ServoInner>
   7: alloc::rc::Rc<T,A>::drop_slow
   8: webview::test_simple_dialog
   9: core::ops::function::FnOnce::call_once
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

Copy link
Copy Markdown
Member

@mukilan mukilan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The description says

We don't really need to do this for Compositor API calls because they are triggered via the WebView and there should be no WebViews for the Painter left. The expect() call still makes sense in that case.

I guess for the case of compositor API calls, we expect at least one WebView in the Painter instead of "there should be no WebViews for the Painter left."

) {
let painter_id = webview_id.into();
let painter = self.maybe_painter(painter_id);
let image_keys = (0..pref!(image_key_batch_size))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason we need to return dummy image keys rather than just an empty vector?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. If the ImageCache is blocked waiting on image keys it could deadlock the ScriptThread that is using it. The dummy key ensures that if this is the case the ScriptThread can finish what it was doing, no matter what order things happen.

|| ImageKey::new(painter_id.into(), 0),
|painter| painter.webrender_api.generate_image_key(),
);
let _ = result_sender.send(image_key);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason we need to send a dummy image key, instead of ignoring the request? Since the only reference to sender will be dropped, the receiving end should unblock with an Err, which is transformed into an None in generate_image_key_blocking. I'm worried introducing dummy keys might make it tricky to debug issues.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's better to let the ScriptThread finish execution properly rather than causing a potential panic...this could happen do to ordering issues that are hard to reason about. These ImageKeys will never be used anyway so it is safe to return dummy values.

self.disabled_lcp_for_webviews.insert(webview_id);
}

pub(crate) fn clear(&mut self) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we removing this method, instead of invoking it from the Drop impl of Painter? It is currently being called when Painter is deinited, so shouldn't we retain that behaviour? It would also mean disabled_lcp_for_webviews grows unbounded as it never gets cleared anywhere else AFAICT.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I looked at the code again and the LCP calculator is part of the Painter's state (and not part of Compositor as I originally assumed), so I guess we don't need to destroy it explicitly. However, it still seems like removing a webview from the painter should also remove it from disabled_lcp_for_webviews.

Copy link
Copy Markdown
Member Author

@mrobinson mrobinson Dec 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense. I'll make that change. EDIT: And yeah, to confirm this was just clearing vectors during deinit which happens before Drop anyway, so it was doing useless work.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've gone ahead and done this.

Remove `Painter`s from the `Compositor` when their final `WebView` is
dropped. This ensures that their resources are properly released and
deinitialized, such as the WebRender instance and all of its resources.
Without this change, these things leak.

This change requires the ability to handle inexistant `Painter`s during
message handling, as messages from other parts of the `Constellation`
for a `Painter` can arrive after it has been released. We don't really
need to do this for `Compositor` API calls because they are triggered
via the `WebView` and there should be no `WebView`s for the `Painter`
left. The `expect()` call still makes sense in that case.

Part of this also involves removing the redundant `RemoveWebView`
message to the `Compositor`. This is handled now in the `WebView` `Drop`
implementation for both simplicity and necessity.

Signed-off-by: Martin Robinson <[email protected]>
@mrobinson
Copy link
Copy Markdown
Member Author

The description says

We don't really need to do this for Compositor API calls because they are triggered via the WebView and there should be no WebViews for the Painter left. The expect() call still makes sense in that case.

I guess for the case of compositor API calls, we expect at least one WebView in the Painter instead of "there should be no WebViews for the Painter left."

My original wording was really confusing. I've reworded this to be a bit clearer.

Thanks for the review, @mukilan. I think I've responded to all your review comments. PTAL.

@servo-highfive servo-highfive removed the S-awaiting-review There is new code that needs to be reviewed. label Dec 9, 2025
@mukilan mukilan added this pull request to the merge queue Dec 9, 2025
@servo-highfive servo-highfive added the S-awaiting-merge The PR is in the process of compiling and running tests on the automated CI. label Dec 9, 2025
Merged via the queue into servo:main with commit 1d0099e Dec 9, 2025
29 checks passed
@servo-highfive servo-highfive removed the S-awaiting-merge The PR is in the process of compiling and running tests on the automated CI. label Dec 9, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants