{"id":2837,"date":"2017-05-01T12:05:46","date_gmt":"2017-05-01T17:05:46","guid":{"rendered":"https:\/\/mikeconley.ca\/blog\/?p=2837"},"modified":"2023-12-20T16:25:10","modified_gmt":"2023-12-20T21:25:10","slug":"making-tabs-close-faster-in-multi-process-firefox","status":"publish","type":"post","link":"https:\/\/mikeconley.ca\/blog\/2017\/05\/01\/making-tabs-close-faster-in-multi-process-firefox\/","title":{"rendered":"Making tabs close faster in multi-process Firefox"},"content":{"rendered":"<p><strong>TL;DR:<\/strong> In <a href=\"https:\/\/bugzilla.mozilla.org\/show_bug.cgi?id=1336763\">bug 1336763<\/a>, I have landed a series of patches that should hopefully make tab closing faster for the majority of cases for users that are using multi-process Firefox.<\/p>\n<p>The rest of this blog post tries to explain why.<\/p>\n<h2>The beforeunload event handler<\/h2>\n<p>Perhaps you\u2019ve seen this dialog before:<\/p>\n<div id=\"attachment_2838\" style=\"width: 1188px\" class=\"wp-caption alignnone\"><img loading=\"lazy\" decoding=\"async\" aria-describedby=\"caption-attachment-2838\" data-attachment-id=\"2838\" data-permalink=\"https:\/\/mikeconley.ca\/blog\/2017\/05\/01\/making-tabs-close-faster-in-multi-process-firefox\/screen-shot-2017-05-01-at-1-33-10-am\/\" data-orig-file=\"https:\/\/mikeconley.ca\/blog\/wp-content\/uploads\/2017\/05\/Screen-Shot-2017-05-01-at-1.33.10-AM.png\" data-orig-size=\"1178,298\" data-comments-opened=\"1\" data-image-meta=\"{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}\" data-image-title=\"beforeunload dialog\" data-image-description=\"\" data-image-caption=\"&lt;p&gt;Are you sure?&lt;\/p&gt;\n\" data-large-file=\"https:\/\/mikeconley.ca\/blog\/wp-content\/uploads\/2017\/05\/Screen-Shot-2017-05-01-at-1.33.10-AM.png\" class=\"size-full wp-image-2838\" src=\"https:\/\/mikeconley.ca\/blog\/wp-content\/uploads\/2017\/05\/Screen-Shot-2017-05-01-at-1.33.10-AM.png\" alt=\"The beforeunload dialog in Firefox\" width=\"1178\" height=\"298\" \/><p id=\"caption-attachment-2838\" class=\"wp-caption-text\">Are you sure?<\/p><\/div>\n<p>This dialog shows up when a website that you\u2019ve interacted with (or one of its subframes) has set an event handler for the <code>beforeunload<\/code> event, and you attempt to close the tab or browse away from the website.<\/p>\n<p><a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/Events\/beforeunload\">Here\u2019s the documentation<\/a> for how <code>beforeunload<\/code> works, but long story short, you can do a thing in the event handler that will cause the browser UI to show that dialog, which means giving the user the opportunity to cancel their request to close or navigate away from the website<sup id=\"rf1-2837\"><a href=\"#fn1-2837\" title=\"It\u2019s not always necessary for the &lt;code&gt;beforeunload&lt;\/code&gt; event handler to show the dialog. The event handler needs to set the &lt;code&gt;returnValue&lt;\/code&gt; property on the event to a string in order for the dialog to show, but plenty of other stuff can happen in that event handler.\" rel=\"footnote\">1<\/a><\/sup>.<\/p>\n<p>In any event<sup id=\"rf2-2837\"><a href=\"#fn2-2837\" title=\"Ooooh pun intended\" rel=\"footnote\">2<\/a><\/sup>, if a page is going to be unloaded, and that page (or one of its subframes) has set one or more <code>beforeunload<\/code> event handlers, then it is necessary to run those event handlers to see if we\u2019re going to show the dialog, or go ahead and unload the page straight away.<\/p>\n<h2>How multi-process Firefox used to handle beforeunload<\/h2>\n<p>When closing a tab in multi-process Firefox, what we\u2019ve been doing is sending a message to the content process for that tab to check for (and run any) <code>beforeunload<\/code> event handlers. The parent sends that message, and then just kinda waits for the content process to respond with whether or not the close should occur. If the content process doesn\u2019t respond within 5 seconds (I know), then we consider it a wash, and just close the tab.<\/p>\n<p>The content process is sometimes doing stuff on the main thread, and sometimes it\u2019s just waiting for messages from the parent. In the latter case, tab closes happen pretty smoothly &#8211; the message comes in, <code>beforeunload<\/code> events are fired (and hopefully those don\u2019t take too long, but you never know), and then hopefully a result goes up to the parent, and it can move on.<\/p>\n<p>That\u2019s the best case scenario &#8211; but <strong>lots of things can prevent the best case scenario<\/strong>; for one thing, the main thread might be busy doing other stuff when the message is sent from the parent. Perhaps it\u2019s doing a garbage collection, or a cycle collection, or it\u2019s blocked on some busy JavaScript that some silly advertisement company is running in the background of one of your tabs. In that case, the message from the parent won\u2019t be processed until the main thread is ready.<\/p>\n<p>Once the message is received, we\u2019re still not out of the woods &#8211; the <code>beforeunload<\/code> event handlers can run any kind of JavaScript inside them, more or less. For example, one anti-pattern I\u2019ve seen in the wild is to use the\u00a0<code>beforeunload<\/code> event as an opportunity to send a sync XMLHttpRequest in order to get some data to a server before the page goes away<sup id=\"rf3-2837\"><a href=\"#fn3-2837\" title=\"A better way would be to use &lt;a href=&quot;https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/Navigator\/sendBeacon&quot;&gt;navigator.sendBeacon&lt;\/a&gt;, which allows the browser to send one last XHR in the background for a page even after it\u2019s gone away.\" rel=\"footnote\">3<\/a><\/sup>. So the script on the page has an opportunity to delay you, even if it\u2019s not going to cause the dialog to appear.<\/p>\n<p>This problem seems to plague all browsers.\u00a0<code>beforeunload<\/code> is a real pain, and our current implementation can cause slow tab closing even if the tab doesn&#8217;t have\u00a0<code>beforeunload<\/code> event handlers set<sup id=\"rf4-2837\"><a href=\"#fn4-2837\" title=\"since we have to check to see if any of those event handlers exist.\" rel=\"footnote\">4<\/a><\/sup>. The patches in <a href=\"https:\/\/bugzilla.mozilla.org\/show_bug.cgi?id=1336763\">bug 1336763<\/a> offer what I think is a decent, simple solution for that common case in Firefox.<\/p>\n<h2>Don\u2019t ask, just remember<\/h2>\n<p>In <a href=\"https:\/\/bugzilla.mozilla.org\/show_bug.cgi?id=1336763\">bug 1336763<\/a>, I\u2019ve made it so that for any given tab running in a content process, if a <code>beforeunload<\/code> event is ever added in that tab (or in any of its subframes), the content process tells the parent process so it can mark that tab as having listeners we need to fire. If the <code>beforeunload<\/code> events are removed, we unmark the tab. If no <code>beforeunload<\/code> events are ever added, there\u2019s no mark at all.<\/p>\n<p>The parent process remembers these markings, so that if the user decides to close the tab, the parent can know immediately whether or not it needs to message to tell the content process to run <code>beforeunload<\/code> event handlers. In the cases where no <code>beforeunload<\/code> event handlers have been set, we can close immediately without asking for permission from the content process at all.<\/p>\n<h2>Details, details<\/h2>\n<p>Using some Gecko terminology here, we start by storing a count on something called the TabChild. It might simplify things a bit if you try to imagine the TabChild as the representative of everything in a particular browser tab, and that underneath that TabChild are a bunch of nodes, forming a tree-like structure.<\/p>\n<p>Let\u2019s call these nodes \u201cinner windows\u201d.<\/p>\n<p>The inner windows under the TabChild contain the documents that are loaded in a tab. For simple web pages, that might just be a single document. In that case, we have a TabChild with just a single inner window node under it.<\/p>\n<p>More complicated pages might contain iframes (which themselves contain iframes, etc). In those cases, we have a TabChild with a single inner window node under it, and that node has any number of inner window children (and those children have any number of inner window children, etc)<sup id=\"rf5-2837\"><a href=\"#fn5-2837\" title=\"For my fellow Gecko Hackers &#8211; yes, this is not quite right. I\u2019m missing other key structures in my description (specifically, the outer window). Forgive me &#8211; this is a very low-resolution mental model to make this post easier to write. :) \" rel=\"footnote\">5<\/a><\/sup>.<\/p>\n<p>When any of those subframes have a <code>beforeunload<\/code> event listener added to them via script, the inner window node tells the TabChild to increment its internal count. If a <code>beforeunload<\/code> event listener is removed via script, the TabChild is told to decrement its internal count.<\/p>\n<p>If the TabChild count ever goes above 0, then we need to tell the parent \u201cHey, you have at least one <code>beforeunload<\/code> event listener here&#8221;. If that count continues to go up, the TabChild doesn\u2019t need to tell the parent anything &#8211; it just needs to record the increase. If the count ever drops back to 0, then the TabChild needs to tell the parent again, \u201cAll <code>beforeunload<\/code> event listeners are clear\u201d.<\/p>\n<p>Pretty straight-forward so far, but there are a few other cases we also have to consider.<\/p>\n<h2>Other cases<\/h2>\n<p>There are a couple of ways for a set of <code>beforeunload<\/code> event handlers to go away. We\u2019ve already mentioned one &#8211; script on the page might remove them via <code>removeEventListener<\/code>.<\/p>\n<p>One way is if the inner window gets navigated away from. If we\u2019re on a page, and that page set a <code>beforeunload<\/code> event handler, and the user clicks on a link, the user might end up navigating away (assuming the dialog wasn\u2019t shown and they didn\u2019t cancel), which essentially replaces the inner window with one for a different page. In that case, script didn\u2019t remove the <code>beforeunload<\/code> event handlers &#8211; the page went away, and so the <code>beforeunload<\/code> event handlers on the page we\u2019ve unloaded are no longer relevant.<\/p>\n<p>Another way is if an &lt;iframe&gt; which has set a <code>beforeunload<\/code> event handler is removed from the DOM. Instead of replacing the inner window, we\u2019re snipping the inner window out of the tree structure entirely.<\/p>\n<p>In both of these cases, if there are <code>beforeunload<\/code> event handlers in the subframe, it\u2019s necessary to tell the TabChild so that the right number can be decremented from the TabChild count.<\/p>\n<p>So what this means is that we need the inner windows to keep a track of how many <code>beforeunload<\/code> event handlers have been set as well. That way, when they start to tear themselves down, they can tell the TabChild, \u201cHey, I\u2019m going away now &#8211; decrement X number of <code>beforeunload<\/code> event handlers\u201d.<\/p>\n<p>It might seem redundant to have these two counts &#8211; counts in the inner windows, and a total count in the TabChild. It would seem like one can be easily inferred from the other; just sum the <code>beforeunload<\/code> counts for the inner windows, and you should have your TabChild count.<\/p>\n<p>Having the TabChild keep a count is an optimization that prevents us from having to walk the inner window tree to collect a sum every time the count changes. It\u2019s a classic space \/ time tradeoff, and I think it\u2019s worth the extra integer member on the TabChild.<\/p>\n<h2>Comparison to other browsers<\/h2>\n<p>Here&#8217;s one way to compare the behaviour across different browsers:<\/p>\n<ol>\n<li>In a browser window with more than one tab open, open the developer tools, and make your way to the JavaScript console.<\/li>\n<li>Drop this tasty little snippet in and press enter:\n<pre>var then = Date.now(); while (Date.now() - then &lt; 15000) {}<\/pre>\n<\/li>\n<\/ol>\n<p>This is going to hang the main thread in that tab for 15 seconds, but is otherwise inert.<\/p>\n<p>Now try to close the tab. In Firefox, the tab closes right way. In Safari and Chrome (the two other browsers I have on this machine), the tab hangs out for a while. In Chrome, it appears to wait the full 15 seconds. In Safari, it seems to hit some kind of shorter timeout<sup id=\"rf6-2837\"><a href=\"#fn6-2837\" title=\"Note that it appears that the script running in the console is treated differently from the script running on the page. If you set up a page with that script, I notice that the tabs close immediately on both Chrome and Safari. I might have goofed in my experiment though &#8211; it is rather late at night. \" rel=\"footnote\">6<\/a><\/sup>.<\/p>\n<h2>Wrapping up<\/h2>\n<p>This was a neat set of patches to work on, precisely because it had me tour the depths of Gecko (dealing with things like <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Inner_and_outer_windows\">inner \/ outer windows<\/a>, <a href=\"http:\/\/searchfox.org\/mozilla-central\/rev\/3dc6ceb42746ab40f1441e1e659ffb8f62ae78e3\/dom\/events\/EventListenerManager.cpp\">the stuff that manages events<\/a>, etc), which end up resulting in <a href=\"http:\/\/searchfox.org\/mozilla-central\/source\/dom\/interfaces\/base\/nsITabParent.idl#73-77\">a simple property<\/a> that the front-end can ultimately access to optimize closing tabs. So it nicely spanned the gap between low-level Gecko and higher-level Firefox, all for <a href=\"https:\/\/bugzilla.mozilla.org\/show_bug.cgi?id=68215\">an event that was added back in 2004<\/a>, for better or worse.<\/p>\n<p>If all goes well, this change should ship in Firefox 55, and apply to multi-process tabs.<\/p>\n<hr class=\"footnotes\"><ol class=\"footnotes\" style=\"list-style-type:decimal\"><li id=\"fn1-2837\"><p >It\u2019s not always necessary for the <code>beforeunload<\/code> event handler to show the dialog. The event handler needs to set the <code>returnValue<\/code> property on the event to a string in order for the dialog to show, but plenty of other stuff can happen in that event handler.&nbsp;<a href=\"#rf1-2837\" class=\"backlink\" title=\"Return to footnote 1.\">&#8617;<\/a><\/p><\/li><li id=\"fn2-2837\"><p >Ooooh pun intended&nbsp;<a href=\"#rf2-2837\" class=\"backlink\" title=\"Return to footnote 2.\">&#8617;<\/a><\/p><\/li><li id=\"fn3-2837\"><p >A better way would be to use <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/Navigator\/sendBeacon\">navigator.sendBeacon<\/a>, which allows the browser to send one last XHR in the background for a page even after it\u2019s gone away.&nbsp;<a href=\"#rf3-2837\" class=\"backlink\" title=\"Return to footnote 3.\">&#8617;<\/a><\/p><\/li><li id=\"fn4-2837\"><p >since we have to check to see if any of those event handlers exist.&nbsp;<a href=\"#rf4-2837\" class=\"backlink\" title=\"Return to footnote 4.\">&#8617;<\/a><\/p><\/li><li id=\"fn5-2837\"><p >For my fellow Gecko Hackers &#8211; yes, this is not quite right. I\u2019m missing other key structures in my description (specifically, the outer window). Forgive me &#8211; this is a very low-resolution mental model to make this post easier to write. \ud83d\ude42 &nbsp;<a href=\"#rf5-2837\" class=\"backlink\" title=\"Return to footnote 5.\">&#8617;<\/a><\/p><\/li><li id=\"fn6-2837\"><p >Note that it appears that the script running in the console is treated differently from the script running on the page. If you set up a page with that script, I notice that the tabs close immediately on both Chrome and Safari. I might have goofed in my experiment though &#8211; it is rather late at night. &nbsp;<a href=\"#rf6-2837\" class=\"backlink\" title=\"Return to footnote 6.\">&#8617;<\/a><\/p><\/li><\/ol>","protected":false},"excerpt":{"rendered":"<p>TL;DR: In bug 1336763, I have landed a series of patches that should hopefully make tab closing faster for the majority of cases for users that are using multi-process Firefox. The rest of this blog post tries to explain why. The beforeunload event handler Perhaps you\u2019ve seen this dialog before: This dialog shows up when [&hellip;]<\/p>\n","protected":false},"author":4,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"jetpack_post_was_ever_published":false,"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":true,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_publicize_message":"","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":true,"jetpack_social_options":{"image_generator_settings":{"template":"highway","default_image_id":0,"font":"","enabled":false},"version":2}},"categories":[5,874,861,79],"tags":[],"class_list":["post-2837","post","type-post","status-publish","format-standard","hentry","category-computer-science","category-firefox-mozilla-2","category-mozilla-2","category-technology"],"jetpack_publicize_connections":[],"jetpack_featured_media_url":"","jetpack_shortlink":"https:\/\/wp.me\/prmTy-JL","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/mikeconley.ca\/blog\/wp-json\/wp\/v2\/posts\/2837","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/mikeconley.ca\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/mikeconley.ca\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/mikeconley.ca\/blog\/wp-json\/wp\/v2\/users\/4"}],"replies":[{"embeddable":true,"href":"https:\/\/mikeconley.ca\/blog\/wp-json\/wp\/v2\/comments?post=2837"}],"version-history":[{"count":13,"href":"https:\/\/mikeconley.ca\/blog\/wp-json\/wp\/v2\/posts\/2837\/revisions"}],"predecessor-version":[{"id":2852,"href":"https:\/\/mikeconley.ca\/blog\/wp-json\/wp\/v2\/posts\/2837\/revisions\/2852"}],"wp:attachment":[{"href":"https:\/\/mikeconley.ca\/blog\/wp-json\/wp\/v2\/media?parent=2837"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/mikeconley.ca\/blog\/wp-json\/wp\/v2\/categories?post=2837"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/mikeconley.ca\/blog\/wp-json\/wp\/v2\/tags?post=2837"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}