Branch: refs/heads/main
Home: https://github.com/WebKit/WebKit
Commit: 1feeb18419beb9b6ecb88bf86708cfd48259572c
https://github.com/WebKit/WebKit/commit/1feeb18419beb9b6ecb88bf86708cfd48259572c
Author: Jean-Yves Avenard <[email protected]>
Date: 2026-05-29 (Fri, 29 May 2026)
Changed paths:
M Source/WebCore/CMakeLists.txt
M Source/WebCore/Headers.cmake
M Source/WebCore/Sources.txt
M Source/WebCore/WebCore.xcodeproj/project.pbxproj
A Source/WebCore/platform/SharedTimebase.cpp
A Source/WebCore/platform/SharedTimebase.h
A Source/WebCore/platform/SharedTimebase.serialization.in
A Source/WebCore/platform/SharedTimebaseHandle.h
M Source/WebCore/platform/graphics/avfoundation/AudioVideoRendererAVFObjC.h
M Source/WebCore/platform/graphics/avfoundation/AudioVideoRendererAVFObjC.mm
M Source/WebKit/CMakeLists.txt
M Source/WebKit/DerivedSources-input.xcfilelist
M Source/WebKit/DerivedSources.make
M Source/WebKit/GPUProcess/media/RemoteAudioVideoRendererProxyManager.cpp
M Source/WebKit/GPUProcess/media/RemoteAudioVideoRendererProxyManager.h
M
Source/WebKit/GPUProcess/media/RemoteAudioVideoRendererProxyManager.messages.in
M Source/WebKit/Scripts/webkit/opaque_ipc_types.tracking.in
M Source/WebKit/WebProcess/GPU/media/AudioVideoRendererRemote.cpp
M Source/WebKit/WebProcess/GPU/media/AudioVideoRendererRemote.h
M
Source/WebKit/WebProcess/GPU/media/AudioVideoRendererRemoteMessageReceiver.messages.in
M Source/WebKit/WebProcess/GPU/media/RemoteAudioVideoRendererState.h
M
Source/WebKit/WebProcess/GPU/media/RemoteAudioVideoRendererState.serialization.in
M Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj
A Tools/TestWebKitAPI/Tests/WebCore/SharedTimebaseTests.cpp
Log Message:
-----------
Introduce SharedTimebase and stop sending time updates every 100ms.
https://bugs.webkit.org/show_bug.cgi?id=315553
rdar://177927416
Reviewed by Jer Noble.
Background
----------
The remote audio/video renderer in the WebContent process used to
learn playback time by way of a periodic IPC: every ~100ms in the
first second after Play, then every ~250ms during steady-state
playback, the GPU sent a TimeObserverUpdate carrying a
MediaTimeUpdateData anchor (currentTime, effectiveRate, wallTime).
An inner class on the WebContent side, TimeProgressEstimator, cached
the latest anchor and extrapolated forward between IPCs to answer
currentTime() / timeIsProgressing() / effectiveRate() synchronously.
The estimator had also accumulated two pieces of state that don't
really belong in a generic time extrapolator: a "startup gate" that
held currentTime() at the cached anchor after Play / SetRate /
FinishSeek until the next GPU update was observed (compensating for
AVSampleBufferRenderSynchronizer publishing a non-zero rate before
its timebase actually starts advancing), and a stall cap that
clamped currentTime() to a boundary the client had asked the GPU to
stall at (compensating for the IPC round-trip on
NotifyTimeReachedAndStall).
The two issues with that shape:
- Steady-state IPC traffic. Every active <video> emits a
cross-process message four times per second purely to refresh a
cached time. The receiver wakes a dedicated work queue in the
WebContent process, decodes the message, locks, and stores three
numbers.
- The estimator quietly accumulated AVFoundation- and
client-specific quirks. AVF lying about its rate is masked far
away from where the lie is told; the stall cap is correlated with
IPCs the renderer issues but lives behind an indirection. Every
future reader inherits both.
Design steps folded into this commit
------------------------------------
1. Make the AVF rate truthful at the source. In
AudioVideoRendererAVFObjC, on a 0 -> non-zero rate transition,
don't forward the new rate to the consumer (or expose it through
effectiveRate()) yet. Instead, install a fine-grained periodic
time observer on the synchronizer; once CMTimebaseGetTime has
advanced past the value captured at the transition, remove the
observer and forward the rate. On any non-zero -> 0 transition,
tear down any pending observer and forward 0 immediately. With
this in place, the rate the GPU publishes is one that has been
observed to be in effect, so the WebContent side never needs a
startup gate to mask the AVF lie.
2. Move the stall cap onto AudioVideoRendererRemote. The cap is a
client-issued boundary tied to an in-flight
NotifyTimeReachedAndStall, not a property of the timebase.
notifyTimeReachedAndStall(time) sets m_stallCap = time directly
on the renderer (under m_lock); cancelTimeReachedAction clears
it; cancelPendingSeek clears or keeps it according to the seek
direction. The renderer's currentTime() applies the cap as a
final clamp on top of whatever the time source returns. This
takes the cap out of the path that any future timebase reader
would have to carry.
3. Replace the periodic time-update IPC with a shared-memory
timebase. With (1) and (2) in place, the only remaining job is
propagating a (currentTime, playbackRate, hostTime) anchor from
the GPU to the WebContent process. Writes happen on a small
number of discrete events (Play, Pause, SetRate, Stall,
PrepareToSeek, FinishSeek); reads happen often (every JS access
to video.currentTime, every monitorSourceBuffers tick). A small
shared-memory anchor the writer updates on those events and the
reader reads cheaply matches that ratio more directly than a
periodic IPC stream.
The new design
--------------
WebCore::SharedTimebase is a writer-side handle to a small
SharedMemory region; the GPU publishes a (currentTime, playbackRate,
hostTime) Snapshot whenever something changes time (Play / Pause /
SetRate / Stall / PrepareToSeek / FinishSeek), plus once per
existing time-observer tick so the anchor stays fresh enough to
bound extrapolation. Reads are lock-free via SequenceLocked<T>.
The WebContent process opens the same region as a
SharedTimebaseReader. With AVF-quirk handling moved to step 1 and
the stall cap moved to step 2, the reader is small: rate-based
extrapolation from the latest snapshot, capped at maxExtrapolation
so a silent writer can't extrapolate forever, and a forward-
monotonic high-water-mark clamp on the value it returns. A clock
function is constructor-injectable so tests can drive deterministic
timing; the default is MonotonicTime::now.
The Create message returns the read handle; if SharedMemory
allocation fails, the WebContent side surfaces it through the
existing m_errorCallback as PlatformMediaError::MemoryError.
With the reader handling time and the renderer handling the cap,
the periodic TimeObserverUpdate IPC is no longer load-bearing and
is removed outright. Play / Pause / SetRate stop using async
replies — their only job had been to feed MediaTimeUpdateData back
to the estimator — and become fire-and-forget.
RemoteAudioVideoRendererState loses its timeUpdateData field; only
`bool paused` remains. The GPU's existing time observer is
preserved (it still drives playback-quality metrics), but its only
effect on the WebContent's notion of time is now a direct
storeSnapshot() into the shared region — no IPC is sent.
Tests: Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj
* Source/WebCore/CMakeLists.txt:
* Source/WebCore/Headers.cmake:
* Source/WebCore/Sources.txt:
* Source/WebCore/WebCore.xcodeproj/project.pbxproj:
* Source/WebCore/platform/SharedTimebase.cpp: Added.
(WebCore::SharedTimebaseHandle::takeOwnershipOfMemory):
(WebCore::SharedTimebase::create):
(WebCore::SharedTimebase::SharedTimebase):
(WebCore::SharedTimebase::createHandle const):
(WebCore::SharedTimebase::storeSnapshot):
(WebCore::SharedTimebaseReader::create):
(WebCore::SharedTimebaseReader::SharedTimebaseReader):
(WebCore::SharedTimebaseReader::currentTime const):
(WebCore::SharedTimebaseReader::currentRate const):
(WebCore::SharedTimebaseReader::resetForTimeDiscontinuity):
* Source/WebCore/platform/SharedTimebase.h: Added.
* Source/WebCore/platform/SharedTimebase.serialization.in: Copied from
Source/WebKit/WebProcess/GPU/media/RemoteAudioVideoRendererState.serialization.in.
* Source/WebCore/platform/SharedTimebaseHandle.h: Copied from
Source/WebKit/WebProcess/GPU/media/RemoteAudioVideoRendererState.h.
* Source/WebCore/platform/graphics/avfoundation/AudioVideoRendererAVFObjC.h:
* Source/WebCore/platform/graphics/avfoundation/AudioVideoRendererAVFObjC.mm:
(WebCore::AudioVideoRendererAVFObjC::~AudioVideoRendererAVFObjC):
(WebCore::AudioVideoRendererAVFObjC::effectiveRate const):
(WebCore::AudioVideoRendererAVFObjC::notifyEffectiveRateChanged):
(WebCore::AudioVideoRendererAVFObjC::handleEffectiveRateChanged):
On 0 -> non-zero rate transitions, install a periodic time
observer on the synchronizer and hold the rate change back from
the consumer / from effectiveRate() until CMTimebaseGetTime has
been observed advancing. On non-zero -> 0 transitions, tear down
any pending observer and forward 0 immediately.
(WebCore::AudioVideoRendererAVFObjC::releaseStartupGateAndForwardRate):
(WebCore::AudioVideoRendererAVFObjC::cancelStartupGateObserver):
* Source/WebCore/platform/graphics/skia/SkiaSerializedImageBuffer.cpp:
* Source/WebKit/CMakeLists.txt:
* Source/WebKit/DerivedSources-input.xcfilelist:
* Source/WebKit/DerivedSources.make:
* Source/WebKit/GPUProcess/media/RemoteAudioVideoRendererProxyManager.cpp:
(WebKit::RemoteAudioVideoRendererProxyManager::updateContextSharedTimebase):
Publishes (currentTime, effectiveRate, MonotonicTime::now()) into
the per-renderer SharedTimebase. Single point where the writer-
side anchor is refreshed.
(WebKit::RemoteAudioVideoRendererProxyManager::create):
Allocates a SharedTimebase per renderer and returns its handle in
the Create reply. nullopt on allocation failure — the WebContent
side maps that to PlatformMediaError::MemoryError.
(WebKit::RemoteAudioVideoRendererProxyManager::addTrack):
(WebKit::RemoteAudioVideoRendererProxyManager::requestMediaDataWhenReady):
(WebKit::RemoteAudioVideoRendererProxyManager::converterFor):
(WebKit::RemoteAudioVideoRendererProxyManager::notifyTimeReachedAndStall):
(WebKit::RemoteAudioVideoRendererProxyManager::performTaskAtTime):
(WebKit::RemoteAudioVideoRendererProxyManager::installTimeObserver):
Drop the periodic TimeObserverUpdate IPC; the same callback now
refreshes the shared anchor directly via updateContextSharedTimebase.
firstTickAfterPlay and maybeUpdateCachedVideoMetrics are unchanged.
(WebKit::RemoteAudioVideoRendererProxyManager::play):
(WebKit::RemoteAudioVideoRendererProxyManager::pause):
(WebKit::RemoteAudioVideoRendererProxyManager::setRate):
No longer return MediaTimeUpdateData; refresh the SharedTimebase
anchor inline. Use MESSAGE_CHECK with an early return so a
malformed identifier can't dereference past the find() failure.
(WebKit::RemoteAudioVideoRendererProxyManager::stall):
Refresh the anchor after the renderer transitions to rate=0.
(WebKit::RemoteAudioVideoRendererProxyManager::prepareToSeek):
(WebKit::RemoteAudioVideoRendererProxyManager::finishSeek):
(WebKit::RemoteAudioVideoRendererProxyManager::notifyWhenHasAvailableVideoFrame):
(WebKit::RemoteAudioVideoRendererProxyManager::setVideoPlaybackMetricsUpdateInterval):
(WebKit::RemoteAudioVideoRendererProxyManager::updateCachedVideoMetrics):
(WebKit::RemoteAudioVideoRendererProxyManager::stateFor const):
Refresh the anchor before producing the StateUpdate snapshot;
state itself is now just `paused`.
(WebKit::RemoteAudioVideoRendererProxyManager::rendereringModeChanged):
* Source/WebKit/GPUProcess/media/RemoteAudioVideoRendererProxyManager.h:
(WebKit::RemoteAudioVideoRendererProxyManager::publishAndSend):
*
Source/WebKit/GPUProcess/media/RemoteAudioVideoRendererProxyManager.messages.in:
Create now returns std::optional<WebCore::SharedTimebaseHandle>.
Play / Pause / SetRate become fire-and-forget (no async reply).
* Source/WebKit/Scripts/webkit/opaque_ipc_types.tracking.in:
Register WebCore::SharedTimebaseHandle as a serializable IPC type.
* Source/WebKit/WebProcess/GPU/media/AudioVideoRendererRemote.cpp:
Delete the TimeProgressEstimator inner class entirely; replace
m_timeEstimator with std::unique_ptr<SharedTimebaseReader>
m_sharedTimebaseReader, guarded by m_lock. Add std::optional<MediaTime>
m_stallCap on the renderer itself; the cap no longer lives in the
time source.
(WebKit::AudioVideoRendererRemote::AudioVideoRendererRemote):
Send Create as sendWithAsyncReply. On nullopt handle or null
SharedTimebase, dispatch m_errorCallback(MemoryError) on the work
queue. On success, build the reader.
(WebKit::AudioVideoRendererRemote::play):
(WebKit::AudioVideoRendererRemote::pause):
(WebKit::AudioVideoRendererRemote::setRate):
(WebKit::AudioVideoRendererRemote::effectiveRate const):
Read currentRate from m_sharedTimebaseReader.
(WebKit::AudioVideoRendererRemote::stall):
(WebKit::AudioVideoRendererRemote::cancelPendingSeek):
Conditionally clears m_stallCap based on rate direction.
(WebKit::AudioVideoRendererRemote::timeIsProgressing const):
(WebKit::AudioVideoRendererRemote::currentTime const):
Read from m_sharedTimebaseReader under m_lock, then clamp by
m_stallCap. Preserves the existing m_seeking / m_lastSeekTime
short-circuit.
(WebKit::AudioVideoRendererRemote::notifyTimeReachedAndStall):
Set m_stallCap = time, then send the IPC.
(WebKit::AudioVideoRendererRemote::cancelTimeReachedAction):
Clear m_stallCap, then send the IPC.
(WebKit::AudioVideoRendererRemote::flush):
(WebKit::AudioVideoRendererRemote::updateCacheState):
State struct no longer carries time data.
(WebKit::AudioVideoRendererRemote::MessageReceiver::effectiveRateChanged):
(WebKit::AudioVideoRendererRemote::MessageReceiver::layerHostingContextChanged):
Read currentRate / currentTime from m_sharedTimebaseReader instead
of state.timeUpdateData.
Delete MessageReceiver::timeObserverUpdate.
(WebKit::AudioVideoRendererRemote::TimeProgressEstimator::currentTime const):
Deleted.
(WebKit::AudioVideoRendererRemote::TimeProgressEstimator::timeIsProgressing
const): Deleted.
(WebKit::AudioVideoRendererRemote::TimeProgressEstimator::setTime): Deleted.
(WebKit::AudioVideoRendererRemote::TimeProgressEstimator::setRate): Deleted.
(WebKit::AudioVideoRendererRemote::TimeProgressEstimator::pause): Deleted.
(WebKit::AudioVideoRendererRemote::TimeProgressEstimator::resetLastReturnedTime):
Deleted.
(WebKit::AudioVideoRendererRemote::TimeProgressEstimator::setStallCap): Deleted.
(WebKit::AudioVideoRendererRemote::TimeProgressEstimator::clearStallCap):
Deleted.
(WebKit::AudioVideoRendererRemote::TimeProgressEstimator::clearStallCapIfBefore):
Deleted.
(WebKit::AudioVideoRendererRemote::MessageReceiver::timeObserverUpdate):
Deleted.
* Source/WebKit/WebProcess/GPU/media/AudioVideoRendererRemote.h:
*
Source/WebKit/WebProcess/GPU/media/AudioVideoRendererRemoteMessageReceiver.messages.in:
Drop TimeObserverUpdate.
* Source/WebKit/WebProcess/GPU/media/RemoteAudioVideoRendererState.h:
*
Source/WebKit/WebProcess/GPU/media/RemoteAudioVideoRendererState.serialization.in:
Drop timeUpdateData; struct is now just `bool paused`. Comment on
remoteAudioVideoRendererUpdateInterval updated to describe the
shared-anchor refresh cadence rather than the dropped IPC.
* Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj:
* Tools/TestWebKitAPI/Tests/WebCore/SharedTimebaseTests.cpp: Added.
Unit coverage for the writer/reader pair: anchor publication,
rate-based extrapolation, monotonicity clamp.
(TestWebKitAPI::TEST(SharedTimebase, Basic)):
Canonical link: https://commits.webkit.org/314135@main
To unsubscribe from these emails, change your notification settings at
https://github.com/WebKit/WebKit/settings/notifications