mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-02-21 09:49:14 +00:00

As well as removing 53-bit precision rounding in >= and <= operators, this resolves a bug where operator== would sometimes return true for very different TimeUnits. Differential Revision: https://phabricator.services.mozilla.com/D217673
434 lines
13 KiB
C++
434 lines
13 KiB
C++
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
|
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
|
/* 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 <cstdint>
|
|
#include <cmath>
|
|
#include <inttypes.h>
|
|
#include <limits>
|
|
#include <type_traits>
|
|
|
|
#include "TimeUnits.h"
|
|
#include "Intervals.h"
|
|
#include "mozilla/CheckedInt.h"
|
|
#include "mozilla/FloatingPoint.h"
|
|
#include "mozilla/Maybe.h"
|
|
#include "mozilla/TimeStamp.h"
|
|
#include "mozilla/IntegerPrintfMacros.h"
|
|
#include "nsDebug.h"
|
|
#include "nsPrintfCString.h"
|
|
#include "nsStringFwd.h"
|
|
|
|
namespace mozilla::media {
|
|
class TimeIntervals;
|
|
} // namespace mozilla::media
|
|
|
|
namespace mozilla {
|
|
|
|
namespace {
|
|
struct Int96 {
|
|
bool operator==(const Int96& aOther) const {
|
|
return high == aOther.high && low == aOther.low;
|
|
}
|
|
bool operator>=(const Int96& aOther) const {
|
|
if (high == aOther.high) {
|
|
return low >= aOther.low;
|
|
}
|
|
return high > aOther.high;
|
|
}
|
|
bool operator<=(const Int96& aOther) const {
|
|
if (high == aOther.high) {
|
|
return low <= aOther.low;
|
|
}
|
|
return high < aOther.high;
|
|
}
|
|
|
|
const int64_t high;
|
|
const uint32_t low;
|
|
};
|
|
} // anonymous namespace
|
|
|
|
static Int96 MultS64xU32(const CheckedInt64& a, int64_t b) {
|
|
MOZ_ASSERT(b >= 0);
|
|
MOZ_ASSERT(b <= UINT32_MAX);
|
|
// Right shift of negative signed integers is implementation-defined until
|
|
// C++20, but the C++20 two's complement behavior, used by all compilers
|
|
// even prior to C++20, is assumed here.
|
|
// (Left shift of negative signed integers would be undefined until C++20).
|
|
int64_t high = (a.value() >> 32) * b;
|
|
uint64_t low = a.value() & 0xFFFFFFFF;
|
|
low *= b;
|
|
// Move overflow from low multiplication to high.
|
|
// This will not overflow because we have divided by 2^32 and multiplied
|
|
// by b, which is less than 2^32.
|
|
high += AssertedCast<int64_t>(low >> 32);
|
|
return Int96{high, AssertedCast<uint32_t>(low & 0xFFFFFFFF)};
|
|
};
|
|
|
|
namespace media {
|
|
|
|
TimeUnit TimeUnit::FromSeconds(double aValue, int64_t aBase) {
|
|
MOZ_ASSERT(!std::isnan(aValue));
|
|
MOZ_ASSERT(aBase > 0);
|
|
|
|
if (std::isinf(aValue)) {
|
|
return aValue > 0 ? FromInfinity() : FromNegativeInfinity();
|
|
}
|
|
// Warn that a particular value won't be able to be roundtrip at the same
|
|
// base -- we can keep this for some time until we're confident this is
|
|
// stable.
|
|
double inBase = aValue * static_cast<double>(aBase);
|
|
if (std::abs(inBase) >
|
|
static_cast<double>(std::numeric_limits<int64_t>::max())) {
|
|
NS_WARNING(
|
|
nsPrintfCString("Warning: base %" PRId64
|
|
" is too high to represent %lfs, returning Infinity.",
|
|
aBase, aValue)
|
|
.get());
|
|
if (inBase > 0) {
|
|
return TimeUnit::FromInfinity();
|
|
}
|
|
return TimeUnit::FromNegativeInfinity();
|
|
}
|
|
|
|
// inBase can large enough that it doesn't map to an exact integer, warn in
|
|
// this case. This happens if aBase is large, and so the loss of precision is
|
|
// likely small.
|
|
if (inBase > std::pow(2, std::numeric_limits<double>::digits) - 1) {
|
|
NS_WARNING(nsPrintfCString("Warning: base %" PRId64
|
|
" is too high to represent %lfs accurately.",
|
|
aBase, aValue)
|
|
.get());
|
|
}
|
|
return TimeUnit(static_cast<int64_t>(std::round(inBase)), aBase);
|
|
}
|
|
|
|
TimeUnit TimeUnit::FromInfinity() { return TimeUnit(INT64_MAX); }
|
|
|
|
TimeUnit TimeUnit::FromNegativeInfinity() { return TimeUnit(INT64_MIN); }
|
|
|
|
TimeUnit TimeUnit::FromTimeDuration(const TimeDuration& aDuration) {
|
|
// This could be made to choose the base
|
|
return TimeUnit(AssertedCast<int64_t>(aDuration.ToMicroseconds()),
|
|
USECS_PER_S);
|
|
}
|
|
|
|
TimeUnit TimeUnit::Invalid() {
|
|
TimeUnit ret;
|
|
ret.mTicks = CheckedInt64(INT64_MAX);
|
|
// Force an overflow to render the CheckedInt invalid.
|
|
ret.mTicks += 1;
|
|
return ret;
|
|
}
|
|
|
|
int64_t TimeUnit::ToMilliseconds() const { return ToCommonUnit(MSECS_PER_S); }
|
|
|
|
int64_t TimeUnit::ToMicroseconds() const { return ToCommonUnit(USECS_PER_S); }
|
|
|
|
int64_t TimeUnit::ToNanoseconds() const { return ToCommonUnit(NSECS_PER_S); }
|
|
|
|
int64_t TimeUnit::ToTicksAtRate(int64_t aRate) const {
|
|
// Common case
|
|
if (aRate == mBase) {
|
|
return mTicks.value();
|
|
}
|
|
// Approximation
|
|
return mTicks.value() * aRate / mBase;
|
|
}
|
|
|
|
bool TimeUnit::IsBase(int64_t aBase) const { return aBase == mBase; }
|
|
|
|
double TimeUnit::ToSeconds() const {
|
|
if (IsPosInf()) {
|
|
return PositiveInfinity<double>();
|
|
}
|
|
if (IsNegInf()) {
|
|
return NegativeInfinity<double>();
|
|
}
|
|
return static_cast<double>(mTicks.value()) / static_cast<double>(mBase);
|
|
}
|
|
|
|
nsCString TimeUnit::ToString() const {
|
|
nsCString dump;
|
|
if (mTicks.isValid()) {
|
|
dump += nsPrintfCString("{%" PRId64 ",%" PRId64 "}", mTicks.value(), mBase);
|
|
} else {
|
|
dump += nsLiteralCString("{invalid}"_ns);
|
|
}
|
|
return dump;
|
|
}
|
|
|
|
TimeDuration TimeUnit::ToTimeDuration() const {
|
|
return TimeDuration::FromSeconds(ToSeconds());
|
|
}
|
|
|
|
bool TimeUnit::IsInfinite() const { return IsPosInf() || IsNegInf(); }
|
|
|
|
bool TimeUnit::IsPositive() const { return mTicks.value() > 0; }
|
|
|
|
bool TimeUnit::IsPositiveOrZero() const { return mTicks.value() >= 0; }
|
|
|
|
bool TimeUnit::IsZero() const { return mTicks.value() == 0; }
|
|
|
|
bool TimeUnit::IsNegative() const { return mTicks.value() < 0; }
|
|
|
|
// Returns true if the fractions are equal when converted to the smallest
|
|
// base.
|
|
bool TimeUnit::EqualsAtLowestResolution(const TimeUnit& aOther) const {
|
|
MOZ_ASSERT(IsValid() && aOther.IsValid());
|
|
if (aOther.mBase == mBase) {
|
|
return mTicks == aOther.mTicks;
|
|
}
|
|
if (mBase > aOther.mBase) {
|
|
TimeUnit thisInBase = ToBase(aOther.mBase);
|
|
return thisInBase.mTicks == aOther.mTicks;
|
|
}
|
|
TimeUnit otherInBase = aOther.ToBase(mBase);
|
|
return otherInBase.mTicks == mTicks;
|
|
}
|
|
|
|
// Strict equality -- the fractions must be exactly equal
|
|
bool TimeUnit::operator==(const TimeUnit& aOther) const {
|
|
MOZ_ASSERT(IsValid() && aOther.IsValid());
|
|
if (aOther.mBase == mBase) {
|
|
return mTicks == aOther.mTicks;
|
|
}
|
|
// debatable mathematically
|
|
if ((IsPosInf() && aOther.IsPosInf()) || (IsNegInf() && aOther.IsNegInf())) {
|
|
return true;
|
|
}
|
|
if ((IsPosInf() && !aOther.IsPosInf()) ||
|
|
(IsNegInf() && !aOther.IsNegInf())) {
|
|
return false;
|
|
}
|
|
Int96 lhs = MultS64xU32(mTicks, aOther.mBase);
|
|
Int96 rhs = MultS64xU32(aOther.mTicks, mBase);
|
|
return lhs == rhs;
|
|
}
|
|
bool TimeUnit::operator!=(const TimeUnit& aOther) const {
|
|
MOZ_ASSERT(IsValid() && aOther.IsValid());
|
|
return !(aOther == *this);
|
|
}
|
|
bool TimeUnit::operator>=(const TimeUnit& aOther) const {
|
|
MOZ_ASSERT(IsValid() && aOther.IsValid());
|
|
if (aOther.mBase == mBase) {
|
|
return mTicks.value() >= aOther.mTicks.value();
|
|
}
|
|
if ((!IsPosInf() && aOther.IsPosInf()) ||
|
|
(IsNegInf() && !aOther.IsNegInf())) {
|
|
return false;
|
|
}
|
|
if ((IsPosInf() && !aOther.IsPosInf()) ||
|
|
(!IsNegInf() && aOther.IsNegInf())) {
|
|
return true;
|
|
}
|
|
Int96 lhs = MultS64xU32(mTicks, aOther.mBase);
|
|
Int96 rhs = MultS64xU32(aOther.mTicks, mBase);
|
|
return lhs >= rhs;
|
|
}
|
|
bool TimeUnit::operator>(const TimeUnit& aOther) const {
|
|
return !(*this <= aOther);
|
|
}
|
|
bool TimeUnit::operator<=(const TimeUnit& aOther) const {
|
|
MOZ_ASSERT(IsValid() && aOther.IsValid());
|
|
if (aOther.mBase == mBase) {
|
|
return mTicks.value() <= aOther.mTicks.value();
|
|
}
|
|
if ((!IsPosInf() && aOther.IsPosInf()) ||
|
|
(IsNegInf() && !aOther.IsNegInf())) {
|
|
return true;
|
|
}
|
|
if ((IsPosInf() && !aOther.IsPosInf()) ||
|
|
(!IsNegInf() && aOther.IsNegInf())) {
|
|
return false;
|
|
}
|
|
Int96 lhs = MultS64xU32(mTicks, aOther.mBase);
|
|
Int96 rhs = MultS64xU32(aOther.mTicks, mBase);
|
|
return lhs <= rhs;
|
|
}
|
|
bool TimeUnit::operator<(const TimeUnit& aOther) const {
|
|
return !(*this >= aOther);
|
|
}
|
|
|
|
TimeUnit TimeUnit::operator%(const TimeUnit& aOther) const {
|
|
MOZ_ASSERT(IsValid() && aOther.IsValid());
|
|
if (aOther.mBase == mBase) {
|
|
return TimeUnit(mTicks % aOther.mTicks, mBase);
|
|
}
|
|
// This path can be made better if need be.
|
|
double a = ToSeconds();
|
|
double b = aOther.ToSeconds();
|
|
return TimeUnit::FromSeconds(fmod(a, b), mBase);
|
|
}
|
|
|
|
TimeUnit TimeUnit::operator+(const TimeUnit& aOther) const {
|
|
if (IsInfinite() || aOther.IsInfinite()) {
|
|
// When adding at least one infinite value, the result is either
|
|
// +/-Inf, or NaN. So do the calculation in floating point for
|
|
// simplicity.
|
|
double result = ToSeconds() + aOther.ToSeconds();
|
|
return std::isnan(result) ? TimeUnit::Invalid() : FromSeconds(result);
|
|
}
|
|
if (aOther.mBase == mBase) {
|
|
return TimeUnit(mTicks + aOther.mTicks, mBase);
|
|
}
|
|
if (aOther.IsZero()) {
|
|
return *this;
|
|
}
|
|
if (IsZero()) {
|
|
return aOther;
|
|
}
|
|
|
|
double error;
|
|
TimeUnit inBase = aOther.ToBase(mBase, error);
|
|
if (error == 0.0) {
|
|
return *this + inBase;
|
|
}
|
|
|
|
// Last ditch: not exact
|
|
double a = ToSeconds();
|
|
double b = aOther.ToSeconds();
|
|
return TimeUnit::FromSeconds(a + b, mBase);
|
|
}
|
|
|
|
TimeUnit TimeUnit::operator-(const TimeUnit& aOther) const {
|
|
if (IsInfinite() || aOther.IsInfinite()) {
|
|
// When subtracting at least one infinite value, the result is either
|
|
// +/-Inf, or NaN. So do the calculation in floating point for
|
|
// simplicity.
|
|
double result = ToSeconds() - aOther.ToSeconds();
|
|
return std::isnan(result) ? TimeUnit::Invalid() : FromSeconds(result);
|
|
}
|
|
if (aOther.mBase == mBase) {
|
|
return TimeUnit(mTicks - aOther.mTicks, mBase);
|
|
}
|
|
if (aOther.IsZero()) {
|
|
return *this;
|
|
}
|
|
|
|
if (IsZero()) {
|
|
return TimeUnit(-aOther.mTicks, aOther.mBase);
|
|
}
|
|
|
|
double error = 0.0;
|
|
TimeUnit inBase = aOther.ToBase(mBase, error);
|
|
if (error == 0) {
|
|
return *this - inBase;
|
|
}
|
|
|
|
// Last ditch: not exact
|
|
double a = ToSeconds();
|
|
double b = aOther.ToSeconds();
|
|
return TimeUnit::FromSeconds(a - b, mBase);
|
|
}
|
|
TimeUnit& TimeUnit::operator+=(const TimeUnit& aOther) {
|
|
if (aOther.mBase == mBase) {
|
|
mTicks += aOther.mTicks;
|
|
return *this;
|
|
}
|
|
*this = *this + aOther;
|
|
return *this;
|
|
}
|
|
TimeUnit& TimeUnit::operator-=(const TimeUnit& aOther) {
|
|
if (aOther.mBase == mBase) {
|
|
mTicks -= aOther.mTicks;
|
|
return *this;
|
|
}
|
|
*this = *this - aOther;
|
|
return *this;
|
|
}
|
|
|
|
TimeUnit TimeUnit::MultDouble(double aVal) const {
|
|
double multiplied = AssertedCast<double>(mTicks.value()) * aVal;
|
|
// Check is the result of the multiplication can be represented exactly as
|
|
// an integer, in a double.
|
|
if (multiplied > std::pow(2, std::numeric_limits<double>::digits) - 1) {
|
|
printf_stderr("TimeUnit tick count after multiplication %" PRId64
|
|
" * %lf is too"
|
|
" high for the result to be exact",
|
|
mTicks.value(), aVal);
|
|
MOZ_CRASH();
|
|
}
|
|
// static_cast is ok, the magnitude of the number has been checked just above.
|
|
return TimeUnit(static_cast<int64_t>(multiplied), mBase);
|
|
}
|
|
|
|
bool TimeUnit::IsValid() const { return mTicks.isValid(); }
|
|
|
|
bool TimeUnit::IsPosInf() const {
|
|
return mTicks.isValid() && mTicks.value() == INT64_MAX;
|
|
}
|
|
bool TimeUnit::IsNegInf() const {
|
|
return mTicks.isValid() && mTicks.value() == INT64_MIN;
|
|
}
|
|
|
|
int64_t TimeUnit::ToCommonUnit(int64_t aRatio) const {
|
|
CheckedInt<int64_t> rv = mTicks;
|
|
// Avoid the risk overflowing in common cases, e.g. converting a TimeUnit
|
|
// with a base of 1e9 back to nanoseconds.
|
|
if (mBase == aRatio) {
|
|
return rv.value();
|
|
}
|
|
// Avoid overflowing in other common cases, e.g. converting a TimeUnit with
|
|
// a base of 1e9 to microseconds: the denominator is divisible by the target
|
|
// unit so we can reorder the computation and keep the number within int64_t
|
|
// range.
|
|
if (aRatio < mBase && (mBase % aRatio) == 0) {
|
|
int64_t exactDivisor = mBase / aRatio;
|
|
rv /= exactDivisor;
|
|
return rv.value();
|
|
}
|
|
rv *= aRatio;
|
|
rv /= mBase;
|
|
if (rv.isValid()) {
|
|
return rv.value();
|
|
}
|
|
// Last ditch, perform the computation in floating point.
|
|
double ratioFloating = AssertedCast<double>(aRatio);
|
|
double baseFloating = AssertedCast<double>(mBase);
|
|
double ticksFloating = static_cast<double>(mTicks.value());
|
|
double approx = ticksFloating * (ratioFloating / baseFloating);
|
|
// Clamp to a valid range. If this is clamped it's outside any usable time
|
|
// value even in nanoseconds (thousands of years).
|
|
if (approx > static_cast<double>(std::numeric_limits<int64_t>::max())) {
|
|
return std::numeric_limits<int64_t>::max();
|
|
}
|
|
if (approx < static_cast<double>(std::numeric_limits<int64_t>::lowest())) {
|
|
return std::numeric_limits<int64_t>::lowest();
|
|
}
|
|
return static_cast<int64_t>(approx);
|
|
}
|
|
|
|
// Reduce a TimeUnit to the smallest possible ticks and base. This is useful
|
|
// to comparison with big time values that can otherwise overflow.
|
|
TimeUnit TimeUnit::Reduced() const {
|
|
int64_t posTicks = abs(mTicks.value());
|
|
bool wasNeg = mTicks.value() < 0;
|
|
int64_t gcd = GCD(posTicks, mBase);
|
|
int64_t signedTicks = wasNeg ? -posTicks : posTicks;
|
|
signedTicks /= gcd;
|
|
return TimeUnit(signedTicks, mBase / gcd);
|
|
}
|
|
|
|
double RoundToMicrosecondResolution(double aSeconds) {
|
|
return std::round(aSeconds * USECS_PER_S) / USECS_PER_S;
|
|
}
|
|
|
|
TimeRanges TimeRanges::ToMicrosecondResolution() const {
|
|
TimeRanges output;
|
|
|
|
for (const auto& interval : mIntervals) {
|
|
TimeRange reducedPrecision{RoundToMicrosecondResolution(interval.mStart),
|
|
RoundToMicrosecondResolution(interval.mEnd),
|
|
RoundToMicrosecondResolution(interval.mFuzz)};
|
|
output += reducedPrecision;
|
|
}
|
|
return output;
|
|
}
|
|
|
|
}; // namespace media
|
|
|
|
} // namespace mozilla
|