Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

compositor: Handle synchronous messages while shutting down #31733

Merged
merged 1 commit into from Mar 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
142 changes: 96 additions & 46 deletions components/compositing/compositor.rs
Expand Up @@ -524,38 +524,38 @@ impl<Window: WindowMethods + ?Sized> IOCompositor<Window> {
}

fn handle_browser_message(&mut self, msg: CompositorMsg) -> bool {
if matches!(msg, CompositorMsg::NewWebRenderFrameReady(..)) {
self.pending_frames -= 1;
}

match (msg, self.shutdown_state) {
(_, ShutdownState::FinishedShuttingDown) => {
match self.shutdown_state {
ShutdownState::NotShuttingDown => {},
ShutdownState::ShuttingDown => {
return self.handle_browser_message_while_shutting_down(msg)
},
ShutdownState::FinishedShuttingDown => {
error!("compositor shouldn't be handling messages after shutting down");
return false;
},
}

(CompositorMsg::ShutdownComplete, _) => {
match msg {
CompositorMsg::ShutdownComplete => {
warn!("Received `ShutdownComplete` while not shutting down.");
Copy link
Member

Choose a reason for hiding this comment

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

The shutdown flow appears to be:

  1. EmbedderEvent::Quit handled by maybe_start_shutting_down. This makes sure the state is set to ShutdownState::ShuttingDown, and sends the exit message to the constellation.
  2. Constellation sends CompositorMsg::ShutdownComplete when it is done shutting down, after a blocking exit call to the font cache thread.

So receiving this message in the ShutdownState::NotShuttingDown state appears impossible, so I think should panic here, or at least error. It appears to me we don't get many issues based on errors, but we do get a lot of issues when servo crashes. So panicking appears to result in more action being taken to address the underlying problems, hence I prefer it. But I know you prefer to log an error so let's at least do that.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, this situation is highly unexpected, but in the end it's a non-fatal error as we are essentially about to quit anyway. This was handled with a warning before this change as well. Regarding crashing versus recovering from errors, I think that we cannot trust the messages that come from other threads / processes, as they may be experiencing runtime errors or be controlled by an attacker. Instead of immediately crashing, I think there is some benefit to attempting to properly handle when this happens. We should obviously be on top of any warnings printed during the execution of Servo, as they are only printed in very unexpected situations.

In my experience working on other browsers though, they are very complex pieces of software. If you turn on debug assertions, it's quite likely that you can hit one simply by browsing the web or running the tests. If the browser crashed every time that happened, it would be pretty unusable.

self.finish_shutting_down();
return false;
},

(
CompositorMsg::ChangeRunningAnimationsState(pipeline_id, animation_state),
ShutdownState::NotShuttingDown,
) => {
CompositorMsg::ChangeRunningAnimationsState(pipeline_id, animation_state) => {
self.change_running_animations_state(pipeline_id, animation_state);
},

(CompositorMsg::SetFrameTree(frame_tree), ShutdownState::NotShuttingDown) => {
CompositorMsg::SetFrameTree(frame_tree) => {
self.set_frame_tree(&frame_tree);
self.send_scroll_positions_to_layout_for_pipeline(&frame_tree.pipeline.id);
},

(CompositorMsg::TouchEventProcessed(result), ShutdownState::NotShuttingDown) => {
CompositorMsg::TouchEventProcessed(result) => {
self.touch_handler.on_event_processed(result);
},

(CompositorMsg::CreatePng(page_rect, reply), ShutdownState::NotShuttingDown) => {
CompositorMsg::CreatePng(page_rect, reply) => {
let res = self.composite_specific_target(CompositeTarget::SharedMemory, page_rect);
if let Err(ref e) = res {
info!("Error retrieving PNG: {:?}", e);
Expand All @@ -566,7 +566,7 @@ impl<Window: WindowMethods + ?Sized> IOCompositor<Window> {
}
},

(CompositorMsg::IsReadyToSaveImageReply(is_ready), ShutdownState::NotShuttingDown) => {
CompositorMsg::IsReadyToSaveImageReply(is_ready) => {
assert_eq!(
self.ready_to_save_state,
ReadyState::WaitingForConstellationReply
Expand All @@ -579,24 +579,20 @@ impl<Window: WindowMethods + ?Sized> IOCompositor<Window> {
self.composite_if_necessary(CompositingReason::Headless);
},

(
CompositorMsg::PipelineVisibilityChanged(pipeline_id, visible),
ShutdownState::NotShuttingDown,
) => {
CompositorMsg::PipelineVisibilityChanged(pipeline_id, visible) => {
self.pipeline_details(pipeline_id).visible = visible;
self.process_animations(true);
},

(CompositorMsg::PipelineExited(pipeline_id, sender), _) => {
CompositorMsg::PipelineExited(pipeline_id, sender) => {
debug!("Compositor got pipeline exited: {:?}", pipeline_id);
self.remove_pipeline_root_layer(pipeline_id);
let _ = sender.send(());
},

(
CompositorMsg::NewWebRenderFrameReady(recomposite_needed),
ShutdownState::NotShuttingDown,
) => {
CompositorMsg::NewWebRenderFrameReady(recomposite_needed) => {
self.pending_frames -= 1;

if recomposite_needed {
if let Some(result) = self.hit_test_at_point(self.cursor_pos) {
self.update_cursor(result);
Expand All @@ -608,13 +604,7 @@ impl<Window: WindowMethods + ?Sized> IOCompositor<Window> {
}
},

(CompositorMsg::Dispatch(func), ShutdownState::NotShuttingDown) => {
// The functions sent here right now are really dumb, so they can't panic.
// But if we start running more complex code here, we should really catch panic here.
func();
},

(CompositorMsg::LoadComplete(_), ShutdownState::NotShuttingDown) => {
CompositorMsg::LoadComplete(_) => {
// If we're painting in headless mode, schedule a recomposite.
if matches!(self.composite_target, CompositeTarget::PngFile(_)) ||
self.exit_after_load
Expand All @@ -623,10 +613,7 @@ impl<Window: WindowMethods + ?Sized> IOCompositor<Window> {
}
},

(
CompositorMsg::WebDriverMouseButtonEvent(mouse_event_type, mouse_button, x, y),
ShutdownState::NotShuttingDown,
) => {
CompositorMsg::WebDriverMouseButtonEvent(mouse_event_type, mouse_button, x, y) => {
let dppx = self.device_pixels_per_page_pixel();
let point = dppx.transform_point(Point2D::new(x, y));
self.on_mouse_window_event_class(match mouse_event_type {
Expand All @@ -636,29 +623,29 @@ impl<Window: WindowMethods + ?Sized> IOCompositor<Window> {
});
},

(CompositorMsg::WebDriverMouseMoveEvent(x, y), ShutdownState::NotShuttingDown) => {
CompositorMsg::WebDriverMouseMoveEvent(x, y) => {
let dppx = self.device_pixels_per_page_pixel();
let point = dppx.transform_point(Point2D::new(x, y));
self.on_mouse_window_move_event_class(DevicePoint::new(point.x, point.y));
},

(CompositorMsg::PendingPaintMetric(pipeline_id, epoch), _) => {
CompositorMsg::PendingPaintMetric(pipeline_id, epoch) => {
self.pending_paint_metrics.insert(pipeline_id, epoch);
},

(CompositorMsg::GetClientWindow(req), ShutdownState::NotShuttingDown) => {
CompositorMsg::GetClientWindow(req) => {
if let Err(e) = req.send(self.embedder_coordinates.window) {
warn!("Sending response to get client window failed ({:?}).", e);
}
},

(CompositorMsg::GetScreenSize(req), ShutdownState::NotShuttingDown) => {
CompositorMsg::GetScreenSize(req) => {
if let Err(e) = req.send(self.embedder_coordinates.screen) {
warn!("Sending response to get screen size failed ({:?}).", e);
}
},

(CompositorMsg::GetScreenAvailSize(req), ShutdownState::NotShuttingDown) => {
CompositorMsg::GetScreenAvailSize(req) => {
if let Err(e) = req.send(self.embedder_coordinates.screen_avail) {
warn!(
"Sending response to get screen avail size failed ({:?}).",
Expand All @@ -667,14 +654,9 @@ impl<Window: WindowMethods + ?Sized> IOCompositor<Window> {
}
},

(CompositorMsg::Forwarded(msg), ShutdownState::NotShuttingDown) => {
CompositorMsg::Forwarded(msg) => {
self.handle_webrender_message(msg);
},

// When we are shutting_down, we need to avoid performing operations
// such as Paint that may crash because we have begun tearing down
// the rest of our resources.
Copy link
Member

Choose a reason for hiding this comment

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

If this comment is still valid, I think it makes sense to copy it to the new location where messages are ignored while shutting down.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think this comment might be invalid now. It refers to Paint which is a pre-WebRender concept as far as my memory serves. The main issue here is that I think we don't want to keep compositing because the other parts of Servo are in a transient state.

(_, ShutdownState::ShuttingDown) => {},
}

true
Expand Down Expand Up @@ -917,6 +899,74 @@ impl<Window: WindowMethods + ?Sized> IOCompositor<Window> {
}
}

/// Handle messages sent to the compositor during the shutdown process. In general,
/// the things the compositor can do in this state are limited. It's very important to
/// answer any synchronous messages though as other threads might be waiting on the
/// results to finish their own shut down process. We try to do as little as possible
/// during this time.
///
/// When that involves generating WebRender ids, our approach here is to simply
/// generate them, but assume they will never be used, since once shutting down the
/// compositor no longer does any WebRender frame generation.
fn handle_browser_message_while_shutting_down(&mut self, msg: CompositorMsg) -> bool {
match msg {
CompositorMsg::ShutdownComplete => {
self.finish_shutting_down();
return false;
},
CompositorMsg::PipelineExited(pipeline_id, sender) => {
debug!("Compositor got pipeline exited: {:?}", pipeline_id);
self.remove_pipeline_root_layer(pipeline_id);
let _ = sender.send(());
},
CompositorMsg::Forwarded(ForwardedToCompositorMsg::Font(
FontToCompositorMsg::AddFontInstance(_, _, sender),
)) => {
let _ = sender.send(self.webrender_api.generate_font_instance_key());
},
CompositorMsg::Forwarded(ForwardedToCompositorMsg::Font(
FontToCompositorMsg::AddFont(_, sender),
)) => {
let _ = sender.send(self.webrender_api.generate_font_key());
},
CompositorMsg::Forwarded(ForwardedToCompositorMsg::Canvas(
CanvasToCompositorMsg::GenerateKey(sender),
)) => {
let _ = sender.send(self.webrender_api.generate_image_key());
},
CompositorMsg::GetClientWindow(sender) => {
if let Err(e) = sender.send(self.embedder_coordinates.window) {
warn!("Sending response to get client window failed ({:?}).", e);
}
},
CompositorMsg::GetScreenSize(sender) => {
if let Err(e) = sender.send(self.embedder_coordinates.screen) {
warn!("Sending response to get screen size failed ({:?}).", e);
}
},
CompositorMsg::GetScreenAvailSize(sender) => {
if let Err(e) = sender.send(self.embedder_coordinates.screen_avail) {
warn!(
"Sending response to get screen avail size failed ({:?}).",
e
);
}
},
CompositorMsg::NewWebRenderFrameReady(_) => {
// Subtract from the number of pending frames, but do not do any compositing.
self.pending_frames -= 1;
Copy link
Member

@gterzian gterzian Mar 19, 2024

Choose a reason for hiding this comment

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

Note: the current code does this even after shutdown has completed, so there is a change of logic, but the change seems to make more sense.

Copy link
Member Author

Choose a reason for hiding this comment

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

This is a really good point that I had overlooked. I agree that it doesn't make sense to keep subtracting one from the pending_frames when shutdown is complete because the count isn't used any longer.

},
CompositorMsg::PendingPaintMetric(pipeline_id, epoch) => {
self.pending_paint_metrics.insert(pipeline_id, epoch);
},

_ => {
debug!("Ignoring message ({:?} while shutting down", msg);
},
}
true
}

/// Queue a new frame in the transaction and increase the pending frames count.
fn generate_frame(&mut self, transaction: &mut Transaction, reason: RenderReasons) {
self.pending_frames += 1;
Expand Down
5 changes: 0 additions & 5 deletions components/shared/compositing/lib.rs
Expand Up @@ -92,10 +92,6 @@ pub enum CompositorMsg {
// sends a reply on the IpcSender, the constellation knows it's safe to
// tear down the other threads associated with this pipeline.
PipelineExited(PipelineId, IpcSender<()>),
/// Runs a closure in the compositor thread.
/// It's used to dispatch functions from webrender to the main thread's event loop.
/// Required to allow WGL GLContext sharing in Windows.
Dispatch(Box<dyn Fn() + Send>),
/// Indicates to the compositor that it needs to record the time when the frame with
/// the given ID (epoch) is painted and report it to the layout of the given
/// pipeline ID.
Expand Down Expand Up @@ -164,7 +160,6 @@ impl Debug for CompositorMsg {
CompositorMsg::PipelineVisibilityChanged(..) => write!(f, "PipelineVisibilityChanged"),
CompositorMsg::PipelineExited(..) => write!(f, "PipelineExited"),
CompositorMsg::NewWebRenderFrameReady(..) => write!(f, "NewWebRenderFrameReady"),
CompositorMsg::Dispatch(..) => write!(f, "Dispatch"),
CompositorMsg::PendingPaintMetric(..) => write!(f, "PendingPaintMetric"),
CompositorMsg::LoadComplete(..) => write!(f, "LoadComplete"),
CompositorMsg::WebDriverMouseButtonEvent(..) => write!(f, "WebDriverMouseButtonEvent"),
Expand Down