/* -*- 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 "TelemetryGeckoViewPersistence.h" #include "jsapi.h" #include "js/JSON.h" #include "mozilla/ErrorNames.h" #include "mozilla/JSONWriter.h" #include "mozilla/Path.h" #include "mozilla/Preferences.h" #include "mozilla/ScopeExit.h" #include "mozilla/StaticPtr.h" #include "mozilla/SystemGroup.h" #include "mozilla/dom/ScriptSettings.h" // for AutoJSAPI #include "mozilla/dom/SimpleGlobalObject.h" #include "nsDirectoryServiceDefs.h" #include "nsIFile.h" #include "nsIInputStream.h" #include "nsIObserverService.h" #include "nsIOutputStream.h" #include "nsISafeOutputStream.h" #include "nsITimer.h" #include "nsLocalFile.h" #include "nsNetUtil.h" #include "nsXULAppAPI.h" #include "prenv.h" #include "prio.h" #include "TelemetryScalar.h" #include "TelemetryHistogram.h" #include "xpcpublic.h" using mozilla::GetErrorName; using mozilla::MakeScopeExit; using mozilla::Preferences; using mozilla::StaticRefPtr; using mozilla::SystemGroup; using mozilla::TaskCategory; using mozilla::dom::AutoJSAPI; using mozilla::dom::SimpleGlobalObject; using PathChar = mozilla::filesystem::Path::value_type; using PathCharPtr = const PathChar*; // Enable logging by default on Debug builds. #ifdef DEBUG // If we're building for Android, use the provided logging facility. #ifdef MOZ_WIDGET_ANDROID #include #define ANDROID_LOG(fmt, ...) \ __android_log_print(ANDROID_LOG_DEBUG, "Telemetry", fmt, ##__VA_ARGS__) #else // If we're building for other platforms (e.g. for running test coverage), try // to print something anyway. #define ANDROID_LOG(...) printf_stderr("\n**** TELEMETRY: " __VA_ARGS__) #endif // MOZ_WIDGET_ANDROID #else // No-op on Release builds. #define ANDROID_LOG(...) #endif // DEBUG // 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 kDefaultPersistenceTimeoutMs = 60 * 1000; // 60s // The name of the persistence file used for saving the // measurements. const char16_t kPersistenceFileName[] = u"gv_measurements.json"; // This topic is notified and propagated up to the application to // make sure it knows that data loading has complete and that snapshotting // can now be performed. const char kLoadCompleteTopic[] = "internal-telemetry-geckoview-load-complete"; // The timer used for persisting measurements data. nsITimer* gPersistenceTimer; // The worker thread to perform persistence. StaticRefPtr gPersistenceThread; namespace { void PersistenceThreadPersist(); /** + * The helper class used by mozilla::JSONWriter to + * serialize the JSON structure to a file. + */ class StreamingJSONWriter : public mozilla::JSONWriteFunc { public: nsresult Open(nsCOMPtr aOutFile) { MOZ_ASSERT(!mStream, "Open must not be called twice"); nsresult rv = NS_NewSafeLocalFileOutputStream(getter_AddRefs(mStream), aOutFile); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } nsresult Close() { MOZ_ASSERT(mStream, "Close must be called on an already opened stream"); // We don't need to care too much about checking if count matches // the length of aData: Finish() will do that for us and fail if // Write did not persist all the data or mStream->Close() failed. // Note that |nsISafeOutputStream| will write to a temp file and only // overwrite the destination if no error was reported. nsCOMPtr safeStream = do_QueryInterface(mStream); MOZ_ASSERT(safeStream); return safeStream->Finish(); } void Write(const char* aStr) override { uint32_t count; mozilla::Unused << mStream->Write(aStr, strlen(aStr), &count); } private: nsCOMPtr mStream; }; /** * Get the path to the Android Data dir. * * @param {nsTString} aOutDir - the variable holding the path. * @return {nsresult} NS_OK if the data dir path was found, a failure value otherwise. */ nsresult GetAndroidDataDir(nsTString& aOutDir) { // This relies on the Java environment to set the location of the // 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("GetAndroidDataDir - Cannot find the data directory in the environment."); return NS_ERROR_FAILURE; } aOutDir.AssignASCII(dataDir); return NS_OK; } /** * Get the path to the persistence file in the Android Data dir. * * @param {nsCOMPtr} aOutFile - the nsIFile pointer holding the file info. * @return {nsresult} NS_OK if the persistence file was found, a failure value otherwise. */ nsresult GetPersistenceFile(nsCOMPtr& aOutFile) { nsTString dataDir; nsresult rv = GetAndroidDataDir(dataDir); NS_ENSURE_SUCCESS(rv, rv); // Append the extension to the filename. nsAutoString fileName; fileName.Assign(kPersistenceFileName); aOutFile = new nsLocalFile(dataDir); aOutFile->Append(fileName); ANDROID_LOG("GetPersistenceFile - %s", aOutFile->HumanReadablePath().get()); return NS_OK; } /** * Read and parses JSON content from a file. * * @param {nsCOMPtr} aFile - the nsIFile handle to the file. * @param {nsACString} fileContent - the content of the file. * @return {nsresult} NS_OK if the file was correctly read, an error code otherwise. */ nsresult ReadFromFile(const nsCOMPtr& aFile, nsACString& fileContent) { int64_t fileSize = 0; nsresult rv = aFile->GetFileSize(&fileSize); if (NS_FAILED(rv)) { return rv; } nsCOMPtr inStream; rv = NS_NewLocalFileInputStream(getter_AddRefs(inStream), aFile, PR_RDONLY); NS_ENSURE_SUCCESS(rv, rv); // Make sure to close the stream. auto scopedStreamClose = MakeScopeExit([inStream] { inStream->Close(); }); rv = NS_ReadInputStreamToString(inStream, fileContent, fileSize); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } /** * Arms the persistence timer and instructs to run the persistence * task off the main thread. */ void MainThreadArmPersistenceTimer() { MOZ_ASSERT(NS_IsMainThread()); ANDROID_LOG("MainThreadArmPersistenceTimer"); // We won't have a persistence timer the first time this runs, so take // care of that. if (!gPersistenceTimer) { gPersistenceTimer = NS_NewTimer(SystemGroup::EventTargetFor(TaskCategory::Other)).take(); if (!gPersistenceTimer) { ANDROID_LOG("MainThreadArmPersistenceTimer - Timer creation failed."); 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("PersistenceThreadPersist", []() -> void { ::PersistenceThreadPersist(); })); }; uint32_t timeout = Preferences::GetUint("toolkit.telemetry.geckoPersistenceTimeout", kDefaultPersistenceTimeoutMs); // Schedule the timer to automatically run and reschedule // every |kPersistenceTimeoutMs|. gPersistenceTimer->InitWithNamedFuncCallback(timerCallback, nullptr, timeout, nsITimer::TYPE_ONE_SHOT_LOW_PRIORITY, "TelemetryGeckoViewPersistence::Persist"); } /** * Parse the string data into a JSON structure, using * the native JS JSON parser. */ void MainThreadParsePersistedProbes(const nsACString& aProbeData) { // We're required to run on the main thread since we're using JS. MOZ_ASSERT(NS_IsMainThread()); ANDROID_LOG("MainThreadParsePersistedProbes"); // We need a JS context to run the parsing stuff in. JSObject* cleanGlobal = SimpleGlobalObject::Create(SimpleGlobalObject::GlobalType::BindingDetail); if (!cleanGlobal) { ANDROID_LOG("MainThreadParsePersistedProbes - Failed to create a JS global object"); return; } AutoJSAPI jsapi; if (NS_WARN_IF(!jsapi.Init(cleanGlobal))) { ANDROID_LOG("MainThreadParsePersistedProbes - Failed to get JS API"); return; } // Parse the JSON using the JS API. JS::RootedValue data(jsapi.cx()); NS_ConvertUTF8toUTF16 utf16Content(aProbeData); if (!JS_ParseJSON(jsapi.cx(), utf16Content.BeginReading(), utf16Content.Length(), &data)) { ANDROID_LOG("MainThreadParsePersistedProbes - Failed to parse the persisted JSON"); return; } // Get the data for the scalars. JS::RootedObject dataObj(jsapi.cx(), &data.toObject()); JS::RootedValue scalarData(jsapi.cx()); if (JS_GetProperty(jsapi.cx(), dataObj, "scalars", &scalarData)) { // If the data is an object, try to parse its properties. If not, // silently skip and try to load the other sections. if (!scalarData.isObject() || NS_FAILED(TelemetryScalar::DeserializePersistedScalars(jsapi.cx(), scalarData))) { ANDROID_LOG("MainThreadParsePersistedProbes - Failed to parse 'scalars'."); MOZ_ASSERT(!JS_IsExceptionPending(jsapi.cx()), "Parsers must suppress exceptions themselves"); } } else { // Getting the "scalars" property failed, suppress the exception // and continue. JS_ClearPendingException(jsapi.cx()); } JS::RootedValue keyedScalarData(jsapi.cx()); if (JS_GetProperty(jsapi.cx(), dataObj, "keyedScalars", &keyedScalarData)) { // If the data is an object, try to parse its properties. If not, // silently skip and try to load the other sections. if (!keyedScalarData.isObject() || NS_FAILED(TelemetryScalar::DeserializePersistedKeyedScalars(jsapi.cx(), keyedScalarData))) { ANDROID_LOG("MainThreadParsePersistedProbes - Failed to parse 'keyedScalars'."); MOZ_ASSERT(!JS_IsExceptionPending(jsapi.cx()), "Parsers must suppress exceptions themselves"); } } else { // Getting the "keyedScalars" property failed, suppress the exception // and continue. JS_ClearPendingException(jsapi.cx()); } // Get the data for the histograms. JS::RootedValue histogramData(jsapi.cx()); if (JS_GetProperty(jsapi.cx(), dataObj, "histograms", &histogramData)) { // If the data is an object, try to parse its properties. If not, // silently skip and try to load the other sections. nsresult rv = NS_OK; if (!histogramData.isObject() || NS_FAILED(rv = TelemetryHistogram::DeserializeHistograms(jsapi.cx(), histogramData))) { nsAutoCString errorName; GetErrorName(rv, errorName); ANDROID_LOG("MainThreadParsePersistedProbes - Failed to parse 'histograms', %s.", errorName.get()); MOZ_ASSERT(!JS_IsExceptionPending(jsapi.cx()), "Parsers must suppress exceptions themselves"); } } else { // Getting the "histogramData" property failed, suppress the exception // and continue. JS_ClearPendingException(jsapi.cx()); } // Get the data for the keyed histograms. JS::RootedValue keyedHistogramData(jsapi.cx()); if (JS_GetProperty(jsapi.cx(), dataObj, "keyedHistograms", &keyedHistogramData)) { // If the data is an object, try to parse its properties. If not, // silently skip and try to load the other sections. nsresult rv = NS_OK; if (!keyedHistogramData.isObject() || NS_FAILED(rv = TelemetryHistogram::DeserializeKeyedHistograms(jsapi.cx(), keyedHistogramData))) { nsAutoCString errorName; GetErrorName(rv, errorName); ANDROID_LOG("MainThreadParsePersistedProbes - Failed to parse 'keyedHistograms', %s.", errorName.get()); MOZ_ASSERT(!JS_IsExceptionPending(jsapi.cx()), "Parsers must suppress exceptions themselves"); } } else { // Getting the "keyedHistogramData" property failed, suppress the exception // and continue. JS_ClearPendingException(jsapi.cx()); } } /** * The persistence worker function, meant to be run off the main thread. */ void PersistenceThreadPersist() { 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("PersistenceThreadPersist"); // If the function completes or fails, make sure to spin up the persistence timer again. auto scopedArmTimer = MakeScopeExit([&] { NS_DispatchToMainThread( NS_NewRunnableFunction("MainThreadArmPersistenceTimer", []() -> void { MainThreadArmPersistenceTimer(); })); }); TelemetryScalar::Add(mozilla::Telemetry::ScalarID::TELEMETRY_PERSISTENCE_TIMER_HIT_COUNT, 1); nsCOMPtr persistenceFile; if (NS_FAILED(GetPersistenceFile(persistenceFile))) { ANDROID_LOG("PersistenceThreadPersist - Failed to get the persistence file."); return; } // Open the persistence file. mozilla::UniquePtr jsonWriter = mozilla::MakeUnique(); if (!jsonWriter || NS_FAILED(jsonWriter->Open(persistenceFile))) { ANDROID_LOG("PersistenceThreadPersist - There was an error opening the persistence file."); return; } // Build the JSON structure: give up the ownership of jsonWriter. mozilla::JSONWriter w(std::move(jsonWriter)); w.Start(); w.StartObjectProperty("scalars"); if (NS_FAILED(TelemetryScalar::SerializeScalars(w))) { ANDROID_LOG("Persist - Failed to persist scalars."); } w.EndObject(); w.StartObjectProperty("keyedScalars"); if (NS_FAILED(TelemetryScalar::SerializeKeyedScalars(w))) { ANDROID_LOG("Persist - Failed to persist keyed scalars."); } w.EndObject(); w.StartObjectProperty("histograms"); if (NS_FAILED(TelemetryHistogram::SerializeHistograms(w))) { ANDROID_LOG("Persist - Failed to persist histograms."); } w.EndObject(); w.StartObjectProperty("keyedHistograms"); if (NS_FAILED(TelemetryHistogram::SerializeKeyedHistograms(w))) { ANDROID_LOG("Persist - Failed to persist keyed histograms."); } w.EndObject(); // End the building process. w.End(); // Android can kill us while we are writing to disk and, if that happens, // we end up with a corrupted json overwriting the old session data. // Luckily, |StreamingJSONWriter::Close| is smart enough to write to a // temporary file and only overwrite the original file if nothing bad happened. nsresult rv = static_cast(w.WriteFunc())->Close(); if (NS_FAILED(rv)) { ANDROID_LOG("PersistenceThreadPersist - There was an error writing to the persistence file."); return; } } /** * 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 PersistenceThreadLoadData() { 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("PersistenceThreadLoadData"); // If the function completes or fails, make sure to spin up the persistence timer. nsAutoCString fileContent; auto scopedArmTimer = MakeScopeExit([&] { NS_DispatchToMainThread( NS_NewRunnableFunction("MainThreadArmPersistenceTimer", [fileContent]() -> void { // Try to parse the probes if the file was not empty. if (!fileContent.IsEmpty()) { MainThreadParsePersistedProbes(fileContent); } TelemetryScalar::ApplyPendingOperations(); // Arm the timer. MainThreadArmPersistenceTimer(); // Notify that we're good to take snapshots! nsCOMPtr os = mozilla::services::GetObserverService(); if (os) { os->NotifyObservers(nullptr, kLoadCompleteTopic, nullptr); } })); }); // Attempt to load the persistence file. This could fail if we're not able // to allocate enough memory for the content. See bug 1460911. nsCOMPtr persistenceFile; if (NS_FAILED(GetPersistenceFile(persistenceFile)) || NS_FAILED(ReadFromFile(persistenceFile, fileContent))) { ANDROID_LOG("PersistenceThreadLoadData - Failed to load cache file at %s", persistenceFile->HumanReadablePath().get()); return; } } } // anonymous namespace // This namespace exposes testing only helpers to simplify writing // gtest cases. namespace TelemetryGeckoViewTesting { void TestDispatchPersist() { gPersistenceThread->Dispatch(NS_NewRunnableFunction("Persist", []() -> void { ::PersistenceThreadPersist(); })); } } // GeckoViewTesting void TelemetryGeckoViewPersistence::InitPersistence() { MOZ_ASSERT(NS_IsMainThread()); if (gPersistenceThread) { ANDROID_LOG("Init must only be called once."); return; } // Only register the persistence timer in the parent process in // order to persist data for all the processes. if (!XRE_IsParentProcess()) { ANDROID_LOG("InitPersistence - Bailing out on child process."); return; } ANDROID_LOG("InitPersistence"); // Spawn a new thread for handling GeckoView Telemetry persistence I/O. // We just spawn it once and re-use it later. nsCOMPtr thread; nsresult rv = NS_NewNamedThread("TelemetryGVIO", getter_AddRefs(thread)); if (NS_WARN_IF(NS_FAILED(rv))) { ANDROID_LOG("InitPersistence - Failed to instantiate the worker thread."); return; } gPersistenceThread = thread.forget(); // From now on all scalar operations should be recorded. TelemetryScalar::DeserializationStarted(); // Trigger the loading of the persistence data. After the function // completes it will automatically arm the persistence timer. gPersistenceThread->Dispatch( NS_NewRunnableFunction("PersistenceThreadLoadData", &PersistenceThreadLoadData)); } void TelemetryGeckoViewPersistence::DeInitPersistence() { MOZ_ASSERT(NS_IsMainThread()); // Bail out if this is not the parent process. if (!XRE_IsParentProcess()) { ANDROID_LOG("DeInitPersistence - Bailing out."); 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"); if (gPersistenceThread) { gPersistenceThread->Shutdown(); gPersistenceThread = nullptr; } if (gPersistenceTimer) { // Always make sure the timer is canceled. MOZ_ALWAYS_SUCCEEDS(gPersistenceTimer->Cancel()); NS_RELEASE(gPersistenceTimer); } } void TelemetryGeckoViewPersistence::ClearPersistenceData() { // This can be run on any thread, as we just dispatch the persistence // task to the persistence thread. MOZ_ASSERT(gPersistenceThread); ANDROID_LOG("ClearPersistenceData"); // Trigger clearing the persisted measurements off the main thread. gPersistenceThread->Dispatch(NS_NewRunnableFunction("ClearPersistedData", []() -> void { nsCOMPtr persistenceFile; if (NS_FAILED(GetPersistenceFile(persistenceFile)) || NS_FAILED(persistenceFile->Remove(false))) { ANDROID_LOG("ClearPersistenceData - Failed to remove the persistence file."); return; } })); }