LibWeb: Do not spin the event loop awaiting text track state changes
Some checks are pending
CI / macOS, arm64, Sanitizer_CI, Clang (push) Waiting to run
CI / Linux, x86_64, Fuzzers_CI, Clang (push) Waiting to run
CI / Linux, x86_64, Sanitizer_CI, GNU (push) Waiting to run
CI / Linux, x86_64, Sanitizer_CI, Clang (push) Waiting to run
Package the js repl as a binary artifact / macOS, arm64 (push) Waiting to run
Package the js repl as a binary artifact / Linux, x86_64 (push) Waiting to run
Run test262 and test-wasm / run_and_update_results (push) Waiting to run
Lint Code / lint (push) Waiting to run
Label PRs with merge conflicts / auto-labeler (push) Waiting to run
Push notes / build (push) Waiting to run

The text track processing model would previously spin forever waiting
for the track URL to change. It would then recursively invoke itself
to handle the new URL, again entering the spin loop. This meant that
tracks could easily cause event loop hangs.

We now have an observer system to be notified when the track state
changes instead. This lets us exit the processing model and move on.
This commit is contained in:
Timothy Flynn 2025-06-12 08:51:45 -04:00 committed by Tim Flynn
commit de34143a4e
Notes: github-actions[bot] 2025-06-12 16:26:50 +00:00
11 changed files with 182 additions and 17 deletions

View file

@ -543,6 +543,7 @@ set(SOURCES
HTML/TextTrackCue.cpp HTML/TextTrackCue.cpp
HTML/TextTrackCueList.cpp HTML/TextTrackCueList.cpp
HTML/TextTrackList.cpp HTML/TextTrackList.cpp
HTML/TextTrackObserver.cpp
HTML/Timer.cpp HTML/Timer.cpp
HTML/TimeRanges.cpp HTML/TimeRanges.cpp
HTML/ToggleEvent.cpp HTML/ToggleEvent.cpp

View file

@ -626,6 +626,7 @@ class TextTrack;
class TextTrackCue; class TextTrackCue;
class TextTrackCueList; class TextTrackCueList;
class TextTrackList; class TextTrackList;
class TextTrackObserver;
class Timer; class Timer;
class TimeRanges; class TimeRanges;
class ToggleEvent; class ToggleEvent;

View file

@ -18,6 +18,7 @@
#include <LibWeb/HTML/HTMLTrackElement.h> #include <LibWeb/HTML/HTMLTrackElement.h>
#include <LibWeb/HTML/PotentialCORSRequest.h> #include <LibWeb/HTML/PotentialCORSRequest.h>
#include <LibWeb/HTML/TextTrack.h> #include <LibWeb/HTML/TextTrack.h>
#include <LibWeb/HTML/TextTrackObserver.h>
#include <LibWeb/Platform/EventLoopPlugin.h> #include <LibWeb/Platform/EventLoopPlugin.h>
namespace Web::HTML { namespace Web::HTML {
@ -37,12 +38,14 @@ void HTMLTrackElement::initialize(JS::Realm& realm)
Base::initialize(realm); Base::initialize(realm);
m_track = TextTrack::create(realm); m_track = TextTrack::create(realm);
m_track_observer = realm.create<TextTrackObserver>(realm, *m_track);
} }
void HTMLTrackElement::visit_edges(Cell::Visitor& visitor) void HTMLTrackElement::visit_edges(Cell::Visitor& visitor)
{ {
Base::visit_edges(visitor); Base::visit_edges(visitor);
visitor.visit(m_track); visitor.visit(m_track);
visitor.visit(m_track_observer);
visitor.visit(m_fetch_algorithms); visitor.visit(m_fetch_algorithms);
visitor.visit(m_fetch_controller); visitor.visit(m_fetch_controller);
} }
@ -132,16 +135,28 @@ void HTMLTrackElement::set_track_url(String track_url)
m_track_url = move(track_url); m_track_url = move(track_url);
auto track_is_hidden_or_showing = first_is_one_of(m_track->mode(), Bindings::TextTrackMode::Hidden, Bindings::TextTrackMode::Showing);
// https://html.spec.whatwg.org/multipage/media.html#start-the-track-processing-model // https://html.spec.whatwg.org/multipage/media.html#start-the-track-processing-model
if (m_loading && m_fetch_controller && first_is_one_of(m_track->mode(), Bindings::TextTrackMode::Hidden, Bindings::TextTrackMode::Showing)) { if (m_loading && m_fetch_controller && track_is_hidden_or_showing) {
m_loading = false; m_loading = false;
m_fetch_controller->abort(realm(), {}); m_fetch_controller->abort(realm(), {});
} }
// https://html.spec.whatwg.org/multipage/media.html#start-the-track-processing-model
if (m_awaiting_track_url_change && track_is_hidden_or_showing) {
m_awaiting_track_url_change = false;
// 13. Jump to the step labeled top.
start_the_track_processing_model_parallel_steps();
}
} }
// https://html.spec.whatwg.org/multipage/media.html#start-the-track-processing-model // https://html.spec.whatwg.org/multipage/media.html#start-the-track-processing-model
void HTMLTrackElement::start_the_track_processing_model() void HTMLTrackElement::start_the_track_processing_model()
{ {
auto& realm = this->realm();
// 1. If another occurrence of this algorithm is already running for this text track and its track element, return, // 1. If another occurrence of this algorithm is already running for this text track and its track element, return,
// letting that other algorithm take care of this element. // letting that other algorithm take care of this element.
if (m_loading) if (m_loading)
@ -158,14 +173,15 @@ void HTMLTrackElement::start_the_track_processing_model()
m_loading = true; m_loading = true;
// 4. Run the remainder of these steps in parallel, allowing whatever caused these steps to run to continue. // 4. Run the remainder of these steps in parallel, allowing whatever caused these steps to run to continue.
auto& realm = this->realm(); Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm.heap(), [this]() {
Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm.heap(), [this, &realm] { start_the_track_processing_model_parallel_steps();
start_the_track_processing_model_parallel_steps(realm);
})); }));
} }
void HTMLTrackElement::start_the_track_processing_model_parallel_steps(JS::Realm& realm) void HTMLTrackElement::start_the_track_processing_model_parallel_steps()
{ {
auto& realm = this->realm();
// 5. Top: Await a stable state. The synchronous section consists of the following steps. // 5. Top: Await a stable state. The synchronous section consists of the following steps.
// 6. ⌛ Set the text track readiness state to loading. // 6. ⌛ Set the text track readiness state to loading.
@ -236,17 +252,22 @@ void HTMLTrackElement::start_the_track_processing_model_parallel_steps(JS::Realm
} }
// 11. Wait until the text track readiness state is no longer set to loading. // 11. Wait until the text track readiness state is no longer set to loading.
HTML::main_thread_event_loop().spin_until(GC::create_function(realm.heap(), [this] { if (m_track->readiness_state() == TextTrack::ReadinessState::Loading) {
return m_track->readiness_state() != TextTrack::ReadinessState::Loading; m_track_observer->set_track_readiness_observer([this, url = move(url)](TextTrack::ReadinessState) mutable {
})); if (m_track->readiness_state() != TextTrack::ReadinessState::Loading)
track_became_ready();
});
} else {
track_became_ready();
}
}
void HTMLTrackElement::track_became_ready()
{
m_track_observer->set_track_readiness_observer({});
// 12. Wait until the track URL is no longer equal to URL, at the same time as the text track mode is set to hidden or showing. // 12. Wait until the track URL is no longer equal to URL, at the same time as the text track mode is set to hidden or showing.
HTML::main_thread_event_loop().spin_until(GC::create_function(realm.heap(), [this, url = move(url)] { m_awaiting_track_url_change = true;
return track_url() != url && first_is_one_of(m_track->mode(), Bindings::TextTrackMode::Hidden, Bindings::TextTrackMode::Showing);
}));
// 13. Jump to the step labeled top.
start_the_track_processing_model_parallel_steps(realm);
} }
void HTMLTrackElement::track_failed_to_load() void HTMLTrackElement::track_failed_to_load()

View file

@ -7,8 +7,8 @@
#pragma once #pragma once
#include <LibWeb/Forward.h>
#include <LibWeb/HTML/HTMLElement.h> #include <LibWeb/HTML/HTMLElement.h>
#include <LibWeb/HTML/TextTrack.h>
namespace Web::HTML { namespace Web::HTML {
@ -33,8 +33,9 @@ private:
void set_track_url(String); void set_track_url(String);
void start_the_track_processing_model(); void start_the_track_processing_model();
void start_the_track_processing_model_parallel_steps(JS::Realm& realm); void start_the_track_processing_model_parallel_steps();
void track_became_ready();
void track_failed_to_load(); void track_failed_to_load();
// ^DOM::Element // ^DOM::Element
@ -42,6 +43,7 @@ private:
virtual void inserted() override; virtual void inserted() override;
GC::Ptr<TextTrack> m_track; GC::Ptr<TextTrack> m_track;
GC::Ptr<TextTrackObserver> m_track_observer;
// https://html.spec.whatwg.org/multipage/media.html#track-url // https://html.spec.whatwg.org/multipage/media.html#track-url
String m_track_url {}; String m_track_url {};
@ -50,6 +52,7 @@ private:
GC::Ptr<Fetch::Infrastructure::FetchController> m_fetch_controller; GC::Ptr<Fetch::Infrastructure::FetchController> m_fetch_controller;
bool m_loading { false }; bool m_loading { false };
bool m_awaiting_track_url_change { false };
}; };
} }

View file

@ -8,6 +8,7 @@
#include <LibWeb/Bindings/Intrinsics.h> #include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/HTML/EventNames.h> #include <LibWeb/HTML/EventNames.h>
#include <LibWeb/HTML/TextTrack.h> #include <LibWeb/HTML/TextTrack.h>
#include <LibWeb/HTML/TextTrackObserver.h>
namespace Web::HTML { namespace Web::HTML {
@ -31,6 +32,12 @@ void TextTrack::initialize(JS::Realm& realm)
Base::initialize(realm); Base::initialize(realm);
} }
void TextTrack::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_observers);
}
// https://html.spec.whatwg.org/multipage/media.html#dom-texttrack-kind // https://html.spec.whatwg.org/multipage/media.html#dom-texttrack-kind
Bindings::TextTrackKind TextTrack::kind() Bindings::TextTrackKind TextTrack::kind()
{ {
@ -98,6 +105,28 @@ WebIDL::CallbackType* TextTrack::oncuechange()
return event_handler_attribute(HTML::EventNames::cuechange); return event_handler_attribute(HTML::EventNames::cuechange);
} }
void TextTrack::set_readiness_state(ReadinessState readiness_state)
{
m_readiness_state = readiness_state;
for (auto observer : m_observers) {
if (auto callback = observer->track_readiness_observer())
callback->function()(m_readiness_state);
}
}
void TextTrack::register_observer(Badge<TextTrackObserver>, TextTrackObserver& observer)
{
auto result = m_observers.set(observer);
VERIFY(result == AK::HashSetResult::InsertedNewEntry);
}
void TextTrack::unregister_observer(Badge<TextTrackObserver>, TextTrackObserver& observer)
{
bool was_removed = m_observers.remove(observer);
VERIFY(was_removed);
}
Bindings::TextTrackKind text_track_kind_from_string(String value) Bindings::TextTrackKind text_track_kind_from_string(String value)
{ {
// https://html.spec.whatwg.org/multipage/media.html#attr-track-kind // https://html.spec.whatwg.org/multipage/media.html#attr-track-kind

View file

@ -51,12 +51,16 @@ public:
WebIDL::CallbackType* oncuechange(); WebIDL::CallbackType* oncuechange();
ReadinessState readiness_state() { return m_readiness_state; } ReadinessState readiness_state() { return m_readiness_state; }
void set_readiness_state(ReadinessState readiness_state) { m_readiness_state = readiness_state; } void set_readiness_state(ReadinessState readiness_state);
void register_observer(Badge<TextTrackObserver>, TextTrackObserver&);
void unregister_observer(Badge<TextTrackObserver>, TextTrackObserver&);
private: private:
TextTrack(JS::Realm&); TextTrack(JS::Realm&);
virtual void initialize(JS::Realm&) override; virtual void initialize(JS::Realm&) override;
virtual void visit_edges(Cell::Visitor&) override;
Bindings::TextTrackKind m_kind { Bindings::TextTrackKind::Subtitles }; Bindings::TextTrackKind m_kind { Bindings::TextTrackKind::Subtitles };
String m_label {}; String m_label {};
@ -67,6 +71,8 @@ private:
Bindings::TextTrackMode m_mode { Bindings::TextTrackMode::Disabled }; Bindings::TextTrackMode m_mode { Bindings::TextTrackMode::Disabled };
ReadinessState m_readiness_state { ReadinessState::NotLoaded }; ReadinessState m_readiness_state { ReadinessState::NotLoaded };
HashTable<GC::Ref<TextTrackObserver>> m_observers;
}; };
Bindings::TextTrackKind text_track_kind_from_string(String); Bindings::TextTrackKind text_track_kind_from_string(String);

View file

@ -0,0 +1,43 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibJS/Runtime/Realm.h>
#include <LibJS/Runtime/VM.h>
#include <LibWeb/HTML/TextTrackObserver.h>
namespace Web::HTML {
GC_DEFINE_ALLOCATOR(TextTrackObserver);
TextTrackObserver::TextTrackObserver(JS::Realm& realm, TextTrack& text_track)
: Bindings::PlatformObject(realm)
, m_text_track(text_track)
{
m_text_track->register_observer({}, *this);
}
void TextTrackObserver::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_text_track);
visitor.visit(m_track_readiness_observer);
}
void TextTrackObserver::finalize()
{
Base::finalize();
m_text_track->unregister_observer({}, *this);
}
void TextTrackObserver::set_track_readiness_observer(Function<void(TextTrack::ReadinessState)> callback)
{
if (callback)
m_track_readiness_observer = GC::create_function(vm().heap(), move(callback));
else
m_track_readiness_observer = nullptr;
}
}

View file

@ -0,0 +1,35 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibGC/Function.h>
#include <LibJS/Forward.h>
#include <LibWeb/Bindings/PlatformObject.h>
#include <LibWeb/Bindings/TextTrackPrototype.h>
#include <LibWeb/HTML/TextTrack.h>
namespace Web::HTML {
class TextTrackObserver final : public Bindings::PlatformObject {
WEB_PLATFORM_OBJECT(TextTrackObserver, Bindings::PlatformObject);
GC_DECLARE_ALLOCATOR(TextTrackObserver);
public:
[[nodiscard]] GC::Ptr<GC::Function<void(TextTrack::ReadinessState)>> track_readiness_observer() const { return m_track_readiness_observer; }
void set_track_readiness_observer(Function<void(TextTrack::ReadinessState)>);
private:
explicit TextTrackObserver(JS::Realm&, TextTrack&);
virtual void visit_edges(Cell::Visitor&) override;
virtual void finalize() override;
GC::Ref<TextTrack> m_text_track;
GC::Ptr<GC::Function<void(TextTrack::ReadinessState)>> m_track_readiness_observer;
};
}

View file

@ -0,0 +1,4 @@
WEBVTT
00:00:00.000 --> 00:00:04.299 line:80%
WFH!

View file

@ -0,0 +1 @@
PASS!

View file

@ -0,0 +1,21 @@
<!DOCTYPE html>
<!-- Ensure the processing model does not block other resources from loading. -->
<img id="image" src="../../../Assets/120.png" />
<video>
<track src="../../../Assets/track.vtt" />
</video>
<script src="../include.js"></script>
<script>
asyncTest(done => {
const complete = () => {
println("PASS!");
done();
};
if (image.complete) {
complete();
} else {
image.addEventListener("load", complete);
}
});
</script>