Files
ladybird/Libraries/LibWeb/Fetch/Infrastructure/FetchController.h
Jonathan Gamble 6686cecf72 LibWeb: Stop blocking main fetch on pending preload results
The lichess.org lobby stylesheet sometimes gets render-blocked,
resulting in a blank page.

The main fetch would queue work and then spin_until wait for a pending
preload result on the main thread. This re-entered the event loop and
stranded the queued PendingResponse callback behind a pile of nested
event processing. So the preload response existed, but the stylesheet
never made it through the normal handoff path.

I chose to follow the PreloadEntry.cpp model to address this and track
at most one pending preload PendingResponse for each FetchParams,
resolving it directly from the preload callback when the response
arrives. This removes the spin_until, keeps the handoff asynchronous,
and lets the blocked stylesheet finish loading normally.

If the current single-consumer preload path ever changes, this logic
must be widened to handle it.
2026-05-06 08:33:12 +02:00

134 lines
4.5 KiB
C++

/*
* Copyright (c) 2022, Linus Groh <linusg@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Badge.h>
#include <AK/HashMap.h>
#include <AK/WeakPtr.h>
#include <LibGC/Function.h>
#include <LibGC/Ptr.h>
#include <LibJS/Forward.h>
#include <LibJS/Heap/Cell.h>
#include <LibJS/Runtime/VM.h>
#include <LibRequests/Forward.h>
#include <LibWeb/Export.h>
#include <LibWeb/Fetch/Infrastructure/FetchTimingInfo.h>
#include <LibWeb/Forward.h>
#include <LibWeb/HTML/EventLoop/Task.h>
#include <LibWeb/HTML/StructuredSerializeTypes.h>
namespace Web::Fetch::Fetching {
class PendingResponse;
}
namespace Web::Fetch::Infrastructure {
// https://fetch.spec.whatwg.org/#fetch-controller
class WEB_API FetchController : public JS::Cell {
GC_CELL(FetchController, JS::Cell);
GC_DECLARE_ALLOCATOR(FetchController);
public:
enum class State : u8 {
Ongoing,
Terminated,
Aborted,
Stopped,
};
[[nodiscard]] static GC::Ref<FetchController> create(JS::VM&);
void set_full_timing_info(GC::Ref<FetchTimingInfo> full_timing_info) { m_full_timing_info = full_timing_info; }
void set_report_timing_steps(Function<void(JS::Object&)> report_timing_steps);
void set_next_manual_redirect_steps(Function<void()> next_manual_redirect_steps);
[[nodiscard]] State state() const { return m_state; }
void report_timing(JS::Object&) const;
void process_next_manual_redirect() const;
[[nodiscard]] GC::Ref<FetchTimingInfo> extract_full_timing_info() const;
void abort(JS::Realm&, Optional<JS::Value>);
JS::Value deserialize_a_serialized_abort_reason(JS::Realm&);
void terminate();
void set_fetch_params(Badge<FetchParams>, GC::Ref<FetchParams> fetch_params) { m_fetch_params = fetch_params; }
[[nodiscard]] GC::Ptr<Fetching::PendingResponse> pending_preloaded_response() const { return m_pending_preloaded_response; }
void set_pending_preloaded_response(GC::Ptr<Fetching::PendingResponse> pending_preloaded_response) { m_pending_preloaded_response = pending_preloaded_response; }
void set_pending_request(RefPtr<Requests::Request> const&);
void set_inner_fetch_controller(GC::Ref<FetchController>);
void stop_fetch();
void stop_request();
u64 next_fetch_task_id() { return m_next_fetch_task_id++; }
void fetch_task_queued(u64 fetch_task_id, HTML::TaskID event_id);
void fetch_task_complete(u64 fetch_task_id);
private:
FetchController();
virtual void visit_edges(JS::Cell::Visitor&) override;
// https://fetch.spec.whatwg.org/#fetch-controller-state
// state (default "ongoing")
// "ongoing", "terminated", or "aborted"
State m_state { State::Ongoing };
// https://fetch.spec.whatwg.org/#fetch-controller-full-timing-info
// full timing info (default null)
// Null or a fetch timing info.
GC::Ptr<FetchTimingInfo> m_full_timing_info;
// https://fetch.spec.whatwg.org/#fetch-controller-report-timing-steps
// report timing steps (default null)
// Null or an algorithm accepting a global object.
GC::Ptr<GC::Function<void(JS::Object&)>> m_report_timing_steps;
// https://fetch.spec.whatwg.org/#fetch-controller-report-timing-steps
// serialized abort reason (default null)
// Null or a Record (result of StructuredSerialize).
Optional<HTML::SerializationRecord> m_serialized_abort_reason;
// https://fetch.spec.whatwg.org/#fetch-controller-next-manual-redirect-steps
// next manual redirect steps (default null)
// Null or an algorithm accepting nothing.
GC::Ptr<GC::Function<void()>> m_next_manual_redirect_steps;
GC::Ptr<FetchParams> m_fetch_params;
// NB: Assumes one waiting consumer for a pending preloaded response.
// Widen this if preload handoff ever supports multiple consumers.
GC::Ptr<Fetching::PendingResponse> m_pending_preloaded_response;
WeakPtr<Requests::Request> m_pending_request;
HashMap<u64, HTML::TaskID> m_ongoing_fetch_tasks;
u64 m_next_fetch_task_id { 0 };
};
class FetchControllerHolder : public JS::Cell {
GC_CELL(FetchControllerHolder, JS::Cell);
GC_DECLARE_ALLOCATOR(FetchControllerHolder);
public:
static GC::Ref<FetchControllerHolder> create(JS::VM&);
[[nodiscard]] GC::Ptr<FetchController> const& controller() const { return m_controller; }
void set_controller(GC::Ref<FetchController> controller) { m_controller = controller; }
private:
FetchControllerHolder();
virtual void visit_edges(Cell::Visitor&) override;
GC::Ptr<FetchController> m_controller;
};
}