Revert "Reland "[ios blink] Implement futures using WaitListEvent""

This reverts commit a218ceac197ff55273d80942e114eea9d6dcc90c.

Reason for revert: Linux TSAN failures in WaitListEventTests:
https://ci.chromium.org/ui/p/chromium/builders/ci/Dawn%20Linux%20TSAN%20Release/60645/overview

Bug: 407801085
Original change's description:
> Reland "[ios blink] Implement futures using WaitListEvent"
>
> This is a reland of commit abc42736ef28ed8fcc11e66814907febff6ee930
>
> The only change over the original is the correct initialization of the
> `mSignaled` atomic_bool in WaitListEvent.
>
> Original change's description:
> > [ios blink] Implement futures using WaitListEvent
> >
> > Currently, most futures are implemented using SystemEvent. This includes
> > already completed and non-progressing events. This is problematic on iOS
> > where BrowserEngineKit child processes are not allowed to open fds, mach
> > ports, etc.
> >
> > This CL introduces WaitListEvent which mimics the base::WaitableEvent
> > implementation in Chromium for POSIX platforms. The event internally
> > maintains a list of waiters corresponding to a WaitAny call. In WaitAny,
> > we create a SyncWaiter that's signaled using a condition variable. The
> > waiter is added to each event that the WaitAny is waiting on. The events
> > also have a mutex to allow multiple threads to wait on them. We acquire
> > the event locks in a globally consistent order (sorted by address) to
> > prevent lock order inversion. WaitListEvents can also be waited on
> > asynchronously by returning a SystemEventReceiver which allows mixing
> > waits on SystemEvents and WaitListEvents.
> >
> > In addition, this CL changes how already signaled and non-progressing
> > TrackedEvents are represented. Already signaled events are backed by
> > WaitListEvents and non-progressing events become a flag on TrackedEvent.
> >
> > The code in EventManager is also cleaned up to aid readability and fix
> > an edge case of waiting on multiple queues with zero timeout - we could
> > end up ticking the same queue multiple times causing a subtle race
> > between MapAsync and OnSubmittedWorkDone futures completion.
> >
> > Bug: 407801085
> > Change-Id: I1c5deb8097339be5beb5e9021d753998a074bea3
> > Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/234277
> > Reviewed-by: Loko Kung <lokokung@google.com>
> > Auto-Submit: Sunny Sachanandani <sunnyps@chromium.org>
> > Commit-Queue: Sunny Sachanandani <sunnyps@chromium.org>
> > Reviewed-by: Kai Ninomiya <kainino@chromium.org>
> > Commit-Queue: Kai Ninomiya <kainino@chromium.org>
>
> Bug: 407801085
> Change-Id: I254398256851f2310ddfe906c79d389dae1d3d77
> Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/237719
> Commit-Queue: Corentin Wallez <cwallez@chromium.org>
> Reviewed-by: Kai Ninomiya <kainino@chromium.org>
> Commit-Queue: Sunny Sachanandani <sunnyps@chromium.org>
> Reviewed-by: Corentin Wallez <cwallez@chromium.org>
> Commit-Queue: Kai Ninomiya <kainino@chromium.org>
> Auto-Submit: Sunny Sachanandani <sunnyps@chromium.org>

TBR=cwallez@chromium.org,kainino@chromium.org,dawn-scoped@luci-project-accounts.iam.gserviceaccount.com,sunnyps@chromium.org,lokokung@google.com

No-Presubmit: true
No-Tree-Checks: true
No-Try: true
Bug: 407801085
Change-Id: I9966a09a2dd202579046c87b6609b53fff12bfac
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/237721
Commit-Queue: rubber-stamper@appspot.gserviceaccount.com <rubber-stamper@appspot.gserviceaccount.com>
Bot-Commit: rubber-stamper@appspot.gserviceaccount.com <rubber-stamper@appspot.gserviceaccount.com>
Auto-Submit: Kai Ninomiya <kainino@chromium.org>
diff --git a/src/dawn/native/BUILD.gn b/src/dawn/native/BUILD.gn
index d3bdd62..fd055fc 100644
--- a/src/dawn/native/BUILD.gn
+++ b/src/dawn/native/BUILD.gn
@@ -398,8 +398,6 @@
     "ValidationUtils.h",
     "VisitableMembers.h",
     "WaitAnySystemEvent.h",
-    "WaitListEvent.cpp",
-    "WaitListEvent.h",
     "dawn_platform.h",
     "stream/BlobSource.cpp",
     "stream/BlobSource.h",
diff --git a/src/dawn/native/Buffer.cpp b/src/dawn/native/Buffer.cpp
index 46aebb1..f99f976 100644
--- a/src/dawn/native/Buffer.cpp
+++ b/src/dawn/native/Buffer.cpp
@@ -212,7 +212,7 @@
                   const std::string& message,
                   WGPUMapAsyncStatus status)
         : TrackedEvent(static_cast<wgpu::CallbackMode>(callbackInfo.mode),
-                       TrackedEvent::Completed{}),
+                       SystemEvent::CreateSignaled()),
           mBufferOrError(BufferErrorData{status, message}),
           mCallback(callbackInfo.callback),
           mUserdata1(callbackInfo.userdata1),
@@ -224,7 +224,7 @@
     ~MapAsyncEvent() override { EnsureComplete(EventCompletionType::Shutdown); }
 
     void Complete(EventCompletionType completionType) override {
-        if (const auto* queueAndSerial = GetIfQueueAndSerial()) {
+        if (const auto* queueAndSerial = std::get_if<QueueAndSerial>(&GetCompletionData())) {
             TRACE_EVENT_ASYNC_END0(queueAndSerial->queue->GetDevice()->GetPlatform(), General,
                                    "Buffer::APIMapAsync",
                                    uint64_t(queueAndSerial->completionSerial));
diff --git a/src/dawn/native/CMakeLists.txt b/src/dawn/native/CMakeLists.txt
index 2ef2e07..06d7845 100644
--- a/src/dawn/native/CMakeLists.txt
+++ b/src/dawn/native/CMakeLists.txt
@@ -153,7 +153,6 @@
     "ValidationUtils.h"
     "VisitableMembers.h"
     "WaitAnySystemEvent.h"
-    "WaitListEvent.h"
     "webgpu_absl_format.h"
 )
 
@@ -254,7 +253,6 @@
     "Toggles.cpp"
     "utils/WGPUHelpers.cpp"
     "ValidationUtils.cpp"
-    "WaitListEvent.cpp"
     "webgpu_absl_format.cpp"
 )
 
diff --git a/src/dawn/native/CreatePipelineAsyncEvent.cpp b/src/dawn/native/CreatePipelineAsyncEvent.cpp
index 3599b45..844d1d2 100644
--- a/src/dawn/native/CreatePipelineAsyncEvent.cpp
+++ b/src/dawn/native/CreatePipelineAsyncEvent.cpp
@@ -41,7 +41,7 @@
 #include "dawn/native/EventManager.h"
 #include "dawn/native/Instance.h"
 #include "dawn/native/RenderPipeline.h"
-#include "dawn/native/WaitListEvent.h"
+#include "dawn/native/SystemEvent.h"
 #include "dawn/native/dawn_platform_autogen.h"
 #include "dawn/native/utils/WGPUHelpers.h"
 #include "dawn/native/wgpu_structs_autogen.h"
@@ -96,8 +96,8 @@
     DeviceBase* device,
     const CreatePipelineAsyncCallbackInfo& callbackInfo,
     Ref<PipelineType> pipeline,
-    Ref<WaitListEvent> event)
-    : TrackedEvent(static_cast<wgpu::CallbackMode>(callbackInfo.mode), std::move(event)),
+    Ref<SystemEvent> systemEvent)
+    : TrackedEvent(static_cast<wgpu::CallbackMode>(callbackInfo.mode), std::move(systemEvent)),
       mCallback(callbackInfo.callback),
       mUserdata1(callbackInfo.userdata1),
       mUserdata2(callbackInfo.userdata2),
diff --git a/src/dawn/native/CreatePipelineAsyncEvent.h b/src/dawn/native/CreatePipelineAsyncEvent.h
index a6bd1e0..b2fefa6 100644
--- a/src/dawn/native/CreatePipelineAsyncEvent.h
+++ b/src/dawn/native/CreatePipelineAsyncEvent.h
@@ -56,12 +56,12 @@
   public:
     using CallbackType = decltype(std::declval<CreatePipelineAsyncCallbackInfo>().callback);
 
-    // Create an event backed by the given wait list event (for async pipeline creation goes through
+    // Create an event backed by the given system event (for async pipeline creation goes through
     // the backend).
     CreatePipelineAsyncEvent(DeviceBase* device,
                              const CreatePipelineAsyncCallbackInfo& callbackInfo,
                              Ref<PipelineType> pipeline,
-                             Ref<WaitListEvent> event);
+                             Ref<SystemEvent> systemEvent);
     // Create an event that's ready at creation (for cached results)
     CreatePipelineAsyncEvent(DeviceBase* device,
                              const CreatePipelineAsyncCallbackInfo& callbackInfo,
diff --git a/src/dawn/native/Device.cpp b/src/dawn/native/Device.cpp
index ce36dc6..e6f0c63 100644
--- a/src/dawn/native/Device.cpp
+++ b/src/dawn/native/Device.cpp
@@ -77,7 +77,6 @@
 #include "dawn/native/SwapChain.h"
 #include "dawn/native/Texture.h"
 #include "dawn/native/ValidationUtils_autogen.h"
-#include "dawn/native/WaitListEvent.h"
 #include "dawn/native/utils/WGPUHelpers.h"
 #include "dawn/platform/DawnPlatform.h"
 #include "dawn/platform/metrics/HistogramMacros.h"
@@ -155,7 +154,7 @@
 
 DeviceBase::DeviceLostEvent::DeviceLostEvent(const WGPUDeviceLostCallbackInfo& callbackInfo)
     : TrackedEvent(static_cast<wgpu::CallbackMode>(callbackInfo.mode),
-                   TrackedEvent::NonProgressing{}),
+                   SystemEvent::CreateNonProgressingEvent()),
       mCallback(callbackInfo.callback),
       mUserdata1(callbackInfo.userdata1),
       mUserdata2(callbackInfo.userdata2) {}
@@ -1283,7 +1282,7 @@
     // New pipeline: create an event backed by system event that is really async.
     Ref<CreateComputePipelineAsyncEvent> event = AcquireRef(new CreateComputePipelineAsyncEvent(
         this, callbackInfo, std::move(uninitializedComputePipeline),
-        AcquireRef(new WaitListEvent())));
+        AcquireRef(new SystemEvent())));
     Future future = GetFuture(event);
     InitializeComputePipelineAsyncImpl(std::move(event));
     return future;
@@ -1351,8 +1350,7 @@
 
     // New pipeline: create an event backed by system event that is really async.
     Ref<CreateRenderPipelineAsyncEvent> event = AcquireRef(new CreateRenderPipelineAsyncEvent(
-        this, callbackInfo, std::move(uninitializedRenderPipeline),
-        AcquireRef(new WaitListEvent())));
+        this, callbackInfo, std::move(uninitializedRenderPipeline), AcquireRef(new SystemEvent())));
     Future future = GetFuture(event);
     InitializeRenderPipelineAsyncImpl(std::move(event));
     return future;
diff --git a/src/dawn/native/EventManager.cpp b/src/dawn/native/EventManager.cpp
index 2780fe2..835f92c 100644
--- a/src/dawn/native/EventManager.cpp
+++ b/src/dawn/native/EventManager.cpp
@@ -29,7 +29,6 @@
 
 #include <algorithm>
 #include <functional>
-#include <type_traits>
 #include <utility>
 #include <vector>
 
@@ -59,186 +58,199 @@
     bool ready;
 };
 
-// Wrapper around an iterator to yield event specific objects and a pointer
-// to the ready bool. We pass this into helpers so that they can extract
-// the event specific objects and get pointers to the ready status - without
-// allocating duplicate storage to store the objects and ready bools.
-template <typename Traits>
-class WrappingIterator {
+// Wrapper around an iterator to yield system event receiver and a pointer
+// to the ready bool. We pass this into WaitAnySystemEvent so it can extract
+// the receivers and get pointers to the ready status - without allocating
+// duplicate storage to store the receivers and ready bools.
+class SystemEventAndReadyStateIterator {
   public:
+    using WrappedIter = std::vector<TrackedFutureWaitInfo>::iterator;
+
     // Specify required iterator traits.
-    using value_type = typename Traits::value_type;
-    using difference_type = typename Traits::WrappedIter::difference_type;
-    using iterator_category = typename Traits::WrappedIter::iterator_category;
+    using value_type = std::pair<const SystemEventReceiver&, bool*>;
+    using difference_type = typename WrappedIter::difference_type;
+    using iterator_category = typename WrappedIter::iterator_category;
     using pointer = value_type*;
     using reference = value_type&;
 
-    WrappingIterator() = default;
-    WrappingIterator(const WrappingIterator&) = default;
-    WrappingIterator& operator=(const WrappingIterator&) = default;
+    SystemEventAndReadyStateIterator() = default;
+    SystemEventAndReadyStateIterator(const SystemEventAndReadyStateIterator&) = default;
+    SystemEventAndReadyStateIterator& operator=(const SystemEventAndReadyStateIterator&) = default;
 
-    explicit WrappingIterator(typename Traits::WrappedIter wrappedIt) : mWrappedIt(wrappedIt) {}
+    explicit SystemEventAndReadyStateIterator(WrappedIter wrappedIt) : mWrappedIt(wrappedIt) {}
 
-    bool operator!=(const WrappingIterator& rhs) const { return rhs.mWrappedIt != mWrappedIt; }
-    bool operator==(const WrappingIterator& rhs) const { return rhs.mWrappedIt == mWrappedIt; }
-
-    difference_type operator-(const WrappingIterator& rhs) const {
+    bool operator!=(const SystemEventAndReadyStateIterator& rhs) const {
+        return rhs.mWrappedIt != mWrappedIt;
+    }
+    bool operator==(const SystemEventAndReadyStateIterator& rhs) const {
+        return rhs.mWrappedIt == mWrappedIt;
+    }
+    difference_type operator-(const SystemEventAndReadyStateIterator& rhs) const {
         return mWrappedIt - rhs.mWrappedIt;
     }
 
-    WrappingIterator operator+(difference_type rhs) const {
-        return WrappingIterator{mWrappedIt + rhs};
+    SystemEventAndReadyStateIterator operator+(difference_type rhs) const {
+        return SystemEventAndReadyStateIterator{mWrappedIt + rhs};
     }
 
-    WrappingIterator& operator++() {
+    SystemEventAndReadyStateIterator& operator++() {
         ++mWrappedIt;
         return *this;
     }
 
-    value_type operator*() { return Traits::Deref(mWrappedIt); }
-
-  private:
-    typename Traits::WrappedIter mWrappedIt;
-};
-
-struct ExtractSystemEventAndReadyStateTraits {
-    using WrappedIter = std::vector<TrackedFutureWaitInfo>::iterator;
-    using value_type = std::pair<const SystemEventReceiver&, bool*>;
-
-    static value_type Deref(const WrappedIter& wrappedIt) {
-        if (auto event = wrappedIt->event->GetIfWaitListEvent()) {
-            return {event->WaitAsync(), &wrappedIt->ready};
-        }
-        DAWN_ASSERT(wrappedIt->event->GetIfSystemEvent());
+    value_type operator*() {
         return {
-            wrappedIt->event->GetIfSystemEvent()->GetOrCreateSystemEventReceiver(),
-            &wrappedIt->ready,
+            std::get<Ref<SystemEvent>>(mWrappedIt->event->GetCompletionData())
+                ->GetOrCreateSystemEventReceiver(),
+            &mWrappedIt->ready,
         };
     }
+
+  private:
+    WrappedIter mWrappedIt;
 };
 
-using SystemEventAndReadyStateIterator = WrappingIterator<ExtractSystemEventAndReadyStateTraits>;
-
-struct ExtractWaitListEventAndReadyStateTraits {
-    using WrappedIter = std::vector<TrackedFutureWaitInfo>::iterator;
-    using value_type = std::pair<Ref<WaitListEvent>, bool*>;
-
-    static value_type Deref(const WrappedIter& wrappedIt) {
-        DAWN_ASSERT(wrappedIt->event->GetIfWaitListEvent());
-        return {wrappedIt->event->GetIfWaitListEvent(), &wrappedIt->ready};
-    }
-};
-
-using WaitListEventAndReadyStateIterator =
-    WrappingIterator<ExtractWaitListEventAndReadyStateTraits>;
-
-// Returns true if at least one future is ready.
-bool PollFutures(std::vector<TrackedFutureWaitInfo>& futures) {
+// Wait/poll the queue for futures in range [begin, end). `waitSerial` should be
+// the serial after which at least one future should be complete. All futures must
+// have completion data of type QueueAndSerial.
+// Returns true if at least one future is ready. If no futures are ready or the wait
+// timed out, returns false.
+bool WaitQueueSerialsImpl(DeviceBase* device,
+                          QueueBase* queue,
+                          ExecutionSerial waitSerial,
+                          std::vector<TrackedFutureWaitInfo>::iterator begin,
+                          std::vector<TrackedFutureWaitInfo>::iterator end,
+                          Nanoseconds timeout) {
     bool success = false;
-    for (auto& future : futures) {
-        if (future.event->IsReadyToComplete()) {
-            success = true;
-            future.ready = true;
+    // TODO(dawn:1662): Make error handling thread-safe.
+    auto deviceLock(device->GetScopedLock());
+    if (device->ConsumedError([&]() -> MaybeError {
+            if (waitSerial > queue->GetLastSubmittedCommandSerial()) {
+                // Serial has not been submitted yet. Submit it now.
+                // TODO(dawn:1413): This doesn't need to be a full tick. It just needs to
+                // flush work up to `waitSerial`. This should be done after the
+                // ExecutionQueue / ExecutionContext refactor.
+                queue->ForceEventualFlushOfCommands();
+                DAWN_TRY(device->Tick());
+            }
+            // Check the completed serial.
+            ExecutionSerial completedSerial = queue->GetCompletedCommandSerial();
+            if (completedSerial < waitSerial) {
+                if (timeout > Nanoseconds(0)) {
+                    // Wait on the serial if it hasn't passed yet.
+                    DAWN_TRY_ASSIGN(success, queue->WaitForQueueSerial(waitSerial, timeout));
+                }
+                // Update completed serials.
+                DAWN_TRY(queue->CheckPassedSerials());
+                completedSerial = queue->GetCompletedCommandSerial();
+            }
+            // Poll futures for completion.
+            for (auto it = begin; it != end; ++it) {
+                ExecutionSerial serial =
+                    std::get<QueueAndSerial>(it->event->GetCompletionData()).completionSerial;
+                if (serial <= completedSerial) {
+                    success = true;
+                    it->ready = true;
+                }
+            }
+            return {};
+        }())) {
+        // There was an error. Pending submit may have failed or waiting for fences
+        // may have lost the device. The device is lost inside ConsumedError.
+        // Mark all futures as ready.
+        for (auto it = begin; it != end; ++it) {
+            it->ready = true;
         }
+        success = true;
     }
     return success;
 }
 
-// Wait/poll queues with given `timeout`. `queueWaitSerials` should contain per queue, the serial up
-// to which we should flush the queue if needed.
-using QueueWaitSerialsMap = absl::flat_hash_map<QueueBase*, ExecutionSerial>;
-void WaitQueueSerials(const QueueWaitSerialsMap& queueWaitSerials, Nanoseconds timeout) {
-    // TODO(dawn:1662): Make error handling thread-safe.
-    // Poll/wait on queues up to the lowest wait serial, but do this once per queue instead of
-    // per event so that events with same serial complete at the same time instead of racing.
-    for (const auto& queueAndSerial : queueWaitSerials) {
-        auto* queue = queueAndSerial.first;
-        auto waitSerial = queueAndSerial.second;
-
-        auto* device = queue->GetDevice();
-        auto deviceLock(device->GetScopedLock());
-
-        [[maybe_unused]] bool error = device->ConsumedError(
-            [&]() -> MaybeError {
-                if (waitSerial > queue->GetLastSubmittedCommandSerial()) {
-                    // Serial has not been submitted yet. Submit it now.
-                    DAWN_TRY(queue->EnsureCommandsFlushed(waitSerial));
-                }
-                // Check the completed serial.
-                if (waitSerial > queue->GetCompletedCommandSerial()) {
-                    if (timeout > Nanoseconds(0)) {
-                        // Wait on the serial if it hasn't passed yet.
-                        [[maybe_unused]] bool waitResult = false;
-                        DAWN_TRY_ASSIGN(waitResult, queue->WaitForQueueSerial(waitSerial, timeout));
-                    }
-                    // Update completed serials.
-                    DAWN_TRY(queue->CheckPassedSerials());
-                }
-                return {};
-            }(),
-            "waiting for work in %s.", queue);
-    }
-}
-
 // We can replace the std::vector& when std::span is available via C++20.
 wgpu::WaitStatus WaitImpl(const InstanceBase* instance,
                           std::vector<TrackedFutureWaitInfo>& futures,
                           Nanoseconds timeout) {
-    bool foundSystemEvent = false;
-    bool foundWaitListEvent = false;
-    QueueWaitSerialsMap queueLowestWaitSerials;
+    auto begin = futures.begin();
+    const auto end = futures.end();
+    bool anySuccess = false;
+    // The following loop will partition [begin, end) based on the type of wait is required.
+    // After each partition, it will wait/poll on the first partition, then advance `begin`
+    // to the start of the next partition. Note that for timeout > 0 and unsupported mixed
+    // sources, we validate that there is a single partition. If there is only one, then the
+    // loop runs only once and the timeout does not stack.
+    while (begin != end) {
+        const auto& first = begin->event->GetCompletionData();
 
-    for (const auto& future : futures) {
-        if (future.event->GetIfSystemEvent()) {
-            foundSystemEvent = true;
+        DeviceBase* waitDevice;
+        ExecutionSerial lowestWaitSerial;
+        if (std::holds_alternative<Ref<SystemEvent>>(first)) {
+            waitDevice = nullptr;
+        } else {
+            const auto& queueAndSerial = std::get<QueueAndSerial>(first);
+            waitDevice = queueAndSerial.queue->GetDevice();
+            lowestWaitSerial = queueAndSerial.completionSerial;
         }
-        if (future.event->GetIfWaitListEvent()) {
-            foundWaitListEvent = true;
-        }
-        if (const auto* queueAndSerial = future.event->GetIfQueueAndSerial()) {
-            auto [it, inserted] = queueLowestWaitSerials.insert(
-                {queueAndSerial->queue.Get(), queueAndSerial->completionSerial});
-            if (!inserted) {
-                it->second = std::min(it->second, queueAndSerial->completionSerial);
+        // Partition the remaining futures based on whether they match the same completion
+        // data type as the first. Also keep track of the lowest wait serial.
+        const auto mid =
+            std::partition(std::next(begin), end, [&](const TrackedFutureWaitInfo& info) {
+                const auto& completionData = info.event->GetCompletionData();
+                if (std::holds_alternative<Ref<SystemEvent>>(completionData)) {
+                    return waitDevice == nullptr;
+                } else {
+                    const auto& queueAndSerial = std::get<QueueAndSerial>(completionData);
+                    if (waitDevice == queueAndSerial.queue->GetDevice()) {
+                        lowestWaitSerial =
+                            std::min(lowestWaitSerial, queueAndSerial.completionSerial);
+                        return true;
+                    } else {
+                        return false;
+                    }
+                }
+            });
+
+        // There's a mix of wait sources if partition yielded an iterator that is not at the end.
+        if (mid != end) {
+            if (timeout > Nanoseconds(0)) {
+                // Multi-source wait is unsupported.
+                // TODO(dawn:2062): Implement support for this when the device supports it.
+                // It should eventually gather the lowest serial from the queue(s), transform them
+                // into completion events, and wait on all of the events. Then for any queues that
+                // saw a completion, poll all futures related to that queue for completion.
+                instance->EmitLog(WGPULoggingType_Error,
+                                  "Mixed source waits with timeouts are not currently supported.");
+                return wgpu::WaitStatus::Error;
             }
         }
-    }
 
-    if (timeout == Nanoseconds(0)) {
-        // This is a no-op if `queueLowestWaitSerials` is empty.
-        WaitQueueSerials(queueLowestWaitSerials, timeout);
-        return PollFutures(futures) ? wgpu::WaitStatus::Success : wgpu::WaitStatus::TimedOut;
-    }
+        bool success;
+        if (waitDevice) {
+            success = WaitQueueSerialsImpl(waitDevice, std::get<QueueAndSerial>(first).queue.Get(),
+                                           lowestWaitSerial, begin, mid, timeout);
+        } else {
+            if (timeout > Nanoseconds(0)) {
+                success = WaitAnySystemEvent(SystemEventAndReadyStateIterator{begin},
+                                             SystemEventAndReadyStateIterator{mid}, timeout);
+            } else {
+                // Poll the completion events.
+                success = false;
+                for (auto it = begin; it != mid; ++it) {
+                    if (std::get<Ref<SystemEvent>>(it->event->GetCompletionData())->IsSignaled()) {
+                        it->ready = true;
+                        success = true;
+                    }
+                }
+            }
+        }
+        anySuccess |= success;
 
-    // We can't have a mix of system/wait-list events and queue-serial events or queue-serial events
-    // from multiple queues with a non-zero timeout.
-    if (queueLowestWaitSerials.size() > 1 ||
-        (!queueLowestWaitSerials.empty() && (foundWaitListEvent || foundSystemEvent))) {
-        // Multi-source wait is unsupported.
-        // TODO(dawn:2062): Implement support for this when the device supports it.
-        // It should eventually gather the lowest serial from the queue(s), transform them
-        // into completion events, and wait on all of the events. Then for any queues that
-        // saw a completion, poll all futures related to that queue for completion.
-        instance->EmitLog(WGPULoggingType_Error,
-                          "Mixed source waits with timeouts are not currently supported.");
-        return wgpu::WaitStatus::Error;
+        // Advance the iterator to the next partition.
+        begin = mid;
     }
-
-    bool success = false;
-    if (foundSystemEvent) {
-        // Can upgrade wait list events to system events.
-        success = WaitAnySystemEvent(SystemEventAndReadyStateIterator{futures.begin()},
-                                     SystemEventAndReadyStateIterator{futures.end()}, timeout);
-    } else if (foundWaitListEvent) {
-        success =
-            WaitListEvent::WaitAny(WaitListEventAndReadyStateIterator{futures.begin()},
-                                   WaitListEventAndReadyStateIterator{futures.end()}, timeout);
-    } else {
-        // This is a no-op if `queueLowestWaitSerials` is empty.
-        WaitQueueSerials(queueLowestWaitSerials, timeout);
-        success = PollFutures(futures);
+    if (!anySuccess) {
+        return wgpu::WaitStatus::TimedOut;
     }
-    return success ? wgpu::WaitStatus::Success : wgpu::WaitStatus::TimedOut;
+    return wgpu::WaitStatus::Success;
 }
 
 // Reorder callbacks to enforce callback ordering required by the spec.
@@ -315,7 +327,17 @@
 
     // Handle the event now if it's spontaneous and ready.
     if (event->mCallbackMode == wgpu::CallbackMode::AllowSpontaneous) {
-        if (event->IsReadyToComplete()) {
+        bool isReady = false;
+        auto completionData = event->GetCompletionData();
+        if (std::holds_alternative<Ref<SystemEvent>>(completionData)) {
+            isReady = std::get<Ref<SystemEvent>>(completionData)->IsSignaled();
+        }
+        if (std::holds_alternative<QueueAndSerial>(completionData)) {
+            auto& queueAndSerial = std::get<QueueAndSerial>(completionData);
+            isReady = queueAndSerial.completionSerial <=
+                      queueAndSerial.queue->GetCompletedCommandSerial();
+        }
+        if (isReady) {
             event->EnsureComplete(EventCompletionType::Ready);
             return futureID;
         }
@@ -338,6 +360,15 @@
 }
 
 void EventManager::SetFutureReady(TrackedEvent* event) {
+    auto completionData = event->GetCompletionData();
+    if (std::holds_alternative<Ref<SystemEvent>>(completionData)) {
+        std::get<Ref<SystemEvent>>(completionData)->Signal();
+    }
+    if (std::holds_alternative<QueueAndSerial>(completionData)) {
+        auto& queueAndSerial = std::get<QueueAndSerial>(completionData);
+        queueAndSerial.completionSerial = queueAndSerial.queue->GetCompletedCommandSerial();
+    }
+
     // Sometimes, events might become ready before they are even tracked. This can happen because
     // tracking is ordered to uphold callback ordering, but events may become ready in any order. If
     // the event is spontaneous, it will be completed when it is tracked.
@@ -345,8 +376,6 @@
         return;
     }
 
-    event->SetReadyToComplete();
-
     // Handle spontaneous completion now.
     if (event->mCallbackMode == wgpu::CallbackMode::AllowSpontaneous) {
         mEvents.Use([&](auto events) {
@@ -377,7 +406,11 @@
                 // Figure out if there are any progressing events. If we only have non-progressing
                 // events, we need to return false to indicate that there isn't any polling work to
                 // be done.
-                if (event->IsProgressing()) {
+                auto completionData = event->GetCompletionData();
+                if (std::holds_alternative<Ref<SystemEvent>>(completionData)) {
+                    hasProgressingEvents |=
+                        std::get<Ref<SystemEvent>>(completionData)->IsProgressing();
+                } else {
                     hasProgressingEvents = true;
                 }
 
@@ -505,10 +538,6 @@
 // EventManager::TrackedEvent
 
 EventManager::TrackedEvent::TrackedEvent(wgpu::CallbackMode callbackMode,
-                                         Ref<WaitListEvent> completionEvent)
-    : mCallbackMode(callbackMode), mCompletionData(std::move(completionEvent)) {}
-
-EventManager::TrackedEvent::TrackedEvent(wgpu::CallbackMode callbackMode,
                                          Ref<SystemEvent> completionEvent)
     : mCallbackMode(callbackMode), mCompletionData(std::move(completionEvent)) {}
 
@@ -518,14 +547,7 @@
     : mCallbackMode(callbackMode), mCompletionData(QueueAndSerial{queue, completionSerial}) {}
 
 EventManager::TrackedEvent::TrackedEvent(wgpu::CallbackMode callbackMode, Completed tag)
-    : mCallbackMode(callbackMode), mCompletionData(AcquireRef(new WaitListEvent())) {
-    GetIfWaitListEvent()->Signal();
-}
-
-EventManager::TrackedEvent::TrackedEvent(wgpu::CallbackMode callbackMode, NonProgressing tag)
-    : mCallbackMode(callbackMode),
-      mCompletionData(AcquireRef(new WaitListEvent())),
-      mIsProgressing(false) {}
+    : TrackedEvent(callbackMode, SystemEvent::CreateSignaled()) {}
 
 EventManager::TrackedEvent::~TrackedEvent() {
     DAWN_ASSERT(mFutureID != kNullFutureID);
@@ -536,31 +558,9 @@
     return {mFutureID};
 }
 
-bool EventManager::TrackedEvent::IsReadyToComplete() const {
-    bool isReady = false;
-    if (auto event = GetIfSystemEvent()) {
-        isReady = event->IsSignaled();
-    }
-    if (auto event = GetIfWaitListEvent()) {
-        isReady = event->IsSignaled();
-    }
-    if (const auto* queueAndSerial = GetIfQueueAndSerial()) {
-        isReady =
-            queueAndSerial->completionSerial <= queueAndSerial->queue->GetCompletedCommandSerial();
-    }
-    return isReady;
-}
-
-void EventManager::TrackedEvent::SetReadyToComplete() {
-    if (auto event = GetIfSystemEvent()) {
-        event->Signal();
-    }
-    if (auto event = GetIfWaitListEvent()) {
-        event->Signal();
-    }
-    if (auto* queueAndSerial = std::get_if<QueueAndSerial>(&mCompletionData)) {
-        queueAndSerial->completionSerial = queueAndSerial->queue->GetCompletedCommandSerial();
-    }
+const EventManager::TrackedEvent::CompletionData& EventManager::TrackedEvent::GetCompletionData()
+    const {
+    return mCompletionData;
 }
 
 void EventManager::TrackedEvent::EnsureComplete(EventCompletionType completionType) {
diff --git a/src/dawn/native/EventManager.h b/src/dawn/native/EventManager.h
index a6a4ec0..6ef872b 100644
--- a/src/dawn/native/EventManager.h
+++ b/src/dawn/native/EventManager.h
@@ -43,7 +43,6 @@
 #include "dawn/native/Forward.h"
 #include "dawn/native/IntegerTypes.h"
 #include "dawn/native/SystemEvent.h"
-#include "dawn/native/WaitListEvent.h"
 #include "partition_alloc/pointers/raw_ptr.h"
 
 namespace dawn::native {
@@ -117,58 +116,7 @@
 // to any TrackedEvents. Any which are not ref'd elsewhere (in order to be `Spontaneous`ly
 // completed) will be cleaned up at that time.
 class EventManager::TrackedEvent : public RefCounted {
-  public:
-    // Subclasses must implement this to complete the event (if not completed) with
-    // EventCompletionType::Shutdown.
-    ~TrackedEvent() override;
-
-    Future GetFuture() const;
-
-    bool IsProgressing() const { return mIsProgressing; }
-
-    bool IsReadyToComplete() const;
-
-    const QueueAndSerial* GetIfQueueAndSerial() const {
-        return std::get_if<QueueAndSerial>(&mCompletionData);
-    }
-
-    Ref<SystemEvent> GetIfSystemEvent() const {
-        if (auto* event = std::get_if<Ref<SystemEvent>>(&mCompletionData)) {
-            return *event;
-        }
-        return nullptr;
-    }
-
-    Ref<WaitListEvent> GetIfWaitListEvent() const {
-        if (auto* event = std::get_if<Ref<WaitListEvent>>(&mCompletionData)) {
-            return *event;
-        }
-        return nullptr;
-    }
-
-    // Events may be one of three types:
-    // - A queue and the ExecutionSerial after which the event will be completed.
-    //   Used for queue completion.
-    // - A SystemEvent which will be signaled usually by the OS / GPU driver. It stores a boolean
-    //   that we can check instead of polling with the OS, or it can be transformed lazily into a
-    //   SystemEventReceiver.
-    // - A WaitListEvent which will be signaled from our code, usually on a separate thread. It also
-    //   stores an atomic boolean that we can check instead of waiting synchronously, or it can be
-    //   transformed into a SystemEventReceiver for asynchronous waits.
-    // The queue ref creates a temporary ref cycle
-    // (Queue->Device->Instance->EventManager->TrackedEvent). This is OK because the instance will
-    // clear out the EventManager on shutdown.
-    // TODO(crbug.com/dawn/2067): This is a bit fragile. Is it possible to remove the ref cycle?
   protected:
-    friend class EventManager;
-
-    using CompletionData = std::variant<QueueAndSerial, Ref<SystemEvent>, Ref<WaitListEvent>>;
-
-    // Create an event from a WaitListEvent that can be signaled and waited-on in user-space only in
-    // the current process. Note that events like RequestAdapter and RequestDevice complete
-    // immediately in dawn native, and may use an already-completed event.
-    TrackedEvent(wgpu::CallbackMode callbackMode, Ref<WaitListEvent> completionEvent);
-
     // Create an event from a SystemEvent. Note that events like RequestAdapter and
     // RequestDevice complete immediately in dawn native, and may use an already-completed event.
     TrackedEvent(wgpu::CallbackMode callbackMode, Ref<SystemEvent> completionEvent);
@@ -178,17 +126,33 @@
                  QueueBase* queue,
                  ExecutionSerial completionSerial);
 
-    // Create a TrackedEvent that is already completed.
     struct Completed {};
+    // Create a TrackedEvent that is already completed.
     TrackedEvent(wgpu::CallbackMode callbackMode, Completed tag);
 
-    // Some SystemEvents may be non-progressing, i.e. DeviceLost. We tag these events so that we can
-    // correctly return whether there is progressing work when users are polling.
-    struct NonProgressing {};
-    TrackedEvent(wgpu::CallbackMode callbackMode, NonProgressing tag);
+  public:
+    // Subclasses must implement this to complete the event (if not completed) with
+    // EventCompletionType::Shutdown.
+    ~TrackedEvent() override;
 
-    void SetReadyToComplete();
+    Future GetFuture() const;
 
+    // Events may be one of two types:
+    // - A queue and the ExecutionSerial after which the event will be completed.
+    //   Used for queue completion.
+    // - A SystemEvent which will be signaled from our code, usually on a separate thread.
+    //   It stores a boolean that we can check instead of polling with the OS, or it can be
+    //   transformed lazily into a SystemEventReceiver. Used for async pipeline creation, and Metal
+    //   queue completion.
+    // The queue ref creates a temporary ref cycle
+    // (Queue->Device->Instance->EventManager->TrackedEvent). This is OK because the instance will
+    // clear out the EventManager on shutdown.
+    // TODO(crbug.com/dawn/2067): This is a bit fragile. Is it possible to remove the ref cycle?
+    using CompletionData = std::variant<QueueAndSerial, Ref<SystemEvent>>;
+
+    const CompletionData& GetCompletionData() const;
+
+  protected:
     void EnsureComplete(EventCompletionType);
     virtual void Complete(EventCompletionType) = 0;
 
@@ -200,8 +164,9 @@
 #endif
 
   private:
+    friend class EventManager;
+
     CompletionData mCompletionData;
-    const bool mIsProgressing = true;
     // Callback has been called.
     std::atomic<bool> mCompleted = false;
 };
diff --git a/src/dawn/native/Queue.cpp b/src/dawn/native/Queue.cpp
index ef30378..c4f3532 100644
--- a/src/dawn/native/Queue.cpp
+++ b/src/dawn/native/Queue.cpp
@@ -55,6 +55,7 @@
 #include "dawn/native/QuerySet.h"
 #include "dawn/native/RenderPassEncoder.h"
 #include "dawn/native/RenderPipeline.h"
+#include "dawn/native/SystemEvent.h"
 #include "dawn/native/Texture.h"
 #include "dawn/platform/DawnPlatform.h"
 #include "dawn/platform/tracing/TraceEvent.h"
diff --git a/src/dawn/native/Queue.h b/src/dawn/native/Queue.h
index 1e166a8..bcc3777 100644
--- a/src/dawn/native/Queue.h
+++ b/src/dawn/native/Queue.h
@@ -38,6 +38,7 @@
 #include "dawn/native/Forward.h"
 #include "dawn/native/IntegerTypes.h"
 #include "dawn/native/ObjectBase.h"
+#include "dawn/native/SystemEvent.h"
 #include "partition_alloc/pointers/raw_ptr.h"
 
 #include "dawn/native/DawnNative.h"
diff --git a/src/dawn/native/SystemEvent.cpp b/src/dawn/native/SystemEvent.cpp
index 2b8fe06..f8dd1c8 100644
--- a/src/dawn/native/SystemEvent.cpp
+++ b/src/dawn/native/SystemEvent.cpp
@@ -52,7 +52,6 @@
 SystemEventReceiver::SystemEventReceiver(SystemHandle primitive)
     : mPrimitive(std::move(primitive)) {}
 
-// static
 SystemEventReceiver SystemEventReceiver::CreateAlreadySignaled() {
     SystemEventPipeSender sender;
     SystemEventReceiver receiver;
@@ -132,6 +131,21 @@
 // SystemEvent
 
 // static
+Ref<SystemEvent> SystemEvent::CreateSignaled() {
+    auto ev = AcquireRef(new SystemEvent());
+    ev->Signal();
+    return ev;
+}
+
+// static
+Ref<SystemEvent> SystemEvent::CreateNonProgressingEvent() {
+    return AcquireRef(new SystemEvent(kNonProgressingPayload));
+}
+
+bool SystemEvent::IsProgressing() const {
+    return GetRefCountPayload() != kNonProgressingPayload;
+}
+
 bool SystemEvent::IsSignaled() const {
     return mSignaled.load(std::memory_order_acquire);
 }
diff --git a/src/dawn/native/SystemEvent.h b/src/dawn/native/SystemEvent.h
index 2a649dc..391d430 100644
--- a/src/dawn/native/SystemEvent.h
+++ b/src/dawn/native/SystemEvent.h
@@ -105,6 +105,12 @@
 
 class SystemEvent : public RefCounted {
   public:
+    using RefCounted::RefCounted;
+
+    static Ref<SystemEvent> CreateSignaled();
+    static Ref<SystemEvent> CreateNonProgressingEvent();
+
+    bool IsProgressing() const;
     bool IsSignaled() const;
     void Signal();
 
@@ -113,6 +119,10 @@
     const SystemEventReceiver& GetOrCreateSystemEventReceiver();
 
   private:
+    // Some SystemEvents may be non-progressing, i.e. DeviceLost. We tag these events so that we can
+    // correctly return whether there is progressing work when users are polling.
+    static constexpr uint64_t kNonProgressingPayload = 1;
+
     // mSignaled indicates whether the event has already been signaled.
     // It is stored outside the mPipe mutex so its status can quickly be checked without
     // acquiring a lock.
diff --git a/src/dawn/native/WaitListEvent.cpp b/src/dawn/native/WaitListEvent.cpp
deleted file mode 100644
index 8a2f422..0000000
--- a/src/dawn/native/WaitListEvent.cpp
+++ /dev/null
@@ -1,73 +0,0 @@
-// Copyright 2025 The Dawn & Tint Authors
-//
-// Redistribution and use in source and binary forms, with or without
-// modification, are permitted provided that the following conditions are met:
-//
-// 1. Redistributions of source code must retain the above copyright notice, this
-//    list of conditions and the following disclaimer.
-//
-// 2. Redistributions in binary form must reproduce the above copyright notice,
-//    this list of conditions and the following disclaimer in the documentation
-//    and/or other materials provided with the distribution.
-//
-// 3. Neither the name of the copyright holder nor the names of its
-//    contributors may be used to endorse or promote products derived from
-//    this software without specific prior written permission.
-//
-// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-#include "dawn/native/WaitListEvent.h"
-
-namespace dawn::native {
-
-WaitListEvent::WaitListEvent() = default;
-WaitListEvent::~WaitListEvent() = default;
-
-bool WaitListEvent::IsSignaled() const {
-    return mSignaled.load(std::memory_order_acquire);
-}
-
-void WaitListEvent::Signal() {
-    std::lock_guard<std::mutex> lock(mMutex);
-    DAWN_ASSERT(!mSignaled);
-    mSignaled.store(true, std::memory_order_release);
-    for (SyncWaiter* w : std::move(mSyncWaiters)) {
-        {
-            std::lock_guard<std::mutex> waiterLock(w->mutex);
-            w->waitDone = true;
-        }
-        w->cv.notify_all();
-    }
-    for (auto& sender : std::move(mAsyncWaiters)) {
-        std::move(sender).Signal();
-    }
-}
-
-bool WaitListEvent::Wait(Nanoseconds timeout) {
-    bool ready = false;
-    std::array<std::pair<Ref<WaitListEvent>, bool*>, 1> events{{{this, &ready}}};
-    return WaitListEvent::WaitAny(events.begin(), events.end(), timeout);
-}
-
-SystemEventReceiver WaitListEvent::WaitAsync() {
-    std::lock_guard<std::mutex> lock(mMutex);
-    if (IsSignaled()) {
-        return SystemEventReceiver::CreateAlreadySignaled();
-    }
-    SystemEventPipeSender sender;
-    SystemEventReceiver receiver;
-    std::tie(sender, receiver) = CreateSystemEventPipe();
-    mAsyncWaiters.push_back(std::move(sender));
-    return receiver;
-}
-
-}  // namespace dawn::native
diff --git a/src/dawn/native/WaitListEvent.h b/src/dawn/native/WaitListEvent.h
deleted file mode 100644
index 1addd70..0000000
--- a/src/dawn/native/WaitListEvent.h
+++ /dev/null
@@ -1,202 +0,0 @@
-// Copyright 2025 The Dawn & Tint Authors
-//
-// Redistribution and use in source and binary forms, with or without
-// modification, are permitted provided that the following conditions are met:
-//
-// 1. Redistributions of source code must retain the above copyright notice, this
-//    list of conditions and the following disclaimer.
-//
-// 2. Redistributions in binary form must reproduce the above copyright notice,
-//    this list of conditions and the following disclaimer in the documentation
-//    and/or other materials provided with the distribution.
-//
-// 3. Neither the name of the copyright holder nor the names of its
-//    contributors may be used to endorse or promote products derived from
-//    this software without specific prior written permission.
-//
-// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-#ifndef SRC_DAWN_NATIVE_WAITLISTEVENT_H_
-#define SRC_DAWN_NATIVE_WAITLISTEVENT_H_
-
-#include <algorithm>
-#include <chrono>
-#include <condition_variable>
-#include <limits>
-#include <mutex>
-#include <utility>
-#include <vector>
-
-#include "dawn/common/RefCounted.h"
-#include "dawn/native/IntegerTypes.h"
-#include "dawn/native/SystemEvent.h"
-#include "partition_alloc/pointers/raw_ptr.h"
-
-namespace dawn::native {
-
-class WaitListEvent : public RefCounted {
-  public:
-    WaitListEvent();
-
-    bool IsSignaled() const;
-    void Signal();
-    bool Wait(Nanoseconds timeout);
-    SystemEventReceiver WaitAsync();
-
-    template <typename It>
-    static bool WaitAny(It eventAndReadyStateBegin, It eventAndReadyStateEnd, Nanoseconds timeout);
-
-  private:
-    ~WaitListEvent() override;
-
-    struct SyncWaiter {
-        std::condition_variable cv;
-        std::mutex mutex;
-        bool waitDone = false;
-    };
-
-    mutable std::mutex mMutex;
-    std::atomic_bool mSignaled{false};
-    std::vector<raw_ptr<SyncWaiter>> mSyncWaiters;
-    std::vector<SystemEventPipeSender> mAsyncWaiters;
-};
-
-template <typename It>
-bool WaitListEvent::WaitAny(It eventAndReadyStateBegin,
-                            It eventAndReadyStateEnd,
-                            Nanoseconds timeout) {
-    static_assert(std::is_base_of_v<std::random_access_iterator_tag,
-                                    typename std::iterator_traits<It>::iterator_category>);
-    static_assert(std::is_same_v<typename std::iterator_traits<It>::value_type,
-                                 std::pair<Ref<WaitListEvent>, bool*>>);
-
-    const size_t count = std::distance(eventAndReadyStateBegin, eventAndReadyStateEnd);
-    if (count == 0) {
-        return false;
-    }
-
-    struct EventState {
-        WaitListEvent* event = nullptr;
-        size_t origIndex;
-        bool isReady = false;
-    };
-    std::vector<EventState> events(count);
-
-    for (size_t i = 0; i < count; i++) {
-        const auto& event = (*(eventAndReadyStateBegin + i)).first;
-        events[i].event = event.Get();
-        events[i].origIndex = i;
-    }
-    // Sort the events by address to get a globally consistent order.
-    std::sort(events.begin(), events.end(),
-              [](const auto& lhs, const auto& rhs) { return lhs.event < rhs.event; });
-
-    // Acquire locks in order and enqueue our waiter.
-    bool foundSignaled = false;
-    for (size_t i = 0; i < count; i++) {
-        WaitListEvent* event = events[i].event;
-        // Skip over multiple waits on the same event, but ensure that we store the same ready state
-        // for duplicates.
-        if (i > 0 && event == events[i - 1].event) {
-            events[i].isReady = events[i - 1].isReady;
-            continue;
-        }
-        event->mMutex.lock();
-        // Check `IsSignaled()` after acquiring the lock so that it doesn't become true immediately
-        // before we acquire the lock - we assume that it is safe to enqueue our waiter after this
-        // point if the event is not already signaled.
-        if (event->IsSignaled()) {
-            events[i].isReady = true;
-            foundSignaled = true;
-        }
-    }
-
-    // If any of the events were already signaled, early out after unlocking the events in reverse
-    // order to prevent lock order inversion.
-    if (foundSignaled) {
-        for (size_t i = 0; i < count; i++) {
-            WaitListEvent* event = events[count - 1 - i].event;
-            // Use the cached value of `IsSignaled()` because we might have unlocked the event
-            // already if it was a duplicate and checking `IsSignaled()` without the lock is racy
-            // and can cause different values of isReady for multiple waits on the same event.
-            if (events[count - 1 - i].isReady) {
-                bool* isReady =
-                    (*(eventAndReadyStateBegin + events[count - 1 - i].origIndex)).second;
-                *isReady = true;
-            }
-            // Skip over multiple waits on the same event.
-            if (i > 0 && event == events[count - i].event) {
-                continue;
-            }
-            event->mMutex.unlock();
-        }
-        return true;
-    }
-
-    // We have acquired locks for all the events we're going to wait on - enqueue our waiter now
-    // after locking it since it could be woken up as soon as the event is unlocked and unlock the
-    // events in reverse order now to prevent lock order inversion.
-    SyncWaiter waiter;
-    std::unique_lock<std::mutex> waiterLock(waiter.mutex);
-    for (size_t i = 0; i < count; i++) {
-        WaitListEvent* event = events[count - 1 - i].event;
-        // Skip over multiple waits on the same event.
-        if (i > 0 && event == events[count - i].event) {
-            continue;
-        }
-        event->mSyncWaiters.push_back(&waiter);
-        event->mMutex.unlock();
-    }
-
-    // Any values larger than those representatable by std::chrono::nanoseconds will be treated as
-    // infinite waits - in particular this covers values greater than INT64_MAX.
-    static constexpr uint64_t kMaxDurationNanos = std::chrono::nanoseconds::max().count();
-    if (timeout > Nanoseconds(kMaxDurationNanos)) {
-        waiter.cv.wait(waiterLock, [&waiter]() { return waiter.waitDone; });
-    } else {
-        waiter.cv.wait_for(waiterLock, std::chrono::nanoseconds(static_cast<uint64_t>(timeout)),
-                           [&waiter]() { return waiter.waitDone; });
-    }
-
-    // Remove our waiter from the events.
-    for (size_t i = 0; i < count; i++) {
-        WaitListEvent* event = events[i].event;
-        // Skip over multiple waits on the same event, but ensure that we store the same ready state
-        // for duplicates.
-        if (i > 0 && event == events[i - 1].event) {
-            events[i].isReady = events[i - 1].isReady;
-        } else {
-            // We could be woken by the condition variable before the atomic release store to
-            // `mSignaled` is visible - locking the mutex ensures that the atomic acquire load in
-            // `IsSignaled()` sees the correct value.
-            std::lock_guard<std::mutex> eventLock(event->mMutex);
-            if (event->IsSignaled()) {
-                events[i].isReady = true;
-            }
-            event->mSyncWaiters.erase(
-                std::remove(event->mSyncWaiters.begin(), event->mSyncWaiters.end(), &waiter),
-                event->mSyncWaiters.end());
-        }
-        if (events[i].isReady) {
-            bool* isReady = (*(eventAndReadyStateBegin + events[i].origIndex)).second;
-            *isReady = true;
-            foundSignaled = true;
-        }
-    }
-
-    DAWN_ASSERT(!waiter.waitDone || foundSignaled);
-    return foundSignaled;
-}
-
-}  // namespace dawn::native
-
-#endif  // SRC_DAWN_NATIVE_WAITLISTEVENT_H_
diff --git a/src/dawn/native/metal/QueueMTL.h b/src/dawn/native/metal/QueueMTL.h
index 1e03df3..67a11c2 100644
--- a/src/dawn/native/metal/QueueMTL.h
+++ b/src/dawn/native/metal/QueueMTL.h
@@ -35,7 +35,7 @@
 #include "dawn/common/SerialMap.h"
 #include "dawn/native/EventManager.h"
 #include "dawn/native/Queue.h"
-#include "dawn/native/WaitListEvent.h"
+#include "dawn/native/SystemEvent.h"
 #include "dawn/native/metal/CommandRecordingContext.h"
 #include "dawn/native/metal/SharedFenceMTL.h"
 
@@ -53,7 +53,7 @@
     id<MTLSharedEvent> GetMTLSharedEvent() const;
     ResultOrError<Ref<SharedFence>> GetOrCreateSharedFence();
 
-    Ref<WaitListEvent> CreateWorkDoneEvent(ExecutionSerial serial);
+    Ref<SystemEvent> CreateWorkDoneSystemEvent(ExecutionSerial serial);
     ResultOrError<bool> WaitForQueueSerial(ExecutionSerial serial, Nanoseconds timeout) override;
 
   private:
@@ -88,7 +88,7 @@
     // TODO(crbug.com/dawn/2065): If we atomically knew a conservative lower bound on the
     // mWaitingEvents serials, we could avoid taking this lock sometimes. Optimize if needed.
     // See old draft code: https://dawn-review.googlesource.com/c/dawn/+/137502/29
-    MutexProtected<SerialMap<ExecutionSerial, Ref<WaitListEvent>>> mWaitingEvents;
+    MutexProtected<SerialMap<ExecutionSerial, Ref<SystemEvent>>> mWaitingEvents;
 
     // A shared event that can be exported for synchronization with other users of Metal.
     // MTLSharedEvent is not available until macOS 10.14+ so use just `id`.
diff --git a/src/dawn/native/metal/QueueMTL.mm b/src/dawn/native/metal/QueueMTL.mm
index b51248a..f1d3f04f 100644
--- a/src/dawn/native/metal/QueueMTL.mm
+++ b/src/dawn/native/metal/QueueMTL.mm
@@ -33,6 +33,7 @@
 #include "dawn/native/Commands.h"
 #include "dawn/native/DynamicUploader.h"
 #include "dawn/native/MetalBackend.h"
+#include "dawn/native/WaitAnySystemEvent.h"
 #include "dawn/native/metal/CommandBufferMTL.h"
 #include "dawn/native/metal/DeviceMTL.h"
 #include "dawn/platform/DawnPlatform.h"
@@ -251,9 +252,10 @@
     }
 }
 
-Ref<WaitListEvent> Queue::CreateWorkDoneEvent(ExecutionSerial serial) {
-    Ref<WaitListEvent> completionEvent = AcquireRef(new WaitListEvent());
+Ref<SystemEvent> Queue::CreateWorkDoneSystemEvent(ExecutionSerial serial) {
+    Ref<SystemEvent> completionEvent = AcquireRef(new SystemEvent());
     mWaitingEvents.Use([&](auto events) {
+        SystemEventReceiver receiver;
         // Now that we hold the lock, check against mCompletedSerial before inserting.
         // This serial may have just completed. If it did, mark the event complete.
         // Also check for device loss. Otherwise, we could enqueue the event
@@ -271,7 +273,11 @@
 }
 
 ResultOrError<bool> Queue::WaitForQueueSerial(ExecutionSerial serial, Nanoseconds timeout) {
-    return CreateWorkDoneEvent(serial)->Wait(timeout);
+    Ref<SystemEvent> event = CreateWorkDoneSystemEvent(serial);
+    bool ready = false;
+    std::array<std::pair<const dawn::native::SystemEventReceiver&, bool*>, 1> events{
+        {{event->GetOrCreateSystemEventReceiver(), &ready}}};
+    return WaitAnySystemEvent(events.begin(), events.end(), timeout);
 }
 
 }  // namespace dawn::native::metal
diff --git a/src/dawn/tests/BUILD.gn b/src/dawn/tests/BUILD.gn
index 05ab8ec..cb9b140 100644
--- a/src/dawn/tests/BUILD.gn
+++ b/src/dawn/tests/BUILD.gn
@@ -780,7 +780,6 @@
     "white_box/SharedBufferMemoryTests.h",
     "white_box/SharedTextureMemoryTests.cpp",
     "white_box/SharedTextureMemoryTests.h",
-    "white_box/WaitListEventTests.cpp",
   ]
   libs = []
 
diff --git a/src/dawn/tests/white_box/WaitListEventTests.cpp b/src/dawn/tests/white_box/WaitListEventTests.cpp
deleted file mode 100644
index b97c7c9..0000000
--- a/src/dawn/tests/white_box/WaitListEventTests.cpp
+++ /dev/null
@@ -1,351 +0,0 @@
-// Copyright 2025 The Dawn & Tint Authors
-//
-// Redistribution and use in source and binary forms, with or without
-// modification, are permitted provided that the following conditions are met:
-//
-// 1. Redistributions of source code must retain the above copyright notice, this
-//    list of conditions and the following disclaimer.
-//
-// 2. Redistributions in binary form must reproduce the above copyright notice,
-//    this list of conditions and the following disclaimer in the documentation
-//    and/or other materials provided with the distribution.
-//
-// 3. Neither the name of the copyright holder nor the names of its
-//    contributors may be used to endorse or promote products derived from
-//    this software without specific prior written permission.
-//
-// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-#include <chrono>
-#include <thread>
-#include <utility>
-#include <vector>
-
-#include "dawn/common/Ref.h"
-#include "dawn/native/WaitAnySystemEvent.h"
-#include "dawn/native/WaitListEvent.h"
-#include "dawn/tests/DawnTest.h"
-
-namespace dawn::native {
-namespace {
-
-constexpr uint64_t kZeroDurationNs = 0;
-constexpr uint64_t kShortDurationNs = 1000000;
-constexpr uint64_t kMediumDurationNs = 50000000;
-
-// Helper to wait on a SystemEventReceiver with a timeout
-bool WaitOnReceiver(const SystemEventReceiver& receiver, Nanoseconds timeout) {
-    bool ready = false;
-    std::pair<const SystemEventReceiver&, bool*> event = {receiver, &ready};
-    return WaitAnySystemEvent(&event, &event + 1, timeout);
-}
-
-class WaitListEventTests : public DawnTest {
-  protected:
-    void SetUp() override {
-        DawnTest::SetUp();
-        DAWN_TEST_UNSUPPORTED_IF(UsesWire());
-    }
-};
-
-// Test basic signaling and state checking
-TEST(WaitListEventTests, SignalAndCheck) {
-    Ref<WaitListEvent> event = AcquireRef(new WaitListEvent());
-    EXPECT_FALSE(event->IsSignaled());
-    event->Signal();
-    EXPECT_TRUE(event->IsSignaled());
-}
-
-// Test waiting on an already signaled event
-TEST(WaitListEventTests, WaitAlreadySignaled) {
-    Ref<WaitListEvent> event = AcquireRef(new WaitListEvent());
-    event->Signal();
-    EXPECT_TRUE(event->IsSignaled());
-    // Wait with zero timeout should return true immediately
-    EXPECT_TRUE(event->Wait(Nanoseconds(kZeroDurationNs)));
-    // Wait with non-zero timeout should return true immediately
-    EXPECT_TRUE(event->Wait(Nanoseconds(kShortDurationNs)));
-}
-
-// Test waiting on an event that gets signaled later
-TEST(WaitListEventTests, WaitThenSignal) {
-    Ref<WaitListEvent> event = AcquireRef(new WaitListEvent());
-    EXPECT_FALSE(event->IsSignaled());
-
-    std::thread signaler([&]() {
-        std::this_thread::sleep_for(std::chrono::nanoseconds(kShortDurationNs));
-        event->Signal();
-    });
-
-    // Wait for longer than the signal delay
-    EXPECT_TRUE(event->Wait(Nanoseconds(kMediumDurationNs)));
-    EXPECT_TRUE(event->IsSignaled());
-
-    signaler.join();
-}
-
-// Test waiting with a timeout that expires
-TEST(WaitListEventTests, WaitTimeout) {
-    Ref<WaitListEvent> event = AcquireRef(new WaitListEvent());
-    EXPECT_FALSE(event->IsSignaled());
-
-    // Wait for a short duration, expect timeout
-    EXPECT_FALSE(event->Wait(Nanoseconds(kShortDurationNs)));
-    EXPECT_FALSE(event->IsSignaled());
-}
-
-// Test waiting with a zero timeout
-TEST(WaitListEventTests, WaitZeroTimeout) {
-    Ref<WaitListEvent> event = AcquireRef(new WaitListEvent());
-    EXPECT_FALSE(event->IsSignaled());
-    // Wait with zero timeout should return false immediately
-    EXPECT_FALSE(event->Wait(Nanoseconds(kZeroDurationNs)));
-    EXPECT_FALSE(event->IsSignaled());
-
-    event->Signal();
-    EXPECT_TRUE(event->IsSignaled());
-    // Wait with zero timeout should return true immediately
-    EXPECT_TRUE(event->Wait(Nanoseconds(kZeroDurationNs)));
-}
-
-// Test WaitAsync on an already signaled event
-TEST(WaitListEventTests, WaitAsyncAlreadySignaled) {
-    Ref<WaitListEvent> event = AcquireRef(new WaitListEvent());
-    event->Signal();
-    EXPECT_TRUE(event->IsSignaled());
-
-    SystemEventReceiver receiver = event->WaitAsync();
-    // The receiver should be immediately ready
-    EXPECT_TRUE(WaitOnReceiver(receiver, Nanoseconds(kZeroDurationNs)));
-}
-
-// Test WaitAsync, signaling the event later
-TEST(WaitListEventTests, WaitAsyncThenSignal) {
-    Ref<WaitListEvent> event = AcquireRef(new WaitListEvent());
-    EXPECT_FALSE(event->IsSignaled());
-
-    SystemEventReceiver receiver = event->WaitAsync();
-
-    // Check it's not ready yet
-    EXPECT_FALSE(WaitOnReceiver(receiver, Nanoseconds(kZeroDurationNs)));
-
-    std::thread signaler([&]() {
-        std::this_thread::sleep_for(std::chrono::nanoseconds(kShortDurationNs));
-        event->Signal();
-    });
-
-    // Wait for the receiver to become signaled
-    EXPECT_TRUE(WaitOnReceiver(receiver, Nanoseconds(kMediumDurationNs)));
-    EXPECT_TRUE(event->IsSignaled());
-
-    signaler.join();
-}
-
-// Test WaitAny with an empty list
-TEST(WaitListEventTests, WaitAnyEmpty) {
-    std::array<std::pair<Ref<WaitListEvent>, bool*>, 0> events;
-    EXPECT_FALSE(
-        WaitListEvent::WaitAny(events.begin(), events.end(), Nanoseconds(kShortDurationNs)));
-}
-
-// Test WaitAny where one event is already signaled
-TEST(WaitListEventTests, WaitAnyOneAlreadySignaled) {
-    Ref<WaitListEvent> event1 = AcquireRef(new WaitListEvent());
-    Ref<WaitListEvent> event2 = AcquireRef(new WaitListEvent());
-    event1->Signal();
-
-    bool ready1 = false;
-    bool ready2 = false;
-    std::array<std::pair<Ref<WaitListEvent>, bool*>, 2> events = {
-        {{event1, &ready1}, {event2, &ready2}}};
-
-    EXPECT_TRUE(
-        WaitListEvent::WaitAny(events.begin(), events.end(), Nanoseconds(kShortDurationNs)));
-    EXPECT_TRUE(ready1);
-    EXPECT_FALSE(ready2);
-}
-
-// Test WaitAny where one event is signaled while waiting
-TEST(WaitListEventTests, WaitAnySignalDuringWait) {
-    Ref<WaitListEvent> event1 = AcquireRef(new WaitListEvent());
-    Ref<WaitListEvent> event2 = AcquireRef(new WaitListEvent());
-
-    bool ready1 = false;
-    bool ready2 = false;
-    std::array<std::pair<Ref<WaitListEvent>, bool*>, 2> events = {
-        {{event1, &ready1}, {event2, &ready2}}};
-
-    std::thread signaler([&]() {
-        std::this_thread::sleep_for(std::chrono::nanoseconds(kShortDurationNs));
-        event2->Signal();  // Signal the second event
-    });
-
-    EXPECT_TRUE(
-        WaitListEvent::WaitAny(events.begin(), events.end(), Nanoseconds(kMediumDurationNs)));
-    EXPECT_FALSE(ready1);
-    EXPECT_TRUE(ready2);  // Expect the second event to be ready
-
-    signaler.join();
-}
-
-// Test WaitAny with a timeout
-TEST(WaitListEventTests, WaitAnyTimeout) {
-    Ref<WaitListEvent> event1 = AcquireRef(new WaitListEvent());
-    Ref<WaitListEvent> event2 = AcquireRef(new WaitListEvent());
-
-    bool ready1 = false;
-    bool ready2 = false;
-    std::array<std::pair<Ref<WaitListEvent>, bool*>, 2> events = {
-        {{event1, &ready1}, {event2, &ready2}}};
-
-    EXPECT_FALSE(
-        WaitListEvent::WaitAny(events.begin(), events.end(), Nanoseconds(kShortDurationNs)));
-    EXPECT_FALSE(ready1);
-    EXPECT_FALSE(ready2);
-}
-
-// Test WaitAny with zero timeout
-TEST(WaitListEventTests, WaitAnyZeroTimeout) {
-    Ref<WaitListEvent> event1 = AcquireRef(new WaitListEvent());
-    Ref<WaitListEvent> event2 = AcquireRef(new WaitListEvent());
-
-    bool ready1 = false;
-    bool ready2 = false;
-    std::array<std::pair<Ref<WaitListEvent>, bool*>, 2> events = {
-        {{event1, &ready1}, {event2, &ready2}}};
-
-    // No events signaled
-    EXPECT_FALSE(
-        WaitListEvent::WaitAny(events.begin(), events.end(), Nanoseconds(kZeroDurationNs)));
-    EXPECT_FALSE(ready1);
-    EXPECT_FALSE(ready2);
-
-    // Signal one event
-    event1->Signal();
-    EXPECT_TRUE(WaitListEvent::WaitAny(events.begin(), events.end(), Nanoseconds(kZeroDurationNs)));
-    EXPECT_TRUE(ready1);
-    EXPECT_FALSE(ready2);
-}
-
-// Test WaitAny with the same event multiple times
-TEST(WaitListEventTests, WaitAnyDuplicateEvents) {
-    Ref<WaitListEvent> event = AcquireRef(new WaitListEvent());
-
-    bool ready1 = false;
-    bool ready2 = false;
-    std::vector<std::pair<Ref<WaitListEvent>, bool*>> events = {
-        {event, &ready1}, {event, &ready2}  // Same event again
-    };
-
-    std::thread signaler([&]() {
-        std::this_thread::sleep_for(std::chrono::nanoseconds(kShortDurationNs));
-        event->Signal();
-    });
-
-    EXPECT_TRUE(
-        WaitListEvent::WaitAny(events.begin(), events.end(), Nanoseconds(kMediumDurationNs)));
-    // Both ready flags corresponding to the same event should be true
-    EXPECT_TRUE(ready1);
-    EXPECT_TRUE(ready2);
-
-    signaler.join();
-}
-
-// Test WaitAny with the same event multiple times, already signaled
-TEST(WaitListEventTests, WaitAnyDuplicateEventsAlreadySignaled) {
-    Ref<WaitListEvent> event = AcquireRef(new WaitListEvent());
-
-    bool ready1 = false;
-    bool ready2 = false;
-    std::vector<std::pair<Ref<WaitListEvent>, bool*>> events = {
-        {event, &ready1}, {event, &ready2}  // Same event again
-    };
-
-    // Signal the event *before* waiting
-    event->Signal();
-    EXPECT_TRUE(event->IsSignaled());
-
-    // WaitAny should return immediately since the event is already signaled
-    EXPECT_TRUE(
-        WaitListEvent::WaitAny(events.begin(), events.end(), Nanoseconds(kMediumDurationNs)));
-
-    // Both ready flags corresponding to the same event should be true
-    EXPECT_TRUE(ready1);
-    EXPECT_TRUE(ready2);
-}
-
-// Test multiple threads waiting on the same event
-TEST(WaitListEventTests, WaitMultiThreadedSingleEvent) {
-    Ref<WaitListEvent> event = AcquireRef(new WaitListEvent());
-
-    constexpr size_t kNumWaiters = 5;
-    std::array<std::thread, kNumWaiters> waiters;
-    std::array<std::optional<bool>, kNumWaiters> results;
-
-    for (size_t i = 0; i < kNumWaiters; ++i) {
-        waiters[i] = std::thread(
-            [&results, &event, i]() { results[i] = event->Wait(Nanoseconds(kMediumDurationNs)); });
-    }
-
-    // Give waiters time to start waiting
-    std::this_thread::sleep_for(std::chrono::nanoseconds(kShortDurationNs));
-    event->Signal();
-
-    // Check all waiters returned true
-    for (size_t i = 0; i < kNumWaiters; ++i) {
-        waiters[i].join();
-        EXPECT_TRUE(results[i].has_value());
-        EXPECT_TRUE(results[i].value());
-    }
-    EXPECT_TRUE(event->IsSignaled());
-}
-
-// Test multiple threads waiting on different events via WaitAny
-TEST(WaitListEventTests, WaitAnyMultiThreaded) {
-    Ref<WaitListEvent> event1 = AcquireRef(new WaitListEvent());
-    Ref<WaitListEvent> event2 = AcquireRef(new WaitListEvent());
-    Ref<WaitListEvent> event3 = AcquireRef(new WaitListEvent());
-
-    bool ready1 = false;
-    bool ready2 = false;
-    bool ready3 = false;
-    std::array<std::pair<Ref<WaitListEvent>, bool*>, 3> events = {
-        {{event1, &ready1}, {event2, &ready2}, {event3, &ready3}}};
-
-    // Start a thread that waits on any of the events
-    bool waitResult = false;
-    std::thread waiter([&]() {
-        waitResult =
-            WaitListEvent::WaitAny(events.begin(), events.end(), Nanoseconds(kMediumDurationNs));
-    });
-
-    // Start another thread that signals one of the events
-    std::thread signaler([&]() {
-        std::this_thread::sleep_for(std::chrono::nanoseconds(kShortDurationNs));
-        event2->Signal();  // Signal the middle event
-    });
-
-    waiter.join();
-
-    // Check that the waiting thread completes successfully
-    EXPECT_TRUE(waitResult);
-
-    // Check that the correct ready flag was set
-    EXPECT_FALSE(ready1);
-    EXPECT_TRUE(ready2);
-    EXPECT_FALSE(ready3);
-
-    signaler.join();
-}
-
-}  // namespace
-}  // namespace dawn::native