diff options
Diffstat (limited to '')
-rw-r--r-- | mozglue/baseprofiler/public/ProportionValue.h | 235 |
1 files changed, 235 insertions, 0 deletions
diff --git a/mozglue/baseprofiler/public/ProportionValue.h b/mozglue/baseprofiler/public/ProportionValue.h new file mode 100644 index 0000000000..61eb2766ec --- /dev/null +++ b/mozglue/baseprofiler/public/ProportionValue.h @@ -0,0 +1,235 @@ +/* -*- 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/. */ + +#ifndef ProportionValue_h +#define ProportionValue_h + +#include "mozilla/Attributes.h" + +#include <algorithm> +#include <limits> + +namespace mozilla { + +// Class storing a proportion value between 0 and 1, effectively 0% to 100%. +// The public interface deals with doubles, but internally the value is encoded +// in an integral type, so arithmetic operations are fast. +// It also supports an invalid value: Use MakeInvalid() to construct, it infects +// any operation, and gets converted to a signaling NaN. +class ProportionValue { + public: + using UnderlyingType = uint32_t; + + // Default-construct at 0%. + constexpr ProportionValue() + // This `noexcept` is necessary to avoid a build error when encapsulating + // `ProportionValue` in `std::Atomic`: + // "use of deleted function + // 'constexpr std::atomic<mozilla::ProportionValue>::atomic()" + // because the default `std::atomic<T>::atomic()` constructor is marked: + // `noexcept(std::is_nothrow_default_constructible_v<T>)` + // and therefore this default constructor here must be explicitly marked + // `noexcept` as well. + noexcept + : mIntegralValue(0u) {} + + // Construct a ProportionValue with the given value, clamped to 0..1. + // Note that it's constexpr, so construction from literal numbers should incur + // no runtime costs. + // If `aValue` is NaN, behavior is undefined! Use `MakeInvalid()` instead. + constexpr explicit ProportionValue(double aValue) + : mIntegralValue(UnderlyingType(std::clamp(aValue, 0.0, 1.0) * scMaxD)) {} + + [[nodiscard]] static constexpr ProportionValue MakeInvalid() { + return ProportionValue(scInvalidU, Internal{}); + } + + [[nodiscard]] constexpr double ToDouble() const { + return IsInvalid() ? std::numeric_limits<double>::signaling_NaN() + : (double(mIntegralValue) * scInvMaxD); + } + + // Retrieve the underlying integral value, for storage or testing purposes. + [[nodiscard]] constexpr UnderlyingType ToUnderlyingType() const { + return mIntegralValue; + }; + + // Re-construct a ProportionValue from an underlying integral value. + [[nodiscard]] static constexpr ProportionValue FromUnderlyingType( + UnderlyingType aUnderlyingType) { + return ProportionValue( + (aUnderlyingType <= scMaxU) ? aUnderlyingType : scInvalidU, Internal{}); + } + + [[nodiscard]] constexpr bool IsExactlyZero() const { + return mIntegralValue == 0u; + } + + [[nodiscard]] constexpr bool IsExactlyOne() const { + return mIntegralValue == scMaxU; + } + + [[nodiscard]] constexpr bool IsValid() const { + // Compare to the maximum value, not just exactly scInvalidU, to catch any + // kind of invalid state. + return mIntegralValue <= scMaxU; + } + [[nodiscard]] constexpr bool IsInvalid() const { + // Compare to the maximum value, not just exactly scInvalidU, to catch any + // kind of invalid state. + return mIntegralValue > scMaxU; + } + + // Strict comparisons based on the underlying integral value. Use + // `CompareWithin` instead to make fuzzy comparisons. + // `ProportionValue::MakeInvalid()`s are equal, and greater than anything + // else; Best to avoid comparisons, and first use IsInvalid() instead. +#define OPERATOR_COMPARISON(CMP) \ + [[nodiscard]] constexpr friend bool operator CMP( \ + const ProportionValue& aLHS, const ProportionValue& aRHS) { \ + return aLHS.mIntegralValue CMP aRHS.mIntegralValue; \ + } + OPERATOR_COMPARISON(==) + OPERATOR_COMPARISON(!=) + OPERATOR_COMPARISON(<) + OPERATOR_COMPARISON(<=) + OPERATOR_COMPARISON(>) + OPERATOR_COMPARISON(>=) +#undef OPERATOR_COMPARISON + + // Arithmetic operations + - *, all working on the underlying integral values + // (i.e, no expensive floating-point operations are used), and always clamping + // to 0..1 range. Invalid values are poisonous. + + [[nodiscard]] constexpr ProportionValue operator+( + ProportionValue aRHS) const { + return ProportionValue( + (IsInvalid() || aRHS.IsInvalid()) + ? scInvalidU + // Adding fixed-point values keep the same scale, so there is no + // adjustment needed for that. [0,1]+[0,1]=[0,2], so we only need to + // ensure that the result is capped at max 1, aka scMaxU: + // a+b<=max <=> b<=max-a, so b is at maximum max-a. + : (mIntegralValue + + std::min(aRHS.mIntegralValue, scMaxU - mIntegralValue)), + Internal{}); + } + + [[nodiscard]] constexpr ProportionValue operator-( + ProportionValue aRHS) const { + return ProportionValue( + (IsInvalid() || aRHS.IsInvalid()) + ? scInvalidU + // Subtracting fixed-point values keep the same scale, so there is + // no adjustment needed for that. [0,1]-[0,1]=[-1,1], so we only + // need to ensure that the value is positive: + // a-b>=0 <=> b<=a, so b is at maximum a. + : (mIntegralValue - std::min(aRHS.mIntegralValue, mIntegralValue)), + Internal{}); + } + + [[nodiscard]] constexpr ProportionValue operator*( + ProportionValue aRHS) const { + // Type to hold the full result of multiplying two maximum numbers. + using DoublePrecisionType = uint64_t; + static_assert(sizeof(DoublePrecisionType) >= 2 * sizeof(UnderlyingType)); + return ProportionValue( + (IsInvalid() || aRHS.IsInvalid()) + ? scInvalidU + // Multiplying fixed-point values doubles the scale (2^31 -> 2^62), + // so we need to adjust the result by dividing it by one scale + // (which is optimized into a binary right-shift). + : (UnderlyingType((DoublePrecisionType(mIntegralValue) * + DoublePrecisionType(aRHS.mIntegralValue)) / + DoublePrecisionType(scMaxU))), + Internal{}); + } + + // Explicitly forbid divisions, they make little sense, and would almost + // always return a clamped 100% (E.g.: 50% / 10% = 0.5 / 0.1 = 5 = 500%). + [[nodiscard]] constexpr ProportionValue operator/( + ProportionValue aRHS) const = delete; + + // Division by a positive integer value, useful to split an interval in equal + // parts (with maybe some spare space at the end, because it is rounded down). + // Division by 0 produces an invalid value. + [[nodiscard]] constexpr ProportionValue operator/(uint32_t aDivisor) const { + return ProportionValue((IsInvalid() || aDivisor == 0u) + ? scInvalidU + : (mIntegralValue / aDivisor), + Internal{}); + } + + // Multiplication by a positive integer value, useful as inverse of the + // integer division above. But it may be lossy because the division is rounded + // down, therefore: PV - u < (PV / u) * u <= PV. + // Clamped to 100% max. + [[nodiscard]] constexpr ProportionValue operator*( + uint32_t aMultiplier) const { + return ProportionValue(IsInvalid() + ? scInvalidU + : ((aMultiplier > scMaxU / mIntegralValue) + ? scMaxU + : (mIntegralValue * aMultiplier)), + Internal{}); + } + + private: + // Tagged constructor for internal construction from the UnderlyingType, so + // that it is never ambiguously considered in constructions from one number. + struct Internal {}; + constexpr ProportionValue(UnderlyingType aIntegralValue, Internal) + : mIntegralValue(aIntegralValue) {} + + // Use all but 1 bit for the fractional part. + // Valid values can go from 0b0 (0%) up to 0b1000...00 (scMaxU aka 100%). + static constexpr unsigned scFractionalBits = sizeof(UnderlyingType) * 8 - 1; + // Maximum value corresponding to 1.0 or 100%. + static constexpr UnderlyingType scMaxU = UnderlyingType(1u) + << scFractionalBits; + // This maximum value corresponding to 1.0 can also be seen as the scaling + // factor from any [0,1] `double` value to the internal integral value. + static constexpr double scMaxD = double(scMaxU); + // The inverse can be used to convert the internal value back to [0,1]. + static constexpr double scInvMaxD = 1.0 / scMaxD; + + // Special value outside [0,max], used to construct invalid values. + static constexpr UnderlyingType scInvalidU = ~UnderlyingType(0u); + + // Internal integral value, guaranteed to always be <= scMaxU, or scInvalidU. + // This is effectively a fixed-point value using 1 bit for the integer part + // and 31 bits for the fractional part. + // It is roughly equal to the `double` value [0,1] multiplied by scMaxD. + UnderlyingType mIntegralValue; +}; + +namespace literals { +inline namespace ProportionValue_literals { + +// User-defined literal for integer percentages, e.g.: `10_pc`, `100_pc` +// (equivalent to `ProportionValue{0.1}` and `ProportionValue{1.0}`). +// Clamped to [0, 100]_pc. +[[nodiscard]] constexpr ProportionValue operator""_pc( + unsigned long long int aPercentage) { + return ProportionValue{ + double(std::clamp<unsigned long long int>(aPercentage, 0u, 100u)) / + 100.0}; +} + +// User-defined literal for non-integer percentages, e.g.: `12.3_pc`, `100.0_pc` +// (equivalent to `ProportionValue{0.123}` and `ProportionValue{1.0}`). +// Clamped to [0.0, 100.0]_pc. +[[nodiscard]] constexpr ProportionValue operator""_pc(long double aPercentage) { + return ProportionValue{ + double(std::clamp<long double>(aPercentage, 0.0, 100.0)) / 100.0}; +} + +} // namespace ProportionValue_literals +} // namespace literals + +} // namespace mozilla + +#endif // ProportionValue_h |