Files
Chun-Min Chang 9ec8ebfa94 Bug 2053103 - Add TimeDuration::ToTicksAtRate for exact tick-rate conversion r=media-playback-reviewers,padenot
Adds an integer/rational conversion from a TimeDuration to a count of
arbitrary-rate ticks, rounded to the nearest tick with no floating point.
Unlike ToMicroseconds()/ToSeconds(), which multiply by a precomputed double
tick factor, this is computed directly from the platform tick count and the
platform's native rational rate, so it is exact for durations representable at
the target rate and unbiased (nearest, no drift) otherwise. The GCD-reduced
64-bit mul/div decomposition avoids 128-bit arithmetic (portable to 32-bit and
to Windows, whose runtime lacks __udivti3). Implemented for the mach (darwin),
POSIX (nanosecond) and Windows (QPC) backends, with a gtest in TestTimeStamp.cpp.

Differential Revision: https://phabricator.services.mozilla.com/D311215
2026-07-20 22:07:05 +00:00

209 lines
9.5 KiB
C++

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "gtest/gtest.h"
#include "mozilla/RoundedMulDiv.h"
#include "mozilla/TimeStamp.h"
#include "prinrval.h"
#include "prthread.h"
using mozilla::TimeDuration;
using mozilla::TimeStamp;
TEST(TimeStamp, RoundedMulDiv)
{
EXPECT_EQ(mozilla::RoundedMulDiv(1, 1, 3), 0);
EXPECT_EQ(mozilla::RoundedMulDiv(2, 1, 3), 1);
EXPECT_EQ(mozilla::RoundedMulDiv(1, 1, 2), 1);
EXPECT_EQ(mozilla::RoundedMulDiv(-1, 1, 2), -1);
// Intermediate overflow saturates in the direction of the input sign.
EXPECT_EQ(mozilla::RoundedMulDiv(3, UINT64_MAX, 1), INT64_MAX);
EXPECT_EQ(mozilla::RoundedMulDiv(-3, UINT64_MAX, 1), INT64_MIN);
EXPECT_EQ(mozilla::RoundedMulDiv(6, UINT64_MAX, 7), INT64_MAX);
EXPECT_EQ(mozilla::RoundedMulDiv(-6, UINT64_MAX, 7), INT64_MIN);
EXPECT_EQ(mozilla::RoundedMulDiv(1, UINT64_MAX, 2), INT64_MAX);
EXPECT_EQ(mozilla::RoundedMulDiv(-1, UINT64_MAX, 2), INT64_MIN);
EXPECT_EQ(mozilla::RoundedMulDiv(3, UINT64_MAX - 2, 2), INT64_MAX);
EXPECT_EQ(mozilla::RoundedMulDiv(-3, UINT64_MAX - 2, 2), INT64_MIN);
EXPECT_EQ(mozilla::RoundedMulDiv(INT64_MAX, 2, 1), INT64_MAX);
EXPECT_EQ(mozilla::RoundedMulDiv(-INT64_MAX, 2, 1), INT64_MIN);
// Conservative saturation: the exact rounded result 2000000000 is
// representable in int64_t, but the intermediate r * aMultiplier overflows
// uint64_t, so RoundedMulDiv saturates rather than computing it.
//
// This is unreachable via ToTicksAtRate: there aMultiplier and aDivisor are
// aRate and the clock frequency, so the GCD reduction cancels their common
// factor and bounds the intermediate r * aMultiplier by aRate * aDivisor.
// Every backend's aDivisor is below 2^32 (posix 1e9, darwin 3e9 on Apple
// Silicon, windows QPC frequency) and aRate <= UINT32_MAX, so that product
// stays below 2^64. This case exceeds it only because its divisor
// (1e10 + 1) is above 2^32 and is paired with a comparably large multiplier.
// It is reachable only by calling RoundedMulDiv directly.
EXPECT_EQ(mozilla::RoundedMulDiv(2000000000, 10000000000, 10000000001),
INT64_MAX);
EXPECT_EQ(mozilla::RoundedMulDiv(-2000000000, 10000000000, 10000000001),
INT64_MIN);
// Boundary of the conservative saturation at the maximum rate
// aMultiplier = UINT32_MAX. The intermediate r * aMultiplier is at most
// (aDivisor - 1) * UINT32_MAX, which crosses 2^64 at aDivisor ~= 2^32: a
// divisor just below 2^32 still returns the exact result, one just above
// saturates. Both divisors are primes near 2^32 coprime to UINT32_MAX, and
// aValue = aDivisor - 1, so no GCD reduction hides the bound.
EXPECT_EQ(mozilla::RoundedMulDiv(4294967290, UINT32_MAX, 4294967291),
4294967294); // aDivisor = 2^32 - 5
EXPECT_EQ(mozilla::RoundedMulDiv(-4294967290, UINT32_MAX, 4294967291),
-4294967294);
EXPECT_EQ(mozilla::RoundedMulDiv(4294967310, UINT32_MAX, 4294967311),
INT64_MAX); // aDivisor = 2^32 + 15
EXPECT_EQ(mozilla::RoundedMulDiv(-4294967310, UINT32_MAX, 4294967311),
INT64_MIN);
// Real clock divisors sit far below that 2^32 boundary, so even at
// aMultiplier = UINT32_MAX the exact result is returned, which is the
// guarantee ToTicksAtRate depends on. On Apple Silicon (mach timebase
// sNumer/sDenom = 125/3) the GCD reduction is load-bearing: the raw
// (3e9 - 1) * 125 * UINT32_MAX ~= 1.6e21 exceeds 2^64, but reduction shrinks
// the intermediate to ~4.6e14. (Intel macOS uses 1/1, i.e. the posix case.)
EXPECT_EQ(mozilla::RoundedMulDiv(999999999, UINT32_MAX, 1000000000),
4294967291); // posix: aDivisor = 1e9
EXPECT_EQ(mozilla::RoundedMulDiv(2999999999, 125ULL * UINT32_MAX, 3000000000),
536870911696); // Apple Silicon (125/3 timebase)
EXPECT_EQ(mozilla::RoundedMulDiv(9999999, UINT32_MAX, 10000000),
4294966866); // windows: aDivisor = 10 MHz QPC
}
TEST(TimeStamp, Main)
{
TimeDuration td;
EXPECT_TRUE(td.ToSeconds() == 0.0);
EXPECT_TRUE(TimeDuration::FromSeconds(5).ToSeconds() == 5.0);
EXPECT_TRUE(TimeDuration::FromMilliseconds(5000).ToSeconds() == 5.0);
EXPECT_TRUE(TimeDuration::FromSeconds(1) < TimeDuration::FromSeconds(2));
EXPECT_FALSE(TimeDuration::FromSeconds(1) < TimeDuration::FromSeconds(1));
EXPECT_TRUE(TimeDuration::FromSeconds(2) > TimeDuration::FromSeconds(1));
EXPECT_FALSE(TimeDuration::FromSeconds(1) > TimeDuration::FromSeconds(1));
EXPECT_TRUE(TimeDuration::FromSeconds(1) <= TimeDuration::FromSeconds(2));
EXPECT_TRUE(TimeDuration::FromSeconds(1) <= TimeDuration::FromSeconds(1));
EXPECT_FALSE(TimeDuration::FromSeconds(2) <= TimeDuration::FromSeconds(1));
EXPECT_TRUE(TimeDuration::FromSeconds(2) >= TimeDuration::FromSeconds(1));
EXPECT_TRUE(TimeDuration::FromSeconds(1) >= TimeDuration::FromSeconds(1));
EXPECT_FALSE(TimeDuration::FromSeconds(1) >= TimeDuration::FromSeconds(2));
TimeStamp ts;
EXPECT_TRUE(ts.IsNull());
ts = TimeStamp::Now();
EXPECT_TRUE(!ts.IsNull());
EXPECT_TRUE((ts - ts).ToSeconds() == 0.0);
PR_Sleep(PR_SecondsToInterval(2));
TimeStamp ts2(TimeStamp::Now());
EXPECT_TRUE(ts2 > ts);
EXPECT_FALSE(ts > ts);
EXPECT_TRUE(ts < ts2);
EXPECT_FALSE(ts < ts);
EXPECT_TRUE(ts <= ts2);
EXPECT_TRUE(ts <= ts);
EXPECT_FALSE(ts2 <= ts);
EXPECT_TRUE(ts2 >= ts);
EXPECT_TRUE(ts2 >= ts);
EXPECT_FALSE(ts >= ts2);
// We can't be sure exactly how long PR_Sleep slept for. It should have
// slept for at least one second. We might have slept a lot longer due
// to process scheduling, but hopefully not more than 10 seconds.
td = ts2 - ts;
EXPECT_TRUE(td.ToSeconds() > 1.0);
EXPECT_TRUE(td.ToSeconds() < 20.0);
td = ts - ts2;
EXPECT_TRUE(td.ToSeconds() < -1.0);
EXPECT_TRUE(td.ToSeconds() > -20.0);
// Now() is trying to ensure the best possible precision on each platform,
// but guarantees at least one millisecond resolution. Given the huge
// difference between the assumed resolution of the clock on modern CPUs
// (< 100ns, often close to 1ns as of 2025) and that guarantee, we can safely
// test for 1ms without fearing intermittents caused by jitter.
TimeStamp start = TimeStamp::Now();
TimeStamp last = start;
int updated = 0;
int same = 0;
while ((last - start).ToMilliseconds() < 1.0) {
TimeStamp next = TimeStamp::Now();
if ((next - last).ToMicroseconds() > 0.0) {
// Only count if we saw progress in the ticks.
++updated;
last = next;
} else {
++same;
}
}
printf(" Poll saw updated iterations in 1ms: %d\n", updated);
printf(" Poll saw same iterations in 1ms: %d\n", same);
// If we saw 2 updates, we can be pretty sure to be able to guarantee at
// least 1ms resolution. In practice we see much higher numbers here (>>1K).
EXPECT_GE(updated, 2);
}
TEST(TimeStamp, ToTicksAtRate)
{
// Zero and the saturating sentinels.
EXPECT_EQ(TimeDuration().ToTicksAtRate(90000), 0);
EXPECT_EQ(TimeDuration::Forever().ToTicksAtRate(90000), INT64_MAX);
EXPECT_EQ((-TimeDuration::Forever()).ToTicksAtRate(90000), INT64_MIN);
const int64_t maxWholeSecondsAtMaxRate =
INT64_MAX / static_cast<int64_t>(UINT32_MAX);
const int64_t maxWholeSecondTicks =
maxWholeSecondsAtMaxRate * static_cast<int64_t>(UINT32_MAX);
const TimeDuration maxWholeSecondDuration =
TimeDuration::FromSeconds(maxWholeSecondsAtMaxRate);
EXPECT_EQ(maxWholeSecondDuration.ToTicksAtRate(UINT32_MAX),
maxWholeSecondTicks);
EXPECT_EQ((-maxWholeSecondDuration).ToTicksAtRate(UINT32_MAX),
-maxWholeSecondTicks);
const TimeDuration overflowingWholeSecondDuration =
TimeDuration::FromSeconds(maxWholeSecondsAtMaxRate + 1.0);
EXPECT_EQ(overflowingWholeSecondDuration.ToTicksAtRate(UINT32_MAX),
INT64_MAX);
EXPECT_EQ((-overflowingWholeSecondDuration).ToTicksAtRate(UINT32_MAX),
INT64_MIN);
const TimeDuration overflowingUint64WholeSecondDuration =
TimeDuration::FromSeconds(5000000000.0);
EXPECT_EQ(overflowingUint64WholeSecondDuration.ToTicksAtRate(UINT32_MAX),
INT64_MAX);
EXPECT_EQ((-overflowingUint64WholeSecondDuration).ToTicksAtRate(UINT32_MAX),
INT64_MIN);
// Durations that are an exact multiple of the target tick must round-trip
// EXACTLY on every platform, regardless of the underlying clock's tick rate.
EXPECT_EQ(TimeDuration::FromSeconds(1).ToTicksAtRate(90000), 90000);
EXPECT_EQ(TimeDuration::FromMilliseconds(500).ToTicksAtRate(90000), 45000);
EXPECT_EQ(TimeDuration::FromMilliseconds(125).ToTicksAtRate(90000), 11250);
EXPECT_EQ(TimeDuration::FromSeconds(17).ToTicksAtRate(90000), 1530000);
EXPECT_EQ(TimeDuration::FromMilliseconds(2125).ToTicksAtRate(90000), 191250);
// Rate-agnostic: production uses the MediaTrackGraph (audio device) rate.
EXPECT_EQ(TimeDuration::FromSeconds(1).ToTicksAtRate(48000), 48000);
EXPECT_EQ(TimeDuration::FromMilliseconds(500).ToTicksAtRate(48000), 24000);
EXPECT_EQ(TimeDuration::FromSeconds(1).ToTicksAtRate(44100), 44100);
// Off-grid durations round to the NEAREST tick (not floored):
// 11 us = 0.99 ticks @ 90 kHz -> 1
// 99999 us = 8999.91 ticks @ 90 kHz -> 9000
EXPECT_EQ(TimeDuration::FromMicroseconds(11).ToTicksAtRate(90000), 1);
EXPECT_EQ(TimeDuration::FromMicroseconds(99999).ToTicksAtRate(90000), 9000);
// Negative durations are symmetric.
EXPECT_EQ((-TimeDuration::FromSeconds(1)).ToTicksAtRate(90000), -90000);
EXPECT_EQ((-TimeDuration::FromMilliseconds(500)).ToTicksAtRate(90000),
-45000);
}