Bug 1753309 - Implement AbortSignal.timeout() r=smaug

Differential Revision: https://phabricator.services.mozilla.com/D137900
This commit is contained in:
Tom Schuster 2022-04-01 17:15:19 +00:00
parent 7743d4fd62
commit 2e72af38c0
13 changed files with 179 additions and 85 deletions

View File

@ -6,13 +6,17 @@
#include "AbortSignal.h"
#include "mozilla/dom/AbortSignalBinding.h"
#include "mozilla/dom/DOMException.h"
#include "mozilla/dom/Event.h"
#include "mozilla/dom/EventBinding.h"
#include "mozilla/dom/AbortSignalBinding.h"
#include "mozilla/dom/TimeoutHandler.h"
#include "mozilla/dom/TimeoutManager.h"
#include "mozilla/dom/ToJSValue.h"
#include "mozilla/dom/WorkerPrivate.h"
#include "mozilla/RefPtr.h"
#include "nsCycleCollectionParticipant.h"
#include "nsPIDOMWindow.h"
namespace mozilla::dom {
@ -143,6 +147,110 @@ already_AddRefed<AbortSignal> AbortSignal::Abort(GlobalObject& aGlobal,
return abortSignal.forget();
}
class AbortSignalTimeoutHandler final : public TimeoutHandler {
public:
AbortSignalTimeoutHandler(JSContext* aCx, AbortSignal* aSignal)
: TimeoutHandler(aCx), mSignal(aSignal) {}
NS_DECL_CYCLE_COLLECTING_ISUPPORTS
NS_DECL_CYCLE_COLLECTION_CLASS(AbortSignalTimeoutHandler)
// https://dom.spec.whatwg.org/#dom-abortsignal-timeout
// Step 3
MOZ_CAN_RUN_SCRIPT bool Call(const char* /* unused */) override {
AutoJSAPI jsapi;
if (NS_WARN_IF(!jsapi.Init(mSignal->GetParentObject()))) {
// (false is only for setInterval, see
// nsGlobalWindowInner::RunTimeoutHandler)
return true;
}
// Step 1. Queue a global task on the timer task source given global to
// signal abort given signal and a new "TimeoutError" DOMException.
JS::Rooted<JS::Value> exception(jsapi.cx());
RefPtr<DOMException> dom = DOMException::Create(NS_ERROR_DOM_TIMEOUT_ERR);
if (NS_WARN_IF(!ToJSValue(jsapi.cx(), dom, &exception))) {
return true;
}
mSignal->SignalAbort(exception);
return true;
}
private:
~AbortSignalTimeoutHandler() override = default;
RefPtr<AbortSignal> mSignal;
};
NS_IMPL_CYCLE_COLLECTION(AbortSignalTimeoutHandler, mSignal)
NS_IMPL_CYCLE_COLLECTING_ADDREF(AbortSignalTimeoutHandler)
NS_IMPL_CYCLE_COLLECTING_RELEASE(AbortSignalTimeoutHandler)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AbortSignalTimeoutHandler)
NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END
static void SetTimeoutForGlobal(GlobalObject& aGlobal, TimeoutHandler& aHandler,
int32_t timeout, ErrorResult& aRv) {
if (NS_IsMainThread()) {
nsCOMPtr<nsPIDOMWindowInner> innerWindow =
do_QueryInterface(aGlobal.GetAsSupports());
if (!innerWindow) {
aRv.ThrowInvalidStateError("Could not find window.");
return;
}
int32_t handle;
nsresult rv = innerWindow->TimeoutManager().SetTimeout(
&aHandler, timeout, /* aIsInterval */ false,
Timeout::Reason::eAbortSignalTimeout, &handle);
if (NS_FAILED(rv)) {
aRv.Throw(rv);
return;
}
} else {
WorkerPrivate* workerPrivate =
GetWorkerPrivateFromContext(aGlobal.Context());
workerPrivate->SetTimeout(aGlobal.Context(), &aHandler, timeout,
/* aIsInterval */ false,
Timeout::Reason::eAbortSignalTimeout, aRv);
if (aRv.Failed()) {
return;
}
}
}
// https://dom.spec.whatwg.org/#dom-abortsignal-timeout
already_AddRefed<AbortSignal> AbortSignal::Timeout(GlobalObject& aGlobal,
uint64_t aMilliseconds,
ErrorResult& aRv) {
// Step 2. Let global be signals relevant global object.
nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
// Step 1. Let signal be a new AbortSignal object.
RefPtr<AbortSignal> signal =
new AbortSignal(global, false, JS::UndefinedHandleValue);
// Step 3. Run steps after a timeout given global, "AbortSignal-timeout",
// milliseconds, and the following step: ...
RefPtr<TimeoutHandler> handler =
new AbortSignalTimeoutHandler(aGlobal.Context(), signal);
// Note: We only supports int32_t range intervals
int32_t timeout =
aMilliseconds > uint64_t(std::numeric_limits<int32_t>::max())
? std::numeric_limits<int32_t>::max()
: static_cast<int32_t>(aMilliseconds);
SetTimeoutForGlobal(aGlobal, *handler, timeout, aRv);
if (aRv.Failed()) {
return nullptr;
}
// Step 4. Return signal.
return signal.forget();
}
// https://dom.spec.whatwg.org/#dom-abortsignal-throwifaborted
void AbortSignal::ThrowIfAborted(JSContext* aCx, ErrorResult& aRv) {
aRv.MightThrowJSException();

View File

@ -45,6 +45,10 @@ class AbortSignal final : public DOMEventTargetHelper,
JS::Handle<JS::Value> aReason,
ErrorResult& aRv);
static already_AddRefed<AbortSignal> Timeout(GlobalObject& aGlobal,
uint64_t aMilliseconds,
ErrorResult& aRv);
void ThrowIfAborted(JSContext* aCx, ErrorResult& aRv);
// AbortSignalImpl

View File

@ -34,6 +34,7 @@ class Timeout final : protected LinkedListElement<RefPtr<Timeout>> {
enum class Reason : uint8_t {
eTimeoutOrInterval,
eIdleCallbackTimeout,
eAbortSignalTimeout,
};
struct TimeoutIdAndReason {

View File

@ -450,8 +450,9 @@ uint32_t TimeoutManager::GetTimeoutId(Timeout::Reason aReason) {
case Timeout::Reason::eIdleCallbackTimeout:
return ++mIdleCallbackTimeoutCounter;
case Timeout::Reason::eTimeoutOrInterval:
default:
return ++mTimeoutIdCounter;
case Timeout::Reason::eAbortSignalTimeout:
return std::numeric_limits<uint32_t>::max(); // no cancellation support
}
}
@ -491,9 +492,13 @@ nsresult TimeoutManager::SetTimeout(TimeoutHandler* aHandler, int32_t interval,
// No popups from timeouts by default
timeout->mPopupState = PopupBlocker::openAbused;
timeout->mNestingLevel = sNestingLevel < DOM_CLAMP_TIMEOUT_NESTING_LEVEL
? sNestingLevel + 1
: sNestingLevel;
// XXX: Does eIdleCallbackTimeout need clamping?
if (aReason == Timeout::Reason::eTimeoutOrInterval ||
aReason == Timeout::Reason::eIdleCallbackTimeout) {
timeout->mNestingLevel = sNestingLevel < DOM_CLAMP_TIMEOUT_NESTING_LEVEL
? sNestingLevel + 1
: sNestingLevel;
}
// Now clamp the actual interval we will use for the timer based on
TimeDuration realInterval = CalculateDelay(timeout);
@ -559,6 +564,10 @@ void TimeoutManager::ClearTimeout(int32_t aTimerId, Timeout::Reason aReason) {
bool TimeoutManager::ClearTimeoutInternal(int32_t aTimerId,
Timeout::Reason aReason,
bool aIsIdle) {
MOZ_ASSERT(aReason == Timeout::Reason::eTimeoutOrInterval ||
aReason == Timeout::Reason::eIdleCallbackTimeout,
"This timeout reason doesn't support cancellation.");
uint32_t timerId = (uint32_t)aTimerId;
Timeouts& timeouts = aIsIdle ? mIdleTimeouts : mTimeouts;
RefPtr<TimeoutExecutor>& executor = aIsIdle ? mIdleExecutor : mExecutor;

View File

@ -6330,9 +6330,8 @@ static const char* GetTimeoutReasonString(Timeout* aTimeout) {
return "setTimeout handler";
case Timeout::Reason::eIdleCallbackTimeout:
return "setIdleCallback handler (timed out)";
default:
MOZ_CRASH("Unexpected enum value");
return "";
case Timeout::Reason::eAbortSignalTimeout:
return "AbortSignal timeout";
}
}

View File

@ -10,6 +10,8 @@
[Exposed=(Window,Worker)]
interface AbortSignal : EventTarget {
[NewObject, Throws] static AbortSignal abort(optional any reason);
[Exposed=(Window,Worker), NewObject, Throws]
static AbortSignal timeout([EnforceRange] unsigned long long milliseconds);
readonly attribute boolean aborted;
readonly attribute any reason;

View File

@ -986,6 +986,7 @@ struct WorkerPrivate::TimeoutInfo {
TimeoutInfo()
: mId(0),
mNestingLevel(0),
mReason(Timeout::Reason::eTimeoutOrInterval),
mIsInterval(false),
mCanceled(false),
mOnChromeWorker(false) {
@ -1026,6 +1027,7 @@ struct WorkerPrivate::TimeoutInfo {
mozilla::TimeDuration mInterval;
int32_t mId;
uint32_t mNestingLevel;
Timeout::Reason mReason;
bool mIsInterval;
bool mCanceled;
bool mOnChromeWorker;
@ -4699,12 +4701,16 @@ void WorkerPrivate::ReportErrorToConsole(const char* aMessage,
int32_t WorkerPrivate::SetTimeout(JSContext* aCx, TimeoutHandler* aHandler,
int32_t aTimeout, bool aIsInterval,
ErrorResult& aRv) {
Timeout::Reason aReason, ErrorResult& aRv) {
auto data = mWorkerThreadAccessible.Access();
MOZ_ASSERT(aHandler);
const int32_t timerId = data->mNextTimeoutId;
data->mNextTimeoutId += 1;
// Reasons that doesn't support cancellation will get -1 as their ids.
int32_t timerId = -1;
if (aReason == Timeout::Reason::eTimeoutOrInterval) {
timerId = data->mNextTimeoutId;
data->mNextTimeoutId += 1;
}
WorkerStatus currentStatus;
{
@ -4719,6 +4725,7 @@ int32_t WorkerPrivate::SetTimeout(JSContext* aCx, TimeoutHandler* aHandler,
}
auto newInfo = MakeUnique<TimeoutInfo>();
newInfo->mReason = aReason;
newInfo->mOnChromeWorker = mIsChromeWorker;
newInfo->mIsInterval = aIsInterval;
newInfo->mId = timerId;
@ -4726,7 +4733,9 @@ int32_t WorkerPrivate::SetTimeout(JSContext* aCx, TimeoutHandler* aHandler,
if (MOZ_UNLIKELY(timerId == INT32_MAX)) {
NS_WARNING("Timeout ids overflowed!");
data->mNextTimeoutId = 1;
if (aReason == Timeout::Reason::eTimeoutOrInterval) {
data->mNextTimeoutId = 1;
}
}
newInfo->mHandler = aHandler;
@ -4773,7 +4782,10 @@ int32_t WorkerPrivate::SetTimeout(JSContext* aCx, TimeoutHandler* aHandler,
return timerId;
}
void WorkerPrivate::ClearTimeout(int32_t aId) {
void WorkerPrivate::ClearTimeout(int32_t aId, Timeout::Reason aReason) {
MOZ_ASSERT(aReason == Timeout::Reason::eTimeoutOrInterval,
"This timeout reason doesn't support cancellation.");
auto data = mWorkerThreadAccessible.Access();
if (!data->mTimeouts.IsEmpty()) {
@ -4781,7 +4793,7 @@ void WorkerPrivate::ClearTimeout(int32_t aId) {
for (uint32_t index = 0; index < data->mTimeouts.Length(); index++) {
const auto& info = data->mTimeouts[index];
if (info->mId == aId) {
if (info->mId == aId && info->mReason == aReason) {
info->mCanceled = true;
break;
}
@ -4858,23 +4870,29 @@ bool WorkerPrivate::RunExpiredTimeouts(JSContext* aCx) {
// Always check JS_IsExceptionPending if something fails, and if
// JS_IsExceptionPending returns false (i.e. uncatchable exception) then
// break out of the loop.
const char* reason;
if (info->mIsInterval) {
reason = "setInterval handler";
} else {
reason = "setTimeout handler";
}
RefPtr<TimeoutHandler> handler(info->mHandler);
if (info->mReason == Timeout::Reason::eTimeoutOrInterval) {
const char* reason;
if (info->mIsInterval) {
reason = "setInterval handler";
} else {
reason = "setTimeout handler";
}
RefPtr<WorkerGlobalScope> scope(this->GlobalScope());
CallbackDebuggerNotificationGuard guard(
scope, info->mIsInterval
? DebuggerNotificationType::SetIntervalCallback
: DebuggerNotificationType::SetTimeoutCallback);
if (!handler->Call(reason)) {
retval = false;
break;
RefPtr<WorkerGlobalScope> scope(this->GlobalScope());
CallbackDebuggerNotificationGuard guard(
scope, info->mIsInterval
? DebuggerNotificationType::SetIntervalCallback
: DebuggerNotificationType::SetTimeoutCallback);
if (!handler->Call(reason)) {
retval = false;
break;
}
} else {
MOZ_ASSERT(info->mReason == Timeout::Reason::eAbortSignalTimeout);
MOZ_ALWAYS_TRUE(handler->Call("AbortSignal timeout"));
}
NS_ASSERTION(data->mRunningExpiredTimeouts, "Someone changed this!");

View File

@ -28,6 +28,7 @@
#include "mozilla/UseCounter.h"
#include "mozilla/dom/ClientSource.h"
#include "mozilla/dom/FlippedOnce.h"
#include "mozilla/dom/Timeout.h"
#include "mozilla/dom/quota/CheckedUnsafePtr.h"
#include "mozilla/dom/Worker.h"
#include "mozilla/dom/WorkerCommon.h"
@ -317,9 +318,10 @@ class WorkerPrivate final
const nsTArray<nsString>& aParams);
int32_t SetTimeout(JSContext* aCx, TimeoutHandler* aHandler, int32_t aTimeout,
bool aIsInterval, ErrorResult& aRv);
bool aIsInterval, Timeout::Reason aReason,
ErrorResult& aRv);
void ClearTimeout(int32_t aId);
void ClearTimeout(int32_t aId, Timeout::Reason aReason);
MOZ_CAN_RUN_SCRIPT bool RunExpiredTimeouts(JSContext* aCx);

View File

@ -521,7 +521,7 @@ void WorkerGlobalScope::ClearTimeout(int32_t aHandle) {
DebuggerNotificationDispatch(this, DebuggerNotificationType::ClearTimeout);
mWorkerPrivate->ClearTimeout(aHandle);
mWorkerPrivate->ClearTimeout(aHandle, Timeout::Reason::eTimeoutOrInterval);
}
int32_t WorkerGlobalScope::SetInterval(JSContext* aCx, Function& aHandler,
@ -544,7 +544,7 @@ void WorkerGlobalScope::ClearInterval(int32_t aHandle) {
DebuggerNotificationDispatch(this, DebuggerNotificationType::ClearInterval);
mWorkerPrivate->ClearTimeout(aHandle);
mWorkerPrivate->ClearTimeout(aHandle, Timeout::Reason::eTimeoutOrInterval);
}
int32_t WorkerGlobalScope::SetTimeoutOrInterval(
@ -565,7 +565,8 @@ int32_t WorkerGlobalScope::SetTimeoutOrInterval(
RefPtr<TimeoutHandler> handler =
new CallbackTimeoutHandler(aCx, this, &aHandler, std::move(args));
return mWorkerPrivate->SetTimeout(aCx, handler, aTimeout, aIsInterval, aRv);
return mWorkerPrivate->SetTimeout(aCx, handler, aTimeout, aIsInterval,
Timeout::Reason::eTimeoutOrInterval, aRv);
}
int32_t WorkerGlobalScope::SetTimeoutOrInterval(JSContext* aCx,
@ -589,7 +590,8 @@ int32_t WorkerGlobalScope::SetTimeoutOrInterval(JSContext* aCx,
RefPtr<TimeoutHandler> handler =
new WorkerScriptTimeoutHandler(aCx, this, aHandler);
return mWorkerPrivate->SetTimeout(aCx, handler, aTimeout, aIsInterval, aRv);
return mWorkerPrivate->SetTimeout(aCx, handler, aTimeout, aIsInterval,
Timeout::Reason::eTimeoutOrInterval, aRv);
}
void WorkerGlobalScope::GetOrigin(nsAString& aOrigin) const {

View File

@ -1,20 +0,0 @@
[AbortSignal.any.html]
[AbortSignal.timeout() returns a non-aborted signal]
expected: FAIL
[Signal returned by AbortSignal.timeout() times out]
expected: FAIL
[AbortSignal timeouts fire in order]
expected: FAIL
[AbortSignal.any.worker.html]
[AbortSignal.timeout() returns a non-aborted signal]
expected: FAIL
[Signal returned by AbortSignal.timeout() times out]
expected: FAIL
[AbortSignal timeouts fire in order]
expected: FAIL

View File

@ -1,3 +0,0 @@
[abort-signal-timeout.html]
[Signal returned by AbortSignal.timeout() is not aborted after frame detach]
expected: FAIL

View File

@ -1,22 +0,0 @@
[idlharness.any.sharedworker.html]
[AbortSignal interface: operation timeout(unsigned long long)]
expected: FAIL
[AbortSignal interface: calling timeout(unsigned long long) on new AbortController().signal with too few arguments must throw TypeError]
expected: FAIL
[idlharness.any.worker.html]
[AbortSignal interface: operation timeout(unsigned long long)]
expected: FAIL
[AbortSignal interface: calling timeout(unsigned long long) on new AbortController().signal with too few arguments must throw TypeError]
expected: FAIL
[idlharness.any.serviceworker.html]
[AbortSignal interface: operation timeout(unsigned long long)]
expected: FAIL
[AbortSignal interface: calling timeout(unsigned long long) on new AbortController().signal with too few arguments must throw TypeError]
expected: FAIL

View File

@ -6,9 +6,3 @@
[Stringification of document.createNSResolver(document.body)]
expected: FAIL
[AbortSignal interface: operation timeout(unsigned long long)]
expected: FAIL
[AbortSignal interface: calling timeout(unsigned long long) on new AbortController().signal with too few arguments must throw TypeError]
expected: FAIL