/* -*- 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 "mozilla/glean/bindings/jog/JOG.h" #include #include "mozilla/AppShutdown.h" #include "mozilla/ClearOnShutdown.h" #include "mozilla/glean/bindings/jog/jog_ffi_generated.h" #include "mozilla/Logging.h" #include "mozilla/IntegerPrintfMacros.h" #include "mozilla/StaticPrefs_telemetry.h" #include "mozilla/AppShutdown.h" #include "nsDirectoryServiceDefs.h" #include "nsDirectoryServiceUtils.h" #include "nsThreadUtils.h" #include "nsTHashMap.h" #include "nsTHashSet.h" namespace mozilla::glean { static LazyLogModule sLog("jog"); // Storage // Thread Safety: Only used on the main thread. StaticAutoPtr> gCategories; StaticAutoPtr> gMetrics; StaticAutoPtr> gMetricNames; StaticAutoPtr> gPings; // static bool JOG::HasCategory(const nsACString& aCategoryName) { MOZ_ASSERT(NS_IsMainThread()); return gCategories && gCategories->Contains(aCategoryName); } static Maybe sFoundAndLoadedJogfile; // static bool JOG::EnsureRuntimeMetricsRegistered(bool aForce) { MOZ_ASSERT(NS_IsMainThread()); if (sFoundAndLoadedJogfile) { return sFoundAndLoadedJogfile.value(); } sFoundAndLoadedJogfile = Some(false); MOZ_LOG(sLog, LogLevel::Debug, ("Determining whether there's JOG for you.")); if (!mozilla::StaticPrefs::telemetry_fog_artifact_build()) { // Supporting Artifact Builds is a developer-only thing. // We're on the main thread here. // Let's not spend any more time than we need to. MOZ_LOG(sLog, LogLevel::Debug, ("!telemetry.fog.artifact_build. No JOG for you.")); return false; } // The metrics we need to process were placed in GreD in jogfile.json // That file was generated by // toolkit/components/glean/build_scripts/glean_parser_ext/jog.py nsCOMPtr jogfile; if (NS_WARN_IF(NS_FAILED( NS_GetSpecialDirectory(NS_GRE_DIR, getter_AddRefs(jogfile))))) { return false; } if (NS_WARN_IF(NS_FAILED(jogfile->Append(u"jogfile.json"_ns)))) { return false; } bool jogfileExists = false; if (NS_WARN_IF(NS_FAILED(jogfile->Exists(&jogfileExists))) || !jogfileExists) { return false; } // We _could_ register everything here in C++ land, // but let's use Rust because (among other reasons) it's more fun. nsAutoString jogfileString; if (NS_WARN_IF(NS_FAILED(jogfile->GetPath(jogfileString)))) { return false; } sFoundAndLoadedJogfile = Some(jog::jog_load_jogfile(&jogfileString)); MOZ_LOG(sLog, LogLevel::Debug, ("%s", sFoundAndLoadedJogfile.value() ? "Found and loaded jogfile. Yes! JOG for you!" : "Couldn't find and load jogfile. No JOG for you.")); return sFoundAndLoadedJogfile.value(); } // static bool JOG::AreRuntimeMetricsComprehensive() { MOZ_ASSERT(NS_IsMainThread()); return sFoundAndLoadedJogfile && sFoundAndLoadedJogfile.value(); } // static void JOG::GetCategoryNames(nsTArray& aNames) { MOZ_ASSERT(NS_IsMainThread()); if (!gCategories) { return; } for (const auto& category : *gCategories) { aNames.EmplaceBack(NS_ConvertUTF8toUTF16(category)); } } // static Maybe JOG::GetMetric(const nsACString& aMetricName) { MOZ_ASSERT(NS_IsMainThread()); return !gMetrics ? Nothing() : gMetrics->MaybeGet(aMetricName); } // static Maybe JOG::GetMetricName(uint32_t aMetricId) { MOZ_ASSERT(NS_IsMainThread()); return !gMetricNames ? Nothing() : gMetricNames->MaybeGet(aMetricId); } // static void JOG::GetMetricNames(const nsACString& aCategoryName, nsTArray& aNames) { MOZ_ASSERT(NS_IsMainThread()); if (!gMetricNames) { return; } for (const auto& identifier : gMetricNames->Values()) { if (StringBeginsWith(identifier, aCategoryName) && identifier.CharAt(aCategoryName.Length()) == '.') { const char* metricName = &identifier.Data()[aCategoryName.Length() + 1]; aNames.AppendElement()->AssignASCII(metricName); } } } // static Maybe JOG::GetPing(const nsACString& aPingName) { MOZ_ASSERT(NS_IsMainThread()); return !gPings ? Nothing() : gPings->MaybeGet(aPingName); } // static void JOG::GetPingNames(nsTArray& aNames) { MOZ_ASSERT(NS_IsMainThread()); if (!gPings) { return; } for (const auto& ping : gPings->Keys()) { aNames.EmplaceBack(NS_ConvertUTF8toUTF16(ping)); } } } // namespace mozilla::glean // static nsCString dottedSnakeToCamel(const nsACString& aSnake) { nsCString camel; bool first = true; for (const nsACString& segment : aSnake.Split('_')) { for (const nsACString& part : segment.Split('.')) { if (first) { first = false; camel.Append(part); } else if (part.Length()) { char lower = part.CharAt(0); if ('a' <= lower && lower <= 'z') { camel.Append( std::toupper(lower, std::locale())); // append the Capital. camel.Append(part.BeginReading() + 1, part.Length() - 1); // append the rest. } else { // Not gonna try to capitalize anything outside a->z. camel.Append(part); } } } } return camel; } // static nsCString kebabToCamel(const nsACString& aKebab) { nsCString camel; bool first = true; for (const nsACString& segment : aKebab.Split('-')) { if (first) { first = false; camel.Append(segment); } else if (segment.Length()) { char lower = segment.CharAt(0); if ('a' <= lower && lower <= 'z') { camel.Append( std::toupper(lower, std::locale())); // append the Capital. camel.Append(segment.BeginReading() + 1, segment.Length() - 1); // append the rest. } else { // Not gonna try to capitalize anything outside a->z. camel.Append(segment); } } } return camel; } using mozilla::AppShutdown; using mozilla::ShutdownPhase; using mozilla::glean::gCategories; using mozilla::glean::gMetricNames; using mozilla::glean::gMetrics; using mozilla::glean::gPings; extern "C" NS_EXPORT void JOG_RegisterMetric( const nsACString& aCategory, const nsACString& aName, uint32_t aMetric, // includes type. uint32_t aMetricId) { MOZ_ASSERT(NS_IsMainThread()); if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMWillShutdown)) { return; } MOZ_LOG(mozilla::glean::sLog, mozilla::LogLevel::Verbose, ("Registering metric %s.%s id %" PRIu32 " id+type %" PRIu32 "", PromiseFlatCString(aCategory).get(), PromiseFlatCString(aName).get(), aMetricId, aMetric)); // aCategory is dotted.snake_case. aName is snake_case. auto categoryCamel = dottedSnakeToCamel(aCategory); auto nameCamel = dottedSnakeToCamel(aName); // Register the category if (!gCategories) { gCategories = new nsTHashSet(); RunOnShutdown([&] { gCategories = nullptr; }, ShutdownPhase::XPCOMWillShutdown); } gCategories->Insert(categoryCamel); // Register the metric if (!gMetrics) { gMetrics = new nsTHashMap(); RunOnShutdown([&] { gMetrics = nullptr; }, ShutdownPhase::XPCOMWillShutdown); } gMetrics->InsertOrUpdate(categoryCamel + "."_ns + nameCamel, aMetric); // Register the metric name (for GIFFT) if (!gMetricNames) { gMetricNames = new nsTHashMap(); RunOnShutdown([&] { gMetricNames = nullptr; }, ShutdownPhase::XPCOMWillShutdown); } gMetricNames->InsertOrUpdate(aMetricId, categoryCamel + "."_ns + nameCamel); } extern "C" NS_EXPORT void JOG_RegisterPing(const nsACString& aPingName, uint32_t aPingId) { MOZ_ASSERT(NS_IsMainThread()); if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMWillShutdown)) { return; } // aPingName is kebab-case. JS expects camelCase. auto pingCamel = kebabToCamel(aPingName); // Register the ping if (!gPings) { gPings = new nsTHashMap(); RunOnShutdown([&] { gPings = nullptr; }, ShutdownPhase::XPCOMWillShutdown); } gPings->InsertOrUpdate(pingCamel, aPingId); }