On macOS, Objective-C methods frequently return autoreleased objects
that accumulate until an autorelease pool is drained. Our event loop
(Core::EventLoop) and rendering thread both lacked autorelease pools,
causing unbounded accumulation of autoreleased objects.
The rendering thread was the worst offender: every Skia flush triggers
Metal resource allocation which sets labels on GPU textures via
-[IOGPUMetalResource setLabel:], creating autoreleased CFData objects.
With ~1M+ such objects at 112 bytes each, this leaked ~121MB. Metal
command buffer objects (_MTLCommandBufferEncoderInfo, etc.) also
accumulated, adding another ~128MB.
Add Core::ScopedAutoreleasePool, a RAII wrapper around the ObjC runtime
autorelease pool (no-op on non-macOS), and drain it:
- Every event loop pump (like NSRunLoop does)
- Every compositor loop iteration on the rendering thread
handle_signal() called ThreadData::the() which can acquire a write
lock on s_thread_data_lock if the thread hasn't initialized its
ThreadData yet. If the signal interrupts a thread that already holds
a read lock on s_thread_data_lock (e.g. in unregister_notifier()),
this deadlocks — the write lock waits for the read lock, but the
read lock holder is blocked in the signal handler.
Fix by accessing the thread-local s_this_thread_data directly. If
the thread has no ThreadData, there's no wake pipe to write to, so
we just return.
If exit() is called on a thread with an EventLoop in the stack, the
ThreadData storing the array of wake pipes will be destroyed first.
Threads can still take a strong reference to the EventLoop after that,
and will read the fds from freed memory.
Instead, take a copy of the write fd, and swallow EBADF when writing to
it, since that only indicates that the thread and event loop are
exiting, so there's nothing to do with the wake.
Add a thread-safe deferred_invoke() API on WeakEventLoopReference that
queues work onto the owning thread's event queue and wakes that thread
via EventLoopManager hooks. This avoids calling wake() from foreign
threads during teardown.
Implement current_thread_handle()/wake_thread() in each backend and
track per-thread data so handles are validated before waking:
- Unix: wake via per-thread wake pipe
- Windows: wake via thread wake event
- macOS: wake via stored CFRunLoopRef
- Qt: wake via event target or QEventLoop::wakeUp()
- Android: wake via stored ALooper
This fixes an issue where RequestServer would churn as poll() returned
immediately due to a file descriptor yielding POLLHUP.
In that case, we should just wake the notifier and let it figure out
what to do.
This patch adds an API for posting a Core::Event::Type to the thread
event queue without requiring a full Core::Event object.
We use this API to avoid heap allocations for every timer and notifier
activation event.
The DeferredInvocationContext only existed to satisfy the requirement
in ThreadEventQueue that each event has an EventReceiver. However,
deferred_invoke() was not even using the EventReceiver to call its
callback. Therefore, we don't need to allocate one for every deferred
invocation.
This also prevents WeakPtr::strong_ref() from racing and leaking the
context object when invoking a function across threads.
In 11b8bbe one thing that was claimed was that we now properly set the
Notifier's actual fd on the NotifierActivationEvent. It turns out that
claim was false because a crucial step was forgotten: actually set the
m_notifier_fd when registering. Despite that mistake, it ultimately was
irrelevant as the methods on NotifierActivationEvent are currently
unused code. We were posting the event to the correct Notifier receiver
so the on_activation was still getting invoked.
Given they are unused, NotifierActivationEvent can be defined the same
way as TimerEvent is, where we just pass the event type enum to the
Event base class. Additionally, NotificationType can be moved to
the Notifier header as this enum is now always used in the context of
creating or using a Notifier instance.
Timers scheduled with identical `fire_time` could fire out of order
because the heap is not stable. This change assigns a monotonically
increasing `sequence_id` when a timer is scheduled and extend the heap
comparator to order by (`fire_time`, `sequence_id`). This guarantees
FIFO among timers with the same deadline.
This matches the HTML "run steps after a timeout" ordering requirement:
older invocations with <= delay complete before newer ones.
https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#run-steps-after-a-timeout
The issue with that refactor was that the same fd can be used in more
than one notifier. This reverts us back to using 2 members to track the
notifiers in play.
POLLHUP is set when the remote end of the monitored fd is closed. There
may still be some buffered data to read from the socket, however. Some
systems do not set POLLIN in these cases. So we should just always try
to read from fds when we receive this event.