Bug 1453591 - Perform persistence I/O off the main thread. r? draft
authorAlessio Placitelli <alessio.placitelli@gmail.com>
Sat, 21 Apr 2018 01:01:50 -0700
changeset 786448 2ac5796ac6566ad219cfecac4a3a0cd81ee8a90b
parent 786447 077f4fb41ed7af4651ff4d27794b230b4ac25f70
push id107466
push userbmo:alessio.placitelli@gmail.com
push dateMon, 23 Apr 2018 09:25:23 +0000
bugs1453591
milestone61.0a1
Bug 1453591 - Perform persistence I/O off the main thread. r? MozReview-Commit-ID: 7lvLGGnaeOJ
gradle.properties
toolkit/components/telemetry/geckoview/GeckoViewTelemetryPersistence.cpp
toolkit/components/telemetry/geckoview/GeckoViewTelemetryPersistence.h
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,3 +1,4 @@
 org.gradle.parallel=true
 org.gradle.daemon=true
 org.gradle.jvmargs=-Xmx2560M
+
--- a/toolkit/components/telemetry/geckoview/GeckoViewTelemetryPersistence.cpp
+++ b/toolkit/components/telemetry/geckoview/GeckoViewTelemetryPersistence.cpp
@@ -9,97 +9,186 @@
 #include "FennecJNIWrappers.h"
 #include "json/json.h"
 #include "nsIFile.h"
 #include "nsITimer.h"
 #include "nsDirectoryServiceDefs.h"
 #include "prenv.h"
 #include "TelemetryScalar.h"
 #include "mozilla/FStream.h"
+#include "mozilla/ScopeExit.h"
 #include "mozilla/SystemGroup.h"
 #include "nsXULAppAPI.h"
 
 // #define ANDROID_LOG(...)
 #define ANDROID_LOG(...) printf_stderr("\n**** TELEMETRY: " __VA_ARGS__)
 
 namespace TelemetryGeckoViewPersistence = mozilla::TelemetryGeckoViewPersistence;
 
 // The Gecko runtime can be killed at anytime. Moreover, we can
 // have very short lived sessions. The persistence timeout governs
 // how frequently measurements are saved to disk.
 const uint32_t kPersistenceTimeoutMs = 60 * 1000; // 60s
 
 // The timer used for persisting measurements data.
 nsITimer* gPersistenceTimer = nullptr;
+// The worker thread to perform persistence.
+nsCOMPtr<nsIThread> gPersistenceThread = nullptr;
 
 namespace {
 
+void Persist();
+
 /**
- * Get the path to the Android cache storage. This is the suggested
- * location for temporary small files (< 1MB). See
- * https://developer.android.com/guide/topics/data/data-storage.html#filesInternal
- *
- * If it fails to get the cache directory, it will attempt to get
- * an OS temporary dir through |NS_GetSpecialDirectory|.
+ * Get the path to the persistence file in the Android Data dir.
  *
  * @param {nsCString} aOutDir - the variable holding the path.
  * @returns NS_OK if a cache path was found, a failure value otherwise.
  */
 nsresult
-GetAndroidCacheDir(nsCString& aOutDir)
+GetPersistenceFilePath(nsCString& aOutDir)
 {
   // This relies on the Java environment to set the location of the
-  // cache directory. If that happens, the following variables are set.
-  // However, things can go wrong. In that case, just fall back to
-  // |NS_OS_TEMP_DIR|.
-  const char *extract = PR_GetEnv("MOZ_LINKER_EXTRACT");
-  if (extract && !strncmp(extract, "1", 2 /* Including '\0' */)) {
-    const char *cachePath = PR_GetEnv("MOZ_LINKER_CACHE");
-    if (cachePath && *cachePath) {
-      ANDROID_LOG("GetAndroidCacheDir - Cache dir %s.\n", cachePath);
-      aOutDir.Assign(cachePath);
-      return NS_OK;
-    }
+  // cache directory. If that happens, the following variable is set.
+  // This should always be the case.
+  const char *dataDir = PR_GetEnv("MOZ_ANDROID_DATA_DIR");
+  if (!dataDir || !*dataDir) {
+    ANDROID_LOG("GetPersistenceFilePath - Cannot find the data directory in the environment.\n");
+    return NS_ERROR_FAILURE;
   }
 
-  // If we cannot find the cache directory, try something else.
-  ANDROID_LOG("GetAndroidCacheDir - Cannot find the cache directory.\n");
-
-  nsCOMPtr<nsIFile> osTempDir;
-  nsresult rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(osTempDir));
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  aOutDir = osTempDir->HumanReadablePath();
-
-  ANDROID_LOG("GetAndroidCacheDir - Falling back to %s.\n", aOutDir.get());
-
+  aOutDir.Assign(dataDir);
+  aOutDir.Append("/gv_measurements.json");
+  ANDROID_LOG("GetPersistenceFilePath - Persistence file path is %s/gv_measurements.json.\n", dataDir);
   return NS_OK;
 }
 
 /**
  * TODO
  */
 void
 ArmPersistenceTimer()
 {
   MOZ_ASSERT(NS_IsMainThread());
 
-  gPersistenceTimer =
-    NS_NewTimer(mozilla::SystemGroup::EventTargetFor(mozilla::TaskCategory::Other)).take();
+  // We won't have a persistence timer the first time this runs, so take
+  // care of that.
   if (!gPersistenceTimer) {
-    ANDROID_LOG("ArmPersistenceTimer - Timer creation failed.\n");
+    gPersistenceTimer =
+      NS_NewTimer(mozilla::SystemGroup::EventTargetFor(mozilla::TaskCategory::Other)).take();
+    if (!gPersistenceTimer) {
+      ANDROID_LOG("ArmPersistenceTimer - Timer creation failed.\n");
+      return;
+    }
+  }
+
+  // Define the callback for the persistence timer: it will dispatch the persistence
+  // task off the main thread. Once finished, it will trigger the timer again.
+  nsTimerCallbackFunc timerCallback = [](nsITimer* aTimer, void* aClosure) {
+    gPersistenceThread->Dispatch(NS_NewRunnableFunction("Persist",
+      []() -> void { ::Persist(); }));
+  };
+
+  // Schedule the timer to automatically run and reschedule
+  // every |kPersistenceTimeoutMs|.
+  gPersistenceTimer->InitWithNamedFuncCallback(timerCallback,
+                                               nullptr, kPersistenceTimeoutMs,
+                                               nsITimer::TYPE_ONE_SHOT_LOW_PRIORITY,
+                                               "TelemetryGeckoViewPersistence::Persist");
+}
+
+void
+Persist()
+{
+  MOZ_ASSERT(XRE_IsParentProcess(), "We must only persist from the parent process.");
+  MOZ_ASSERT(!NS_IsMainThread(), "This function must be called off the main thread.");
+  ANDROID_LOG("Persist\n");
+
+  // If the function completes or fails, make sure to spin up the persistence timer again.
+  auto scopedArmTimer = mozilla::MakeScopeExit([&] {
+    NS_DispatchToMainThread(
+      NS_NewRunnableFunction("ArmPersistenceTimer", []() -> void { ArmPersistenceTimer(); }));
+  });
+
+  // TODO: Remove me, just testing.
+  TelemetryScalar::Add(mozilla::Telemetry::ScalarID::TELEMETRY_TEST_UNSIGNED_INT_KIND, 1);
+
+  Json::Value root;
+  Json::Value scalars;
+  if (NS_FAILED(TelemetryScalar::PersistMeasurementsToFile(scalars))) {
+    ANDROID_LOG("Persist - Failed to persist scalars.\n");
+  }
+  root["scalars"] = scalars;
+
+  // Get the file path.
+  nsAutoCString persistenceFilePath;
+  if (NS_FAILED(GetPersistenceFilePath(persistenceFilePath))) {
+    ANDROID_LOG("Persist - Failed to get the path to the persistence file\n");
     return;
   }
 
-  // Schedule the timer to automatically run and reschedule
-  // every |kPersistenceTimeoutMs|.
-  gPersistenceTimer->InitWithNamedFuncCallback(TelemetryGeckoViewPersistence::Persist,
-                                               nullptr, kPersistenceTimeoutMs,
-                                               nsITimer::TYPE_REPEATING_SLACK,
-                                               "TelemetryGeckoViewPersistence::Persist");
+  // Write the persistence file to the disk.
+  mozilla::OFStream f(persistenceFilePath.get(), std::ios::out | std::ios::trunc);
+  if (f.is_open()) {
+    Json::FastWriter writer;
+    f << writer.write(root);
+    f.close();
+    ANDROID_LOG("Persist - dump %s\n", writer.write(root).c_str());
+  }
+}
+
+/**
+ * This function loads the persisted metrics from a JSON file
+ * and adds them to the related storage. After it completes,
+ * it spins up the persistence timer.
+ *
+ * Please note that this function is meant to be run off the
+ * main-thread.
+ */
+void
+LoadPersistedData()
+{
+  MOZ_ASSERT(XRE_IsParentProcess(), "We must only persist from the parent process.");
+  MOZ_ASSERT(!NS_IsMainThread(), "We must perform I/O off the main thread.");
+  ANDROID_LOG("LoadPersistedData\n");
+
+  // If the function completes or fails, make sure to spin up the persistence timer.
+  auto scopedArmTimer = mozilla::MakeScopeExit([&] {
+    NS_DispatchToMainThread(
+      NS_NewRunnableFunction("ArmPersistenceTimer", []() -> void { ArmPersistenceTimer(); }));
+  });
+
+  // Get the file path.
+  nsAutoCString persistenceFilePath;
+  if (NS_FAILED(GetPersistenceFilePath(persistenceFilePath))) {
+    ANDROID_LOG("LoadPersistedData - Failed to get the path to the persistence file\n");
+    return;
+  }
+
+  // TODO: Check if there's a "bak" persistence file: this means Android killed
+  // us while we were persisting.
+
+  // Load the file and parse the JSON.
+  Json::Value root;
+  Json::Reader reader;
+  mozilla::IFStream f(persistenceFilePath.get());
+  if (!reader.parse(f, root, false)) {
+    ANDROID_LOG("LoadPersistedData - Failed to load cache file at %s\n", persistenceFilePath.get());
+    return;
+  }
+
+  if (root.isMember("scalars") && root["scalars"].isObject()) {
+    if (NS_FAILED(TelemetryScalar::LoadPersistedMeasurements(root["scalars"]))) {
+      ANDROID_LOG("LoadPersistedData - Failed to parse 'scalars'.");
+    }
+  }
+
+  // DEBUG TEST:
+  Json::FastWriter writer;
+  ANDROID_LOG("LoadPersistedData - read %s\n", writer.write(root).c_str());
 }
 
 } // anonymous namespace
 
 void
 TelemetryGeckoViewPersistence::InitPersistence()
 {
   // We only want to add persistence for GeckoView, but both
@@ -113,103 +202,56 @@ TelemetryGeckoViewPersistence::InitPersi
   // order to persist data for all the processes.
   if (!XRE_IsParentProcess()) {
     ANDROID_LOG("InitPersistence - Bailing out on child process.\n");
     return;
   }
 
   ANDROID_LOG("InitPersistence\n");
 
-  // Test:
-  nsAutoCString testPath;
-  if (NS_SUCCEEDED(GetAndroidCacheDir(testPath))) {
-    ANDROID_LOG("Cache found!");
+  // Spawn a new thread for handling GeckoView Telemetry persistence I/O.
+  // We just spawn it once and re-use it later.
+  nsresult rv = NS_NewNamedThread("PersistenceIO", getter_AddRefs(gPersistenceThread));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    ANDROID_LOG("InitPersistence -  Failed to instantiate the worker thread.\n");
+    return;
   }
 
-  TelemetryGeckoViewPersistence::Load();
-
-  ArmPersistenceTimer();
+  // Trigger the loading of the persistence data. After the function
+  // completes it will automatically arm the persistence timer.
+  gPersistenceThread->Dispatch(
+    NS_NewRunnableFunction("LoadPersistedData", []() -> void { LoadPersistedData(); }));
 }
 
 void
 TelemetryGeckoViewPersistence::DeInitPersistence()
 {
-  ANDROID_LOG("DeInitPersistence\n");
-  // TODO: do we need to check if isFennec()?
+  // Bail out if this is Fennec or not the parent process.
+  if (mozilla::jni::IsFennec() || !XRE_IsParentProcess()) {
+    ANDROID_LOG("DeInitPersistence - Bailing out.\n");
+    return;
+  }
 
   // Even though we need to implement this function, it might end up
   // not being called: Android might kill us without notice to reclaim
   // our memory in case some other foreground application needs it.
+  ANDROID_LOG("DeInitPersistence\n");
+
+  if (gPersistenceThread) {
+    gPersistenceThread->Shutdown();
+    gPersistenceThread = nullptr;
+  }
+
   if (gPersistenceTimer) {
     // Always make sure the timer is canceled.
     MOZ_ALWAYS_SUCCEEDS(gPersistenceTimer->Cancel());
-  }
-  gPersistenceTimer = nullptr;
-}
-
-void
-TelemetryGeckoViewPersistence::Persist(nsITimer* aTimer, void* aClosure)
-{
-  MOZ_ASSERT(XRE_IsParentProcess(), "We must only persist from the parent process.");
-  ANDROID_LOG("Persist\n");
-
-  // TODO: Remove me, just testing.
-  TelemetryScalar::Add(Telemetry::ScalarID::TELEMETRY_TEST_UNSIGNED_INT_KIND, 1);
-
-  Json::Value root;
-  Json::Value scalars;
-  if (NS_FAILED(TelemetryScalar::PersistMeasurementsToFile(scalars))) {
-    ANDROID_LOG("Persist - Failed to persist scalars.\n");
-  }
-  root["scalars"] = scalars;
-
-  // Test: get the file path.
-  nsAutoCString testPath;
-  if (NS_SUCCEEDED(GetAndroidCacheDir(testPath))) {
-    ANDROID_LOG("Cache found!");
-  }
-  testPath.Append("/gv_measurements.json");
-  ANDROID_LOG("Persist - writing to %s\n", testPath.get());
-
-  // TODO: this has to happen off the main thread
-  mozilla::OFStream f(testPath.get(), std::ios::out | std::ios::trunc);
-  if (f.is_open()) {
-    Json::FastWriter writer;
-    f << writer.write(root);
-    f.close();
-    ANDROID_LOG("Persist - dump %s\n", writer.write(root).c_str());
+    gPersistenceTimer = nullptr;
   }
 }
 
 void
-TelemetryGeckoViewPersistence::Load()
+TelemetryGeckoViewPersistence::ClearPersistenceData()
 {
-  MOZ_ASSERT(XRE_IsParentProcess(), "We must only persist from the parent process.");
-  ANDROID_LOG("Load\n");
-
-  // Test: get the file path.
-  nsAutoCString testPath;
-  if (NS_SUCCEEDED(GetAndroidCacheDir(testPath))) {
-    ANDROID_LOG("Cache found!");
-  }
-  testPath.Append("/gv_measurements.json");
-  ANDROID_LOG("Load - loading from %s\n", testPath.get());
-
-  // Load the file and parse the JSON.
-  // TODO: this has to happen off the main thread.
-  Json::Value root;
-  Json::Reader reader;
-  mozilla::IFStream f(testPath.get());
-  if (!reader.parse(f, root, false)) {
-    ANDROID_LOG("Load - Failed to load cache file at %s\n", testPath.get());
-    return;
-  }
-
-  if (root.isMember("scalars") && root["scalars"].isObject()) {
-    if (NS_FAILED(TelemetryScalar::LoadPersistedMeasurements(root["scalars"]))) {
-      ANDROID_LOG("Load - Failed to parse 'scalars'.");
-    }
-  }
-
-  // DEBUG TEST:
-  Json::FastWriter writer;
-  ANDROID_LOG("Load - read %s\n", writer.write(root).c_str());
+  // TODO:
+  // - Cancel the timer
+  // - delete the persistence file off the main thread.
+  // - re-arm the timer
 }
--- a/toolkit/components/telemetry/geckoview/GeckoViewTelemetryPersistence.h
+++ b/toolkit/components/telemetry/geckoview/GeckoViewTelemetryPersistence.h
@@ -21,19 +21,14 @@ void InitPersistence();
 /**
  * TODO.
  */
 void DeInitPersistence();
 
 /**
  * TODO.
  */
-void Persist(nsITimer* aTimer, void* aClosure);
-
-/**
- * TODO.
- */
-void Load();
+void ClearPersistenceData();
 
 } // namespace TelemetryGeckoViewPersistence
 } // namespace mozilla
 
 #endif // GeckoViewTelemetryPersistence_h__