Previously, `CheckUnloadingCanceledState::start_phase2()` used
`queue_global_task()` to fire `beforeunload` on each relevant document.
However, tasks associated with a document are only runnable when the
document is fully active and it is possible for the document to become
not fully-active before the task is run. This caused a 15 second delay
in `CheckUnloadingCanceledState` while we waited for a timeout, which
could cause test timeouts when under heavy load.
Previously, we had to recompute targetStep in the middle of history
step application because our session history traversal queue (SHTQ)
implementation was broken and didn't provide actual task serialization.
This meant the step could change while we were waiting for tasks to
complete.
Now that the SHTQ correctly serializes tasks, the step should no longer
change mid-application. Replace the recomputation with a VERIFY() assert
to enforce this invariant.
WebContent process keeps session history entries for pages we have
navigated away from. Before this change, those entries could prevent GC
objects (e.g. PolicyContainer and its CSP PolicyList) from being
collected, since the GC-allocated SHE/DocumentState held live GC::Ref
pointers into the heap.
By making both classes RefCounted and storing SerializedPolicyContainer
instead of a live PolicyContainer, history entries no longer keep alive
any GC objects. This eliminates the leak and is also a step toward
moving the session history entry tree to the UI process.
Now that Navigable directly owns its active document (m_active_document)
we can have Navigable maintain a back-pointer on Document instead of
using the old cache-with-validation pattern that fell back to a linear
scan of all navigables via navigable_with_active_document().
Previously, the active document's lifecycle was bound to
SessionHistoryEntry via DocumentState. The ownership chain was:
Navigable → SessionHistoryEntry → DocumentState → Document
This made it impossible to move SessionHistoryEntry to the UI process
(which cannot own DOM::Document). This commit decouples the two by
giving Navigable a direct m_active_document field that serves as the
authoritative source for active_document().
- Navigable owns m_active_document directly; active_document() reads
from it instead of going through the active session history entry.
- DocumentState no longer holds a Document pointer. Instead, it stores
a document_id for "same document?" checks. Same-document navigations
share a DocumentState and thus the same document_id, while
cross-document navigations create a new DocumentState with a new ID.
- A pending_document parameter is threaded through
finalize_a_cross_document_navigation → apply_the_push_or_replace →
apply_the_history_step so the newly created document reaches
activation without being stored on DocumentState.
- For traversal, the population output delivers the document.
A resolved_document is computed per continuation from either the
pending document, the population output, or the current active
document (for same-document traversals).
Replace the blocking spin_processing_tasks_with_source_until calls
in apply_the_history_step_after_unload_check() with an event-driven
ApplyHistoryStepState GC cell that tracks 5 phases, following the
same pattern used by CheckUnloadingCanceledState.
Key changes:
- Introduce ApplyHistoryStepState with phases:
WaitingForDocumentPopulation, ProcessingContinuations,
WaitingForChangeJobCompletion, WaitingForNonChangingJobs and Completed
- Add on_complete callbacks to apply_the_push_or_replace_history_step,
finalize_a_same_document_navigation,
finalize_a_cross_document_navigation, and
update_for_navigable_creation_or_destruction
- Remove spin_until from Document::open()
- Use null-document tasks for non-changing navigable updates and
document unload/destroy to avoid stuck tasks when documents become
non-fully-active
- Defer completely_finish_loading when document has no navigable yet,
and re-trigger post-load steps in activate_history_entry for documents
that completed loading before activation
Co-Authored-By: Shannon Booth <shannon@serenityos.org>
Replace the two spin_processing_tasks_with_source_until() calls in
TraversableNavigable::check_if_unloading_is_canceled() with a
callback-based GC cell (CheckUnloadingCanceledState) that tracks
completion across both phases (traverse navigate event + per-document
beforeunload handlers) and invokes a callback when done.
This required making check_if_unloading_is_canceled() async
(callback-based), splitting apply_the_history_step() into pre-check
and continuation parts, and updating all callers to move session
history traversal queue promise resolution into callbacks.
The trusted-event test is rebaselined because beforeunload now fires
as a queued NavigationAndTraversal task rather than being processed
inline by spin_until. This allows the unhandledrejection microtask
to run before the beforeunload task, swapping their order.
With apply_to() now self-contained (carrying its own replacement
DocumentState rather than reading from the live entry), the clone at
the traversal call site is no longer needed.
The clone previously served two purposes:
1. Input snapshot: freeze entry fields before deferred population.
Now solved by changing populate_session_history_entry_document() to
take explicit input parameters, snapshotted before the
deferred_invoke.
2. Output isolation: absorb apply_to() and post-population adjustments
without mutating the live entry during unload. Now solved by storing
the PopulateSessionHistoryEntryDocumentOutput on the continuation
state and deferring all mutations (including the origin-based
classic_history_api_state reset and navigable_target_name clear)
to after_potential_unload.
The post-population adjustments run unconditionally in
after_potential_unload, covering both the population path and the
non-population path (e.g. traversal to an already-populated error
entry).
Previously, populate_session_history_entry_document() took a
SessionHistoryEntry as both input and output — reading URL and
document_state fields while also mutating the entry across a chain of
async functions. This made it very hard to reason about data flow.
Refactor the internal helpers
(create_navigation_params_from_a_srcdoc_resource,
create_navigation_params_by_fetching, NavigationParamsFetchStateHolder,
perform_navigation_params_fetch) to take individual field values instead
of reading from the entry, and accumulate redirect mutations on the
state holder rather than writing them to the entry immediately.
Introduce PopulateSessionHistoryEntryDocumentOutput, a GC cell that
collects all mutations (document, redirect URL, classic history API
state, replacement document state, resource cleared flag, and
finalization data). The completion_steps callback now receives this
output object (or nullptr on cancellation), and callers apply it to the
entry via apply_to().
The replacement DocumentState for the redirect path is built eagerly at
redirect time from values captured on the state holder, making
apply_to() fully self-contained — it never reads from the target entry's
live document_state. This is important for the traversal path where the
entry may be mutated during unload (e.g. window.name writes
navigable_target_name through the active session history entry).
We pump the event loop just before these steps which can cause the
displayed document to be destroyed and lose its navigable. This was a
cause for crashes in the `encoding` WPT tests.
`close_top_level_traversable()` checks the `is_closing` flag to prevent
duplicate closes, but it is only set by callers of
`definitely_close_top_level_traversable()`. The flag is a bit in between
specs as things move from browsing contexts to navigables, but its
purpose is clear: without setting it, the check is ineffective and
`definitely_close_top_level_traversable()` runs multiple times for the
same traversable when the page has child navigables. This queues
duplicate session history traversal steps, where the second step
accesses the already-destroyed active document and segfaults.
1. unload_a_document_and_its_descendants() now follows the spec
algorithm structure: recursively unload child navigables via queued
tasks (step 4), wait for them (step 5), then queue this document's
own unload as a separate task (step 6). Previously, this was
flattened into a single spin that unloaded all descendants and the
document together, followed by a non-spec call to
destroy_a_document_and_its_descendants().
2. unload() step 19 now calls destroy() when the document is not
salvageable, as the spec requires. Previously this was a no-op with a
comment deferring to unload_a_document_and_its_descendants().
3. destroy_top_level_traversable() step 2 now calls
destroy_a_document_and_its_descendants() instead of destroy().
The iframe-unloading-order test, which exercises named iframe access
during unload handlers (the scenario the previous logic was designed to
protect), still passes.
Fixes#7825
check_if_unloading_is_canceled() can be called nested inside an outer
spin_processing_tasks_with_source_until (via apply_the_history_step ->
pump -> deferred_invoke -> begin_navigation). A regular spin_until()
deadlocks in that scenario because m_skip_event_loop_processing_steps
is true, causing process() to return early without ever executing the
queued tasks.
Since both spins here wait exclusively for NavigationAndTraversal
tasks, switch them to spin_processing_tasks_with_source_until() which
bypasses process() and directly executes matching tasks from the queue.
The find_if lambdas in CustomElementRegistry and TraversableNavigable
were taking GC::Root parameters, causing implicit conversion from
GC::Ref (or copying of GC::Root) for each element visited. Each
GC::Root creation/destruction allocates and frees a RootImpl, making
linear scans over these vectors far more expensive than necessary.
Use auto const& to match the actual vector element types.
This was 2.3% of CPU time while opening a YouTube video.
Remove 11 heavy includes from Document.h that were only needed for
pointer/reference types (already forward-declared in Forward.h), and
extract the nested ViewportClient interface to a standalone header.
This reduces Document.h's recompilation cascade from ~1228 files to
~717 files (42% reduction). Headers like BrowsingContext.h that were
previously transitively included see even larger improvements (from
~1228 down to ~73 dependents).
When queuing tasks for changing navigables in step 12 of
apply_the_history_step, skip navigables that have been destroyed and
increment completed_change_jobs directly. This is necessary because
Document::destroy() removes tasks associated with its document from the
task queue, so a task queued for a destroyed navigable's window may
never run, causing the subsequent spin_until to wait forever.
This change prepares for a future where the rendering thread handles
input events directly, allowing it to trigger repainting without
waiting for the main thread. To support this, the compositor needs to
own the display list, scroll state, and backing stores rather than
receiving them per-frame from the main thread.
We never want to capture GC::Roots in GC::Function lambdas, since that
very easily creates reference cycles and leak huge object graphs.
Capturing a raw pointer or GC::Ptr/Ref is fine, since that's exactly
what GC::Function is good at.
If we copy instead of move, the GC::Root source locations get
overwritten and we can't tell from a heap dump who created the roots.
(And it's also more efficient to move instead of copy ofc.)
If multiple cross-document navigations are queued on
SessionHistoryTraversalQueue, running the next entry before the current
document load is finished may result in a deadlock. If the new document
has a navigable element of its own, it will append steps to SHTQ and
hang in nested spin_until.
This change uses promises to ensure that the current document loads
before the next entry is executed.
Fixes timeouts in the imported tests.
Co-authored-by: Sam Atkins <sam@ladybird.org>
And make it a DOM::Node, not DOM::Element. This makes everything flow
much better, such as spec texts that explicitly mention "focused area"
as the fact that we don't necessarily need to traverse a tree of
elements, since a Node can be focusable as well.
Eventually this will need to be a struct with a separate "focused area"
and "DOM anchor", but this change will make it easier to achieve that.
Now, when Skia backend context is available by the time backing stores
are allocated, there is no need to have a separate BackingStore class.
This allows us to get rid of BackingStore -> PaintingSurface cache.
Making navigables responsible for backing store allocation will allow us
to have separate backing stores for iframes and run paint updates for
them independently, which is a step toward isolating them into separate
processes.
Another nice side effect is that now Skia backend context is ready by
the time backing stores are allocated, so we will be able to get rid of
BackingStore class in the upcoming changes and allocate PaintingSurface
directly.
This implements enough of the Geolocation spec that it is now possible
for websites to retrieve the current geo position or try to watch for
updates (which currently never happen).
As it stands now, it only returns a single emulated position that points
to San Francisco.
In upcoming changes StorageBottle will own pointers to GC-allocated
objects, so it needs to be a GC-allocated object itself to avoid
introducing more GC roots.
With this change we save a copy of of scroll state at the time of
recording a display list, instead of actual ScrollState pointer that
could be modifed by the main thread while display list is beings
rasterized on the rendering thread, which leads to a frame painted with
inconsistent scroll state.
Fixes https://github.com/LadybirdBrowser/ladybird/issues/4288
Skia has a check in debug mode to verify that surface is only used
within one thread. Before this change we were violating this by
allocating surfaces on the main thread while using and destructing them
on the rendering thread.
The display list is an immutable data structure, so once it's created,
rasterization can be moved to a separate thread. This allows more room
for performing other tasks between processing HTML rendering tasks.
This change makes PaintingSurface, ImmutableBitmap, and GlyphRun atomic
ref-counted, as they are shared between the main and rendering threads
by being included in the display list.
This change fixes a bug that can be reproduced with the following steps:
```js
const iframe = document.createElement("iframe");
document.body.appendChild(iframe);
iframe.contentWindow.location.href = ("http://localhost:8080/demo.html");
```
These steps are executed in the following order:
1. Create iframe and schedule session history traversal task that adds
session history entry for the iframe.
2. Generate navigation id for scheduled navigation to
`http://localhost:8080/demo.html`.
3. Execute the scheduled session history traversal task, which adds
session history entry for the iframe.
4. Ooops, navigation to `http://localhost:8080/demo.html` is aborted
because addings SHE for the iframe resets the navigation id.
This change fixes this by delaying all navigations until SHE for a
navigable is created.
This is required to store Content Security Policies, as their
Directives are implemented as subclasses with overridden virtual
functions. Thus, they cannot be stored as generic Directive classes, as
it'll lose the ability to call overridden functions when they are
copied.
Before this change, `m_needs_repaint` was reset in
`Document::record_display_list()` only when the cached display list was
absent. This meant that if the last triggered repaint used the cached
display list, we would keep repainting indefinitely until the display
list was invalidated (We schedule a task that checks if repainting is
required 60/s).
This change also moves `m_needs_repaint` from Document to
TraversableNavigable as we only ever need to repaint a document that
belongs to traversable.