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).
The Crash/HTML/image-load-after-iframe-navigated.html test was
crashing on CI with a null pointer dereference at
NavigableContainer.cpp:178. The crash occurs because content_document()
dereferences the return value of active_document() without checking for
null.
When an iframe is navigated, Document::destroy() sets the old
document state's document to null via set_document(nullptr), but
the navigable (m_content_navigable) remains non-null since it is
reused for the new navigation. During the window between the old
document being destroyed and the new document being set,
active_document() returns null. If JS code accesses
iframe.contentDocument during this window (e.g. via a timer
callback), content_document() would dereference the null pointer.
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>
HTMLParser::the_end() had three spin_until calls that blocked the event
loop: step 5 (deferred scripts), step 7 (ASAP scripts), and step 8
(load event delay). This replaces them with an HTMLParserEndState state
machine that progresses asynchronously via callbacks.
The state machine has three phases matching the three spin_until calls:
- WaitingForDeferredScripts: loops executing ready deferred scripts
- WaitingForASAPScripts: waits for ASAP script lists to empty
- WaitingForLoadEventDelay: waits for nothing to delay the load event
Notification triggers re-evaluate the state machine when conditions
change: HTMLScriptElement::mark_as_ready, stylesheet unblocking in
StyleElementBase/HTMLLinkElement, did_stop_being_active_document, and
DocumentLoadEventDelayer decrements. NavigableContainer state changes
(session history readiness, content navigable cleared, lazy load flag)
also trigger re-evaluation of the load event delay check.
Key design decisions and why:
1. Microtask checkpoint in schedule_progress_check(): The old spin_until
called perform_a_microtask_checkpoint() before checking conditions.
This is critical because HTMLImageElement::update_the_image_data step
8 queues a microtask that creates the DocumentLoadEventDelayer.
Without the checkpoint, check_progress() would see zero delayers and
complete before images start delaying the load event.
2. deferred_invoke in schedule_progress_check():
I tried Core::Timer (0ms), queue_global_task, and synchronous calls.
Timers caused non-deterministic ordering with the HTML event loop's
task processing timer, leading to image layout tests failing (wrong
subtest pass/fail patterns). Synchronous calls fired too early during
image load processing before dimensions were set, causing 0-height
images in layout tests. queue_global_task had task ordering issues
with the session history traversal queue. deferred_invoke runs after
the current callback returns but within the same event loop pump,
giving the right balance.
3. Navigation load event guard (m_navigation_load_event_guard): During
cross-document navigation, finalize_a_cross_document_navigation step
2 calls set_delaying_load_events(false) before the session history
traversal activates the new document. This creates a transient state
where the parent's load event delay check sees the about:blank (which
has ready_for_post_load_tasks=true) as the active document and
completes prematurely.
When a child navigable starts loading, the navigate algorithm calls
set_delaying_load_events(true), creating a DocumentLoadEventDelayer
on the parent document. This delayer is normally cleared when the
navigation finalizes via set_delaying_load_events(false).
However, when an iframe is removed from the DOM, the child navigable
is destroyed. If the finalize step hasn't run yet, the delayer
lingers until GC collects the Navigable, which can block the parent
document's load event indefinitely.
Fix this by explicitly clearing the "is delaying load events" flag
in destroy_the_child_navigable() right after marking the navigable
as destroyed.
Add a clang plugin check that flags GC::Cell subclasses (and their
base classes within the Cell hierarchy) that have destructors with
non-trivial bodies. Such logic should use Cell::finalize() instead.
Add GC_ALLOW_CELL_DESTRUCTOR annotation macro for opting out in
exceptional cases (currently only JS::Object).
This prevents us from accidentally adding code in destructors that
runs after something we're pointing to may have been destroyed.
(This could become a problem when the garbage collector sweeps
objects in an unfortunate order.)
This new check uncovered a handful of bugs which are then also fixed
in this commit. :^)
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>
...navigable shared attribute processing steps.
After e095bf3a5fhttps://wpt.live/html/rendering/pixel-length-attributes.html ends up in
this function with null navigable, which leads to crash while executing
steps. Adding early return in case iframe's document doesn't have a
navigable is enough to make the test pass again.
This commit doesn't import the WPT test, because it's already imported
and we have it disabled since it takes too much time on CI.
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 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.
MediaQueryList will now remember if a state change occurred when
evaluating its match state. This memory can then be used by the document
later on when it's updating all queries, to ensure that we don't forget
to fire at least one change event.
This also required plumbing the system visibility state to initial
about:blank documents, since otherwise they would be stuck in "hidden"
state indefinitely and never evaluate their media queries.
The use of this HashMap looks very spooky, but let's at least use
finalize when cleaning them up on destruction to make things slightly
less dangerous looking.
Resulting in a massive rename across almost everywhere! Alongside the
namespace change, we now have the following names:
* JS::NonnullGCPtr -> GC::Ref
* JS::GCPtr -> GC::Ptr
* JS::HeapFunction -> GC::Function
* JS::CellImpl -> GC::Cell
* JS::Handle -> GC::Root
Now that the heap has no knowledge about a JavaScript realm and is
purely for managing the memory of the heap, it does not make sense
to name this function to say that it is a non-realm variant.