Bug 1430841 Eliminate float fuzziness in ReduceTimerPrecision r?froydnj draft
authorTom Ritter <tom@mozilla.com>
Mon, 12 Feb 2018 16:17:03 -0600
changeset 758828 501e78ed4ace9d637f76480b0ea5bb7713260070
parent 758827 3e89f12fe14691944f64c8c5922ff212cb6d9516
child 758829 9926dc444373772e2fb11659267a55608fb5ace6
push id100188
push userbmo:tom@mozilla.com
push dateFri, 23 Feb 2018 03:38:57 +0000
reviewersfroydnj
bugs1430841
milestone60.0a1
Bug 1430841 Eliminate float fuzziness in ReduceTimerPrecision r?froydnj We eliminate float fuzziness by calculating the reduced precision using integers in microseconds. (This means we automatically lose any granularity about nanoseconds, but that's okay.) MozReview-Commit-ID: D9ReLknsafo
toolkit/components/resistfingerprinting/nsRFPService.cpp
toolkit/components/resistfingerprinting/nsRFPService.h
toolkit/components/resistfingerprinting/tests/test_reduceprecision.cpp
--- a/toolkit/components/resistfingerprinting/nsRFPService.cpp
+++ b/toolkit/components/resistfingerprinting/nsRFPService.cpp
@@ -120,81 +120,105 @@ nsRFPService::IsResistFingerprintingEnab
 bool
 nsRFPService::IsTimerPrecisionReductionEnabled(TimerPrecisionType aType)
 {
   if (aType == TimerPrecisionType::RFPOnly) {
     return IsResistFingerprintingEnabled();
   }
 
   return (sPrivacyTimerPrecisionReduction || IsResistFingerprintingEnabled()) &&
-         TimerResolution() != 0;
+         TimerResolution() > 0;
 }
 
-/*
-  DOC TODO
-*/
+/**
+ * Given a precision value, this function will reduce a given input time to the nearest
+ * multiple of that precision.
+ *
+ * It will check if it is appropriate to clamp the input time according to the values
+ * of the privacy.resistFingerprinting and privacy.reduceTimerPrecision preferences.
+ * Note that while it will check these prefs, it will use whatever precision is given to
+ * it, so if one desires a minimum precision for Resist Fingerprinting, it is the
+ * caller's responsibility to provide the correct value. This means you should pass
+ * TimerPrecision(), which enforces a minimum vale on the precision based on
+ * preferences.
+ *
+ * It ensures the given precision value is greater than zero, if it is not it returns
+ * the input time.
+ *
+ * @param aTime           [in] The input time to be clamped.
+ * @param aTimeScale      [in] The units the input time is in (Seconds, Milliseconds, or Microseconds).
+ * @param aResolutionUSec [in] The precision (in microseconds) to clamp to.
+ * @return                 If clamping is appropriate, the clamped value of the input, otherwise the input.
+ */
 /* static */
 double
 nsRFPService::ReduceTimePrecisionImpl(
   double aTime,
-  double aResolutionUS,
-  double aResolutionScaleCorrection,
+  TimeScale aTimeScale,
+  double aResolutionUSec,
   TimerPrecisionType aType)
  {
-   if (!IsTimerPrecisionReductionEnabled(aType)) {
+   if (!IsTimerPrecisionReductionEnabled(aType) || aResolutionUSec <= 0) {
      return aTime;
    }
-  if (aResolutionScaleCorrection != 1 &&
-      aResolutionScaleCorrection != 1000 &&
-      aResolutionScaleCorrection != 1000000) {
-    MOZ_ASSERT(false, "Only scale corrections of 1, 1000, and 1000000 are supported.");
-    return aTime;
-  }
 
-  double ret;
-  const double reducedResolution = aResolutionUS / aResolutionScaleCorrection;
-  if (aResolutionScaleCorrection >= 1000000 && aResolutionUS < 1000000) {
-    // The resolution is so small we need to use the reciprocal to avoid floating point error.
-    const double resolutionReciprocal = 1000000.0 / reducedResolution;
-    ret = floor(aTime * resolutionReciprocal) / resolutionReciprocal;
-#if defined(DEBUG)
-   MOZ_LOG(gResistFingerprintingLog, LogLevel::Verbose,
-    ("Given: %.*f, Reciprocal Rounding with %.*f, Intermediate: %.*f, Got: %.*f",
-      DBL_DIG-1, aTime, DBL_DIG-1, resolutionReciprocal, DBL_DIG-1, floor(aTime * resolutionReciprocal), DBL_DIG-1, ret));
-#endif
-  } else {
-    ret = floor(aTime / reducedResolution) * reducedResolution;
+  // Increase the time as needed until it is in microseconds.
+  // Note that a double can hold up to 2**53 with integer precision. This gives us
+  // only until June 5, 2255 in time-since-the-epoch with integer precision.
+  // So we will be losing microseconds precision after that date.
+  // We think this is okay, and we codify it in some tests.
+  double timeScaled = aTime * (1000000 / aTimeScale);
+  // Cut off anything less than a microsecond.
+  long long timeAsInt = timeScaled;
+  // Cast the resolution (in microseconds) to an int.
+  long long resolutionAsInt = aResolutionUSec;
+  // Perform the clamping.
+  // We do a cast back to double to perform the division with doubles, then floor the result
+  // and the rest occurs with integer precision.
+  // This is because it gives consistency above and below zero. Above zero, performing the
+  // division in integers truncates decimals, taking the result closer to zero (a floor).
+  // Below zero, performing the division in integers truncates decimals, taking the result
+  // closer to zero (a ceil).
+  // The impact of this is that comparing two clamped values that should be related by a
+  // constant (e.g. 10s) that are across the zero barrier will no longer work. We need to
+  // round consistently towards positive infinity or negative infinity (we chose negative.)
+  // This can't be done with a truncation, it must be done with floor.
+  long long clamped = floor(double(timeAsInt) / resolutionAsInt) * resolutionAsInt;
+  // Cast it back to a double and reduce it to the correct units.
+  double ret = double(clamped) / (1000000.0 / aTimeScale);
+
 #if defined(DEBUG)
     MOZ_LOG(gResistFingerprintingLog, LogLevel::Verbose,
-      ("Given: %.*f, Rounding with %.*f, Intermediate: %.*f, Got: %.*f",
-        DBL_DIG-1, aTime, DBL_DIG-1, reducedResolution, DBL_DIG-1, floor(aTime / reducedResolution), DBL_DIG-1, ret));
+      ("Given: (%.*f, Scaled: %.*f, Converted: %lli), Rounding with (%lli, Originally %.*f), Intermediate: (%lli), Got: (%lli Converted: %.*f)",
+      DBL_DIG-1, aTime, DBL_DIG-1, timeScaled, timeAsInt, resolutionAsInt, DBL_DIG-1, aResolutionUSec,
+      (long long)floor(double(timeAsInt) / resolutionAsInt), clamped, DBL_DIG-1, ret));
 #endif
-  }
+
    return ret;
  }
 
 /* static */
 double
 nsRFPService::ReduceTimePrecisionAsUSecs(double aTime, TimerPrecisionType aType /* = TimerPrecisionType::All */)
 {
-  return nsRFPService::ReduceTimePrecisionImpl(aTime, TimerResolution(), 1, aType);
+  return nsRFPService::ReduceTimePrecisionImpl(aTime, MicroSeconds, TimerResolution(), aType);
 }
 
 /* static */
 double
 nsRFPService::ReduceTimePrecisionAsMSecs(double aTime, TimerPrecisionType aType /* = TimerPrecisionType::All */)
 {
-  return nsRFPService::ReduceTimePrecisionImpl(aTime, TimerResolution(), 1000, aType);
+  return nsRFPService::ReduceTimePrecisionImpl(aTime, MilliSeconds, TimerResolution(), aType);
 }
 
 /* static */
 double
 nsRFPService::ReduceTimePrecisionAsSecs(double aTime, TimerPrecisionType aType /* = TimerPrecisionType::All */)
 {
-  return nsRFPService::ReduceTimePrecisionImpl(aTime, TimerResolution(), 1000000, aType);
+  return nsRFPService::ReduceTimePrecisionImpl(aTime, Seconds, TimerResolution(), aType);
 }
 
 /* static */
 uint32_t
 nsRFPService::CalculateTargetVideoResolution(uint32_t aVideoQuality)
 {
   return aVideoQuality * NSToIntCeil(aVideoQuality * 16 / 9.0);
 }
--- a/toolkit/components/resistfingerprinting/nsRFPService.h
+++ b/toolkit/components/resistfingerprinting/nsRFPService.h
@@ -158,31 +158,37 @@ class nsRFPService final : public nsIObs
 public:
   NS_DECL_ISUPPORTS
   NS_DECL_NSIOBSERVER
 
   static nsRFPService* GetOrCreate();
   static bool IsResistFingerprintingEnabled();
   static bool IsTimerPrecisionReductionEnabled(TimerPrecisionType aType);
 
+  enum TimeScale {
+    Seconds      = 1,
+    MilliSeconds = 1000,
+    MicroSeconds = 1000000
+  };
+
   // The following Reduce methods can be called off main thread.
   static double ReduceTimePrecisionAsUSecs(
     double aTime,
     TimerPrecisionType aType = TimerPrecisionType::All);
   static double ReduceTimePrecisionAsMSecs(
     double aTime,
     TimerPrecisionType aType = TimerPrecisionType::All);
   static double ReduceTimePrecisionAsSecs(
     double aTime,
     TimerPrecisionType aType = TimerPrecisionType::All);
   // Public only for testing purposes
   static double ReduceTimePrecisionImpl(
     double aTime,
+    TimeScale aTimeScale,
     double aResolutionUSec,
-    double aTimeScaleCorrection,
     TimerPrecisionType aType);
 
 
   // This method calculates the video resolution (i.e. height x width) based
   // on the video quality (480p, 720p, etc).
   static uint32_t CalculateTargetVideoResolution(uint32_t aVideoQuality);
 
   // Methods for getting spoofed media statistics and the return value will
--- a/toolkit/components/resistfingerprinting/tests/test_reduceprecision.cpp
+++ b/toolkit/components/resistfingerprinting/tests/test_reduceprecision.cpp
@@ -37,116 +37,138 @@ using namespace mozilla;
 
    Look at the last two values:
       Got: 2064.83383999999978
       Got: 2064.83381999999983
 
    They're supposed to be equal. They're not. But they both round to 2064.83.
 */
 
-void process(double clock, double precision, double precisionUnits) {
-  double reduced1 = nsRFPService::ReduceTimePrecisionImpl(clock, precision, precisionUnits, TimerPrecisionType::All);
-  double reduced2 = nsRFPService::ReduceTimePrecisionImpl(reduced1, precision, precisionUnits, TimerPrecisionType::All);
+void process(double clock, nsRFPService::TimeScale clockUnits, double precision) {
+  double reduced1 = nsRFPService::ReduceTimePrecisionImpl(clock, clockUnits, precision, TimerPrecisionType::All);
+  double reduced2 = nsRFPService::ReduceTimePrecisionImpl(reduced1, clockUnits, precision, TimerPrecisionType::All);
   ASSERT_EQ(reduced1, reduced2);
 }
 
 TEST(ResistFingerprinting, ReducePrecision_Assumptions) {
   ASSERT_EQ(FLT_RADIX, 2);
   ASSERT_EQ(DBL_MANT_DIG, 53);
 }
 
 TEST(ResistFingerprinting, ReducePrecision_Reciprocal) {
   // This one has a rounding error in the Reciprocal case:
-  process(2064.8338460, 20, 1000000);
+  process(2064.8338460, nsRFPService::TimeScale::MicroSeconds, 20);
   // These are just big values
-  process(1516305819, 20, 1000000);
-  process(69053.12, 20, 1000000);
+  process(1516305819, nsRFPService::TimeScale::MicroSeconds, 20);
+  process(69053.12, nsRFPService::TimeScale::MicroSeconds, 20);
 }
 
 TEST(ResistFingerprinting, ReducePrecision_KnownGood) {
-  process(2064.8338460, 20, 1000);
-  process(69027.62, 20, 1000);
-  process(69053.12, 20, 1000);
+  process(2064.8338460, nsRFPService::TimeScale::MilliSeconds, 20);
+  process(69027.62, nsRFPService::TimeScale::MilliSeconds, 20);
+  process(69053.12, nsRFPService::TimeScale::MilliSeconds, 20);
 }
 
 TEST(ResistFingerprinting, ReducePrecision_KnownBad) {
-  process(1054.842405, 20, 1000);
-  process(273.53038600000002, 20, 1000);
-  process(628.66686500000003, 20, 1000);
-  process(521.28919100000007, 20, 1000);
+  process(1054.842405, nsRFPService::TimeScale::MilliSeconds, 20);
+  process(273.53038600000002, nsRFPService::TimeScale::MilliSeconds, 20);
+  process(628.66686500000003, nsRFPService::TimeScale::MilliSeconds, 20);
+  process(521.28919100000007, nsRFPService::TimeScale::MilliSeconds, 20);
 }
 
 TEST(ResistFingerprinting, ReducePrecision_Edge) {
-  process(2611.14, 20, 1000);
-  process(2611.16, 20, 1000);
-  process(2612.16, 20, 1000);
-  process(2601.64, 20, 1000);
-  process(2595.16, 20, 1000);
-  process(2578.66, 20, 1000);
+  process(2611.14, nsRFPService::TimeScale::MilliSeconds, 20);
+  process(2611.16, nsRFPService::TimeScale::MilliSeconds, 20);
+  process(2612.16, nsRFPService::TimeScale::MilliSeconds, 20);
+  process(2601.64, nsRFPService::TimeScale::MilliSeconds, 20);
+  process(2595.16, nsRFPService::TimeScale::MilliSeconds, 20);
+  process(2578.66, nsRFPService::TimeScale::MilliSeconds, 20);
 }
 
 TEST(ResistFingerprinting, ReducePrecision_Expectations) {
   double result;
-  result = nsRFPService::ReduceTimePrecisionImpl(2611.14, 20, 1000, TimerPrecisionType::All);
+  result = nsRFPService::ReduceTimePrecisionImpl(2611.14, nsRFPService::TimeScale::MilliSeconds, 20, TimerPrecisionType::All);
+  ASSERT_EQ(result, 2611.14);
+  result = nsRFPService::ReduceTimePrecisionImpl(2611.145, nsRFPService::TimeScale::MilliSeconds, 20, TimerPrecisionType::All);
   ASSERT_EQ(result, 2611.14);
-  result = nsRFPService::ReduceTimePrecisionImpl(2611.145, 20, 1000, TimerPrecisionType::All);
+  result = nsRFPService::ReduceTimePrecisionImpl(2611.141, nsRFPService::TimeScale::MilliSeconds, 20, TimerPrecisionType::All);
   ASSERT_EQ(result, 2611.14);
-  result = nsRFPService::ReduceTimePrecisionImpl(2611.141, 20, 1000, TimerPrecisionType::All);
+  result = nsRFPService::ReduceTimePrecisionImpl(2611.15999, nsRFPService::TimeScale::MilliSeconds, 20, TimerPrecisionType::All);
+  ASSERT_EQ(result, 2611.14);
+  result = nsRFPService::ReduceTimePrecisionImpl(2611.15, nsRFPService::TimeScale::MilliSeconds, 20, TimerPrecisionType::All);
   ASSERT_EQ(result, 2611.14);
-  result = nsRFPService::ReduceTimePrecisionImpl(2611.15999, 20, 1000, TimerPrecisionType::All);
-  ASSERT_EQ(result, 2611.14);
-  result = nsRFPService::ReduceTimePrecisionImpl(2611.15, 20, 1000, TimerPrecisionType::All);
-  ASSERT_EQ(result, 2611.14);
-  result = nsRFPService::ReduceTimePrecisionImpl(2611.13, 20, 1000, TimerPrecisionType::All);
+  result = nsRFPService::ReduceTimePrecisionImpl(2611.13, nsRFPService::TimeScale::MilliSeconds, 20, TimerPrecisionType::All);
   ASSERT_EQ(result, 2611.12);
 }
 
+TEST(ResistFingerprinting, ReducePrecision_ExpectedLossOfPrecision) {
+  double result;
+  // We lose integer precision at 9007199254740992 - let's confirm that.
+  result = nsRFPService::ReduceTimePrecisionImpl(9007199254740992.0, nsRFPService::TimeScale::MicroSeconds, 5, TimerPrecisionType::All);
+  ASSERT_EQ(result, 9007199254740990.0);
+  // 9007199254740995 is approximated to 9007199254740996
+  result = nsRFPService::ReduceTimePrecisionImpl(9007199254740995.0, nsRFPService::TimeScale::MicroSeconds, 5, TimerPrecisionType::All);
+  ASSERT_EQ(result, 9007199254740996);
+  // 9007199254740999 is approximated as 9007199254741000
+  result = nsRFPService::ReduceTimePrecisionImpl(9007199254740999.0, nsRFPService::TimeScale::MicroSeconds, 5, TimerPrecisionType::All);
+  ASSERT_EQ(result, 9007199254741000.0);
+  // 9007199254743568 can be represented exactly, but will be clamped to 9007199254743564
+  result = nsRFPService::ReduceTimePrecisionImpl(9007199254743568.0, nsRFPService::TimeScale::MicroSeconds, 5, TimerPrecisionType::All);
+  ASSERT_EQ(result, 9007199254743564.0);
+}
+
 // Use an ugly but simple hack to turn an integer-based rand()
-// function to a double-based one
+// function to a double-based one.
 #define RAND_DOUBLE (rand() * (rand() / (double)rand()))
 
+// If you're doing logging, you really don't want to run this test.
+#define RUN_AGGRESSIVE false
+
 TEST(ResistFingerprinting, ReducePrecision_Aggressive) {
+  if(!RUN_AGGRESSIVE) {
+    return;
+  }
+
   for (int i=0; i<10000; i++) {
     // Test three different time magnitudes, with decimals.
     // Note that we need separate variables for the different units, as scaling
-    // them after calculating them will erase effects of approximation
-    // A magnitude in the seconds since epoch range
-    double time1_s = fmod(RAND_DOUBLE, 1516305819);
-    double time1_ms = fmod(RAND_DOUBLE, 1516305819000);
-    double time1_us = fmod(RAND_DOUBLE, 1516305819000000);
-    // A magnitude in the 'couple of minutes worth of milliseconds' range
-    double time2_s = fmod(RAND_DOUBLE, (60 * 60 * 5));
-    double time2_ms = fmod(RAND_DOUBLE, (1000 * 60 * 60 * 5));
-    double time2_us = fmod(RAND_DOUBLE, (1000000 * 60 * 60 * 5));
+    // them after calculating them will erase effects of approximation.
+    // A magnitude in the seconds since epoch range.
+    double time1_s = fmod(RAND_DOUBLE, 1516305819.0);
+    double time1_ms = fmod(RAND_DOUBLE, 1516305819000.0);
+    double time1_us = fmod(RAND_DOUBLE, 1516305819000000.0);
+    // A magnitude in the 'couple of minutes worth of milliseconds' range.
+    double time2_s = fmod(RAND_DOUBLE, (60.0 * 60 * 5));
+    double time2_ms = fmod(RAND_DOUBLE, (1000.0 * 60 * 60 * 5));
+    double time2_us = fmod(RAND_DOUBLE, (1000000.0 * 60 * 60 * 5));
     // A magnitude in the small range
     double time3_s = fmod(RAND_DOUBLE, 10);
     double time3_ms = fmod(RAND_DOUBLE, 10000);
     double time3_us = fmod(RAND_DOUBLE, 10000000);
 
-    // Test two precision magnitudes, no decimals
-    // A magnitude in the high milliseconds
+    // Test two precision magnitudes, no decimals.
+    // A magnitude in the high milliseconds.
     double precision1 = rand() % 250000;
-    // a magnitude in the low microseconds
+    // a magnitude in the low microseconds.
     double precision2 = rand() % 200;
 
-    //printf("%.*f, %.*f, %.*f\n", DBL_DIG-1, time1, DBL_DIG-1, time2, DBL_DIG-1, time3);
-    process(time1_s, precision1, 1000000);
-    process(time1_s, precision2, 1000000);
-    process(time2_s, precision1, 1000000);
-    process(time2_s, precision2, 1000000);
-    process(time3_s, precision1, 1000000);
-    process(time3_s, precision2, 1000000);
+    process(time1_s, nsRFPService::TimeScale::Seconds, precision1);
+    process(time1_s, nsRFPService::TimeScale::Seconds, precision2);
+    process(time2_s, nsRFPService::TimeScale::Seconds, precision1);
+    process(time2_s, nsRFPService::TimeScale::Seconds, precision2);
+    process(time3_s, nsRFPService::TimeScale::Seconds, precision1);
+    process(time3_s, nsRFPService::TimeScale::Seconds, precision2);
 
-    process(time1_ms, precision1, 1000);
-    process(time1_ms, precision2, 1000);
-    process(time2_ms, precision1, 1000);
-    process(time2_ms, precision2, 1000);
-    process(time3_ms, precision1, 1000);
-    process(time3_ms, precision2, 1000);
+    process(time1_ms, nsRFPService::TimeScale::MilliSeconds, precision1);
+    process(time1_ms, nsRFPService::TimeScale::MilliSeconds, precision2);
+    process(time2_ms, nsRFPService::TimeScale::MilliSeconds, precision1);
+    process(time2_ms, nsRFPService::TimeScale::MilliSeconds, precision2);
+    process(time3_ms, nsRFPService::TimeScale::MilliSeconds, precision1);
+    process(time3_ms, nsRFPService::TimeScale::MilliSeconds, precision2);
 
-    process(time1_us, precision1, 1);
-    process(time1_us, precision2, 1);
-    process(time2_us, precision1, 1);
-    process(time2_us, precision2, 1);
-    process(time3_us, precision1, 1);
-    process(time3_us, precision2, 1);
+    process(time1_us, nsRFPService::TimeScale::MicroSeconds, precision1);
+    process(time1_us, nsRFPService::TimeScale::MicroSeconds, precision2);
+    process(time2_us, nsRFPService::TimeScale::MicroSeconds, precision1);
+    process(time2_us, nsRFPService::TimeScale::MicroSeconds, precision2);
+    process(time3_us, nsRFPService::TimeScale::MicroSeconds, precision1);
+    process(time3_us, nsRFPService::TimeScale::MicroSeconds, precision2);
   }
 }