From a9aecbbd6ff145a34aaca0e3e47c63daaa54d0a8 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Thu, 27 Jul 2023 15:05:42 +0200 Subject: [PATCH] LibWeb: Batch processing of successfully downloaded images Before this change, we would process each image as it finished downloading. This often led to a situation where we'd decode 1 image, schedule a layout, do the layout, then decode another image, schedule a layout, do the layout, etc. Basically decoding and layouts would get interleaved even though we had multiple images fetched and ready for decoding. This patch adds a simple BatchingDispatcher thingy that HTMLImageElement uses to batch the handling of successful fetches. With this, the number of layouts while loading https://shopify.com/ goes from 48 to 6, and the page loads noticeably faster. :^) --- .../LibWeb/HTML/HTMLImageElement.cpp | 96 +++++++++++++------ 1 file changed, 66 insertions(+), 30 deletions(-) diff --git a/Userland/Libraries/LibWeb/HTML/HTMLImageElement.cpp b/Userland/Libraries/LibWeb/HTML/HTMLImageElement.cpp index 6e85e0e167..b6f70a1c00 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLImageElement.cpp +++ b/Userland/Libraries/LibWeb/HTML/HTMLImageElement.cpp @@ -262,6 +262,40 @@ bool HTMLImageElement::uses_srcset_or_picture() const return has_attribute(HTML::AttributeNames::srcset) || (parent() && is(*parent())); } +// We batch handling of successfully fetched images to avoid interleaving 1 image, 1 layout, 1 image, 1 layout, etc. +// The processing timer is 1ms instead of 0ms, since layout is driven by a 0ms timer, and if we use 0ms here, +// the event loop will process them in insertion order. This is a bit of a hack, but it works. +struct BatchingDispatcher { +public: + BatchingDispatcher() + : m_timer(Core::Timer::create_single_shot(1, [this] { process(); }).release_value_but_fixme_should_propagate_errors()) + { + } + + void enqueue(JS::SafeFunction callback) + { + m_queue.append(move(callback)); + m_timer->restart(); + } + +private: + void process() + { + auto queue = move(m_queue); + for (auto& callback : queue) + callback(); + } + + NonnullRefPtr m_timer; + Vector> m_queue; +}; + +static BatchingDispatcher& batching_dispatcher() +{ + static BatchingDispatcher dispatcher; + return dispatcher; +} + // https://html.spec.whatwg.org/multipage/images.html#update-the-image-data ErrorOr HTMLImageElement::update_the_image_data(bool restart_animations, bool maybe_omit_events) { @@ -473,44 +507,46 @@ after_step_7: image_request->add_callbacks( [this, image_request, maybe_omit_events, url_string, previous_url]() { - VERIFY(image_request->shared_image_request()); - auto image_data = image_request->shared_image_request()->image_data(); - image_request->set_image_data(image_data); + batching_dispatcher().enqueue([this, image_request, maybe_omit_events, url_string, previous_url] { + VERIFY(image_request->shared_image_request()); + auto image_data = image_request->shared_image_request()->image_data(); + image_request->set_image_data(image_data); - ListOfAvailableImages::Key key; - key.url = url_string; - key.mode = m_cors_setting; - key.origin = document().origin(); + ListOfAvailableImages::Key key; + key.url = url_string; + key.mode = m_cors_setting; + key.origin = document().origin(); - // 1. If image request is the pending request, abort the image request for the current request, - // upgrade the pending request to the current request - // and prepare image request for presentation given the img element. - if (image_request == m_pending_request) { - abort_the_image_request(realm(), m_current_request); - upgrade_pending_request_to_current_request(); - image_request->prepare_for_presentation(*this); - } + // 1. If image request is the pending request, abort the image request for the current request, + // upgrade the pending request to the current request + // and prepare image request for presentation given the img element. + if (image_request == m_pending_request) { + abort_the_image_request(realm(), m_current_request); + upgrade_pending_request_to_current_request(); + image_request->prepare_for_presentation(*this); + } - // 2. Set image request to the completely available state. - image_request->set_state(ImageRequest::State::CompletelyAvailable); + // 2. Set image request to the completely available state. + image_request->set_state(ImageRequest::State::CompletelyAvailable); - // 3. Add the image to the list of available images using the key key, with the ignore higher-layer caching flag set. - document().list_of_available_images().add(key, *image_data, true).release_value_but_fixme_should_propagate_errors(); + // 3. Add the image to the list of available images using the key key, with the ignore higher-layer caching flag set. + document().list_of_available_images().add(key, *image_data, true).release_value_but_fixme_should_propagate_errors(); - // 4. If maybe omit events is not set or previousURL is not equal to urlString, then fire an event named load at the img element. - if (!maybe_omit_events || previous_url != url_string) - dispatch_event(DOM::Event::create(realm(), HTML::EventNames::load).release_value_but_fixme_should_propagate_errors()); + // 4. If maybe omit events is not set or previousURL is not equal to urlString, then fire an event named load at the img element. + if (!maybe_omit_events || previous_url != url_string) + dispatch_event(DOM::Event::create(realm(), HTML::EventNames::load).release_value_but_fixme_should_propagate_errors()); - set_needs_style_update(true); - document().set_needs_layout(); + set_needs_style_update(true); + document().set_needs_layout(); - if (image_data->is_animated() && image_data->frame_count() > 1) { - m_current_frame_index = 0; - m_animation_timer->set_interval(image_data->frame_duration(0)); - m_animation_timer->start(); - } + if (image_data->is_animated() && image_data->frame_count() > 1) { + m_current_frame_index = 0; + m_animation_timer->set_interval(image_data->frame_duration(0)); + m_animation_timer->start(); + } - m_load_event_delayer.clear(); + m_load_event_delayer.clear(); + }); }, [this, image_request, maybe_omit_events, url_string, previous_url, selected_source]() { // The image data is not in a supported file format;