/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ /* 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 "FOGIPC.h" #include #include "mozilla/glean/fog_ffi_generated.h" #include "mozilla/glean/GleanMetrics.h" #include "mozilla/dom/BrowsingContextGroup.h" #include "mozilla/dom/ContentChild.h" #include "mozilla/dom/ContentParent.h" #include "mozilla/dom/DocGroup.h" #include "mozilla/dom/Document.h" #include "mozilla/dom/Promise.h" #include "mozilla/gfx/GPUChild.h" #include "mozilla/gfx/GPUParent.h" #include "mozilla/gfx/GPUProcessManager.h" #include "mozilla/Hal.h" #include "mozilla/MozPromise.h" #include "mozilla/net/SocketProcessChild.h" #include "mozilla/net/SocketProcessParent.h" #include "mozilla/ProcInfo.h" #include "mozilla/RDDChild.h" #include "mozilla/RDDParent.h" #include "mozilla/RDDProcessManager.h" #include "mozilla/ipc/UtilityProcessChild.h" #include "mozilla/ipc/UtilityProcessManager.h" #include "mozilla/ipc/UtilityProcessParent.h" #include "mozilla/ipc/UtilityProcessSandboxing.h" #include "mozilla/Unused.h" #include "GMPPlatform.h" #include "GMPServiceParent.h" #include "nsIClassifiedChannel.h" #include "nsIXULRuntime.h" #include "nsTArray.h" #include "nsThreadUtils.h" using mozilla::dom::ContentParent; using mozilla::gfx::GPUChild; using mozilla::gfx::GPUProcessManager; using mozilla::ipc::ByteBuf; using mozilla::ipc::UtilityProcessChild; using mozilla::ipc::UtilityProcessManager; using mozilla::ipc::UtilityProcessParent; using FlushFOGDataPromise = mozilla::dom::ContentParent::FlushFOGDataPromise; namespace geckoprofiler::markers { using namespace mozilla; struct ProcessingTimeMarker { static constexpr Span MarkerTypeName() { return MakeStringSpan("ProcessingTime"); } static void StreamJSONMarkerData(baseprofiler::SpliceableJSONWriter& aWriter, int64_t aDiffMs, const ProfilerString8View& aType, const ProfilerString8View& aTrackerType) { aWriter.IntProperty("time", aDiffMs); aWriter.StringProperty("label", aType); if (aTrackerType.Length() > 0) { aWriter.StringProperty("tracker", aTrackerType); } } static MarkerSchema MarkerTypeDisplay() { using MS = MarkerSchema; MS schema{MS::Location::MarkerChart, MS::Location::MarkerTable}; schema.AddKeyLabelFormat("time", "Recorded Time", MS::Format::Milliseconds); schema.AddKeyLabelFormat("tracker", "Tracker Type", MS::Format::String); schema.SetTooltipLabel("{marker.name} - {marker.data.label}"); schema.SetTableLabel( "{marker.name} - {marker.data.label}: {marker.data.time}"); return schema; } }; } // namespace geckoprofiler::markers namespace mozilla::glean { // Echoes processtools/metrics.yaml's power.wakeups_per_thread enum ProcessType { eParentActive, eParentInactive, eContentForeground, eContentBackground, eGpuProcess, eUnknown, }; // This static global is set within RecordPowerMetrics on the main thread, // using information only gettable on the main thread, and is read within // RecordThreadCpuUse on any thread. static Atomic gThisProcessType(eUnknown); #ifdef NIGHTLY_BUILD // It is fine to call RecordThreadCpuUse during startup before the first // RecordPowerMetrics call. In that case the parent process will be recorded // as inactive, and other processes will be ignored (content processes start // in the 'prealloc' type for which we don't record per-thread CPU use data). void RecordThreadCpuUse(const nsACString& aThreadName, uint64_t aCpuTimeMs, uint64_t aWakeCount) { ProcessType processType = gThisProcessType; if (processType == ProcessType::eUnknown) { if (XRE_IsParentProcess()) { // During startup we might not have gotten a RecordPowerMetrics call. // That's fine. Default to eParentInactive. processType = eParentInactive; } else { // We are not interested in per-thread CPU use data for the current // process type. return; } } nsAutoCString threadName(aThreadName); for (size_t i = 0; i < threadName.Length(); ++i) { const char c = threadName.CharAt(i); // Valid characters. if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || c == '-' || c == '_') { continue; } // Should only use lower case characters if (c >= 'A' && c <= 'Z') { threadName.SetCharAt(c + ('a' - 'A'), i); continue; } // Replace everything else with _ threadName.SetCharAt('_', i); } if (aCpuTimeMs != 0 && MOZ_LIKELY(aCpuTimeMs < std::numeric_limits::max())) { switch (processType) { case eParentActive: power_cpu_ms_per_thread::parent_active.Get(threadName) .Add(int32_t(aCpuTimeMs)); break; case eParentInactive: power_cpu_ms_per_thread::parent_inactive.Get(threadName) .Add(int32_t(aCpuTimeMs)); break; case eContentForeground: power_cpu_ms_per_thread::content_foreground.Get(threadName) .Add(int32_t(aCpuTimeMs)); break; case eContentBackground: power_cpu_ms_per_thread::content_background.Get(threadName) .Add(int32_t(aCpuTimeMs)); break; case eGpuProcess: power_cpu_ms_per_thread::gpu_process.Get(threadName) .Add(int32_t(aCpuTimeMs)); break; case eUnknown: // Nothing to do. break; } } if (aWakeCount != 0 && MOZ_LIKELY(aWakeCount < std::numeric_limits::max())) { switch (processType) { case eParentActive: power_wakeups_per_thread::parent_active.Get(threadName) .Add(int32_t(aWakeCount)); break; case eParentInactive: power_wakeups_per_thread::parent_inactive.Get(threadName) .Add(int32_t(aWakeCount)); break; case eContentForeground: power_wakeups_per_thread::content_foreground.Get(threadName) .Add(int32_t(aWakeCount)); break; case eContentBackground: power_wakeups_per_thread::content_background.Get(threadName) .Add(int32_t(aWakeCount)); break; case eGpuProcess: power_wakeups_per_thread::gpu_process.Get(threadName) .Add(int32_t(aWakeCount)); break; case eUnknown: // Nothing to do. break; } } } #endif void GetTrackerType(nsAutoCString& aTrackerType) { using namespace mozilla::dom; uint32_t trackingFlags = (nsIClassifiedChannel::CLASSIFIED_CRYPTOMINING | nsIClassifiedChannel::CLASSIFIED_FINGERPRINTING | nsIClassifiedChannel::CLASSIFIED_TRACKING | nsIClassifiedChannel::CLASSIFIED_TRACKING_AD | nsIClassifiedChannel::CLASSIFIED_TRACKING_ANALYTICS | nsIClassifiedChannel::CLASSIFIED_TRACKING_SOCIAL); AutoTArray, 5> bcGroups; BrowsingContextGroup::GetAllGroups(bcGroups); for (auto& bcGroup : bcGroups) { AutoTArray docGroups; bcGroup->GetDocGroups(docGroups); for (auto* docGroup : docGroups) { for (Document* doc : *docGroup) { nsCOMPtr classifiedChannel = do_QueryInterface(doc->GetChannel()); if (classifiedChannel) { uint32_t classificationFlags = classifiedChannel->GetThirdPartyClassificationFlags(); trackingFlags &= classificationFlags; if (!trackingFlags) { return; } } } } } // The if-elseif-else chain works because the tracker types listed here are // currently mutually exclusive and should be maintained that way by policy. if (trackingFlags == nsIClassifiedChannel::CLASSIFIED_TRACKING_AD) { aTrackerType = "ad"; } else if (trackingFlags == nsIClassifiedChannel::CLASSIFIED_TRACKING_ANALYTICS) { aTrackerType = "analytics"; } else if (trackingFlags == nsIClassifiedChannel::CLASSIFIED_TRACKING_SOCIAL) { aTrackerType = "social"; } else if (trackingFlags == nsIClassifiedChannel::CLASSIFIED_CRYPTOMINING) { aTrackerType = "cryptomining"; } else if (trackingFlags == nsIClassifiedChannel::CLASSIFIED_FINGERPRINTING) { aTrackerType = "fingerprinting"; } else if (trackingFlags == nsIClassifiedChannel::CLASSIFIED_TRACKING) { // CLASSIFIED_TRACKING means we were not able to identify the type of // classification. aTrackerType = "unknown"; } } void RecordPowerMetrics() { static uint64_t previousCpuTime = 0, previousGpuTime = 0; uint64_t cpuTime, newCpuTime = 0; if (NS_SUCCEEDED(GetCpuTimeSinceProcessStartInMs(&cpuTime)) && cpuTime > previousCpuTime) { newCpuTime = cpuTime - previousCpuTime; } uint64_t gpuTime, newGpuTime = 0; // Avoid loading gdi32.dll for the Socket process where the GPU is never used. if (!XRE_IsSocketProcess() && NS_SUCCEEDED(GetGpuTimeSinceProcessStartInMs(&gpuTime)) && gpuTime > previousGpuTime) { newGpuTime = gpuTime - previousGpuTime; } if (!newCpuTime && !newGpuTime) { // Nothing to record. return; } // Compute the process type string. nsAutoCString type(XRE_GetProcessTypeString()); nsAutoCString trackerType; if (XRE_IsContentProcess()) { auto* cc = dom::ContentChild::GetSingleton(); if (cc) { type.Assign(dom::RemoteTypePrefix(cc->GetRemoteType())); if (StringBeginsWith(type, WEB_REMOTE_TYPE)) { type.AssignLiteral("web"); switch (cc->GetProcessPriority()) { case hal::PROCESS_PRIORITY_BACKGROUND: type.AppendLiteral(".background"); gThisProcessType = ProcessType::eContentBackground; break; case hal::PROCESS_PRIORITY_FOREGROUND: type.AppendLiteral(".foreground"); gThisProcessType = ProcessType::eContentForeground; break; case hal::PROCESS_PRIORITY_BACKGROUND_PERCEIVABLE: type.AppendLiteral(".background-perceivable"); gThisProcessType = ProcessType::eUnknown; break; default: gThisProcessType = ProcessType::eUnknown; break; } } GetTrackerType(trackerType); } else { gThisProcessType = ProcessType::eUnknown; } } else if (XRE_IsParentProcess()) { if (nsContentUtils::GetUserIsInteracting()) { type.AssignLiteral("parent.active"); gThisProcessType = ProcessType::eParentActive; } else { type.AssignLiteral("parent.inactive"); gThisProcessType = ProcessType::eParentInactive; } hal::WakeLockInformation info; GetWakeLockInfo(u"video-playing"_ns, &info); if (info.numLocks() != 0 && info.numHidden() < info.numLocks()) { type.AppendLiteral(".playing-video"); } else { GetWakeLockInfo(u"audio-playing"_ns, &info); if (info.numLocks()) { type.AppendLiteral(".playing-audio"); } } } else if (XRE_IsGPUProcess()) { gThisProcessType = ProcessType::eGpuProcess; } else { gThisProcessType = ProcessType::eUnknown; } if (newCpuTime) { // The counters are reset at least once a day. Assuming all cores are used // continuously, an int32 can hold the data for 24.85 cores. // This should be fine for now, but may overflow in the future. // Bug 1751277 tracks a newer, bigger counter. int32_t nNewCpuTime = int32_t(newCpuTime); if (newCpuTime < std::numeric_limits::max()) { power::total_cpu_time_ms.Add(nNewCpuTime); power::cpu_time_per_process_type_ms.Get(type).Add(nNewCpuTime); if (!trackerType.IsEmpty()) { power::cpu_time_per_tracker_type_ms.Get(trackerType).Add(nNewCpuTime); } } else { power::cpu_time_bogus_values.Add(1); } PROFILER_MARKER("Process CPU Time", OTHER, {}, ProcessingTimeMarker, nNewCpuTime, type, trackerType); previousCpuTime += newCpuTime; } if (newGpuTime) { int32_t nNewGpuTime = int32_t(newGpuTime); if (newGpuTime < std::numeric_limits::max()) { power::total_gpu_time_ms.Add(nNewGpuTime); power::gpu_time_per_process_type_ms.Get(type).Add(nNewGpuTime); } else { power::gpu_time_bogus_values.Add(1); } PROFILER_MARKER("Process GPU Time", OTHER, {}, ProcessingTimeMarker, nNewGpuTime, type, trackerType); previousGpuTime += newGpuTime; } profiler_record_wakeup_count(type); } /** * Flush your data ASAP, either because the parent process is asking you to * or because the process is about to shutdown. * * @param aResolver - The function you need to call with the bincoded, * serialized payload that the Rust impl hands you. */ void FlushFOGData(std::function&& aResolver) { // Record power metrics right before data is sent to the parent. RecordPowerMetrics(); ByteBuf buf; uint32_t ipcBufferSize = impl::fog_serialize_ipc_buf(); bool ok = buf.Allocate(ipcBufferSize); if (!ok) { return; } uint32_t writtenLen = impl::fog_give_ipc_buf(buf.mData, buf.mLen); if (writtenLen != ipcBufferSize) { return; } aResolver(std::move(buf)); } /** * Called by FOG on the parent process when it wants to flush all its * children's data. * @param aResolver - The function that'll be called with the results. */ void FlushAllChildData( std::function&&)>&& aResolver) { auto timerId = fog_ipc::flush_durations.Start(); nsTArray parents; ContentParent::GetAll(parents); nsTArray> promises; for (auto* parent : parents) { promises.EmplaceBack(parent->SendFlushFOGData()); } if (GPUProcessManager* gpuManager = GPUProcessManager::Get()) { if (GPUChild* gpuChild = gpuManager->GetGPUChild()) { promises.EmplaceBack(gpuChild->SendFlushFOGData()); } } if (RDDProcessManager* rddManager = RDDProcessManager::Get()) { if (RDDChild* rddChild = rddManager->GetRDDChild()) { promises.EmplaceBack(rddChild->SendFlushFOGData()); } } if (net::SocketProcessParent* socketParent = net::SocketProcessParent::GetSingleton()) { promises.EmplaceBack(socketParent->SendFlushFOGData()); } if (RefPtr gmps = mozilla::gmp::GeckoMediaPluginServiceParent::GetSingleton()) { // There can be multiple Gecko Media Plugin processes, but iterating // through them requires locking a mutex and the IPCs need to be sent // from a different thread, so it's better to let the // GeckoMediaPluginServiceParent code do it for us. gmps->SendFlushFOGData(promises); } if (RefPtr utilityManager = UtilityProcessManager::GetIfExists()) { for (RefPtr& parent : utilityManager->GetAllProcessesProcessParent()) { promises.EmplaceBack(parent->SendFlushFOGData()); } } if (promises.Length() == 0) { // No child processes at the moment. Resolve synchronously. fog_ipc::flush_durations.Cancel(std::move(timerId)); nsTArray results; aResolver(std::move(results)); return; } // If fog.ipc.flush_failures ever gets too high: // TODO: Don't throw away resolved data if some of the promises reject. // (not sure how, but it'll mean not using ::All... maybe a custom copy of // AllPromiseHolder? Might be impossible outside MozPromise.h) FlushFOGDataPromise::All(GetCurrentSerialEventTarget(), promises) ->Then(GetCurrentSerialEventTarget(), __func__, [aResolver = std::move(aResolver), timerId]( FlushFOGDataPromise::AllPromiseType::ResolveOrRejectValue&& aValue) { fog_ipc::flush_durations.StopAndAccumulate(std::move(timerId)); if (aValue.IsResolve()) { aResolver(std::move(aValue.ResolveValue())); } else { fog_ipc::flush_failures.Add(1); nsTArray results; aResolver(std::move(results)); } }); } /** * A child process has sent you this buf as a treat. * @param buf - a bincoded serialized payload that the Rust impl understands. */ void FOGData(ipc::ByteBuf&& buf) { JOG::EnsureRuntimeMetricsRegistered(); fog_ipc::buffer_sizes.Accumulate(buf.mLen); impl::fog_use_ipc_buf(buf.mData, buf.mLen); } /** * Called by FOG on a child process when it wants to send a buf to the parent. * @param buf - a bincoded serialized payload that the Rust impl understands. */ void SendFOGData(ipc::ByteBuf&& buf) { switch (XRE_GetProcessType()) { case GeckoProcessType_Content: mozilla::dom::ContentChild::GetSingleton()->SendFOGData(std::move(buf)); break; case GeckoProcessType_GMPlugin: { mozilla::gmp::SendFOGData(std::move(buf)); } break; case GeckoProcessType_GPU: Unused << mozilla::gfx::GPUParent::GetSingleton()->SendFOGData( std::move(buf)); break; case GeckoProcessType_RDD: Unused << mozilla::RDDParent::GetSingleton()->SendFOGData(std::move(buf)); break; case GeckoProcessType_Socket: Unused << net::SocketProcessChild::GetSingleton()->SendFOGData( std::move(buf)); break; case GeckoProcessType_Utility: Unused << ipc::UtilityProcessChild::GetSingleton()->SendFOGData( std::move(buf)); break; default: MOZ_ASSERT_UNREACHABLE("Unsuppored process type"); } } /** * Called on the parent process to ask all child processes for data, * sending it all down into Rust to be used. */ RefPtr FlushAndUseFOGData() { // Record power metrics on the parent before sending requests to child // processes. RecordPowerMetrics(); RefPtr ret = new GenericPromise::Private(__func__); std::function&&)> resolver = [ret](nsTArray&& bufs) { for (ByteBuf& buf : bufs) { FOGData(std::move(buf)); } ret->Resolve(true, __func__); }; FlushAllChildData(std::move(resolver)); return ret; } void TestTriggerMetrics(uint32_t aProcessType, const RefPtr& promise) { switch (aProcessType) { case nsIXULRuntime::PROCESS_TYPE_GMPLUGIN: { RefPtr gmps( mozilla::gmp::GeckoMediaPluginServiceParent::GetSingleton()); gmps->TestTriggerMetrics()->Then( GetCurrentSerialEventTarget(), __func__, [promise]() { promise->MaybeResolveWithUndefined(); }, [promise]() { promise->MaybeRejectWithUndefined(); }); } break; case nsIXULRuntime::PROCESS_TYPE_GPU: mozilla::gfx::GPUProcessManager::Get()->TestTriggerMetrics()->Then( GetCurrentSerialEventTarget(), __func__, [promise]() { promise->MaybeResolveWithUndefined(); }, [promise]() { promise->MaybeRejectWithUndefined(); }); break; case nsIXULRuntime::PROCESS_TYPE_RDD: RDDProcessManager::Get()->TestTriggerMetrics()->Then( GetCurrentSerialEventTarget(), __func__, [promise]() { promise->MaybeResolveWithUndefined(); }, [promise]() { promise->MaybeRejectWithUndefined(); }); break; case nsIXULRuntime::PROCESS_TYPE_SOCKET: Unused << net::SocketProcessParent::GetSingleton() ->SendTestTriggerMetrics() ->Then( GetCurrentSerialEventTarget(), __func__, [promise]() { promise->MaybeResolveWithUndefined(); }, [promise]() { promise->MaybeRejectWithUndefined(); }); break; case nsIXULRuntime::PROCESS_TYPE_UTILITY: Unused << ipc::UtilityProcessManager::GetSingleton() ->GetProcessParent(ipc::SandboxingKind::GENERIC_UTILITY) ->SendTestTriggerMetrics() ->Then( GetCurrentSerialEventTarget(), __func__, [promise]() { promise->MaybeResolveWithUndefined(); }, [promise]() { promise->MaybeRejectWithUndefined(); }); break; default: promise->MaybeRejectWithUndefined(); break; } } } // namespace mozilla::glean