Bug 1430841 Refactor ReduceTimePrecision and add (failing) gtests r=bkelly
authorTom Ritter <tom@mozilla.com>
Mon, 12 Feb 2018 13:11:18 -0600
changeset 759121 369ecd04363611e34a3ed32621d6795141d65764
parent 759120 77b6661a3618890dc88ca07a1392357e945f3190
child 759122 9c10a641e698c732402d5762543a904ac8ee81a0
push id100272
push userrwood@mozilla.com
push dateFri, 23 Feb 2018 18:27:33 +0000
reviewersbkelly
bugs1430841
milestone60.0a1
Bug 1430841 Refactor ReduceTimePrecision and add (failing) gtests r=bkelly This commit adds a gtest calling ReduceTimePrecision that illustrates several failing test cases from float fuzziness, as well as generating a ton of test cases at random that also fail. MozReview-Commit-ID: Epia5gm5Ahb
toolkit/components/resistfingerprinting/moz.build
toolkit/components/resistfingerprinting/nsRFPService.cpp
toolkit/components/resistfingerprinting/nsRFPService.h
toolkit/components/resistfingerprinting/tests/moz.build
toolkit/components/resistfingerprinting/tests/test_reduceprecision.cpp
--- a/toolkit/components/resistfingerprinting/moz.build
+++ b/toolkit/components/resistfingerprinting/moz.build
@@ -1,14 +1,16 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
+TEST_DIRS += ['tests']
+
 UNIFIED_SOURCES += [
     'nsRFPService.cpp',
 ]
 
 FINAL_LIBRARY = 'xul'
 
 EXPORTS += [
     'nsRFPService.h',
--- a/toolkit/components/resistfingerprinting/nsRFPService.cpp
+++ b/toolkit/components/resistfingerprinting/nsRFPService.cpp
@@ -123,89 +123,88 @@ nsRFPService::IsTimerPrecisionReductionE
   if (aType == TimerPrecisionType::RFPOnly) {
     return IsResistFingerprintingEnabled();
   }
 
   return (sPrivacyTimerPrecisionReduction || IsResistFingerprintingEnabled()) &&
          TimerResolution() != 0;
 }
 
+/*
+  DOC TODO
+*/
 /* static */
 double
-nsRFPService::ReduceTimePrecisionAsMSecs(double aTime, TimerPrecisionType aType /* = TimerPrecisionType::All */)
-{
-  if (!IsTimerPrecisionReductionEnabled(aType)) {
+nsRFPService::ReduceTimePrecisionImpl(
+  double aTime,
+  double aResolutionUS,
+  double aResolutionScaleCorrection,
+  TimerPrecisionType aType)
+ {
+   if (!IsTimerPrecisionReductionEnabled(aType)) {
+     return aTime;
+   }
+  if (aResolutionScaleCorrection != 1 &&
+      aResolutionScaleCorrection != 1000 &&
+      aResolutionScaleCorrection != 1000000) {
+    MOZ_ASSERT(false, "Only scale corrections of 1, 1000, and 1000000 are supported.");
     return aTime;
   }
-  const double resolutionMSec = TimerResolution() / 1000.0;
-  double ret = floor(aTime / resolutionMSec) * resolutionMSec;
+
+  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, Rounding with %.*f, Intermediate: %.*f, Got: %.*f",
-      DBL_DIG-1, aTime, DBL_DIG-1, resolutionMSec, DBL_DIG-1, floor(aTime / resolutionMSec), DBL_DIG-1, ret));
+   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
-  return ret;
-}
+  } else {
+    ret = floor(aTime / reducedResolution) * reducedResolution;
+#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));
+#endif
+  }
+   return ret;
+ }
 
 /* static */
 double
 nsRFPService::ReduceTimePrecisionAsUSecs(double aTime, TimerPrecisionType aType /* = TimerPrecisionType::All */)
 {
-  if (!IsTimerPrecisionReductionEnabled(aType)) {
-    return aTime;
-  }
-  double resolutionUSec = TimerResolution();
-  double ret = floor(aTime / resolutionUSec) * resolutionUSec;
-#if defined(DEBUG)
-  double tmp_sResolutionUSec = resolutionUSec;
-  MOZ_LOG(gResistFingerprintingLog, LogLevel::Verbose,
-    ("Given: %.*f, Rounding with %.*f, Intermediate: %.*f, Got: %.*f",
-      DBL_DIG-1, aTime, DBL_DIG-1, tmp_sResolutionUSec, DBL_DIG-1, floor(aTime / tmp_sResolutionUSec), DBL_DIG-1, ret));
-#endif
-  return ret;
+  return nsRFPService::ReduceTimePrecisionImpl(aTime, TimerResolution(), 1, aType);
+}
+
+/* static */
+double
+nsRFPService::ReduceTimePrecisionAsMSecs(double aTime, TimerPrecisionType aType /* = TimerPrecisionType::All */)
+{
+  return nsRFPService::ReduceTimePrecisionImpl(aTime, TimerResolution(), 1000, aType);
+}
+
+/* static */
+double
+nsRFPService::ReduceTimePrecisionAsSecs(double aTime, TimerPrecisionType aType /* = TimerPrecisionType::All */)
+{
+  return nsRFPService::ReduceTimePrecisionImpl(aTime, TimerResolution(), 1000000, aType);
 }
 
 /* static */
 uint32_t
 nsRFPService::CalculateTargetVideoResolution(uint32_t aVideoQuality)
 {
   return aVideoQuality * NSToIntCeil(aVideoQuality * 16 / 9.0);
 }
 
 /* static */
-double
-nsRFPService::ReduceTimePrecisionAsSecs(double aTime, TimerPrecisionType aType /* = TimerPrecisionType::All */)
-{
-  if (!IsTimerPrecisionReductionEnabled(aType)) {
-    return aTime;
-  }
-  double resolutionUSec = TimerResolution();
-  if (TimerResolution() < 1000000) {
-    // The resolution is smaller than one sec.  Use the reciprocal to avoid
-    // floating point error.
-    const double resolutionSecReciprocal = 1000000.0 / resolutionUSec;
-    double ret = floor(aTime * resolutionSecReciprocal) / resolutionSecReciprocal;
-#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, resolutionSecReciprocal, DBL_DIG-1, floor(aTime * resolutionSecReciprocal), DBL_DIG-1, ret));
-#endif
-    return ret;
-  }
-  const double resolutionSec = resolutionUSec / 1000000.0;
-  double ret = floor(aTime / resolutionSec) * resolutionSec;
-#if defined(DEBUG)
-  MOZ_LOG(gResistFingerprintingLog, LogLevel::Verbose,
-    ("Given: %.*f, Rounding with %.*f, Intermediate: %.*f, Got: %.*f",
-      DBL_DIG-1, aTime, DBL_DIG-1, resolutionSec, DBL_DIG-1, floor(aTime / resolutionSec), DBL_DIG-1, ret));
-#endif
-  return ret;
-}
-
-/* static */
 uint32_t
 nsRFPService::GetSpoofedTotalFrames(double aTime)
 {
   double time = ReduceTimePrecisionAsSecs(aTime);
 
   return NSToIntFloor(time * sVideoFramesPerSec);
 }
 
--- a/toolkit/components/resistfingerprinting/nsRFPService.h
+++ b/toolkit/components/resistfingerprinting/nsRFPService.h
@@ -155,25 +155,32 @@ public:
   NS_DECL_ISUPPORTS
   NS_DECL_NSIOBSERVER
 
   static nsRFPService* GetOrCreate();
   static bool IsResistFingerprintingEnabled();
   static bool IsTimerPrecisionReductionEnabled(TimerPrecisionType aType);
 
   // The following Reduce methods can be called off main thread.
-  static double ReduceTimePrecisionAsMSecs(
+  static double ReduceTimePrecisionAsUSecs(
     double aTime,
     TimerPrecisionType aType = TimerPrecisionType::All);
-  static double ReduceTimePrecisionAsUSecs(
+  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,
+    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
   // depend on the video resolution.
   static uint32_t GetSpoofedTotalFrames(double aTime);
new file mode 100644
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/moz.build
@@ -0,0 +1,5 @@
+UNIFIED_SOURCES += [
+    'test_reduceprecision.cpp',
+]
+
+FINAL_LIBRARY = "xul-gtest"
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/toolkit/components/resistfingerprinting/tests/test_reduceprecision.cpp
@@ -0,0 +1,152 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
+ * 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 <math.h>
+
+#include "gtest/gtest.h"
+#include "nsRFPService.h"
+
+using namespace mozilla;
+
+/*
+   Hello! Are you looking at this file because you got an error you don't understand?
+   Perhaps something that looks like the following?
+
+    toolkit/components/resistfingerprinting/tests/test_reduceprecision.cpp:15: Failure
+      Expected: reduced1
+        Which is: 2064.83
+      To be equal to: reduced2
+        Which is: 2064.83
+
+   "Gosh," you might say, "They sure look equal to me. What the heck is going on here?"
+
+   The answer lies beyond what you can see, in that which you cannot see. One must
+   journey into the depths, the hidden, that which the world fights its hardest to
+   conceal from us.
+
+   Specially: you need to look at more decimal places. Run the test with:
+       MOZ_LOG="nsResistFingerprinting:5"
+
+   And look for two successive lines similar to the below (the format will certainly
+   be different by the time you read this comment):
+      V/nsResistFingerprinting Given: 2064.83384599999999, Reciprocal Rounding with 50000.00000000000000, Intermediate: 103241692.00000000000000, Got: 2064.83383999999978
+      V/nsResistFingerprinting Given: 2064.83383999999978, Reciprocal Rounding with 50000.00000000000000, Intermediate: 103241691.00000000000000, Got: 2064.83381999999983
+
+   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);
+  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);
+  // These are just big values
+  process(1516305819, 20, 1000000);
+  process(69053.12, 20, 1000000);
+}
+
+TEST(ResistFingerprinting, ReducePrecision_KnownGood) {
+  process(2064.8338460, 20, 1000);
+  process(69027.62, 20, 1000);
+  process(69053.12, 20, 1000);
+}
+
+TEST(ResistFingerprinting, ReducePrecision_KnownBad) {
+  process(1054.842405, 20, 1000);
+  process(273.53038600000002, 20, 1000);
+  process(628.66686500000003, 20, 1000);
+  process(521.28919100000007, 20, 1000);
+}
+
+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);
+}
+
+TEST(ResistFingerprinting, ReducePrecision_Expectations) {
+  double result;
+  result = nsRFPService::ReduceTimePrecisionImpl(2611.14, 20, 1000, TimerPrecisionType::All);
+  ASSERT_EQ(result, 2611.14);
+  result = nsRFPService::ReduceTimePrecisionImpl(2611.145, 20, 1000, TimerPrecisionType::All);
+  ASSERT_EQ(result, 2611.14);
+  result = nsRFPService::ReduceTimePrecisionImpl(2611.141, 20, 1000, 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);
+  ASSERT_EQ(result, 2611.12);
+}
+
+// Use an ugly but simple hack to turn an integer-based rand()
+// function to a double-based one
+#define RAND_DOUBLE (rand() * (rand() / (double)rand()))
+
+TEST(ResistFingerprinting, ReducePrecision_Aggressive) {
+  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));
+    // 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
+    double precision1 = rand() % 250000;
+    // 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_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_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);
+  }
+}