summaryrefslogtreecommitdiffstats
path: root/tools/profiler/tests/gtest/GeckoProfiler.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
commit0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch)
treea31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /tools/profiler/tests/gtest/GeckoProfiler.cpp
parentInitial commit. (diff)
downloadfirefox-esr-upstream/115.8.0esr.tar.xz
firefox-esr-upstream/115.8.0esr.zip
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'tools/profiler/tests/gtest/GeckoProfiler.cpp')
-rw-r--r--tools/profiler/tests/gtest/GeckoProfiler.cpp5099
1 files changed, 5099 insertions, 0 deletions
diff --git a/tools/profiler/tests/gtest/GeckoProfiler.cpp b/tools/profiler/tests/gtest/GeckoProfiler.cpp
new file mode 100644
index 0000000000..78456662f5
--- /dev/null
+++ b/tools/profiler/tests/gtest/GeckoProfiler.cpp
@@ -0,0 +1,5099 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+// This file tests a lot of the profiler_*() functions in GeckoProfiler.h.
+// Most of the tests just check that nothing untoward (e.g. crashes, deadlocks)
+// happens when calling these functions. They don't do much inspection of
+// profiler internals.
+
+#include "mozilla/ProfilerThreadPlatformData.h"
+#include "mozilla/ProfilerThreadRegistration.h"
+#include "mozilla/ProfilerThreadRegistrationInfo.h"
+#include "mozilla/ProfilerThreadRegistry.h"
+#include "mozilla/ProfilerUtils.h"
+#include "mozilla/ProgressLogger.h"
+#include "mozilla/UniquePtrExtensions.h"
+
+#include "nsIThread.h"
+#include "nsThreadUtils.h"
+#include "prthread.h"
+
+#include "gtest/gtest.h"
+#include "mozilla/gtest/MozAssertions.h"
+
+#include <thread>
+
+#if defined(_MSC_VER) || defined(__MINGW32__)
+# include <processthreadsapi.h>
+# include <realtimeapiset.h>
+#elif defined(__APPLE__)
+# include <mach/thread_act.h>
+#endif
+
+#ifdef XP_WIN
+#include "mozilla/WindowsVersion.h"
+#endif
+
+#ifdef MOZ_GECKO_PROFILER
+
+# include "GeckoProfiler.h"
+# include "mozilla/ProfilerMarkerTypes.h"
+# include "mozilla/ProfilerMarkers.h"
+# include "NetworkMarker.h"
+# include "platform.h"
+# include "ProfileBuffer.h"
+# include "ProfilerControl.h"
+
+# include "js/Initialization.h"
+# include "js/Printf.h"
+# include "jsapi.h"
+# include "json/json.h"
+# include "mozilla/Atomics.h"
+# include "mozilla/BlocksRingBuffer.h"
+# include "mozilla/DataMutex.h"
+# include "mozilla/ProfileBufferEntrySerializationGeckoExtensions.h"
+# include "mozilla/ProfileJSONWriter.h"
+# include "mozilla/ScopeExit.h"
+# include "mozilla/net/HttpBaseChannel.h"
+# include "nsIChannelEventSink.h"
+# include "nsIThread.h"
+# include "nsThreadUtils.h"
+
+# include <cstring>
+# include <set>
+
+#endif // MOZ_GECKO_PROFILER
+
+// Note: profiler_init() has already been called in XRE_main(), so we can't
+// test it here. Likewise for profiler_shutdown(), and AutoProfilerInit
+// (which is just an RAII wrapper for profiler_init() and profiler_shutdown()).
+
+using namespace mozilla;
+
+TEST(GeckoProfiler, ProfilerUtils)
+{
+ profiler_init_main_thread_id();
+
+ static_assert(std::is_same_v<decltype(profiler_current_process_id()),
+ ProfilerProcessId>);
+ static_assert(
+ std::is_same_v<decltype(profiler_current_process_id()),
+ decltype(baseprofiler::profiler_current_process_id())>);
+ ProfilerProcessId processId = profiler_current_process_id();
+ EXPECT_TRUE(processId.IsSpecified());
+ EXPECT_EQ(processId, baseprofiler::profiler_current_process_id());
+
+ static_assert(
+ std::is_same_v<decltype(profiler_current_thread_id()), ProfilerThreadId>);
+ static_assert(
+ std::is_same_v<decltype(profiler_current_thread_id()),
+ decltype(baseprofiler::profiler_current_thread_id())>);
+ EXPECT_EQ(profiler_current_thread_id(),
+ baseprofiler::profiler_current_thread_id());
+
+ ProfilerThreadId mainTestThreadId = profiler_current_thread_id();
+ EXPECT_TRUE(mainTestThreadId.IsSpecified());
+
+ ProfilerThreadId mainThreadId = profiler_main_thread_id();
+ EXPECT_TRUE(mainThreadId.IsSpecified());
+
+ EXPECT_EQ(mainThreadId, mainTestThreadId)
+ << "Test should run on the main thread";
+ EXPECT_TRUE(profiler_is_main_thread());
+
+ std::thread testThread([&]() {
+ EXPECT_EQ(profiler_current_process_id(), processId);
+
+ const ProfilerThreadId testThreadId = profiler_current_thread_id();
+ EXPECT_TRUE(testThreadId.IsSpecified());
+ EXPECT_NE(testThreadId, mainThreadId);
+ EXPECT_FALSE(profiler_is_main_thread());
+
+ EXPECT_EQ(baseprofiler::profiler_current_process_id(), processId);
+ EXPECT_EQ(baseprofiler::profiler_current_thread_id(), testThreadId);
+ EXPECT_EQ(baseprofiler::profiler_main_thread_id(), mainThreadId);
+ EXPECT_FALSE(baseprofiler::profiler_is_main_thread());
+ });
+ testThread.join();
+}
+
+TEST(GeckoProfiler, ThreadRegistrationInfo)
+{
+ profiler_init_main_thread_id();
+
+ TimeStamp ts = TimeStamp::Now();
+ {
+ profiler::ThreadRegistrationInfo trInfo{
+ "name", ProfilerThreadId::FromNumber(123), false, ts};
+ EXPECT_STREQ(trInfo.Name(), "name");
+ EXPECT_NE(trInfo.Name(), "name")
+ << "ThreadRegistrationInfo should keep its own copy of the name";
+ EXPECT_EQ(trInfo.RegisterTime(), ts);
+ EXPECT_EQ(trInfo.ThreadId(), ProfilerThreadId::FromNumber(123));
+ EXPECT_EQ(trInfo.IsMainThread(), false);
+ }
+
+ // Make sure the next timestamp will be different from `ts`.
+ while (TimeStamp::Now() == ts) {
+ }
+
+ {
+ profiler::ThreadRegistrationInfo trInfoHere{"Here"};
+ EXPECT_STREQ(trInfoHere.Name(), "Here");
+ EXPECT_NE(trInfoHere.Name(), "Here")
+ << "ThreadRegistrationInfo should keep its own copy of the name";
+ TimeStamp baseRegistrationTime =
+ baseprofiler::detail::GetThreadRegistrationTime();
+ if (baseRegistrationTime) {
+ EXPECT_EQ(trInfoHere.RegisterTime(), baseRegistrationTime);
+ } else {
+ EXPECT_GT(trInfoHere.RegisterTime(), ts);
+ }
+ EXPECT_EQ(trInfoHere.ThreadId(), profiler_current_thread_id());
+ EXPECT_EQ(trInfoHere.ThreadId(), profiler_main_thread_id())
+ << "Gtests are assumed to run on the main thread";
+ EXPECT_EQ(trInfoHere.IsMainThread(), true)
+ << "Gtests are assumed to run on the main thread";
+ }
+
+ {
+ // Sub-thread test.
+ // These will receive sub-thread data (to test move at thread end).
+ TimeStamp tsThread;
+ ProfilerThreadId threadThreadId;
+ UniquePtr<profiler::ThreadRegistrationInfo> trInfoThreadPtr;
+
+ std::thread testThread([&]() {
+ profiler::ThreadRegistrationInfo trInfoThread{"Thread"};
+ EXPECT_STREQ(trInfoThread.Name(), "Thread");
+ EXPECT_NE(trInfoThread.Name(), "Thread")
+ << "ThreadRegistrationInfo should keep its own copy of the name";
+ EXPECT_GT(trInfoThread.RegisterTime(), ts);
+ EXPECT_EQ(trInfoThread.ThreadId(), profiler_current_thread_id());
+ EXPECT_NE(trInfoThread.ThreadId(), profiler_main_thread_id());
+ EXPECT_EQ(trInfoThread.IsMainThread(), false);
+
+ tsThread = trInfoThread.RegisterTime();
+ threadThreadId = trInfoThread.ThreadId();
+ trInfoThreadPtr =
+ MakeUnique<profiler::ThreadRegistrationInfo>(std::move(trInfoThread));
+ });
+ testThread.join();
+
+ ASSERT_NE(trInfoThreadPtr, nullptr);
+ EXPECT_STREQ(trInfoThreadPtr->Name(), "Thread");
+ EXPECT_EQ(trInfoThreadPtr->RegisterTime(), tsThread);
+ EXPECT_EQ(trInfoThreadPtr->ThreadId(), threadThreadId);
+ EXPECT_EQ(trInfoThreadPtr->IsMainThread(), false)
+ << "Gtests are assumed to run on the main thread";
+ }
+}
+
+static constexpr ThreadProfilingFeatures scEachAndAnyThreadProfilingFeatures[] =
+ {ThreadProfilingFeatures::CPUUtilization, ThreadProfilingFeatures::Sampling,
+ ThreadProfilingFeatures::Markers, ThreadProfilingFeatures::Any};
+
+TEST(GeckoProfiler, ThreadProfilingFeaturesType)
+{
+ ASSERT_EQ(static_cast<uint32_t>(ThreadProfilingFeatures::Any), 1u + 2u + 4u)
+ << "This test assumes that there are 3 binary choices 1+2+4; "
+ "Is this test up to date?";
+
+ EXPECT_EQ(Combine(ThreadProfilingFeatures::CPUUtilization,
+ ThreadProfilingFeatures::Sampling,
+ ThreadProfilingFeatures::Markers),
+ ThreadProfilingFeatures::Any);
+
+ constexpr ThreadProfilingFeatures allThreadProfilingFeatures[] = {
+ ThreadProfilingFeatures::NotProfiled,
+ ThreadProfilingFeatures::CPUUtilization,
+ ThreadProfilingFeatures::Sampling, ThreadProfilingFeatures::Markers,
+ ThreadProfilingFeatures::Any};
+
+ for (ThreadProfilingFeatures f1 : allThreadProfilingFeatures) {
+ // Combine and Intersect are commutative.
+ for (ThreadProfilingFeatures f2 : allThreadProfilingFeatures) {
+ EXPECT_EQ(Combine(f1, f2), Combine(f2, f1));
+ EXPECT_EQ(Intersect(f1, f2), Intersect(f2, f1));
+ }
+
+ // Combine works like OR.
+ EXPECT_EQ(Combine(f1, f1), f1);
+ EXPECT_EQ(Combine(f1, f1, f1), f1);
+
+ // 'OR NotProfiled' doesn't change anything.
+ EXPECT_EQ(Combine(f1, ThreadProfilingFeatures::NotProfiled), f1);
+
+ // 'OR Any' makes Any.
+ EXPECT_EQ(Combine(f1, ThreadProfilingFeatures::Any),
+ ThreadProfilingFeatures::Any);
+
+ // Intersect works like AND.
+ EXPECT_EQ(Intersect(f1, f1), f1);
+ EXPECT_EQ(Intersect(f1, f1, f1), f1);
+
+ // 'AND NotProfiled' erases anything.
+ EXPECT_EQ(Intersect(f1, ThreadProfilingFeatures::NotProfiled),
+ ThreadProfilingFeatures::NotProfiled);
+
+ // 'AND Any' doesn't change anything.
+ EXPECT_EQ(Intersect(f1, ThreadProfilingFeatures::Any), f1);
+ }
+
+ for (ThreadProfilingFeatures f1 : scEachAndAnyThreadProfilingFeatures) {
+ EXPECT_TRUE(DoFeaturesIntersect(f1, f1));
+
+ // NotProfiled doesn't intersect with any feature.
+ EXPECT_FALSE(DoFeaturesIntersect(f1, ThreadProfilingFeatures::NotProfiled));
+
+ // Any intersects with any feature.
+ EXPECT_TRUE(DoFeaturesIntersect(f1, ThreadProfilingFeatures::Any));
+ }
+}
+
+static void TestConstUnlockedConstReader(
+ const profiler::ThreadRegistration::UnlockedConstReader& aData,
+ const TimeStamp& aBeforeRegistration, const TimeStamp& aAfterRegistration,
+ const void* aOnStackObject,
+ ProfilerThreadId aThreadId = profiler_current_thread_id()) {
+ EXPECT_STREQ(aData.Info().Name(), "Test thread");
+ EXPECT_GE(aData.Info().RegisterTime(), aBeforeRegistration);
+ EXPECT_LE(aData.Info().RegisterTime(), aAfterRegistration);
+ EXPECT_EQ(aData.Info().ThreadId(), aThreadId);
+ EXPECT_FALSE(aData.Info().IsMainThread());
+
+#if (defined(_MSC_VER) || defined(__MINGW32__)) && defined(MOZ_GECKO_PROFILER)
+ HANDLE threadHandle = aData.PlatformDataCRef().ProfiledThread();
+ EXPECT_NE(threadHandle, nullptr);
+ EXPECT_EQ(ProfilerThreadId::FromNumber(::GetThreadId(threadHandle)),
+ aThreadId);
+ // Test calling QueryThreadCycleTime, we cannot assume that it will always
+ // work, but at least it shouldn't crash.
+ ULONG64 cycles;
+ (void)QueryThreadCycleTime(threadHandle, &cycles);
+#elif defined(__APPLE__) && defined(MOZ_GECKO_PROFILER)
+ // Test calling thread_info, we cannot assume that it will always work, but at
+ // least it shouldn't crash.
+ thread_basic_info_data_t threadBasicInfo;
+ mach_msg_type_number_t basicCount = THREAD_BASIC_INFO_COUNT;
+ (void)thread_info(
+ aData.PlatformDataCRef().ProfiledThread(), THREAD_BASIC_INFO,
+ reinterpret_cast<thread_info_t>(&threadBasicInfo), &basicCount);
+#elif (defined(__linux__) || defined(__ANDROID__) || defined(__FreeBSD__)) && \
+ defined(MOZ_GECKO_PROFILER)
+ // Test calling GetClockId, we cannot assume that it will always work, but at
+ // least it shouldn't crash.
+ Maybe<clockid_t> maybeClockId = aData.PlatformDataCRef().GetClockId();
+ if (maybeClockId) {
+ // Test calling clock_gettime, we cannot assume that it will always work,
+ // but at least it shouldn't crash.
+ timespec ts;
+ (void)clock_gettime(*maybeClockId, &ts);
+ }
+#else
+ (void)aData.PlatformDataCRef();
+#endif
+
+ EXPECT_GE(aData.StackTop(), aOnStackObject)
+ << "StackTop should be at &onStackChar, or higher on some "
+ "platforms";
+};
+
+static void TestConstUnlockedConstReaderAndAtomicRW(
+ const profiler::ThreadRegistration::UnlockedConstReaderAndAtomicRW& aData,
+ const TimeStamp& aBeforeRegistration, const TimeStamp& aAfterRegistration,
+ const void* aOnStackObject,
+ ProfilerThreadId aThreadId = profiler_current_thread_id()) {
+ TestConstUnlockedConstReader(aData, aBeforeRegistration, aAfterRegistration,
+ aOnStackObject, aThreadId);
+
+ (void)aData.ProfilingStackCRef();
+
+ EXPECT_EQ(aData.ProfilingFeatures(), ThreadProfilingFeatures::NotProfiled);
+
+ EXPECT_FALSE(aData.IsSleeping());
+};
+
+static void TestUnlockedConstReaderAndAtomicRW(
+ profiler::ThreadRegistration::UnlockedConstReaderAndAtomicRW& aData,
+ const TimeStamp& aBeforeRegistration, const TimeStamp& aAfterRegistration,
+ const void* aOnStackObject,
+ ProfilerThreadId aThreadId = profiler_current_thread_id()) {
+ TestConstUnlockedConstReaderAndAtomicRW(aData, aBeforeRegistration,
+ aAfterRegistration, aOnStackObject,
+ aThreadId);
+
+ (void)aData.ProfilingStackRef();
+
+ EXPECT_FALSE(aData.IsSleeping());
+ aData.SetSleeping();
+ EXPECT_TRUE(aData.IsSleeping());
+ aData.SetAwake();
+ EXPECT_FALSE(aData.IsSleeping());
+
+ aData.ReinitializeOnResume();
+
+ EXPECT_FALSE(aData.CanDuplicateLastSampleDueToSleep());
+ EXPECT_FALSE(aData.CanDuplicateLastSampleDueToSleep());
+ aData.SetSleeping();
+ // After sleeping, the 2nd+ calls can duplicate.
+ EXPECT_FALSE(aData.CanDuplicateLastSampleDueToSleep());
+ EXPECT_TRUE(aData.CanDuplicateLastSampleDueToSleep());
+ EXPECT_TRUE(aData.CanDuplicateLastSampleDueToSleep());
+ aData.ReinitializeOnResume();
+ // After reinit (and sleeping), the 2nd+ calls can duplicate.
+ EXPECT_FALSE(aData.CanDuplicateLastSampleDueToSleep());
+ EXPECT_TRUE(aData.CanDuplicateLastSampleDueToSleep());
+ EXPECT_TRUE(aData.CanDuplicateLastSampleDueToSleep());
+ aData.SetAwake();
+ EXPECT_FALSE(aData.CanDuplicateLastSampleDueToSleep());
+ EXPECT_FALSE(aData.CanDuplicateLastSampleDueToSleep());
+};
+
+static void TestConstUnlockedRWForLockedProfiler(
+ const profiler::ThreadRegistration::UnlockedRWForLockedProfiler& aData,
+ const TimeStamp& aBeforeRegistration, const TimeStamp& aAfterRegistration,
+ const void* aOnStackObject,
+ ProfilerThreadId aThreadId = profiler_current_thread_id()) {
+ TestConstUnlockedConstReaderAndAtomicRW(aData, aBeforeRegistration,
+ aAfterRegistration, aOnStackObject,
+ aThreadId);
+
+ // We can't create a PSAutoLock here, so just verify that the call would
+ // compile and return the expected type.
+ static_assert(std::is_same_v<decltype(aData.GetProfiledThreadData(
+ std::declval<PSAutoLock>())),
+ const ProfiledThreadData*>);
+};
+
+static void TestConstUnlockedReaderAndAtomicRWOnThread(
+ const profiler::ThreadRegistration::UnlockedReaderAndAtomicRWOnThread&
+ aData,
+ const TimeStamp& aBeforeRegistration, const TimeStamp& aAfterRegistration,
+ const void* aOnStackObject,
+ ProfilerThreadId aThreadId = profiler_current_thread_id()) {
+ TestConstUnlockedRWForLockedProfiler(aData, aBeforeRegistration,
+ aAfterRegistration, aOnStackObject,
+ aThreadId);
+
+ EXPECT_EQ(aData.GetJSContext(), nullptr);
+};
+
+static void TestUnlockedRWForLockedProfiler(
+ profiler::ThreadRegistration::UnlockedRWForLockedProfiler& aData,
+ const TimeStamp& aBeforeRegistration, const TimeStamp& aAfterRegistration,
+ const void* aOnStackObject,
+ ProfilerThreadId aThreadId = profiler_current_thread_id()) {
+ TestConstUnlockedRWForLockedProfiler(aData, aBeforeRegistration,
+ aAfterRegistration, aOnStackObject,
+ aThreadId);
+ TestUnlockedConstReaderAndAtomicRW(aData, aBeforeRegistration,
+ aAfterRegistration, aOnStackObject,
+ aThreadId);
+
+ // No functions to test here.
+};
+
+static void TestUnlockedReaderAndAtomicRWOnThread(
+ profiler::ThreadRegistration::UnlockedReaderAndAtomicRWOnThread& aData,
+ const TimeStamp& aBeforeRegistration, const TimeStamp& aAfterRegistration,
+ const void* aOnStackObject,
+ ProfilerThreadId aThreadId = profiler_current_thread_id()) {
+ TestConstUnlockedReaderAndAtomicRWOnThread(aData, aBeforeRegistration,
+ aAfterRegistration, aOnStackObject,
+ aThreadId);
+ TestUnlockedRWForLockedProfiler(aData, aBeforeRegistration,
+ aAfterRegistration, aOnStackObject,
+ aThreadId);
+
+ // No functions to test here.
+};
+
+static void TestConstLockedRWFromAnyThread(
+ const profiler::ThreadRegistration::LockedRWFromAnyThread& aData,
+ const TimeStamp& aBeforeRegistration, const TimeStamp& aAfterRegistration,
+ const void* aOnStackObject,
+ ProfilerThreadId aThreadId = profiler_current_thread_id()) {
+ TestConstUnlockedReaderAndAtomicRWOnThread(aData, aBeforeRegistration,
+ aAfterRegistration, aOnStackObject,
+ aThreadId);
+
+ EXPECT_EQ(aData.GetJsFrameBuffer(), nullptr);
+ EXPECT_EQ(aData.GetEventTarget(), nullptr);
+};
+
+static void TestLockedRWFromAnyThread(
+ profiler::ThreadRegistration::LockedRWFromAnyThread& aData,
+ const TimeStamp& aBeforeRegistration, const TimeStamp& aAfterRegistration,
+ const void* aOnStackObject,
+ ProfilerThreadId aThreadId = profiler_current_thread_id()) {
+ TestConstLockedRWFromAnyThread(aData, aBeforeRegistration, aAfterRegistration,
+ aOnStackObject, aThreadId);
+ TestUnlockedReaderAndAtomicRWOnThread(aData, aBeforeRegistration,
+ aAfterRegistration, aOnStackObject,
+ aThreadId);
+
+ // We can't create a ProfiledThreadData nor PSAutoLock here, so just verify
+ // that the call would compile and return the expected type.
+ static_assert(std::is_same_v<decltype(aData.SetProfilingFeaturesAndData(
+ std::declval<ThreadProfilingFeatures>(),
+ std::declval<ProfiledThreadData*>(),
+ std::declval<PSAutoLock>())),
+ void>);
+
+ aData.ResetMainThread(nullptr);
+
+ TimeDuration delay = TimeDuration::FromSeconds(1);
+ TimeDuration running = TimeDuration::FromSeconds(1);
+ aData.GetRunningEventDelay(TimeStamp::Now(), delay, running);
+ EXPECT_TRUE(delay.IsZero());
+ EXPECT_TRUE(running.IsZero());
+
+ aData.StartJSSampling(123u);
+ aData.StopJSSampling();
+};
+
+static void TestConstLockedRWOnThread(
+ const profiler::ThreadRegistration::LockedRWOnThread& aData,
+ const TimeStamp& aBeforeRegistration, const TimeStamp& aAfterRegistration,
+ const void* aOnStackObject,
+ ProfilerThreadId aThreadId = profiler_current_thread_id()) {
+ TestConstLockedRWFromAnyThread(aData, aBeforeRegistration, aAfterRegistration,
+ aOnStackObject, aThreadId);
+
+ // No functions to test here.
+};
+
+static void TestLockedRWOnThread(
+ profiler::ThreadRegistration::LockedRWOnThread& aData,
+ const TimeStamp& aBeforeRegistration, const TimeStamp& aAfterRegistration,
+ const void* aOnStackObject,
+ ProfilerThreadId aThreadId = profiler_current_thread_id()) {
+ TestConstLockedRWOnThread(aData, aBeforeRegistration, aAfterRegistration,
+ aOnStackObject, aThreadId);
+ TestLockedRWFromAnyThread(aData, aBeforeRegistration, aAfterRegistration,
+ aOnStackObject, aThreadId);
+
+ // We don't want to really call SetJSContext here, so just verify that
+ // the call would compile and return the expected type.
+ static_assert(
+ std::is_same_v<decltype(aData.SetJSContext(std::declval<JSContext*>())),
+ void>);
+ aData.ClearJSContext();
+ aData.PollJSSampling();
+};
+
+TEST(GeckoProfiler, ThreadRegistration_DataAccess)
+{
+ using TR = profiler::ThreadRegistration;
+
+ profiler_init_main_thread_id();
+ ASSERT_TRUE(profiler_is_main_thread())
+ << "This test assumes it runs on the main thread";
+
+ // Note that the main thread could already be registered, so we work in a new
+ // thread to test an actual registration that we control.
+
+ std::thread testThread([&]() {
+ ASSERT_FALSE(TR::IsRegistered())
+ << "A new std::thread should not start registered";
+ EXPECT_FALSE(TR::GetOnThreadPtr());
+ EXPECT_FALSE(TR::WithOnThreadRefOr([&](auto) { return true; }, false));
+
+ char onStackChar;
+
+ TimeStamp beforeRegistration = TimeStamp::Now();
+ TR tr{"Test thread", &onStackChar};
+ TimeStamp afterRegistration = TimeStamp::Now();
+
+ ASSERT_TRUE(TR::IsRegistered());
+
+ // Note: This test will mostly be about checking the correct access to
+ // thread data, depending on how it's obtained. Not all the functionality
+ // related to that data is tested (e.g., because it involves JS or other
+ // external dependencies that would be difficult to control here.)
+
+ auto TestOnThreadRef = [&](TR::OnThreadRef aOnThreadRef) {
+ // To test const-qualified member functions.
+ const TR::OnThreadRef& onThreadCRef = aOnThreadRef;
+
+ // const UnlockedConstReader (always const)
+
+ TestConstUnlockedConstReader(onThreadCRef.UnlockedConstReaderCRef(),
+ beforeRegistration, afterRegistration,
+ &onStackChar);
+ onThreadCRef.WithUnlockedConstReader(
+ [&](const TR::UnlockedConstReader& aData) {
+ TestConstUnlockedConstReader(aData, beforeRegistration,
+ afterRegistration, &onStackChar);
+ });
+
+ // const UnlockedConstReaderAndAtomicRW
+
+ TestConstUnlockedConstReaderAndAtomicRW(
+ onThreadCRef.UnlockedConstReaderAndAtomicRWCRef(), beforeRegistration,
+ afterRegistration, &onStackChar);
+ onThreadCRef.WithUnlockedConstReaderAndAtomicRW(
+ [&](const TR::UnlockedConstReaderAndAtomicRW& aData) {
+ TestConstUnlockedConstReaderAndAtomicRW(
+ aData, beforeRegistration, afterRegistration, &onStackChar);
+ });
+
+ // non-const UnlockedConstReaderAndAtomicRW
+
+ TestUnlockedConstReaderAndAtomicRW(
+ aOnThreadRef.UnlockedConstReaderAndAtomicRWRef(), beforeRegistration,
+ afterRegistration, &onStackChar);
+ aOnThreadRef.WithUnlockedConstReaderAndAtomicRW(
+ [&](TR::UnlockedConstReaderAndAtomicRW& aData) {
+ TestUnlockedConstReaderAndAtomicRW(aData, beforeRegistration,
+ afterRegistration, &onStackChar);
+ });
+
+ // const UnlockedRWForLockedProfiler
+
+ TestConstUnlockedRWForLockedProfiler(
+ onThreadCRef.UnlockedRWForLockedProfilerCRef(), beforeRegistration,
+ afterRegistration, &onStackChar);
+ onThreadCRef.WithUnlockedRWForLockedProfiler(
+ [&](const TR::UnlockedRWForLockedProfiler& aData) {
+ TestConstUnlockedRWForLockedProfiler(
+ aData, beforeRegistration, afterRegistration, &onStackChar);
+ });
+
+ // non-const UnlockedRWForLockedProfiler
+
+ TestUnlockedRWForLockedProfiler(
+ aOnThreadRef.UnlockedRWForLockedProfilerRef(), beforeRegistration,
+ afterRegistration, &onStackChar);
+ aOnThreadRef.WithUnlockedRWForLockedProfiler(
+ [&](TR::UnlockedRWForLockedProfiler& aData) {
+ TestUnlockedRWForLockedProfiler(aData, beforeRegistration,
+ afterRegistration, &onStackChar);
+ });
+
+ // const UnlockedReaderAndAtomicRWOnThread
+
+ TestConstUnlockedReaderAndAtomicRWOnThread(
+ onThreadCRef.UnlockedReaderAndAtomicRWOnThreadCRef(),
+ beforeRegistration, afterRegistration, &onStackChar);
+ onThreadCRef.WithUnlockedReaderAndAtomicRWOnThread(
+ [&](const TR::UnlockedReaderAndAtomicRWOnThread& aData) {
+ TestConstUnlockedReaderAndAtomicRWOnThread(
+ aData, beforeRegistration, afterRegistration, &onStackChar);
+ });
+
+ // non-const UnlockedReaderAndAtomicRWOnThread
+
+ TestUnlockedReaderAndAtomicRWOnThread(
+ aOnThreadRef.UnlockedReaderAndAtomicRWOnThreadRef(),
+ beforeRegistration, afterRegistration, &onStackChar);
+ aOnThreadRef.WithUnlockedReaderAndAtomicRWOnThread(
+ [&](TR::UnlockedReaderAndAtomicRWOnThread& aData) {
+ TestUnlockedReaderAndAtomicRWOnThread(
+ aData, beforeRegistration, afterRegistration, &onStackChar);
+ });
+
+ // LockedRWFromAnyThread
+ // Note: It cannot directly be accessed on the thread, this will be
+ // tested through LockedRWOnThread.
+
+ // const LockedRWOnThread
+
+ EXPECT_FALSE(TR::IsDataMutexLockedOnCurrentThread());
+ {
+ TR::OnThreadRef::ConstRWOnThreadWithLock constRWOnThreadWithLock =
+ onThreadCRef.ConstLockedRWOnThread();
+ EXPECT_TRUE(TR::IsDataMutexLockedOnCurrentThread());
+ TestConstLockedRWOnThread(constRWOnThreadWithLock.DataCRef(),
+ beforeRegistration, afterRegistration,
+ &onStackChar);
+ }
+ EXPECT_FALSE(TR::IsDataMutexLockedOnCurrentThread());
+ onThreadCRef.WithConstLockedRWOnThread(
+ [&](const TR::LockedRWOnThread& aData) {
+ EXPECT_TRUE(TR::IsDataMutexLockedOnCurrentThread());
+ TestConstLockedRWOnThread(aData, beforeRegistration,
+ afterRegistration, &onStackChar);
+ });
+ EXPECT_FALSE(TR::IsDataMutexLockedOnCurrentThread());
+
+ // non-const LockedRWOnThread
+
+ EXPECT_FALSE(TR::IsDataMutexLockedOnCurrentThread());
+ {
+ TR::OnThreadRef::RWOnThreadWithLock rwOnThreadWithLock =
+ aOnThreadRef.GetLockedRWOnThread();
+ EXPECT_TRUE(TR::IsDataMutexLockedOnCurrentThread());
+ TestConstLockedRWOnThread(rwOnThreadWithLock.DataCRef(),
+ beforeRegistration, afterRegistration,
+ &onStackChar);
+ TestLockedRWOnThread(rwOnThreadWithLock.DataRef(), beforeRegistration,
+ afterRegistration, &onStackChar);
+ }
+ EXPECT_FALSE(TR::IsDataMutexLockedOnCurrentThread());
+ aOnThreadRef.WithLockedRWOnThread([&](TR::LockedRWOnThread& aData) {
+ EXPECT_TRUE(TR::IsDataMutexLockedOnCurrentThread());
+ TestLockedRWOnThread(aData, beforeRegistration, afterRegistration,
+ &onStackChar);
+ });
+ EXPECT_FALSE(TR::IsDataMutexLockedOnCurrentThread());
+ };
+
+ TR::OnThreadPtr onThreadPtr = TR::GetOnThreadPtr();
+ ASSERT_TRUE(onThreadPtr);
+ TestOnThreadRef(*onThreadPtr);
+
+ TR::WithOnThreadRef(
+ [&](TR::OnThreadRef aOnThreadRef) { TestOnThreadRef(aOnThreadRef); });
+
+ EXPECT_TRUE(TR::WithOnThreadRefOr(
+ [&](TR::OnThreadRef aOnThreadRef) {
+ TestOnThreadRef(aOnThreadRef);
+ return true;
+ },
+ false));
+ });
+ testThread.join();
+}
+
+// Thread name if registered, nullptr otherwise.
+static const char* GetThreadName() {
+ return profiler::ThreadRegistration::WithOnThreadRefOr(
+ [](profiler::ThreadRegistration::OnThreadRef onThreadRef) {
+ return onThreadRef.WithUnlockedConstReader(
+ [](const profiler::ThreadRegistration::UnlockedConstReader& aData) {
+ return aData.Info().Name();
+ });
+ },
+ nullptr);
+}
+
+// Get the thread name, as registered in the PRThread, nullptr on failure.
+static const char* GetPRThreadName() {
+ nsIThread* nsThread = NS_GetCurrentThread();
+ if (!nsThread) {
+ return nullptr;
+ }
+ PRThread* prThread = nullptr;
+ if (NS_FAILED(nsThread->GetPRThread(&prThread))) {
+ return nullptr;
+ }
+ if (!prThread) {
+ return nullptr;
+ }
+ return PR_GetThreadName(prThread);
+}
+
+TEST(GeckoProfiler, ThreadRegistration_MainThreadName)
+{
+ EXPECT_TRUE(profiler::ThreadRegistration::IsRegistered());
+ EXPECT_STREQ(GetThreadName(), "GeckoMain");
+
+ // Check that the real thread name (outside the profiler) is *not* GeckoMain.
+ EXPECT_STRNE(GetPRThreadName(), "GeckoMain");
+}
+
+TEST(GeckoProfiler, ThreadRegistration_NestedRegistrations)
+{
+ using TR = profiler::ThreadRegistration;
+
+ profiler_init_main_thread_id();
+ ASSERT_TRUE(profiler_is_main_thread())
+ << "This test assumes it runs on the main thread";
+
+ // Note that the main thread could already be registered, so we work in a new
+ // thread to test actual registrations that we control.
+
+ std::thread testThread([&]() {
+ ASSERT_FALSE(TR::IsRegistered())
+ << "A new std::thread should not start registered";
+
+ char onStackChar;
+
+ // Blocks {} are mostly for clarity, but some control on-stack registration
+ // lifetimes.
+
+ // On-stack registration.
+ {
+ TR rt{"Test thread #1", &onStackChar};
+ ASSERT_TRUE(TR::IsRegistered());
+ EXPECT_STREQ(GetThreadName(), "Test thread #1");
+ EXPECT_STREQ(GetPRThreadName(), "Test thread #1");
+ }
+ ASSERT_FALSE(TR::IsRegistered());
+
+ // Off-stack registration.
+ {
+ TR::RegisterThread("Test thread #2", &onStackChar);
+ ASSERT_TRUE(TR::IsRegistered());
+ EXPECT_STREQ(GetThreadName(), "Test thread #2");
+ EXPECT_STREQ(GetPRThreadName(), "Test thread #2");
+
+ TR::UnregisterThread();
+ ASSERT_FALSE(TR::IsRegistered());
+ }
+
+ // Extra un-registration should be ignored.
+ TR::UnregisterThread();
+ ASSERT_FALSE(TR::IsRegistered());
+
+ // Nested on-stack.
+ {
+ TR rt2{"Test thread #3", &onStackChar};
+ ASSERT_TRUE(TR::IsRegistered());
+ EXPECT_STREQ(GetThreadName(), "Test thread #3");
+ EXPECT_STREQ(GetPRThreadName(), "Test thread #3");
+
+ {
+ TR rt3{"Test thread #4", &onStackChar};
+ ASSERT_TRUE(TR::IsRegistered());
+ EXPECT_STREQ(GetThreadName(), "Test thread #3")
+ << "Nested registration shouldn't change the name";
+ EXPECT_STREQ(GetPRThreadName(), "Test thread #3")
+ << "Nested registration shouldn't change the PRThread name";
+ }
+ ASSERT_TRUE(TR::IsRegistered())
+ << "Thread should still be registered after nested un-registration";
+ EXPECT_STREQ(GetThreadName(), "Test thread #3")
+ << "Thread should still be registered after nested un-registration";
+ EXPECT_STREQ(GetPRThreadName(), "Test thread #3");
+ }
+ ASSERT_FALSE(TR::IsRegistered());
+
+ // Nested off-stack.
+ {
+ TR::RegisterThread("Test thread #5", &onStackChar);
+ ASSERT_TRUE(TR::IsRegistered());
+ EXPECT_STREQ(GetThreadName(), "Test thread #5");
+ EXPECT_STREQ(GetPRThreadName(), "Test thread #5");
+
+ {
+ TR::RegisterThread("Test thread #6", &onStackChar);
+ ASSERT_TRUE(TR::IsRegistered());
+ EXPECT_STREQ(GetThreadName(), "Test thread #5")
+ << "Nested registration shouldn't change the name";
+ EXPECT_STREQ(GetPRThreadName(), "Test thread #5")
+ << "Nested registration shouldn't change the PRThread name";
+
+ TR::UnregisterThread();
+ ASSERT_TRUE(TR::IsRegistered())
+ << "Thread should still be registered after nested un-registration";
+ EXPECT_STREQ(GetThreadName(), "Test thread #5")
+ << "Thread should still be registered after nested un-registration";
+ EXPECT_STREQ(GetPRThreadName(), "Test thread #5");
+ }
+
+ TR::UnregisterThread();
+ ASSERT_FALSE(TR::IsRegistered());
+ }
+
+ // Nested on- and off-stack.
+ {
+ TR rt2{"Test thread #7", &onStackChar};
+ ASSERT_TRUE(TR::IsRegistered());
+ EXPECT_STREQ(GetThreadName(), "Test thread #7");
+ EXPECT_STREQ(GetPRThreadName(), "Test thread #7");
+
+ {
+ TR::RegisterThread("Test thread #8", &onStackChar);
+ ASSERT_TRUE(TR::IsRegistered());
+ EXPECT_STREQ(GetThreadName(), "Test thread #7")
+ << "Nested registration shouldn't change the name";
+ EXPECT_STREQ(GetPRThreadName(), "Test thread #7")
+ << "Nested registration shouldn't change the PRThread name";
+
+ TR::UnregisterThread();
+ ASSERT_TRUE(TR::IsRegistered())
+ << "Thread should still be registered after nested un-registration";
+ EXPECT_STREQ(GetThreadName(), "Test thread #7")
+ << "Thread should still be registered after nested un-registration";
+ EXPECT_STREQ(GetPRThreadName(), "Test thread #7");
+ }
+ }
+ ASSERT_FALSE(TR::IsRegistered());
+
+ // Nested off- and on-stack.
+ {
+ TR::RegisterThread("Test thread #9", &onStackChar);
+ ASSERT_TRUE(TR::IsRegistered());
+ EXPECT_STREQ(GetThreadName(), "Test thread #9");
+ EXPECT_STREQ(GetPRThreadName(), "Test thread #9");
+
+ {
+ TR rt3{"Test thread #10", &onStackChar};
+ ASSERT_TRUE(TR::IsRegistered());
+ EXPECT_STREQ(GetThreadName(), "Test thread #9")
+ << "Nested registration shouldn't change the name";
+ EXPECT_STREQ(GetPRThreadName(), "Test thread #9")
+ << "Nested registration shouldn't change the PRThread name";
+ }
+ ASSERT_TRUE(TR::IsRegistered())
+ << "Thread should still be registered after nested un-registration";
+ EXPECT_STREQ(GetThreadName(), "Test thread #9")
+ << "Thread should still be registered after nested un-registration";
+ EXPECT_STREQ(GetPRThreadName(), "Test thread #9");
+
+ TR::UnregisterThread();
+ ASSERT_FALSE(TR::IsRegistered());
+ }
+
+ // Excess UnregisterThread with on-stack TR.
+ {
+ TR rt2{"Test thread #11", &onStackChar};
+ ASSERT_TRUE(TR::IsRegistered());
+ EXPECT_STREQ(GetThreadName(), "Test thread #11");
+ EXPECT_STREQ(GetPRThreadName(), "Test thread #11");
+
+ TR::UnregisterThread();
+ ASSERT_TRUE(TR::IsRegistered())
+ << "On-stack thread should still be registered after off-stack "
+ "un-registration";
+ EXPECT_STREQ(GetThreadName(), "Test thread #11")
+ << "On-stack thread should still be registered after off-stack "
+ "un-registration";
+ EXPECT_STREQ(GetPRThreadName(), "Test thread #11");
+ }
+ ASSERT_FALSE(TR::IsRegistered());
+
+ // Excess on-thread TR destruction with already-unregistered root off-thread
+ // registration.
+ {
+ TR::RegisterThread("Test thread #12", &onStackChar);
+ ASSERT_TRUE(TR::IsRegistered());
+ EXPECT_STREQ(GetThreadName(), "Test thread #12");
+ EXPECT_STREQ(GetPRThreadName(), "Test thread #12");
+
+ {
+ TR rt3{"Test thread #13", &onStackChar};
+ ASSERT_TRUE(TR::IsRegistered());
+ EXPECT_STREQ(GetThreadName(), "Test thread #12")
+ << "Nested registration shouldn't change the name";
+ EXPECT_STREQ(GetPRThreadName(), "Test thread #12")
+ << "Nested registration shouldn't change the PRThread name";
+
+ // Note that we unregister the root registration, while nested `rt3` is
+ // still alive.
+ TR::UnregisterThread();
+ ASSERT_FALSE(TR::IsRegistered())
+ << "UnregisterThread() of the root RegisterThread() should always work";
+
+ // At this end of this block, `rt3` will be destroyed, but nothing
+ // should happen.
+ }
+ ASSERT_FALSE(TR::IsRegistered());
+ }
+
+ ASSERT_FALSE(TR::IsRegistered());
+ });
+ testThread.join();
+}
+
+TEST(GeckoProfiler, ThreadRegistry_DataAccess)
+{
+ using TR = profiler::ThreadRegistration;
+ using TRy = profiler::ThreadRegistry;
+
+ profiler_init_main_thread_id();
+ ASSERT_TRUE(profiler_is_main_thread())
+ << "This test assumes it runs on the main thread";
+
+ // Note that the main thread could already be registered, so we work in a new
+ // thread to test an actual registration that we control.
+
+ std::thread testThread([&]() {
+ ASSERT_FALSE(TR::IsRegistered())
+ << "A new std::thread should not start registered";
+ EXPECT_FALSE(TR::GetOnThreadPtr());
+ EXPECT_FALSE(TR::WithOnThreadRefOr([&](auto) { return true; }, false));
+
+ char onStackChar;
+
+ TimeStamp beforeRegistration = TimeStamp::Now();
+ TR tr{"Test thread", &onStackChar};
+ TimeStamp afterRegistration = TimeStamp::Now();
+
+ ASSERT_TRUE(TR::IsRegistered());
+
+ // Note: This test will mostly be about checking the correct access to
+ // thread data, depending on how it's obtained. Not all the functionality
+ // related to that data is tested (e.g., because it involves JS or other
+ // external dependencies that would be difficult to control here.)
+
+ const ProfilerThreadId testThreadId = profiler_current_thread_id();
+
+ auto testThroughRegistry = [&]() {
+ auto TestOffThreadRef = [&](TRy::OffThreadRef aOffThreadRef) {
+ // To test const-qualified member functions.
+ const TRy::OffThreadRef& offThreadCRef = aOffThreadRef;
+
+ // const UnlockedConstReader (always const)
+
+ TestConstUnlockedConstReader(offThreadCRef.UnlockedConstReaderCRef(),
+ beforeRegistration, afterRegistration,
+ &onStackChar, testThreadId);
+ offThreadCRef.WithUnlockedConstReader(
+ [&](const TR::UnlockedConstReader& aData) {
+ TestConstUnlockedConstReader(aData, beforeRegistration,
+ afterRegistration, &onStackChar,
+ testThreadId);
+ });
+
+ // const UnlockedConstReaderAndAtomicRW
+
+ TestConstUnlockedConstReaderAndAtomicRW(
+ offThreadCRef.UnlockedConstReaderAndAtomicRWCRef(),
+ beforeRegistration, afterRegistration, &onStackChar, testThreadId);
+ offThreadCRef.WithUnlockedConstReaderAndAtomicRW(
+ [&](const TR::UnlockedConstReaderAndAtomicRW& aData) {
+ TestConstUnlockedConstReaderAndAtomicRW(
+ aData, beforeRegistration, afterRegistration, &onStackChar,
+ testThreadId);
+ });
+
+ // non-const UnlockedConstReaderAndAtomicRW
+
+ TestUnlockedConstReaderAndAtomicRW(
+ aOffThreadRef.UnlockedConstReaderAndAtomicRWRef(),
+ beforeRegistration, afterRegistration, &onStackChar, testThreadId);
+ aOffThreadRef.WithUnlockedConstReaderAndAtomicRW(
+ [&](TR::UnlockedConstReaderAndAtomicRW& aData) {
+ TestUnlockedConstReaderAndAtomicRW(aData, beforeRegistration,
+ afterRegistration,
+ &onStackChar, testThreadId);
+ });
+
+ // const UnlockedRWForLockedProfiler
+
+ TestConstUnlockedRWForLockedProfiler(
+ offThreadCRef.UnlockedRWForLockedProfilerCRef(), beforeRegistration,
+ afterRegistration, &onStackChar, testThreadId);
+ offThreadCRef.WithUnlockedRWForLockedProfiler(
+ [&](const TR::UnlockedRWForLockedProfiler& aData) {
+ TestConstUnlockedRWForLockedProfiler(aData, beforeRegistration,
+ afterRegistration,
+ &onStackChar, testThreadId);
+ });
+
+ // non-const UnlockedRWForLockedProfiler
+
+ TestUnlockedRWForLockedProfiler(
+ aOffThreadRef.UnlockedRWForLockedProfilerRef(), beforeRegistration,
+ afterRegistration, &onStackChar, testThreadId);
+ aOffThreadRef.WithUnlockedRWForLockedProfiler(
+ [&](TR::UnlockedRWForLockedProfiler& aData) {
+ TestUnlockedRWForLockedProfiler(aData, beforeRegistration,
+ afterRegistration, &onStackChar,
+ testThreadId);
+ });
+
+ // UnlockedReaderAndAtomicRWOnThread
+ // Note: It cannot directly be accessed off the thread, this will be
+ // tested through LockedRWFromAnyThread.
+
+ // const LockedRWFromAnyThread
+
+ EXPECT_FALSE(TR::IsDataMutexLockedOnCurrentThread());
+ {
+ TRy::OffThreadRef::ConstRWFromAnyThreadWithLock
+ constRWFromAnyThreadWithLock =
+ offThreadCRef.ConstLockedRWFromAnyThread();
+ if (profiler_current_thread_id() == testThreadId) {
+ EXPECT_TRUE(TR::IsDataMutexLockedOnCurrentThread());
+ }
+ TestConstLockedRWFromAnyThread(
+ constRWFromAnyThreadWithLock.DataCRef(), beforeRegistration,
+ afterRegistration, &onStackChar, testThreadId);
+ }
+ EXPECT_FALSE(TR::IsDataMutexLockedOnCurrentThread());
+ offThreadCRef.WithConstLockedRWFromAnyThread(
+ [&](const TR::LockedRWFromAnyThread& aData) {
+ if (profiler_current_thread_id() == testThreadId) {
+ EXPECT_TRUE(TR::IsDataMutexLockedOnCurrentThread());
+ }
+ TestConstLockedRWFromAnyThread(aData, beforeRegistration,
+ afterRegistration, &onStackChar,
+ testThreadId);
+ });
+ EXPECT_FALSE(TR::IsDataMutexLockedOnCurrentThread());
+
+ // non-const LockedRWFromAnyThread
+
+ EXPECT_FALSE(TR::IsDataMutexLockedOnCurrentThread());
+ {
+ TRy::OffThreadRef::RWFromAnyThreadWithLock rwFromAnyThreadWithLock =
+ aOffThreadRef.GetLockedRWFromAnyThread();
+ if (profiler_current_thread_id() == testThreadId) {
+ EXPECT_TRUE(TR::IsDataMutexLockedOnCurrentThread());
+ }
+ TestLockedRWFromAnyThread(rwFromAnyThreadWithLock.DataRef(),
+ beforeRegistration, afterRegistration,
+ &onStackChar, testThreadId);
+ }
+ EXPECT_FALSE(TR::IsDataMutexLockedOnCurrentThread());
+ aOffThreadRef.WithLockedRWFromAnyThread(
+ [&](TR::LockedRWFromAnyThread& aData) {
+ if (profiler_current_thread_id() == testThreadId) {
+ EXPECT_TRUE(TR::IsDataMutexLockedOnCurrentThread());
+ }
+ TestLockedRWFromAnyThread(aData, beforeRegistration,
+ afterRegistration, &onStackChar,
+ testThreadId);
+ });
+ EXPECT_FALSE(TR::IsDataMutexLockedOnCurrentThread());
+
+ // LockedRWOnThread
+ // Note: It can never be accessed off the thread.
+ };
+
+ int ranTest = 0;
+ TRy::WithOffThreadRef(testThreadId, [&](TRy::OffThreadRef aOffThreadRef) {
+ TestOffThreadRef(aOffThreadRef);
+ ++ranTest;
+ });
+ EXPECT_EQ(ranTest, 1);
+
+ EXPECT_TRUE(TRy::WithOffThreadRefOr(
+ testThreadId,
+ [&](TRy::OffThreadRef aOffThreadRef) {
+ TestOffThreadRef(aOffThreadRef);
+ return true;
+ },
+ false));
+
+ ranTest = 0;
+ EXPECT_FALSE(TRy::IsRegistryMutexLockedOnCurrentThread());
+ for (TRy::OffThreadRef offThreadRef : TRy::LockedRegistry{}) {
+ EXPECT_TRUE(TRy::IsRegistryMutexLockedOnCurrentThread() ||
+ !TR::IsRegistered());
+ if (offThreadRef.UnlockedConstReaderCRef().Info().ThreadId() ==
+ testThreadId) {
+ TestOffThreadRef(offThreadRef);
+ ++ranTest;
+ }
+ }
+ EXPECT_EQ(ranTest, 1);
+ EXPECT_FALSE(TRy::IsRegistryMutexLockedOnCurrentThread());
+
+ {
+ ranTest = 0;
+ EXPECT_FALSE(TRy::IsRegistryMutexLockedOnCurrentThread());
+ TRy::LockedRegistry lockedRegistry{};
+ EXPECT_TRUE(TRy::IsRegistryMutexLockedOnCurrentThread() ||
+ !TR::IsRegistered());
+ for (TRy::OffThreadRef offThreadRef : lockedRegistry) {
+ if (offThreadRef.UnlockedConstReaderCRef().Info().ThreadId() ==
+ testThreadId) {
+ TestOffThreadRef(offThreadRef);
+ ++ranTest;
+ }
+ }
+ EXPECT_EQ(ranTest, 1);
+ }
+ EXPECT_FALSE(TRy::IsRegistryMutexLockedOnCurrentThread());
+ };
+
+ // Test on the current thread.
+ testThroughRegistry();
+
+ // Test from another thread.
+ std::thread otherThread([&]() {
+ ASSERT_NE(profiler_current_thread_id(), testThreadId);
+ testThroughRegistry();
+
+ // Test that this unregistered thread is really not registered.
+ int ranTest = 0;
+ TRy::WithOffThreadRef(
+ profiler_current_thread_id(),
+ [&](TRy::OffThreadRef aOffThreadRef) { ++ranTest; });
+ EXPECT_EQ(ranTest, 0);
+
+ EXPECT_FALSE(TRy::WithOffThreadRefOr(
+ profiler_current_thread_id(),
+ [&](TRy::OffThreadRef aOffThreadRef) {
+ ++ranTest;
+ return true;
+ },
+ false));
+ EXPECT_EQ(ranTest, 0);
+ });
+ otherThread.join();
+ });
+ testThread.join();
+}
+
+TEST(GeckoProfiler, ThreadRegistration_RegistrationEdgeCases)
+{
+ using TR = profiler::ThreadRegistration;
+ using TRy = profiler::ThreadRegistry;
+
+ profiler_init_main_thread_id();
+ ASSERT_TRUE(profiler_is_main_thread())
+ << "This test assumes it runs on the main thread";
+
+ // Note that the main thread could already be registered, so we work in a new
+ // thread to test an actual registration that we control.
+
+ int registrationCount = 0;
+ int otherThreadLoops = 0;
+ int otherThreadReads = 0;
+
+ // This thread will register and unregister in a loop, with some pauses.
+ // Another thread will attempty to access the test thread, and lock its data.
+ // The main goal is to check edges cases around (un)registrations.
+ std::thread testThread([&]() {
+ const ProfilerThreadId testThreadId = profiler_current_thread_id();
+
+ const TimeStamp endTestAt = TimeStamp::Now() + TimeDuration::FromSeconds(1);
+
+ std::thread otherThread([&]() {
+ // Initial sleep so that testThread can start its loop.
+ PR_Sleep(PR_MillisecondsToInterval(1));
+
+ while (TimeStamp::Now() < endTestAt) {
+ ++otherThreadLoops;
+
+ TRy::WithOffThreadRef(testThreadId, [&](TRy::OffThreadRef
+ aOffThreadRef) {
+ if (otherThreadLoops % 1000 == 0) {
+ PR_Sleep(PR_MillisecondsToInterval(1));
+ }
+ TRy::OffThreadRef::RWFromAnyThreadWithLock rwFromAnyThreadWithLock =
+ aOffThreadRef.GetLockedRWFromAnyThread();
+ ++otherThreadReads;
+ if (otherThreadReads % 1000 == 0) {
+ PR_Sleep(PR_MillisecondsToInterval(1));
+ }
+ });
+ }
+ });
+
+ while (TimeStamp::Now() < endTestAt) {
+ ASSERT_FALSE(TR::IsRegistered())
+ << "A new std::thread should not start registered";
+ EXPECT_FALSE(TR::GetOnThreadPtr());
+ EXPECT_FALSE(TR::WithOnThreadRefOr([&](auto) { return true; }, false));
+
+ char onStackChar;
+
+ TR tr{"Test thread", &onStackChar};
+ ++registrationCount;
+
+ ASSERT_TRUE(TR::IsRegistered());
+
+ int ranTest = 0;
+ TRy::WithOffThreadRef(testThreadId, [&](TRy::OffThreadRef aOffThreadRef) {
+ if (registrationCount % 2000 == 0) {
+ PR_Sleep(PR_MillisecondsToInterval(1));
+ }
+ ++ranTest;
+ });
+ EXPECT_EQ(ranTest, 1);
+
+ if (registrationCount % 1000 == 0) {
+ PR_Sleep(PR_MillisecondsToInterval(1));
+ }
+ }
+
+ otherThread.join();
+ });
+
+ testThread.join();
+
+ // It's difficult to guess what these numbers should be, but they definitely
+ // should be non-zero. The main goal was to test that nothing goes wrong.
+ EXPECT_GT(registrationCount, 0);
+ EXPECT_GT(otherThreadLoops, 0);
+ EXPECT_GT(otherThreadReads, 0);
+}
+
+#ifdef MOZ_GECKO_PROFILER
+
+TEST(BaseProfiler, BlocksRingBuffer)
+{
+ constexpr uint32_t MBSize = 256;
+ uint8_t buffer[MBSize * 3];
+ for (size_t i = 0; i < MBSize * 3; ++i) {
+ buffer[i] = uint8_t('A' + i);
+ }
+ BlocksRingBuffer rb(BlocksRingBuffer::ThreadSafety::WithMutex,
+ &buffer[MBSize], MakePowerOfTwo32<MBSize>());
+
+ {
+ nsCString cs("nsCString"_ns);
+ nsString s(u"nsString"_ns);
+ nsAutoCString acs("nsAutoCString"_ns);
+ nsAutoString as(u"nsAutoString"_ns);
+ nsAutoCStringN<8> acs8("nsAutoCStringN"_ns);
+ nsAutoStringN<8> as8(u"nsAutoStringN"_ns);
+ JS::UniqueChars jsuc = JS_smprintf("%s", "JS::UniqueChars");
+
+ rb.PutObjects(cs, s, acs, as, acs8, as8, jsuc);
+ }
+
+ rb.ReadEach([](ProfileBufferEntryReader& aER) {
+ ASSERT_EQ(aER.ReadObject<nsCString>(), "nsCString"_ns);
+ ASSERT_EQ(aER.ReadObject<nsString>(), u"nsString"_ns);
+ ASSERT_EQ(aER.ReadObject<nsAutoCString>(), "nsAutoCString"_ns);
+ ASSERT_EQ(aER.ReadObject<nsAutoString>(), u"nsAutoString"_ns);
+ ASSERT_EQ(aER.ReadObject<nsAutoCStringN<8>>(), "nsAutoCStringN"_ns);
+ ASSERT_EQ(aER.ReadObject<nsAutoStringN<8>>(), u"nsAutoStringN"_ns);
+ auto jsuc2 = aER.ReadObject<JS::UniqueChars>();
+ ASSERT_TRUE(!!jsuc2);
+ ASSERT_TRUE(strcmp(jsuc2.get(), "JS::UniqueChars") == 0);
+ });
+
+ // Everything around the sub-buffer should be unchanged.
+ for (size_t i = 0; i < MBSize; ++i) {
+ ASSERT_EQ(buffer[i], uint8_t('A' + i));
+ }
+ for (size_t i = MBSize * 2; i < MBSize * 3; ++i) {
+ ASSERT_EQ(buffer[i], uint8_t('A' + i));
+ }
+}
+
+// Common JSON checks.
+
+// Check that the given JSON string include no JSON whitespace characters
+// (excluding those in property names and strings).
+void JSONWhitespaceCheck(const char* aOutput) {
+ ASSERT_NE(aOutput, nullptr);
+
+ enum class State { Data, String, StringEscaped };
+ State state = State::Data;
+ size_t length = 0;
+ size_t whitespaces = 0;
+ for (const char* p = aOutput; *p != '\0'; ++p) {
+ ++length;
+ const char c = *p;
+
+ switch (state) {
+ case State::Data:
+ if (c == '\n' || c == '\r' || c == ' ' || c == '\t') {
+ ++whitespaces;
+ } else if (c == '"') {
+ state = State::String;
+ }
+ break;
+
+ case State::String:
+ if (c == '"') {
+ state = State::Data;
+ } else if (c == '\\') {
+ state = State::StringEscaped;
+ }
+ break;
+
+ case State::StringEscaped:
+ state = State::String;
+ break;
+ }
+ }
+
+ EXPECT_EQ(whitespaces, 0u);
+ EXPECT_GT(length, 0u);
+}
+
+// Does the GETTER return a non-null TYPE? (Non-critical)
+# define EXPECT_HAS_JSON(GETTER, TYPE) \
+ do { \
+ if ((GETTER).isNull()) { \
+ EXPECT_FALSE((GETTER).isNull()) \
+ << #GETTER " doesn't exist or is null"; \
+ } else if (!(GETTER).is##TYPE()) { \
+ EXPECT_TRUE((GETTER).is##TYPE()) \
+ << #GETTER " didn't return type " #TYPE; \
+ } \
+ } while (false)
+
+// Does the GETTER return a non-null TYPE? (Critical)
+# define ASSERT_HAS_JSON(GETTER, TYPE) \
+ do { \
+ ASSERT_FALSE((GETTER).isNull()); \
+ ASSERT_TRUE((GETTER).is##TYPE()); \
+ } while (false)
+
+// Does the GETTER return a non-null TYPE? (Critical)
+// If yes, store the reference to Json::Value into VARIABLE.
+# define GET_JSON(VARIABLE, GETTER, TYPE) \
+ ASSERT_HAS_JSON(GETTER, TYPE); \
+ const Json::Value& VARIABLE = (GETTER)
+
+// Does the GETTER return a non-null TYPE? (Critical)
+// If yes, store the value as `const TYPE` into VARIABLE.
+# define GET_JSON_VALUE(VARIABLE, GETTER, TYPE) \
+ ASSERT_HAS_JSON(GETTER, TYPE); \
+ const auto VARIABLE = (GETTER).as##TYPE()
+
+// Non-const GET_JSON_VALUE.
+# define GET_JSON_MUTABLE_VALUE(VARIABLE, GETTER, TYPE) \
+ ASSERT_HAS_JSON(GETTER, TYPE); \
+ auto VARIABLE = (GETTER).as##TYPE()
+
+// Checks that the GETTER's value is present, is of the expected TYPE, and has
+// the expected VALUE. (Non-critical)
+# define EXPECT_EQ_JSON(GETTER, TYPE, VALUE) \
+ do { \
+ if ((GETTER).isNull()) { \
+ EXPECT_FALSE((GETTER).isNull()) \
+ << #GETTER " doesn't exist or is null"; \
+ } else if (!(GETTER).is##TYPE()) { \
+ EXPECT_TRUE((GETTER).is##TYPE()) \
+ << #GETTER " didn't return type " #TYPE; \
+ } else { \
+ EXPECT_EQ((GETTER).as##TYPE(), (VALUE)); \
+ } \
+ } while (false)
+
+// Checks that the GETTER's value is present, and is a valid index into the
+// STRINGTABLE array, pointing at the expected STRING.
+# define EXPECT_EQ_STRINGTABLE(GETTER, STRINGTABLE, STRING) \
+ do { \
+ if ((GETTER).isNull()) { \
+ EXPECT_FALSE((GETTER).isNull()) \
+ << #GETTER " doesn't exist or is null"; \
+ } else if (!(GETTER).isUInt()) { \
+ EXPECT_TRUE((GETTER).isUInt()) << #GETTER " didn't return an index"; \
+ } else { \
+ EXPECT_LT((GETTER).asUInt(), (STRINGTABLE).size()); \
+ EXPECT_EQ_JSON((STRINGTABLE)[(GETTER).asUInt()], String, (STRING)); \
+ } \
+ } while (false)
+
+# define EXPECT_JSON_ARRAY_CONTAINS(GETTER, TYPE, VALUE) \
+ do { \
+ if ((GETTER).isNull()) { \
+ EXPECT_FALSE((GETTER).isNull()) \
+ << #GETTER " doesn't exist or is null"; \
+ } else if (!(GETTER).isArray()) { \
+ EXPECT_TRUE((GETTER).is##TYPE()) << #GETTER " is not an array"; \
+ } else if (const Json::ArrayIndex size = (GETTER).size(); size == 0u) { \
+ EXPECT_NE(size, 0u) << #GETTER " is an empty array"; \
+ } else { \
+ bool found = false; \
+ for (Json::ArrayIndex i = 0; i < size; ++i) { \
+ if (!(GETTER)[i].is##TYPE()) { \
+ EXPECT_TRUE((GETTER)[i].is##TYPE()) \
+ << #GETTER "[" << i << "] is not " #TYPE; \
+ break; \
+ } \
+ if ((GETTER)[i].as##TYPE() == (VALUE)) { \
+ found = true; \
+ break; \
+ } \
+ } \
+ EXPECT_TRUE(found) << #GETTER " doesn't contain " #VALUE; \
+ } \
+ } while (false)
+
+# define EXPECT_JSON_ARRAY_EXCLUDES(GETTER, TYPE, VALUE) \
+ do { \
+ if ((GETTER).isNull()) { \
+ EXPECT_FALSE((GETTER).isNull()) \
+ << #GETTER " doesn't exist or is null"; \
+ } else if (!(GETTER).isArray()) { \
+ EXPECT_TRUE((GETTER).is##TYPE()) << #GETTER " is not an array"; \
+ } else { \
+ const Json::ArrayIndex size = (GETTER).size(); \
+ for (Json::ArrayIndex i = 0; i < size; ++i) { \
+ if (!(GETTER)[i].is##TYPE()) { \
+ EXPECT_TRUE((GETTER)[i].is##TYPE()) \
+ << #GETTER "[" << i << "] is not " #TYPE; \
+ break; \
+ } \
+ if ((GETTER)[i].as##TYPE() == (VALUE)) { \
+ EXPECT_TRUE((GETTER)[i].as##TYPE() != (VALUE)) \
+ << #GETTER " contains " #VALUE; \
+ break; \
+ } \
+ } \
+ } \
+ } while (false)
+
+// Check that the given process root contains all the expected properties.
+static void JSONRootCheck(const Json::Value& aRoot,
+ bool aWithMainThread = true) {
+ ASSERT_TRUE(aRoot.isObject());
+
+ EXPECT_HAS_JSON(aRoot["libs"], Array);
+
+ GET_JSON(meta, aRoot["meta"], Object);
+ EXPECT_HAS_JSON(meta["version"], UInt);
+ EXPECT_HAS_JSON(meta["startTime"], Double);
+ EXPECT_HAS_JSON(meta["profilingStartTime"], Double);
+ EXPECT_HAS_JSON(meta["contentEarliestTime"], Double);
+ EXPECT_HAS_JSON(meta["profilingEndTime"], Double);
+
+ EXPECT_HAS_JSON(aRoot["pages"], Array);
+
+ EXPECT_HAS_JSON(aRoot["profilerOverhead"], Object);
+
+ // "counters" is only present if there is any data to report.
+ // Test that expect "counters" should test for its presence first.
+ if (aRoot.isMember("counters")) {
+ // We have "counters", test their overall validity.
+ GET_JSON(counters, aRoot["counters"], Array);
+ for (const Json::Value& counter : counters) {
+ ASSERT_TRUE(counter.isObject());
+ EXPECT_HAS_JSON(counter["name"], String);
+ EXPECT_HAS_JSON(counter["category"], String);
+ EXPECT_HAS_JSON(counter["description"], String);
+ GET_JSON(sampleGroups, counter["sample_groups"], Array);
+ for (const Json::Value& sampleGroup : sampleGroups) {
+ ASSERT_TRUE(sampleGroup.isObject());
+ EXPECT_HAS_JSON(sampleGroup["id"], UInt);
+
+ GET_JSON(samples, sampleGroup["samples"], Object);
+ GET_JSON(samplesSchema, samples["schema"], Object);
+ EXPECT_GE(samplesSchema.size(), 3u);
+ GET_JSON_VALUE(samplesTime, samplesSchema["time"], UInt);
+ GET_JSON_VALUE(samplesNumber, samplesSchema["number"], UInt);
+ GET_JSON_VALUE(samplesCount, samplesSchema["count"], UInt);
+ GET_JSON(samplesData, samples["data"], Array);
+ double previousTime = 0.0;
+ for (const Json::Value& sample : samplesData) {
+ ASSERT_TRUE(sample.isArray());
+ GET_JSON_VALUE(time, sample[samplesTime], Double);
+ EXPECT_GE(time, previousTime);
+ previousTime = time;
+ if (sample.isValidIndex(samplesNumber)) {
+ EXPECT_HAS_JSON(sample[samplesNumber], UInt64);
+ }
+ if (sample.isValidIndex(samplesCount)) {
+ EXPECT_HAS_JSON(sample[samplesCount], Int64);
+ }
+ }
+ }
+ }
+ }
+
+ GET_JSON(threads, aRoot["threads"], Array);
+ const Json::ArrayIndex threadCount = threads.size();
+ for (Json::ArrayIndex i = 0; i < threadCount; ++i) {
+ GET_JSON(thread, threads[i], Object);
+ EXPECT_HAS_JSON(thread["processType"], String);
+ EXPECT_HAS_JSON(thread["name"], String);
+ EXPECT_HAS_JSON(thread["registerTime"], Double);
+ GET_JSON(samples, thread["samples"], Object);
+ EXPECT_HAS_JSON(thread["markers"], Object);
+ EXPECT_HAS_JSON(thread["pid"], Int64);
+ EXPECT_HAS_JSON(thread["tid"], Int64);
+ GET_JSON(stackTable, thread["stackTable"], Object);
+ GET_JSON(frameTable, thread["frameTable"], Object);
+ GET_JSON(stringTable, thread["stringTable"], Array);
+
+ GET_JSON(stackTableSchema, stackTable["schema"], Object);
+ EXPECT_GE(stackTableSchema.size(), 2u);
+ GET_JSON_VALUE(stackTablePrefix, stackTableSchema["prefix"], UInt);
+ GET_JSON_VALUE(stackTableFrame, stackTableSchema["frame"], UInt);
+ GET_JSON(stackTableData, stackTable["data"], Array);
+
+ GET_JSON(frameTableSchema, frameTable["schema"], Object);
+ EXPECT_GE(frameTableSchema.size(), 1u);
+ GET_JSON_VALUE(frameTableLocation, frameTableSchema["location"], UInt);
+ GET_JSON(frameTableData, frameTable["data"], Array);
+
+ GET_JSON(samplesSchema, samples["schema"], Object);
+ GET_JSON_VALUE(sampleStackIndex, samplesSchema["stack"], UInt);
+ GET_JSON(samplesData, samples["data"], Array);
+ for (const Json::Value& sample : samplesData) {
+ ASSERT_TRUE(sample.isArray());
+ if (sample.isValidIndex(sampleStackIndex)) {
+ if (!sample[sampleStackIndex].isNull()) {
+ GET_JSON_MUTABLE_VALUE(stack, sample[sampleStackIndex], UInt);
+ EXPECT_TRUE(stackTableData.isValidIndex(stack));
+ for (;;) {
+ // `stack` (from the sample, or from the callee frame's "prefix" in
+ // the previous loop) points into the stackTable.
+ GET_JSON(stackTableEntry, stackTableData[stack], Array);
+ GET_JSON_VALUE(frame, stackTableEntry[stackTableFrame], UInt);
+
+ // The stackTable entry's "frame" points into the frameTable.
+ EXPECT_TRUE(frameTableData.isValidIndex(frame));
+ GET_JSON(frameTableEntry, frameTableData[frame], Array);
+ GET_JSON_VALUE(location, frameTableEntry[frameTableLocation], UInt);
+
+ // The frameTable entry's "location" points at a string.
+ EXPECT_TRUE(stringTable.isValidIndex(location));
+
+ // The stackTable entry's "prefix" is null for the root frame.
+ if (stackTableEntry[stackTablePrefix].isNull()) {
+ break;
+ }
+ // Otherwise it recursively points at the caller in the stackTable.
+ GET_JSON_VALUE(prefix, stackTableEntry[stackTablePrefix], UInt);
+ EXPECT_TRUE(stackTableData.isValidIndex(prefix));
+ stack = prefix;
+ }
+ }
+ }
+ }
+ }
+
+ if (aWithMainThread) {
+ ASSERT_GT(threadCount, 0u);
+ GET_JSON(thread0, threads[0], Object);
+ EXPECT_EQ_JSON(thread0["name"], String, "GeckoMain");
+ }
+
+ EXPECT_HAS_JSON(aRoot["pausedRanges"], Array);
+
+ const Json::Value& processes = aRoot["processes"];
+ if (!processes.isNull()) {
+ ASSERT_TRUE(processes.isArray());
+ const Json::ArrayIndex processCount = processes.size();
+ for (Json::ArrayIndex i = 0; i < processCount; ++i) {
+ GET_JSON(process, processes[i], Object);
+ JSONRootCheck(process, aWithMainThread);
+ }
+ }
+
+ GET_JSON(profilingLog, aRoot["profilingLog"], Object);
+ EXPECT_EQ(profilingLog.size(), 1u);
+ for (auto it = profilingLog.begin(); it != profilingLog.end(); ++it) {
+ // The key should be a pid.
+ const auto key = it.name();
+ for (const auto letter : key) {
+ EXPECT_GE(letter, '0');
+ EXPECT_LE(letter, '9');
+ }
+ // And the value should be an object.
+ GET_JSON(logForPid, profilingLog[key], Object);
+ // Its content is not defined, but we expect at least these:
+ EXPECT_HAS_JSON(logForPid["profilingLogBegin_TSms"], Double);
+ EXPECT_HAS_JSON(logForPid["profilingLogEnd_TSms"], Double);
+ }
+}
+
+// Check that various expected top properties are in the JSON, and then call the
+// provided `aJSONCheckFunction` with the JSON root object.
+template <typename JSONCheckFunction>
+void JSONOutputCheck(const char* aOutput,
+ JSONCheckFunction&& aJSONCheckFunction) {
+ ASSERT_NE(aOutput, nullptr);
+
+ JSONWhitespaceCheck(aOutput);
+
+ // Extract JSON.
+ Json::Value parsedRoot;
+ Json::CharReaderBuilder builder;
+ const std::unique_ptr<Json::CharReader> reader(builder.newCharReader());
+ ASSERT_TRUE(
+ reader->parse(aOutput, strchr(aOutput, '\0'), &parsedRoot, nullptr));
+
+ JSONRootCheck(parsedRoot);
+
+ std::forward<JSONCheckFunction>(aJSONCheckFunction)(parsedRoot);
+}
+
+// Returns `static_cast<SamplingState>(-1)` if callback could not be installed.
+static SamplingState WaitForSamplingState() {
+ Atomic<int> samplingState{-1};
+
+ if (!profiler_callback_after_sampling([&](SamplingState aSamplingState) {
+ samplingState = static_cast<int>(aSamplingState);
+ })) {
+ return static_cast<SamplingState>(-1);
+ }
+
+ while (samplingState == -1) {
+ }
+
+ return static_cast<SamplingState>(static_cast<int>(samplingState));
+}
+
+typedef Vector<const char*> StrVec;
+
+static void InactiveFeaturesAndParamsCheck() {
+ int entries;
+ Maybe<double> duration;
+ double interval;
+ uint32_t features;
+ StrVec filters;
+ uint64_t activeTabID;
+
+ ASSERT_TRUE(!profiler_is_active());
+ ASSERT_TRUE(!profiler_feature_active(ProfilerFeature::MainThreadIO));
+ ASSERT_TRUE(!profiler_feature_active(ProfilerFeature::NativeAllocations));
+
+ profiler_get_start_params(&entries, &duration, &interval, &features, &filters,
+ &activeTabID);
+
+ ASSERT_TRUE(entries == 0);
+ ASSERT_TRUE(duration == Nothing());
+ ASSERT_TRUE(interval == 0);
+ ASSERT_TRUE(features == 0);
+ ASSERT_TRUE(filters.empty());
+ ASSERT_TRUE(activeTabID == 0);
+}
+
+static void ActiveParamsCheck(int aEntries, double aInterval,
+ uint32_t aFeatures, const char** aFilters,
+ size_t aFiltersLen, uint64_t aActiveTabID,
+ const Maybe<double>& aDuration = Nothing()) {
+ int entries;
+ Maybe<double> duration;
+ double interval;
+ uint32_t features;
+ StrVec filters;
+ uint64_t activeTabID;
+
+ profiler_get_start_params(&entries, &duration, &interval, &features, &filters,
+ &activeTabID);
+
+ ASSERT_TRUE(entries == aEntries);
+ ASSERT_TRUE(duration == aDuration);
+ ASSERT_TRUE(interval == aInterval);
+ ASSERT_TRUE(features == aFeatures);
+ ASSERT_TRUE(filters.length() == aFiltersLen);
+ ASSERT_TRUE(activeTabID == aActiveTabID);
+ for (size_t i = 0; i < aFiltersLen; i++) {
+ ASSERT_TRUE(strcmp(filters[i], aFilters[i]) == 0);
+ }
+}
+
+TEST(GeckoProfiler, FeaturesAndParams)
+{
+ InactiveFeaturesAndParamsCheck();
+
+ // Try a couple of features and filters.
+ {
+ uint32_t features = ProfilerFeature::JS;
+ const char* filters[] = {"GeckoMain", "Compositor"};
+
+# define PROFILER_DEFAULT_DURATION 20 /* seconds, for tests only */
+ profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL,
+ features, filters, MOZ_ARRAY_LENGTH(filters), 100,
+ Some(PROFILER_DEFAULT_DURATION));
+
+ ASSERT_TRUE(profiler_is_active());
+ ASSERT_TRUE(!profiler_feature_active(ProfilerFeature::MainThreadIO));
+ ASSERT_TRUE(!profiler_feature_active(ProfilerFeature::IPCMessages));
+
+ ActiveParamsCheck(PROFILER_DEFAULT_ENTRIES.Value(),
+ PROFILER_DEFAULT_INTERVAL, features, filters,
+ MOZ_ARRAY_LENGTH(filters), 100,
+ Some(PROFILER_DEFAULT_DURATION));
+
+ profiler_stop();
+
+ InactiveFeaturesAndParamsCheck();
+ }
+
+ // Try some different features and filters.
+ {
+ uint32_t features =
+ ProfilerFeature::MainThreadIO | ProfilerFeature::IPCMessages;
+ const char* filters[] = {"GeckoMain", "Foo", "Bar"};
+
+ // Testing with some arbitrary buffer size (as could be provided by
+ // external code), which we convert to the appropriate power of 2.
+ profiler_start(PowerOfTwo32(999999), 3, features, filters,
+ MOZ_ARRAY_LENGTH(filters), 123, Some(25.0));
+
+ ASSERT_TRUE(profiler_is_active());
+ ASSERT_TRUE(profiler_feature_active(ProfilerFeature::MainThreadIO));
+ ASSERT_TRUE(profiler_feature_active(ProfilerFeature::IPCMessages));
+
+ ActiveParamsCheck(int(PowerOfTwo32(999999).Value()), 3, features, filters,
+ MOZ_ARRAY_LENGTH(filters), 123, Some(25.0));
+
+ profiler_stop();
+
+ InactiveFeaturesAndParamsCheck();
+ }
+
+ // Try with no duration
+ {
+ uint32_t features =
+ ProfilerFeature::MainThreadIO | ProfilerFeature::IPCMessages;
+ const char* filters[] = {"GeckoMain", "Foo", "Bar"};
+
+ profiler_start(PowerOfTwo32(999999), 3, features, filters,
+ MOZ_ARRAY_LENGTH(filters), 0, Nothing());
+
+ ASSERT_TRUE(profiler_is_active());
+ ASSERT_TRUE(profiler_feature_active(ProfilerFeature::MainThreadIO));
+ ASSERT_TRUE(profiler_feature_active(ProfilerFeature::IPCMessages));
+
+ ActiveParamsCheck(int(PowerOfTwo32(999999).Value()), 3, features, filters,
+ MOZ_ARRAY_LENGTH(filters), 0, Nothing());
+
+ profiler_stop();
+
+ InactiveFeaturesAndParamsCheck();
+ }
+
+ // Try all supported features, and filters that match all threads.
+ {
+ uint32_t availableFeatures = profiler_get_available_features();
+ const char* filters[] = {""};
+
+ profiler_start(PowerOfTwo32(88888), 10, availableFeatures, filters,
+ MOZ_ARRAY_LENGTH(filters), 0, Some(15.0));
+
+ ASSERT_TRUE(profiler_is_active());
+ ASSERT_TRUE(profiler_feature_active(ProfilerFeature::MainThreadIO));
+ ASSERT_TRUE(profiler_feature_active(ProfilerFeature::IPCMessages));
+
+ ActiveParamsCheck(PowerOfTwo32(88888).Value(), 10, availableFeatures,
+ filters, MOZ_ARRAY_LENGTH(filters), 0, Some(15.0));
+
+ // Don't call profiler_stop() here.
+ }
+
+ // Try no features, and filters that match no threads.
+ {
+ uint32_t features = 0;
+ const char* filters[] = {"NoThreadWillMatchThis"};
+
+ // Second profiler_start() call in a row without an intervening
+ // profiler_stop(); this will do an implicit profiler_stop() and restart.
+ profiler_start(PowerOfTwo32(0), 0, features, filters,
+ MOZ_ARRAY_LENGTH(filters), 0, Some(0.0));
+
+ ASSERT_TRUE(profiler_is_active());
+ ASSERT_TRUE(!profiler_feature_active(ProfilerFeature::MainThreadIO));
+ ASSERT_TRUE(!profiler_feature_active(ProfilerFeature::IPCMessages));
+
+ // Entries and intervals go to defaults if 0 is specified.
+ ActiveParamsCheck(PROFILER_DEFAULT_ENTRIES.Value(),
+ PROFILER_DEFAULT_INTERVAL, features, filters,
+ MOZ_ARRAY_LENGTH(filters), 0, Nothing());
+
+ profiler_stop();
+
+ InactiveFeaturesAndParamsCheck();
+
+ // These calls are no-ops.
+ profiler_stop();
+ profiler_stop();
+
+ InactiveFeaturesAndParamsCheck();
+ }
+}
+
+TEST(GeckoProfiler, EnsureStarted)
+{
+ InactiveFeaturesAndParamsCheck();
+
+ uint32_t features = ProfilerFeature::JS;
+ const char* filters[] = {"GeckoMain", "Compositor"};
+ {
+ // Inactive -> Active
+ profiler_ensure_started(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL,
+ features, filters, MOZ_ARRAY_LENGTH(filters), 0,
+ Some(PROFILER_DEFAULT_DURATION));
+
+ ActiveParamsCheck(
+ PROFILER_DEFAULT_ENTRIES.Value(), PROFILER_DEFAULT_INTERVAL, features,
+ filters, MOZ_ARRAY_LENGTH(filters), 0, Some(PROFILER_DEFAULT_DURATION));
+ }
+
+ {
+ // Active -> Active with same settings
+
+ Maybe<ProfilerBufferInfo> info0 = profiler_get_buffer_info();
+ ASSERT_TRUE(info0->mRangeEnd > 0);
+
+ // First, write some samples into the buffer.
+ PR_Sleep(PR_MillisecondsToInterval(500));
+
+ Maybe<ProfilerBufferInfo> info1 = profiler_get_buffer_info();
+ ASSERT_TRUE(info1->mRangeEnd > info0->mRangeEnd);
+
+ // Call profiler_ensure_started with the same settings as before.
+ // This operation must not clear our buffer!
+ profiler_ensure_started(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL,
+ features, filters, MOZ_ARRAY_LENGTH(filters), 0,
+ Some(PROFILER_DEFAULT_DURATION));
+
+ ActiveParamsCheck(
+ PROFILER_DEFAULT_ENTRIES.Value(), PROFILER_DEFAULT_INTERVAL, features,
+ filters, MOZ_ARRAY_LENGTH(filters), 0, Some(PROFILER_DEFAULT_DURATION));
+
+ // Check that our position in the buffer stayed the same or advanced, but
+ // not by much, and the range-start after profiler_ensure_started shouldn't
+ // have passed the range-end before.
+ Maybe<ProfilerBufferInfo> info2 = profiler_get_buffer_info();
+ ASSERT_TRUE(info2->mRangeEnd >= info1->mRangeEnd);
+ ASSERT_TRUE(info2->mRangeEnd - info1->mRangeEnd <
+ info1->mRangeEnd - info0->mRangeEnd);
+ ASSERT_TRUE(info2->mRangeStart < info1->mRangeEnd);
+ }
+
+ {
+ // Active -> Active with *different* settings
+
+ Maybe<ProfilerBufferInfo> info1 = profiler_get_buffer_info();
+
+ // Call profiler_ensure_started with a different feature set than the one
+ // it's currently running with. This is supposed to stop and restart the
+ // profiler, thereby discarding the buffer contents.
+ uint32_t differentFeatures = features | ProfilerFeature::CPUUtilization;
+ profiler_ensure_started(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL,
+ differentFeatures, filters,
+ MOZ_ARRAY_LENGTH(filters), 0);
+
+ ActiveParamsCheck(PROFILER_DEFAULT_ENTRIES.Value(),
+ PROFILER_DEFAULT_INTERVAL, differentFeatures, filters,
+ MOZ_ARRAY_LENGTH(filters), 0);
+
+ // Check the the buffer was cleared, so its range-start should be at/after
+ // its range-end before.
+ Maybe<ProfilerBufferInfo> info2 = profiler_get_buffer_info();
+ ASSERT_TRUE(info2->mRangeStart >= info1->mRangeEnd);
+ }
+
+ {
+ // Active -> Inactive
+
+ profiler_stop();
+
+ InactiveFeaturesAndParamsCheck();
+ }
+}
+
+TEST(GeckoProfiler, MultiRegistration)
+{
+ // This whole test only checks that function calls don't crash, they don't
+ // actually verify that threads get profiled or not.
+
+ {
+ std::thread thread([]() {
+ char top;
+ profiler_register_thread("thread, no unreg", &top);
+ });
+ thread.join();
+ }
+
+ {
+ std::thread thread([]() { profiler_unregister_thread(); });
+ thread.join();
+ }
+
+ {
+ std::thread thread([]() {
+ char top;
+ profiler_register_thread("thread 1st", &top);
+ profiler_unregister_thread();
+ profiler_register_thread("thread 2nd", &top);
+ profiler_unregister_thread();
+ });
+ thread.join();
+ }
+
+ {
+ std::thread thread([]() {
+ char top;
+ profiler_register_thread("thread once", &top);
+ profiler_register_thread("thread again", &top);
+ profiler_unregister_thread();
+ });
+ thread.join();
+ }
+
+ {
+ std::thread thread([]() {
+ char top;
+ profiler_register_thread("thread to unreg twice", &top);
+ profiler_unregister_thread();
+ profiler_unregister_thread();
+ });
+ thread.join();
+ }
+}
+
+TEST(GeckoProfiler, DifferentThreads)
+{
+ InactiveFeaturesAndParamsCheck();
+
+ nsCOMPtr<nsIThread> thread;
+ nsresult rv = NS_NewNamedThread("GeckoProfGTest", getter_AddRefs(thread));
+ ASSERT_NS_SUCCEEDED(rv);
+
+ // Control the profiler on a background thread and verify flags on the
+ // main thread.
+ {
+ uint32_t features = ProfilerFeature::JS;
+ const char* filters[] = {"GeckoMain", "Compositor"};
+
+ NS_DispatchAndSpinEventLoopUntilComplete(
+ "GeckoProfiler_DifferentThreads_Test::TestBody"_ns, thread,
+ NS_NewRunnableFunction(
+ "GeckoProfiler_DifferentThreads_Test::TestBody", [&]() {
+ profiler_start(PROFILER_DEFAULT_ENTRIES,
+ PROFILER_DEFAULT_INTERVAL, features, filters,
+ MOZ_ARRAY_LENGTH(filters), 0);
+ }));
+
+ ASSERT_TRUE(profiler_is_active());
+ ASSERT_TRUE(!profiler_feature_active(ProfilerFeature::MainThreadIO));
+ ASSERT_TRUE(!profiler_feature_active(ProfilerFeature::IPCMessages));
+
+ ActiveParamsCheck(PROFILER_DEFAULT_ENTRIES.Value(),
+ PROFILER_DEFAULT_INTERVAL, features, filters,
+ MOZ_ARRAY_LENGTH(filters), 0);
+
+ NS_DispatchAndSpinEventLoopUntilComplete(
+ "GeckoProfiler_DifferentThreads_Test::TestBody"_ns, thread,
+ NS_NewRunnableFunction("GeckoProfiler_DifferentThreads_Test::TestBody",
+ [&]() { profiler_stop(); }));
+
+ InactiveFeaturesAndParamsCheck();
+ }
+
+ // Control the profiler on the main thread and verify flags on a
+ // background thread.
+ {
+ uint32_t features = ProfilerFeature::JS;
+ const char* filters[] = {"GeckoMain", "Compositor"};
+
+ profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL,
+ features, filters, MOZ_ARRAY_LENGTH(filters), 0);
+
+ NS_DispatchAndSpinEventLoopUntilComplete(
+ "GeckoProfiler_DifferentThreads_Test::TestBody"_ns, thread,
+ NS_NewRunnableFunction(
+ "GeckoProfiler_DifferentThreads_Test::TestBody", [&]() {
+ ASSERT_TRUE(profiler_is_active());
+ ASSERT_TRUE(
+ !profiler_feature_active(ProfilerFeature::MainThreadIO));
+ ASSERT_TRUE(
+ !profiler_feature_active(ProfilerFeature::IPCMessages));
+
+ ActiveParamsCheck(PROFILER_DEFAULT_ENTRIES.Value(),
+ PROFILER_DEFAULT_INTERVAL, features, filters,
+ MOZ_ARRAY_LENGTH(filters), 0);
+ }));
+
+ profiler_stop();
+
+ NS_DispatchAndSpinEventLoopUntilComplete(
+ "GeckoProfiler_DifferentThreads_Test::TestBody"_ns, thread,
+ NS_NewRunnableFunction("GeckoProfiler_DifferentThreads_Test::TestBody",
+ [&]() { InactiveFeaturesAndParamsCheck(); }));
+ }
+
+ thread->Shutdown();
+}
+
+TEST(GeckoProfiler, GetBacktrace)
+{
+ ASSERT_TRUE(!profiler_get_backtrace());
+
+ {
+ uint32_t features = ProfilerFeature::StackWalk;
+ const char* filters[] = {"GeckoMain"};
+
+ profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL,
+ features, filters, MOZ_ARRAY_LENGTH(filters), 0);
+
+ // These will be destroyed while the profiler is active.
+ static const int N = 100;
+ {
+ UniqueProfilerBacktrace u[N];
+ for (int i = 0; i < N; i++) {
+ u[i] = profiler_get_backtrace();
+ ASSERT_TRUE(u[i]);
+ }
+ }
+
+ // These will be destroyed after the profiler stops.
+ UniqueProfilerBacktrace u[N];
+ for (int i = 0; i < N; i++) {
+ u[i] = profiler_get_backtrace();
+ ASSERT_TRUE(u[i]);
+ }
+
+ profiler_stop();
+ }
+
+ ASSERT_TRUE(!profiler_get_backtrace());
+}
+
+TEST(GeckoProfiler, Pause)
+{
+ profiler_init_main_thread_id();
+ ASSERT_TRUE(profiler_is_main_thread())
+ << "This test must run on the main thread";
+
+ uint32_t features = ProfilerFeature::StackWalk;
+ const char* filters[] = {"GeckoMain", "Profiled GeckoProfiler.Pause"};
+
+ ASSERT_TRUE(!profiler_is_paused());
+ for (ThreadProfilingFeatures features : scEachAndAnyThreadProfilingFeatures) {
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(features));
+ ASSERT_TRUE(
+ !profiler_thread_is_being_profiled(ProfilerThreadId{}, features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(profiler_current_thread_id(),
+ features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(profiler_main_thread_id(),
+ features));
+ }
+
+ std::thread{[&]() {
+ {
+ for (ThreadProfilingFeatures features :
+ scEachAndAnyThreadProfilingFeatures) {
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(features));
+ ASSERT_TRUE(
+ !profiler_thread_is_being_profiled(ProfilerThreadId{}, features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(
+ profiler_current_thread_id(), features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(
+ profiler_main_thread_id(), features));
+ }
+ }
+ {
+ AUTO_PROFILER_REGISTER_THREAD(
+ "Ignored GeckoProfiler.Pause - before start");
+ for (ThreadProfilingFeatures features :
+ scEachAndAnyThreadProfilingFeatures) {
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(features));
+ ASSERT_TRUE(
+ !profiler_thread_is_being_profiled(ProfilerThreadId{}, features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(
+ profiler_current_thread_id(), features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(
+ profiler_main_thread_id(), features));
+ }
+ }
+ {
+ AUTO_PROFILER_REGISTER_THREAD(
+ "Profiled GeckoProfiler.Pause - before start");
+ for (ThreadProfilingFeatures features :
+ scEachAndAnyThreadProfilingFeatures) {
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(features));
+ ASSERT_TRUE(
+ !profiler_thread_is_being_profiled(ProfilerThreadId{}, features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(
+ profiler_current_thread_id(), features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(
+ profiler_main_thread_id(), features));
+ }
+ }
+ }}.join();
+
+ profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL, features,
+ filters, MOZ_ARRAY_LENGTH(filters), 0);
+
+ ASSERT_TRUE(!profiler_is_paused());
+ for (ThreadProfilingFeatures features : scEachAndAnyThreadProfilingFeatures) {
+ ASSERT_TRUE(profiler_thread_is_being_profiled(features));
+ ASSERT_TRUE(
+ profiler_thread_is_being_profiled(ProfilerThreadId{}, features));
+ ASSERT_TRUE(profiler_thread_is_being_profiled(profiler_current_thread_id(),
+ features));
+ }
+
+ std::thread{[&]() {
+ {
+ for (ThreadProfilingFeatures features :
+ scEachAndAnyThreadProfilingFeatures) {
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(features));
+ ASSERT_TRUE(
+ !profiler_thread_is_being_profiled(ProfilerThreadId{}, features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(
+ profiler_current_thread_id(), features));
+ ASSERT_TRUE(profiler_thread_is_being_profiled(profiler_main_thread_id(),
+ features));
+ }
+ }
+ {
+ AUTO_PROFILER_REGISTER_THREAD(
+ "Ignored GeckoProfiler.Pause - after start");
+ for (ThreadProfilingFeatures features :
+ scEachAndAnyThreadProfilingFeatures) {
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(features));
+ ASSERT_TRUE(
+ !profiler_thread_is_being_profiled(ProfilerThreadId{}, features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(
+ profiler_current_thread_id(), features));
+ ASSERT_TRUE(profiler_thread_is_being_profiled(profiler_main_thread_id(),
+ features));
+ }
+ }
+ {
+ AUTO_PROFILER_REGISTER_THREAD(
+ "Profiled GeckoProfiler.Pause - after start");
+ for (ThreadProfilingFeatures features :
+ scEachAndAnyThreadProfilingFeatures) {
+ ASSERT_TRUE(profiler_thread_is_being_profiled(features));
+ ASSERT_TRUE(
+ profiler_thread_is_being_profiled(ProfilerThreadId{}, features));
+ ASSERT_TRUE(profiler_thread_is_being_profiled(
+ profiler_current_thread_id(), features));
+ ASSERT_TRUE(profiler_thread_is_being_profiled(profiler_main_thread_id(),
+ features));
+ }
+ }
+ }}.join();
+
+ // Check that we are writing samples while not paused.
+ Maybe<ProfilerBufferInfo> info1 = profiler_get_buffer_info();
+ PR_Sleep(PR_MillisecondsToInterval(500));
+ Maybe<ProfilerBufferInfo> info2 = profiler_get_buffer_info();
+ ASSERT_TRUE(info1->mRangeEnd != info2->mRangeEnd);
+
+ // Check that we are writing markers while not paused.
+ ASSERT_TRUE(profiler_thread_is_being_profiled_for_markers());
+ ASSERT_TRUE(
+ profiler_thread_is_being_profiled_for_markers(ProfilerThreadId{}));
+ ASSERT_TRUE(profiler_thread_is_being_profiled_for_markers(
+ profiler_current_thread_id()));
+ ASSERT_TRUE(
+ profiler_thread_is_being_profiled_for_markers(profiler_main_thread_id()));
+ info1 = profiler_get_buffer_info();
+ PROFILER_MARKER_UNTYPED("Not paused", OTHER, {});
+ info2 = profiler_get_buffer_info();
+ ASSERT_TRUE(info1->mRangeEnd != info2->mRangeEnd);
+
+ profiler_pause();
+
+ ASSERT_TRUE(profiler_is_paused());
+ for (ThreadProfilingFeatures features : scEachAndAnyThreadProfilingFeatures) {
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(features));
+ ASSERT_TRUE(
+ !profiler_thread_is_being_profiled(ProfilerThreadId{}, features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(profiler_current_thread_id(),
+ features));
+ }
+ ASSERT_TRUE(!profiler_thread_is_being_profiled_for_markers());
+ ASSERT_TRUE(
+ !profiler_thread_is_being_profiled_for_markers(ProfilerThreadId{}));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled_for_markers(
+ profiler_current_thread_id()));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled_for_markers(
+ profiler_main_thread_id()));
+
+ std::thread{[&]() {
+ {
+ for (ThreadProfilingFeatures features :
+ scEachAndAnyThreadProfilingFeatures) {
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(features));
+ ASSERT_TRUE(
+ !profiler_thread_is_being_profiled(ProfilerThreadId{}, features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(
+ profiler_current_thread_id(), features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(
+ profiler_main_thread_id(), features));
+ }
+ }
+ {
+ AUTO_PROFILER_REGISTER_THREAD(
+ "Ignored GeckoProfiler.Pause - after pause");
+ for (ThreadProfilingFeatures features :
+ scEachAndAnyThreadProfilingFeatures) {
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(features));
+ ASSERT_TRUE(
+ !profiler_thread_is_being_profiled(ProfilerThreadId{}, features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(
+ profiler_current_thread_id(), features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(
+ profiler_main_thread_id(), features));
+ }
+ }
+ {
+ AUTO_PROFILER_REGISTER_THREAD(
+ "Profiled GeckoProfiler.Pause - after pause");
+ for (ThreadProfilingFeatures features :
+ scEachAndAnyThreadProfilingFeatures) {
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(features));
+ ASSERT_TRUE(
+ !profiler_thread_is_being_profiled(ProfilerThreadId{}, features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(
+ profiler_current_thread_id(), features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(
+ profiler_main_thread_id(), features));
+ }
+ }
+ }}.join();
+
+ // Check that we are not writing samples while paused.
+ info1 = profiler_get_buffer_info();
+ PR_Sleep(PR_MillisecondsToInterval(500));
+ info2 = profiler_get_buffer_info();
+ ASSERT_TRUE(info1->mRangeEnd == info2->mRangeEnd);
+
+ // Check that we are now writing markers while paused.
+ info1 = profiler_get_buffer_info();
+ PROFILER_MARKER_UNTYPED("Paused", OTHER, {});
+ info2 = profiler_get_buffer_info();
+ ASSERT_TRUE(info1->mRangeEnd == info2->mRangeEnd);
+ PROFILER_MARKER_UNTYPED("Paused v2", OTHER, {});
+ Maybe<ProfilerBufferInfo> info3 = profiler_get_buffer_info();
+ ASSERT_TRUE(info2->mRangeEnd == info3->mRangeEnd);
+
+ profiler_resume();
+
+ ASSERT_TRUE(!profiler_is_paused());
+ for (ThreadProfilingFeatures features : scEachAndAnyThreadProfilingFeatures) {
+ ASSERT_TRUE(profiler_thread_is_being_profiled(features));
+ ASSERT_TRUE(
+ profiler_thread_is_being_profiled(ProfilerThreadId{}, features));
+ ASSERT_TRUE(profiler_thread_is_being_profiled(profiler_current_thread_id(),
+ features));
+ }
+
+ std::thread{[&]() {
+ {
+ for (ThreadProfilingFeatures features :
+ scEachAndAnyThreadProfilingFeatures) {
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(features));
+ ASSERT_TRUE(
+ !profiler_thread_is_being_profiled(ProfilerThreadId{}, features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(
+ profiler_current_thread_id(), features));
+ ASSERT_TRUE(profiler_thread_is_being_profiled(profiler_main_thread_id(),
+ features));
+ }
+ }
+ {
+ AUTO_PROFILER_REGISTER_THREAD(
+ "Ignored GeckoProfiler.Pause - after resume");
+ for (ThreadProfilingFeatures features :
+ scEachAndAnyThreadProfilingFeatures) {
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(features));
+ ASSERT_TRUE(
+ !profiler_thread_is_being_profiled(ProfilerThreadId{}, features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(
+ profiler_current_thread_id(), features));
+ ASSERT_TRUE(profiler_thread_is_being_profiled(profiler_main_thread_id(),
+ features));
+ }
+ }
+ {
+ AUTO_PROFILER_REGISTER_THREAD(
+ "Profiled GeckoProfiler.Pause - after resume");
+ for (ThreadProfilingFeatures features :
+ scEachAndAnyThreadProfilingFeatures) {
+ ASSERT_TRUE(profiler_thread_is_being_profiled(features));
+ ASSERT_TRUE(
+ profiler_thread_is_being_profiled(ProfilerThreadId{}, features));
+ ASSERT_TRUE(profiler_thread_is_being_profiled(
+ profiler_current_thread_id(), features));
+ ASSERT_TRUE(profiler_thread_is_being_profiled(profiler_main_thread_id(),
+ features));
+ }
+ }
+ }}.join();
+
+ profiler_stop();
+
+ ASSERT_TRUE(!profiler_is_paused());
+ for (ThreadProfilingFeatures features : scEachAndAnyThreadProfilingFeatures) {
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(features));
+ ASSERT_TRUE(
+ !profiler_thread_is_being_profiled(ProfilerThreadId{}, features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(profiler_current_thread_id(),
+ features));
+ }
+
+ std::thread{[&]() {
+ {
+ for (ThreadProfilingFeatures features :
+ scEachAndAnyThreadProfilingFeatures) {
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(features));
+ ASSERT_TRUE(
+ !profiler_thread_is_being_profiled(ProfilerThreadId{}, features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(
+ profiler_current_thread_id(), features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(
+ profiler_main_thread_id(), features));
+ }
+ }
+ {
+ AUTO_PROFILER_REGISTER_THREAD("Ignored GeckoProfiler.Pause - after stop");
+ for (ThreadProfilingFeatures features :
+ scEachAndAnyThreadProfilingFeatures) {
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(features));
+ ASSERT_TRUE(
+ !profiler_thread_is_being_profiled(ProfilerThreadId{}, features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(
+ profiler_current_thread_id(), features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(
+ profiler_main_thread_id(), features));
+ }
+ }
+ {
+ AUTO_PROFILER_REGISTER_THREAD(
+ "Profiled GeckoProfiler.Pause - after stop");
+ for (ThreadProfilingFeatures features :
+ scEachAndAnyThreadProfilingFeatures) {
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(features));
+ ASSERT_TRUE(
+ !profiler_thread_is_being_profiled(ProfilerThreadId{}, features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(
+ profiler_current_thread_id(), features));
+ ASSERT_TRUE(!profiler_thread_is_being_profiled(
+ profiler_main_thread_id(), features));
+ }
+ }
+ }}.join();
+}
+
+TEST(GeckoProfiler, Markers)
+{
+ uint32_t features = ProfilerFeature::StackWalk;
+ const char* filters[] = {"GeckoMain"};
+
+ profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL, features,
+ filters, MOZ_ARRAY_LENGTH(filters), 0);
+
+ PROFILER_MARKER("tracing event", OTHER, {}, Tracing, "A");
+ PROFILER_MARKER("tracing start", OTHER, MarkerTiming::IntervalStart(),
+ Tracing, "A");
+ PROFILER_MARKER("tracing end", OTHER, MarkerTiming::IntervalEnd(), Tracing,
+ "A");
+
+ auto bt = profiler_capture_backtrace();
+ PROFILER_MARKER("tracing event with stack", OTHER,
+ MarkerStack::TakeBacktrace(std::move(bt)), Tracing, "B");
+
+ { AUTO_PROFILER_TRACING_MARKER("C", "auto tracing", OTHER); }
+
+ PROFILER_MARKER_UNTYPED("M1", OTHER, {});
+ PROFILER_MARKER_UNTYPED("M3", OTHER, {});
+
+ // Create three strings: two that are the maximum allowed length, and one that
+ // is one char longer.
+ static const size_t kMax = ProfileBuffer::kMaxFrameKeyLength;
+ UniquePtr<char[]> okstr1 = MakeUnique<char[]>(kMax);
+ UniquePtr<char[]> okstr2 = MakeUnique<char[]>(kMax);
+ UniquePtr<char[]> longstr = MakeUnique<char[]>(kMax + 1);
+ UniquePtr<char[]> longstrCut = MakeUnique<char[]>(kMax + 1);
+ for (size_t i = 0; i < kMax; i++) {
+ okstr1[i] = 'a';
+ okstr2[i] = 'b';
+ longstr[i] = 'c';
+ longstrCut[i] = 'c';
+ }
+ okstr1[kMax - 1] = '\0';
+ okstr2[kMax - 1] = '\0';
+ longstr[kMax] = '\0';
+ longstrCut[kMax] = '\0';
+ // Should be output as-is.
+ AUTO_PROFILER_LABEL_DYNAMIC_CSTR("", LAYOUT, "");
+ AUTO_PROFILER_LABEL_DYNAMIC_CSTR("", LAYOUT, okstr1.get());
+ // Should be output as label + space + okstr2.
+ AUTO_PROFILER_LABEL_DYNAMIC_CSTR("okstr2", LAYOUT, okstr2.get());
+ // Should be output with kMax length, ending with "...\0".
+ AUTO_PROFILER_LABEL_DYNAMIC_CSTR("", LAYOUT, longstr.get());
+ ASSERT_EQ(longstrCut[kMax - 4], 'c');
+ longstrCut[kMax - 4] = '.';
+ ASSERT_EQ(longstrCut[kMax - 3], 'c');
+ longstrCut[kMax - 3] = '.';
+ ASSERT_EQ(longstrCut[kMax - 2], 'c');
+ longstrCut[kMax - 2] = '.';
+ ASSERT_EQ(longstrCut[kMax - 1], 'c');
+ longstrCut[kMax - 1] = '\0';
+
+ // Test basic markers 2.0.
+ EXPECT_TRUE(
+ profiler_add_marker("default-templated markers 2.0 with empty options",
+ geckoprofiler::category::OTHER, {}));
+
+ PROFILER_MARKER_UNTYPED(
+ "default-templated markers 2.0 with option", OTHER,
+ MarkerStack::TakeBacktrace(profiler_capture_backtrace()));
+
+ PROFILER_MARKER("explicitly-default-templated markers 2.0 with empty options",
+ OTHER, {}, NoPayload);
+
+ EXPECT_TRUE(profiler_add_marker(
+ "explicitly-default-templated markers 2.0 with option",
+ geckoprofiler::category::OTHER, {},
+ ::geckoprofiler::markers::NoPayload{}));
+
+ // Used in markers below.
+ TimeStamp ts1 = TimeStamp::Now();
+
+ // Sleep briefly to ensure a sample is taken and the pending markers are
+ // processed.
+ PR_Sleep(PR_MillisecondsToInterval(500));
+
+ // Used in markers below.
+ TimeStamp ts2 = TimeStamp::Now();
+ // ts1 and ts2 should be different thanks to the sleep.
+ EXPECT_NE(ts1, ts2);
+
+ // Test most marker payloads.
+
+ // Keep this one first! (It's used to record `ts1` and `ts2`, to compare
+ // to serialized numbers in other markers.)
+ EXPECT_TRUE(profiler_add_marker("FirstMarker", geckoprofiler::category::OTHER,
+ MarkerTiming::Interval(ts1, ts2),
+ geckoprofiler::markers::TextMarker{},
+ "First Marker"));
+
+ // User-defined marker type with different properties, and fake schema.
+ struct GtestMarker {
+ static constexpr Span<const char> MarkerTypeName() {
+ return MakeStringSpan("markers-gtest");
+ }
+ static void StreamJSONMarkerData(
+ mozilla::baseprofiler::SpliceableJSONWriter& aWriter, int aInt,
+ double aDouble, const mozilla::ProfilerString8View& aText,
+ const mozilla::ProfilerString8View& aUniqueText,
+ const mozilla::TimeStamp& aTime) {
+ aWriter.NullProperty("null");
+ aWriter.BoolProperty("bool-false", false);
+ aWriter.BoolProperty("bool-true", true);
+ aWriter.IntProperty("int", aInt);
+ aWriter.DoubleProperty("double", aDouble);
+ aWriter.StringProperty("text", aText);
+ aWriter.UniqueStringProperty("unique text", aUniqueText);
+ aWriter.UniqueStringProperty("unique text again", aUniqueText);
+ aWriter.TimeProperty("time", aTime);
+ }
+ static mozilla::MarkerSchema MarkerTypeDisplay() {
+ // Note: This is an test function that is not intended to actually output
+ // that correctly matches StreamJSONMarkerData data above! Instead we only
+ // test that it outputs the expected JSON at the end.
+ using MS = mozilla::MarkerSchema;
+ MS schema{MS::Location::MarkerChart, MS::Location::MarkerTable,
+ MS::Location::TimelineOverview, MS::Location::TimelineMemory,
+ MS::Location::TimelineIPC, MS::Location::TimelineFileIO,
+ MS::Location::StackChart};
+ // All label functions.
+ schema.SetChartLabel("chart label");
+ schema.SetTooltipLabel("tooltip label");
+ schema.SetTableLabel("table label");
+ // All data functions, all formats, all "searchable" values.
+ schema.AddKeyFormat("key with url", MS::Format::Url);
+ schema.AddKeyLabelFormat("key with label filePath", "label filePath",
+ MS::Format::FilePath);
+ schema.AddKeyFormatSearchable("key with string not-searchable",
+ MS::Format::String,
+ MS::Searchable::NotSearchable);
+ schema.AddKeyLabelFormatSearchable("key with label duration searchable",
+ "label duration", MS::Format::Duration,
+ MS::Searchable::Searchable);
+ schema.AddKeyFormat("key with time", MS::Format::Time);
+ schema.AddKeyFormat("key with seconds", MS::Format::Seconds);
+ schema.AddKeyFormat("key with milliseconds", MS::Format::Milliseconds);
+ schema.AddKeyFormat("key with microseconds", MS::Format::Microseconds);
+ schema.AddKeyFormat("key with nanoseconds", MS::Format::Nanoseconds);
+ schema.AddKeyFormat("key with bytes", MS::Format::Bytes);
+ schema.AddKeyFormat("key with percentage", MS::Format::Percentage);
+ schema.AddKeyFormat("key with integer", MS::Format::Integer);
+ schema.AddKeyFormat("key with decimal", MS::Format::Decimal);
+ schema.AddStaticLabelValue("static label", "static value");
+ return schema;
+ }
+ };
+ EXPECT_TRUE(
+ profiler_add_marker("Gtest custom marker", geckoprofiler::category::OTHER,
+ MarkerTiming::Interval(ts1, ts2), GtestMarker{}, 42,
+ 43.0, "gtest text", "gtest unique text", ts1));
+
+ // User-defined marker type with no data, special frontend schema.
+ struct GtestSpecialMarker {
+ static constexpr Span<const char> MarkerTypeName() {
+ return MakeStringSpan("markers-gtest-special");
+ }
+ static void StreamJSONMarkerData(
+ mozilla::baseprofiler::SpliceableJSONWriter& aWriter) {}
+ static mozilla::MarkerSchema MarkerTypeDisplay() {
+ return mozilla::MarkerSchema::SpecialFrontendLocation{};
+ }
+ };
+ EXPECT_TRUE(profiler_add_marker("Gtest special marker",
+ geckoprofiler::category::OTHER, {},
+ GtestSpecialMarker{}));
+
+ // User-defined marker type that is never used, so it shouldn't appear in the
+ // output.
+ struct GtestUnusedMarker {
+ static constexpr Span<const char> MarkerTypeName() {
+ return MakeStringSpan("markers-gtest-unused");
+ }
+ static void StreamJSONMarkerData(
+ mozilla::baseprofiler::SpliceableJSONWriter& aWriter) {}
+ static mozilla::MarkerSchema MarkerTypeDisplay() {
+ return mozilla::MarkerSchema::SpecialFrontendLocation{};
+ }
+ };
+
+ // Make sure the compiler doesn't complain about this unused struct.
+ mozilla::Unused << GtestUnusedMarker{};
+
+ // Other markers in alphabetical order of payload class names.
+
+ nsCOMPtr<nsIURI> uri;
+ ASSERT_TRUE(
+ NS_SUCCEEDED(NS_NewURI(getter_AddRefs(uri), "http://mozilla.org/"_ns)));
+ // The marker name will be "Load <aChannelId>: <aURI>".
+ profiler_add_network_marker(
+ /* nsIURI* aURI */ uri,
+ /* const nsACString& aRequestMethod */ "GET"_ns,
+ /* int32_t aPriority */ 34,
+ /* uint64_t aChannelId */ 1,
+ /* NetworkLoadType aType */ net::NetworkLoadType::LOAD_START,
+ /* mozilla::TimeStamp aStart */ ts1,
+ /* mozilla::TimeStamp aEnd */ ts2,
+ /* int64_t aCount */ 56,
+ /* mozilla::net::CacheDisposition aCacheDisposition */
+ net::kCacheHit,
+ /* uint64_t aInnerWindowID */ 78,
+ /* bool aIsPrivateBrowsing */ false
+ /* const mozilla::net::TimingStruct* aTimings = nullptr */
+ /* mozilla::UniquePtr<mozilla::ProfileChunkedBuffer> aSource =
+ nullptr */
+ /* const mozilla::Maybe<nsDependentCString>& aContentType =
+ mozilla::Nothing() */
+ /* nsIURI* aRedirectURI = nullptr */
+ /* uint64_t aRedirectChannelId = 0 */
+ );
+
+ profiler_add_network_marker(
+ /* nsIURI* aURI */ uri,
+ /* const nsACString& aRequestMethod */ "GET"_ns,
+ /* int32_t aPriority */ 34,
+ /* uint64_t aChannelId */ 2,
+ /* NetworkLoadType aType */ net::NetworkLoadType::LOAD_STOP,
+ /* mozilla::TimeStamp aStart */ ts1,
+ /* mozilla::TimeStamp aEnd */ ts2,
+ /* int64_t aCount */ 56,
+ /* mozilla::net::CacheDisposition aCacheDisposition */
+ net::kCacheUnresolved,
+ /* uint64_t aInnerWindowID */ 78,
+ /* bool aIsPrivateBrowsing */ false,
+ /* const mozilla::net::TimingStruct* aTimings = nullptr */ nullptr,
+ /* mozilla::UniquePtr<mozilla::ProfileChunkedBuffer> aSource =
+ nullptr */
+ nullptr,
+ /* const mozilla::Maybe<nsDependentCString>& aContentType =
+ mozilla::Nothing() */
+ Some(nsDependentCString("text/html")),
+ /* nsIURI* aRedirectURI = nullptr */ nullptr,
+ /* uint64_t aRedirectChannelId = 0 */ 0);
+
+ nsCOMPtr<nsIURI> redirectURI;
+ ASSERT_TRUE(NS_SUCCEEDED(
+ NS_NewURI(getter_AddRefs(redirectURI), "http://example.com/"_ns)));
+ profiler_add_network_marker(
+ /* nsIURI* aURI */ uri,
+ /* const nsACString& aRequestMethod */ "GET"_ns,
+ /* int32_t aPriority */ 34,
+ /* uint64_t aChannelId */ 3,
+ /* NetworkLoadType aType */ net::NetworkLoadType::LOAD_REDIRECT,
+ /* mozilla::TimeStamp aStart */ ts1,
+ /* mozilla::TimeStamp aEnd */ ts2,
+ /* int64_t aCount */ 56,
+ /* mozilla::net::CacheDisposition aCacheDisposition */
+ net::kCacheUnresolved,
+ /* uint64_t aInnerWindowID */ 78,
+ /* bool aIsPrivateBrowsing */ false,
+ /* const mozilla::net::TimingStruct* aTimings = nullptr */ nullptr,
+ /* mozilla::UniquePtr<mozilla::ProfileChunkedBuffer> aSource =
+ nullptr */
+ nullptr,
+ /* const mozilla::Maybe<nsDependentCString>& aContentType =
+ mozilla::Nothing() */
+ mozilla::Nothing(),
+ /* nsIURI* aRedirectURI = nullptr */ redirectURI,
+ /* uint32_t aRedirectFlags = 0 */
+ nsIChannelEventSink::REDIRECT_TEMPORARY,
+ /* uint64_t aRedirectChannelId = 0 */ 103);
+
+ profiler_add_network_marker(
+ /* nsIURI* aURI */ uri,
+ /* const nsACString& aRequestMethod */ "GET"_ns,
+ /* int32_t aPriority */ 34,
+ /* uint64_t aChannelId */ 4,
+ /* NetworkLoadType aType */ net::NetworkLoadType::LOAD_REDIRECT,
+ /* mozilla::TimeStamp aStart */ ts1,
+ /* mozilla::TimeStamp aEnd */ ts2,
+ /* int64_t aCount */ 56,
+ /* mozilla::net::CacheDisposition aCacheDisposition */
+ net::kCacheUnresolved,
+ /* uint64_t aInnerWindowID */ 78,
+ /* bool aIsPrivateBrowsing */ false,
+ /* const mozilla::net::TimingStruct* aTimings = nullptr */ nullptr,
+ /* mozilla::UniquePtr<mozilla::ProfileChunkedBuffer> aSource =
+ nullptr */
+ nullptr,
+ /* const mozilla::Maybe<nsDependentCString>& aContentType =
+ mozilla::Nothing() */
+ mozilla::Nothing(),
+ /* nsIURI* aRedirectURI = nullptr */ redirectURI,
+ /* uint32_t aRedirectFlags = 0 */
+ nsIChannelEventSink::REDIRECT_PERMANENT,
+ /* uint64_t aRedirectChannelId = 0 */ 104);
+
+ profiler_add_network_marker(
+ /* nsIURI* aURI */ uri,
+ /* const nsACString& aRequestMethod */ "GET"_ns,
+ /* int32_t aPriority */ 34,
+ /* uint64_t aChannelId */ 5,
+ /* NetworkLoadType aType */ net::NetworkLoadType::LOAD_REDIRECT,
+ /* mozilla::TimeStamp aStart */ ts1,
+ /* mozilla::TimeStamp aEnd */ ts2,
+ /* int64_t aCount */ 56,
+ /* mozilla::net::CacheDisposition aCacheDisposition */
+ net::kCacheUnresolved,
+ /* uint64_t aInnerWindowID */ 78,
+ /* bool aIsPrivateBrowsing */ false,
+ /* const mozilla::net::TimingStruct* aTimings = nullptr */ nullptr,
+ /* mozilla::UniquePtr<mozilla::ProfileChunkedBuffer> aSource =
+ nullptr */
+ nullptr,
+ /* const mozilla::Maybe<nsDependentCString>& aContentType =
+ mozilla::Nothing() */
+ mozilla::Nothing(),
+ /* nsIURI* aRedirectURI = nullptr */ redirectURI,
+ /* uint32_t aRedirectFlags = 0 */ nsIChannelEventSink::REDIRECT_INTERNAL,
+ /* uint64_t aRedirectChannelId = 0 */ 105);
+
+ profiler_add_network_marker(
+ /* nsIURI* aURI */ uri,
+ /* const nsACString& aRequestMethod */ "GET"_ns,
+ /* int32_t aPriority */ 34,
+ /* uint64_t aChannelId */ 6,
+ /* NetworkLoadType aType */ net::NetworkLoadType::LOAD_REDIRECT,
+ /* mozilla::TimeStamp aStart */ ts1,
+ /* mozilla::TimeStamp aEnd */ ts2,
+ /* int64_t aCount */ 56,
+ /* mozilla::net::CacheDisposition aCacheDisposition */
+ net::kCacheUnresolved,
+ /* uint64_t aInnerWindowID */ 78,
+ /* bool aIsPrivateBrowsing */ false,
+ /* const mozilla::net::TimingStruct* aTimings = nullptr */ nullptr,
+ /* mozilla::UniquePtr<mozilla::ProfileChunkedBuffer> aSource =
+ nullptr */
+ nullptr,
+ /* const mozilla::Maybe<nsDependentCString>& aContentType =
+ mozilla::Nothing() */
+ mozilla::Nothing(),
+ /* nsIURI* aRedirectURI = nullptr */ redirectURI,
+ /* uint32_t aRedirectFlags = 0 */ nsIChannelEventSink::REDIRECT_INTERNAL |
+ nsIChannelEventSink::REDIRECT_STS_UPGRADE,
+ /* uint64_t aRedirectChannelId = 0 */ 106);
+ profiler_add_network_marker(
+ /* nsIURI* aURI */ uri,
+ /* const nsACString& aRequestMethod */ "GET"_ns,
+ /* int32_t aPriority */ 34,
+ /* uint64_t aChannelId */ 7,
+ /* NetworkLoadType aType */ net::NetworkLoadType::LOAD_START,
+ /* mozilla::TimeStamp aStart */ ts1,
+ /* mozilla::TimeStamp aEnd */ ts2,
+ /* int64_t aCount */ 56,
+ /* mozilla::net::CacheDisposition aCacheDisposition */
+ net::kCacheUnresolved,
+ /* uint64_t aInnerWindowID */ 78,
+ /* bool aIsPrivateBrowsing */ true
+ /* const mozilla::net::TimingStruct* aTimings = nullptr */
+ /* mozilla::UniquePtr<mozilla::ProfileChunkedBuffer> aSource =
+ nullptr */
+ /* const mozilla::Maybe<nsDependentCString>& aContentType =
+ mozilla::Nothing() */
+ /* nsIURI* aRedirectURI = nullptr */
+ /* uint64_t aRedirectChannelId = 0 */
+ );
+
+ EXPECT_TRUE(profiler_add_marker(
+ "Text in main thread with stack", geckoprofiler::category::OTHER,
+ {MarkerStack::Capture(), MarkerTiming::Interval(ts1, ts2)},
+ geckoprofiler::markers::TextMarker{}, ""));
+ EXPECT_TRUE(profiler_add_marker(
+ "Text from main thread with stack", geckoprofiler::category::OTHER,
+ MarkerOptions(MarkerThreadId::MainThread(), MarkerStack::Capture()),
+ geckoprofiler::markers::TextMarker{}, ""));
+
+ std::thread registeredThread([]() {
+ AUTO_PROFILER_REGISTER_THREAD("Marker test sub-thread");
+ // Marker in non-profiled thread won't be stored.
+ EXPECT_FALSE(profiler_add_marker(
+ "Text in registered thread with stack", geckoprofiler::category::OTHER,
+ MarkerStack::Capture(), geckoprofiler::markers::TextMarker{}, ""));
+ // Marker will be stored in main thread, with stack from registered thread.
+ EXPECT_TRUE(profiler_add_marker(
+ "Text from registered thread with stack",
+ geckoprofiler::category::OTHER,
+ MarkerOptions(MarkerThreadId::MainThread(), MarkerStack::Capture()),
+ geckoprofiler::markers::TextMarker{}, ""));
+ });
+ registeredThread.join();
+
+ std::thread unregisteredThread([]() {
+ // Marker in unregistered thread won't be stored.
+ EXPECT_FALSE(profiler_add_marker("Text in unregistered thread with stack",
+ geckoprofiler::category::OTHER,
+ MarkerStack::Capture(),
+ geckoprofiler::markers::TextMarker{}, ""));
+ // Marker will be stored in main thread, but stack cannot be captured in an
+ // unregistered thread.
+ EXPECT_TRUE(profiler_add_marker(
+ "Text from unregistered thread with stack",
+ geckoprofiler::category::OTHER,
+ MarkerOptions(MarkerThreadId::MainThread(), MarkerStack::Capture()),
+ geckoprofiler::markers::TextMarker{}, ""));
+ });
+ unregisteredThread.join();
+
+ EXPECT_TRUE(profiler_add_marker("Tracing", geckoprofiler::category::OTHER, {},
+ geckoprofiler::markers::Tracing{},
+ "category"));
+
+ EXPECT_TRUE(profiler_add_marker("Text", geckoprofiler::category::OTHER, {},
+ geckoprofiler::markers::TextMarker{},
+ "Text text"));
+
+ // Ensure that we evaluate to false for markers with very long texts by
+ // testing against a ~3mb string. A string of this size should exceed the
+ // available buffer chunks (max: 2) that are available and be discarded.
+ EXPECT_FALSE(profiler_add_marker("Text", geckoprofiler::category::OTHER, {},
+ geckoprofiler::markers::TextMarker{},
+ std::string(3 * 1024 * 1024, 'x')));
+
+ EXPECT_TRUE(profiler_add_marker(
+ "MediaSample", geckoprofiler::category::OTHER, {},
+ geckoprofiler::markers::MediaSampleMarker{}, 123, 456, 789));
+
+ SpliceableChunkedJSONWriter w{FailureLatchInfallibleSource::Singleton()};
+ w.Start();
+ EXPECT_TRUE(::profiler_stream_json_for_this_process(w).isOk());
+ w.End();
+
+ EXPECT_FALSE(w.Failed());
+
+ UniquePtr<char[]> profile = w.ChunkedWriteFunc().CopyData();
+ ASSERT_TRUE(!!profile.get());
+
+ // Expected markers, in order.
+ enum State {
+ S_tracing_event,
+ S_tracing_start,
+ S_tracing_end,
+ S_tracing_event_with_stack,
+ S_tracing_auto_tracing_start,
+ S_tracing_auto_tracing_end,
+ S_M1,
+ S_M3,
+ S_Markers2DefaultEmptyOptions,
+ S_Markers2DefaultWithOptions,
+ S_Markers2ExplicitDefaultEmptyOptions,
+ S_Markers2ExplicitDefaultWithOptions,
+ S_FirstMarker,
+ S_CustomMarker,
+ S_SpecialMarker,
+ S_NetworkMarkerPayload_start,
+ S_NetworkMarkerPayload_stop,
+ S_NetworkMarkerPayload_redirect_temporary,
+ S_NetworkMarkerPayload_redirect_permanent,
+ S_NetworkMarkerPayload_redirect_internal,
+ S_NetworkMarkerPayload_redirect_internal_sts,
+ S_NetworkMarkerPayload_private_browsing,
+
+ S_TextWithStack,
+ S_TextToMTWithStack,
+ S_RegThread_TextToMTWithStack,
+ S_UnregThread_TextToMTWithStack,
+
+ S_LAST,
+ } state = State(0);
+
+ // These will be set when first read from S_FirstMarker, then
+ // compared in following markers.
+ // TODO: Compute these values from the timestamps.
+ double ts1Double = 0.0;
+ double ts2Double = 0.0;
+
+ JSONOutputCheck(profile.get(), [&](const Json::Value& root) {
+ {
+ GET_JSON(threads, root["threads"], Array);
+ ASSERT_EQ(threads.size(), 1u);
+
+ {
+ GET_JSON(thread0, threads[0], Object);
+
+ // Keep a reference to the string table in this block, it will be used
+ // below.
+ GET_JSON(stringTable, thread0["stringTable"], Array);
+ ASSERT_TRUE(stringTable.isArray());
+
+ // Test the expected labels in the string table.
+ bool foundEmpty = false;
+ bool foundOkstr1 = false;
+ bool foundOkstr2 = false;
+ const std::string okstr2Label = std::string("okstr2 ") + okstr2.get();
+ bool foundTooLong = false;
+ for (const auto& s : stringTable) {
+ ASSERT_TRUE(s.isString());
+ std::string sString = s.asString();
+ if (sString.empty()) {
+ EXPECT_FALSE(foundEmpty);
+ foundEmpty = true;
+ } else if (sString == okstr1.get()) {
+ EXPECT_FALSE(foundOkstr1);
+ foundOkstr1 = true;
+ } else if (sString == okstr2Label) {
+ EXPECT_FALSE(foundOkstr2);
+ foundOkstr2 = true;
+ } else if (sString == longstrCut.get()) {
+ EXPECT_FALSE(foundTooLong);
+ foundTooLong = true;
+ } else {
+ EXPECT_NE(sString, longstr.get());
+ }
+ }
+ EXPECT_TRUE(foundEmpty);
+ EXPECT_TRUE(foundOkstr1);
+ EXPECT_TRUE(foundOkstr2);
+ EXPECT_TRUE(foundTooLong);
+
+ {
+ GET_JSON(markers, thread0["markers"], Object);
+
+ {
+ GET_JSON(data, markers["data"], Array);
+
+ for (const Json::Value& marker : data) {
+ // Name the indexes into the marker tuple:
+ // [name, startTime, endTime, phase, category, payload]
+ const unsigned int NAME = 0u;
+ const unsigned int START_TIME = 1u;
+ const unsigned int END_TIME = 2u;
+ const unsigned int PHASE = 3u;
+ const unsigned int CATEGORY = 4u;
+ const unsigned int PAYLOAD = 5u;
+
+ const unsigned int PHASE_INSTANT = 0;
+ const unsigned int PHASE_INTERVAL = 1;
+ const unsigned int PHASE_START = 2;
+ const unsigned int PHASE_END = 3;
+
+ const unsigned int SIZE_WITHOUT_PAYLOAD = 5u;
+ const unsigned int SIZE_WITH_PAYLOAD = 6u;
+
+ ASSERT_TRUE(marker.isArray());
+ // The payload is optional.
+ ASSERT_GE(marker.size(), SIZE_WITHOUT_PAYLOAD);
+ ASSERT_LE(marker.size(), SIZE_WITH_PAYLOAD);
+
+ // root.threads[0].markers.data[i] is an array with 5 or 6
+ // elements.
+
+ ASSERT_TRUE(marker[NAME].isUInt()); // name id
+ GET_JSON(name, stringTable[marker[NAME].asUInt()], String);
+ std::string nameString = name.asString();
+
+ EXPECT_TRUE(marker[START_TIME].isNumeric());
+ EXPECT_TRUE(marker[END_TIME].isNumeric());
+ EXPECT_TRUE(marker[PHASE].isUInt());
+ EXPECT_TRUE(marker[PHASE].asUInt() < 4);
+ EXPECT_TRUE(marker[CATEGORY].isUInt());
+
+# define EXPECT_TIMING_INSTANT \
+ EXPECT_NE(marker[START_TIME].asDouble(), 0); \
+ EXPECT_EQ(marker[END_TIME].asDouble(), 0); \
+ EXPECT_EQ(marker[PHASE].asUInt(), PHASE_INSTANT);
+# define EXPECT_TIMING_INTERVAL \
+ EXPECT_NE(marker[START_TIME].asDouble(), 0); \
+ EXPECT_NE(marker[END_TIME].asDouble(), 0); \
+ EXPECT_EQ(marker[PHASE].asUInt(), PHASE_INTERVAL);
+# define EXPECT_TIMING_START \
+ EXPECT_NE(marker[START_TIME].asDouble(), 0); \
+ EXPECT_EQ(marker[END_TIME].asDouble(), 0); \
+ EXPECT_EQ(marker[PHASE].asUInt(), PHASE_START);
+# define EXPECT_TIMING_END \
+ EXPECT_EQ(marker[START_TIME].asDouble(), 0); \
+ EXPECT_NE(marker[END_TIME].asDouble(), 0); \
+ EXPECT_EQ(marker[PHASE].asUInt(), PHASE_END);
+
+# define EXPECT_TIMING_INSTANT_AT(t) \
+ EXPECT_EQ(marker[START_TIME].asDouble(), t); \
+ EXPECT_EQ(marker[END_TIME].asDouble(), 0); \
+ EXPECT_EQ(marker[PHASE].asUInt(), PHASE_INSTANT);
+# define EXPECT_TIMING_INTERVAL_AT(start, end) \
+ EXPECT_EQ(marker[START_TIME].asDouble(), start); \
+ EXPECT_EQ(marker[END_TIME].asDouble(), end); \
+ EXPECT_EQ(marker[PHASE].asUInt(), PHASE_INTERVAL);
+# define EXPECT_TIMING_START_AT(start) \
+ EXPECT_EQ(marker[START_TIME].asDouble(), start); \
+ EXPECT_EQ(marker[END_TIME].asDouble(), 0); \
+ EXPECT_EQ(marker[PHASE].asUInt(), PHASE_START);
+# define EXPECT_TIMING_END_AT(end) \
+ EXPECT_EQ(marker[START_TIME].asDouble(), 0); \
+ EXPECT_EQ(marker[END_TIME].asDouble(), end); \
+ EXPECT_EQ(marker[PHASE].asUInt(), PHASE_END);
+
+ if (marker.size() == SIZE_WITHOUT_PAYLOAD) {
+ // root.threads[0].markers.data[i] is an array with 5 elements,
+ // so there is no payload.
+ if (nameString == "M1") {
+ ASSERT_EQ(state, S_M1);
+ state = State(state + 1);
+ } else if (nameString == "M3") {
+ ASSERT_EQ(state, S_M3);
+ state = State(state + 1);
+ } else if (nameString ==
+ "default-templated markers 2.0 with empty options") {
+ EXPECT_EQ(state, S_Markers2DefaultEmptyOptions);
+ state = State(S_Markers2DefaultEmptyOptions + 1);
+// TODO: Re-enable this when bug 1646714 lands, and check for stack.
+# if 0
+ } else if (nameString ==
+ "default-templated markers 2.0 with option") {
+ EXPECT_EQ(state, S_Markers2DefaultWithOptions);
+ state = State(S_Markers2DefaultWithOptions + 1);
+# endif
+ } else if (nameString ==
+ "explicitly-default-templated markers 2.0 with "
+ "empty "
+ "options") {
+ EXPECT_EQ(state, S_Markers2ExplicitDefaultEmptyOptions);
+ state = State(S_Markers2ExplicitDefaultEmptyOptions + 1);
+ } else if (nameString ==
+ "explicitly-default-templated markers 2.0 with "
+ "option") {
+ EXPECT_EQ(state, S_Markers2ExplicitDefaultWithOptions);
+ state = State(S_Markers2ExplicitDefaultWithOptions + 1);
+ }
+ } else {
+ // root.threads[0].markers.data[i] is an array with 6 elements,
+ // so there is a payload.
+ GET_JSON(payload, marker[PAYLOAD], Object);
+
+ // root.threads[0].markers.data[i][PAYLOAD] is an object
+ // (payload).
+
+ // It should at least have a "type" string.
+ GET_JSON(type, payload["type"], String);
+ std::string typeString = type.asString();
+
+ if (nameString == "tracing event") {
+ EXPECT_EQ(state, S_tracing_event);
+ state = State(S_tracing_event + 1);
+ EXPECT_EQ(typeString, "tracing");
+ EXPECT_TIMING_INSTANT;
+ EXPECT_EQ_JSON(payload["category"], String, "A");
+ EXPECT_TRUE(payload["stack"].isNull());
+
+ } else if (nameString == "tracing start") {
+ EXPECT_EQ(state, S_tracing_start);
+ state = State(S_tracing_start + 1);
+ EXPECT_EQ(typeString, "tracing");
+ EXPECT_TIMING_START;
+ EXPECT_EQ_JSON(payload["category"], String, "A");
+ EXPECT_TRUE(payload["stack"].isNull());
+
+ } else if (nameString == "tracing end") {
+ EXPECT_EQ(state, S_tracing_end);
+ state = State(S_tracing_end + 1);
+ EXPECT_EQ(typeString, "tracing");
+ EXPECT_TIMING_END;
+ EXPECT_EQ_JSON(payload["category"], String, "A");
+ EXPECT_TRUE(payload["stack"].isNull());
+
+ } else if (nameString == "tracing event with stack") {
+ EXPECT_EQ(state, S_tracing_event_with_stack);
+ state = State(S_tracing_event_with_stack + 1);
+ EXPECT_EQ(typeString, "tracing");
+ EXPECT_TIMING_INSTANT;
+ EXPECT_EQ_JSON(payload["category"], String, "B");
+ EXPECT_TRUE(payload["stack"].isObject());
+
+ } else if (nameString == "auto tracing") {
+ switch (state) {
+ case S_tracing_auto_tracing_start:
+ state = State(S_tracing_auto_tracing_start + 1);
+ EXPECT_EQ(typeString, "tracing");
+ EXPECT_TIMING_START;
+ EXPECT_EQ_JSON(payload["category"], String, "C");
+ EXPECT_TRUE(payload["stack"].isNull());
+ break;
+ case S_tracing_auto_tracing_end:
+ state = State(S_tracing_auto_tracing_end + 1);
+ EXPECT_EQ(typeString, "tracing");
+ EXPECT_TIMING_END;
+ EXPECT_EQ_JSON(payload["category"], String, "C");
+ ASSERT_TRUE(payload["stack"].isNull());
+ break;
+ default:
+ EXPECT_TRUE(state == S_tracing_auto_tracing_start ||
+ state == S_tracing_auto_tracing_end);
+ break;
+ }
+
+ } else if (nameString ==
+ "default-templated markers 2.0 with option") {
+ // TODO: Remove this when bug 1646714 lands.
+ EXPECT_EQ(state, S_Markers2DefaultWithOptions);
+ state = State(S_Markers2DefaultWithOptions + 1);
+ EXPECT_EQ(typeString, "NoPayloadUserData");
+ EXPECT_FALSE(payload["stack"].isNull());
+
+ } else if (nameString == "FirstMarker") {
+ // Record start and end times, to compare with timestamps in
+ // following markers.
+ EXPECT_EQ(state, S_FirstMarker);
+ ts1Double = marker[START_TIME].asDouble();
+ ts2Double = marker[END_TIME].asDouble();
+ state = State(S_FirstMarker + 1);
+ EXPECT_EQ(typeString, "Text");
+ EXPECT_EQ_JSON(payload["name"], String, "First Marker");
+
+ } else if (nameString == "Gtest custom marker") {
+ EXPECT_EQ(state, S_CustomMarker);
+ state = State(S_CustomMarker + 1);
+ EXPECT_EQ(typeString, "markers-gtest");
+ EXPECT_EQ(payload.size(), 1u + 9u);
+ EXPECT_TRUE(payload["null"].isNull());
+ EXPECT_EQ_JSON(payload["bool-false"], Bool, false);
+ EXPECT_EQ_JSON(payload["bool-true"], Bool, true);
+ EXPECT_EQ_JSON(payload["int"], Int64, 42);
+ EXPECT_EQ_JSON(payload["double"], Double, 43.0);
+ EXPECT_EQ_JSON(payload["text"], String, "gtest text");
+ // Unique strings can be fetched from the string table.
+ ASSERT_TRUE(payload["unique text"].isUInt());
+ auto textIndex = payload["unique text"].asUInt();
+ GET_JSON(uniqueText, stringTable[textIndex], String);
+ ASSERT_TRUE(uniqueText.isString());
+ ASSERT_EQ(uniqueText.asString(), "gtest unique text");
+ // The duplicate unique text should have the exact same index.
+ EXPECT_EQ_JSON(payload["unique text again"], UInt, textIndex);
+ EXPECT_EQ_JSON(payload["time"], Double, ts1Double);
+
+ } else if (nameString == "Gtest special marker") {
+ EXPECT_EQ(state, S_SpecialMarker);
+ state = State(S_SpecialMarker + 1);
+ EXPECT_EQ(typeString, "markers-gtest-special");
+ EXPECT_EQ(payload.size(), 1u) << "Only 'type' in the payload";
+
+ } else if (nameString == "Load 1: http://mozilla.org/") {
+ EXPECT_EQ(state, S_NetworkMarkerPayload_start);
+ state = State(S_NetworkMarkerPayload_start + 1);
+ EXPECT_EQ(typeString, "Network");
+ EXPECT_EQ_JSON(payload["startTime"], Double, ts1Double);
+ EXPECT_EQ_JSON(payload["endTime"], Double, ts2Double);
+ EXPECT_EQ_JSON(payload["id"], Int64, 1);
+ EXPECT_EQ_JSON(payload["URI"], String, "http://mozilla.org/");
+ EXPECT_EQ_JSON(payload["requestMethod"], String, "GET");
+ EXPECT_EQ_JSON(payload["pri"], Int64, 34);
+ EXPECT_EQ_JSON(payload["count"], Int64, 56);
+ EXPECT_EQ_JSON(payload["cache"], String, "Hit");
+ EXPECT_TRUE(payload["isPrivateBrowsing"].isNull());
+ EXPECT_TRUE(payload["RedirectURI"].isNull());
+ EXPECT_TRUE(payload["redirectType"].isNull());
+ EXPECT_TRUE(payload["isHttpToHttpsRedirect"].isNull());
+ EXPECT_TRUE(payload["redirectId"].isNull());
+ EXPECT_TRUE(payload["contentType"].isNull());
+
+ } else if (nameString == "Load 2: http://mozilla.org/") {
+ EXPECT_EQ(state, S_NetworkMarkerPayload_stop);
+ state = State(S_NetworkMarkerPayload_stop + 1);
+ EXPECT_EQ(typeString, "Network");
+ EXPECT_EQ_JSON(payload["startTime"], Double, ts1Double);
+ EXPECT_EQ_JSON(payload["endTime"], Double, ts2Double);
+ EXPECT_EQ_JSON(payload["id"], Int64, 2);
+ EXPECT_EQ_JSON(payload["URI"], String, "http://mozilla.org/");
+ EXPECT_EQ_JSON(payload["requestMethod"], String, "GET");
+ EXPECT_EQ_JSON(payload["pri"], Int64, 34);
+ EXPECT_EQ_JSON(payload["count"], Int64, 56);
+ EXPECT_EQ_JSON(payload["cache"], String, "Unresolved");
+ EXPECT_TRUE(payload["isPrivateBrowsing"].isNull());
+ EXPECT_TRUE(payload["RedirectURI"].isNull());
+ EXPECT_TRUE(payload["redirectType"].isNull());
+ EXPECT_TRUE(payload["isHttpToHttpsRedirect"].isNull());
+ EXPECT_TRUE(payload["redirectId"].isNull());
+ EXPECT_EQ_JSON(payload["contentType"], String, "text/html");
+
+ } else if (nameString == "Load 3: http://mozilla.org/") {
+ EXPECT_EQ(state, S_NetworkMarkerPayload_redirect_temporary);
+ state = State(S_NetworkMarkerPayload_redirect_temporary + 1);
+ EXPECT_EQ(typeString, "Network");
+ EXPECT_EQ_JSON(payload["startTime"], Double, ts1Double);
+ EXPECT_EQ_JSON(payload["endTime"], Double, ts2Double);
+ EXPECT_EQ_JSON(payload["id"], Int64, 3);
+ EXPECT_EQ_JSON(payload["URI"], String, "http://mozilla.org/");
+ EXPECT_EQ_JSON(payload["requestMethod"], String, "GET");
+ EXPECT_EQ_JSON(payload["pri"], Int64, 34);
+ EXPECT_EQ_JSON(payload["count"], Int64, 56);
+ EXPECT_EQ_JSON(payload["cache"], String, "Unresolved");
+ EXPECT_TRUE(payload["isPrivateBrowsing"].isNull());
+ EXPECT_EQ_JSON(payload["RedirectURI"], String,
+ "http://example.com/");
+ EXPECT_EQ_JSON(payload["redirectType"], String, "Temporary");
+ EXPECT_EQ_JSON(payload["isHttpToHttpsRedirect"], Bool, false);
+ EXPECT_EQ_JSON(payload["redirectId"], Int64, 103);
+ EXPECT_TRUE(payload["contentType"].isNull());
+
+ } else if (nameString == "Load 4: http://mozilla.org/") {
+ EXPECT_EQ(state, S_NetworkMarkerPayload_redirect_permanent);
+ state = State(S_NetworkMarkerPayload_redirect_permanent + 1);
+ EXPECT_EQ(typeString, "Network");
+ EXPECT_EQ_JSON(payload["startTime"], Double, ts1Double);
+ EXPECT_EQ_JSON(payload["endTime"], Double, ts2Double);
+ EXPECT_EQ_JSON(payload["id"], Int64, 4);
+ EXPECT_EQ_JSON(payload["URI"], String, "http://mozilla.org/");
+ EXPECT_EQ_JSON(payload["requestMethod"], String, "GET");
+ EXPECT_EQ_JSON(payload["pri"], Int64, 34);
+ EXPECT_EQ_JSON(payload["count"], Int64, 56);
+ EXPECT_EQ_JSON(payload["cache"], String, "Unresolved");
+ EXPECT_TRUE(payload["isPrivateBrowsing"].isNull());
+ EXPECT_EQ_JSON(payload["RedirectURI"], String,
+ "http://example.com/");
+ EXPECT_EQ_JSON(payload["redirectType"], String, "Permanent");
+ EXPECT_EQ_JSON(payload["isHttpToHttpsRedirect"], Bool, false);
+ EXPECT_EQ_JSON(payload["redirectId"], Int64, 104);
+ EXPECT_TRUE(payload["contentType"].isNull());
+
+ } else if (nameString == "Load 5: http://mozilla.org/") {
+ EXPECT_EQ(state, S_NetworkMarkerPayload_redirect_internal);
+ state = State(S_NetworkMarkerPayload_redirect_internal + 1);
+ EXPECT_EQ(typeString, "Network");
+ EXPECT_EQ_JSON(payload["startTime"], Double, ts1Double);
+ EXPECT_EQ_JSON(payload["endTime"], Double, ts2Double);
+ EXPECT_EQ_JSON(payload["id"], Int64, 5);
+ EXPECT_EQ_JSON(payload["URI"], String, "http://mozilla.org/");
+ EXPECT_EQ_JSON(payload["requestMethod"], String, "GET");
+ EXPECT_EQ_JSON(payload["pri"], Int64, 34);
+ EXPECT_EQ_JSON(payload["count"], Int64, 56);
+ EXPECT_EQ_JSON(payload["cache"], String, "Unresolved");
+ EXPECT_TRUE(payload["isPrivateBrowsing"].isNull());
+ EXPECT_EQ_JSON(payload["RedirectURI"], String,
+ "http://example.com/");
+ EXPECT_EQ_JSON(payload["redirectType"], String, "Internal");
+ EXPECT_EQ_JSON(payload["isHttpToHttpsRedirect"], Bool, false);
+ EXPECT_EQ_JSON(payload["redirectId"], Int64, 105);
+ EXPECT_TRUE(payload["contentType"].isNull());
+
+ } else if (nameString == "Load 6: http://mozilla.org/") {
+ EXPECT_EQ(state,
+ S_NetworkMarkerPayload_redirect_internal_sts);
+ state =
+ State(S_NetworkMarkerPayload_redirect_internal_sts + 1);
+ EXPECT_EQ(typeString, "Network");
+ EXPECT_EQ_JSON(payload["startTime"], Double, ts1Double);
+ EXPECT_EQ_JSON(payload["endTime"], Double, ts2Double);
+ EXPECT_EQ_JSON(payload["id"], Int64, 6);
+ EXPECT_EQ_JSON(payload["URI"], String, "http://mozilla.org/");
+ EXPECT_EQ_JSON(payload["requestMethod"], String, "GET");
+ EXPECT_EQ_JSON(payload["pri"], Int64, 34);
+ EXPECT_EQ_JSON(payload["count"], Int64, 56);
+ EXPECT_EQ_JSON(payload["cache"], String, "Unresolved");
+ EXPECT_TRUE(payload["isPrivateBrowsing"].isNull());
+ EXPECT_EQ_JSON(payload["RedirectURI"], String,
+ "http://example.com/");
+ EXPECT_EQ_JSON(payload["redirectType"], String, "Internal");
+ EXPECT_EQ_JSON(payload["isHttpToHttpsRedirect"], Bool, true);
+ EXPECT_EQ_JSON(payload["redirectId"], Int64, 106);
+ EXPECT_TRUE(payload["contentType"].isNull());
+
+ } else if (nameString == "Load 7: http://mozilla.org/") {
+ EXPECT_EQ(state, S_NetworkMarkerPayload_private_browsing);
+ state = State(S_NetworkMarkerPayload_private_browsing + 1);
+ EXPECT_EQ(typeString, "Network");
+ EXPECT_EQ_JSON(payload["startTime"], Double, ts1Double);
+ EXPECT_EQ_JSON(payload["endTime"], Double, ts2Double);
+ EXPECT_EQ_JSON(payload["id"], Int64, 7);
+ EXPECT_EQ_JSON(payload["URI"], String, "http://mozilla.org/");
+ EXPECT_EQ_JSON(payload["requestMethod"], String, "GET");
+ EXPECT_EQ_JSON(payload["pri"], Int64, 34);
+ EXPECT_EQ_JSON(payload["count"], Int64, 56);
+ EXPECT_EQ_JSON(payload["cache"], String, "Unresolved");
+ EXPECT_EQ_JSON(payload["isPrivateBrowsing"], Bool, true);
+ EXPECT_TRUE(payload["RedirectURI"].isNull());
+ EXPECT_TRUE(payload["redirectType"].isNull());
+ EXPECT_TRUE(payload["isHttpToHttpsRedirect"].isNull());
+ EXPECT_TRUE(payload["redirectId"].isNull());
+ EXPECT_TRUE(payload["contentType"].isNull());
+ } else if (nameString == "Text in main thread with stack") {
+ EXPECT_EQ(state, S_TextWithStack);
+ state = State(S_TextWithStack + 1);
+ EXPECT_EQ(typeString, "Text");
+ EXPECT_FALSE(payload["stack"].isNull());
+ EXPECT_TIMING_INTERVAL_AT(ts1Double, ts2Double);
+ EXPECT_EQ_JSON(payload["name"], String, "");
+
+ } else if (nameString == "Text from main thread with stack") {
+ EXPECT_EQ(state, S_TextToMTWithStack);
+ state = State(S_TextToMTWithStack + 1);
+ EXPECT_EQ(typeString, "Text");
+ EXPECT_FALSE(payload["stack"].isNull());
+ EXPECT_EQ_JSON(payload["name"], String, "");
+
+ } else if (nameString ==
+ "Text in registered thread with stack") {
+ ADD_FAILURE()
+ << "Unexpected 'Text in registered thread with stack'";
+
+ } else if (nameString ==
+ "Text from registered thread with stack") {
+ EXPECT_EQ(state, S_RegThread_TextToMTWithStack);
+ state = State(S_RegThread_TextToMTWithStack + 1);
+ EXPECT_EQ(typeString, "Text");
+ EXPECT_FALSE(payload["stack"].isNull());
+ EXPECT_EQ_JSON(payload["name"], String, "");
+
+ } else if (nameString ==
+ "Text in unregistered thread with stack") {
+ ADD_FAILURE()
+ << "Unexpected 'Text in unregistered thread with stack'";
+
+ } else if (nameString ==
+ "Text from unregistered thread with stack") {
+ EXPECT_EQ(state, S_UnregThread_TextToMTWithStack);
+ state = State(S_UnregThread_TextToMTWithStack + 1);
+ EXPECT_EQ(typeString, "Text");
+ EXPECT_TRUE(payload["stack"].isNull());
+ EXPECT_EQ_JSON(payload["name"], String, "");
+ }
+ } // marker with payload
+ } // for (marker : data)
+ } // markers.data
+ } // markers
+ } // thread0
+ } // threads
+ // We should have read all expected markers.
+ EXPECT_EQ(state, S_LAST);
+
+ {
+ GET_JSON(meta, root["meta"], Object);
+
+ {
+ GET_JSON(markerSchema, meta["markerSchema"], Array);
+
+ std::set<std::string> testedSchemaNames;
+
+ for (const Json::Value& schema : markerSchema) {
+ GET_JSON(name, schema["name"], String);
+ const std::string nameString = name.asString();
+
+ GET_JSON(display, schema["display"], Array);
+
+ GET_JSON(data, schema["data"], Array);
+
+ EXPECT_TRUE(
+ testedSchemaNames
+ .insert(std::string(nameString.data(), nameString.size()))
+ .second)
+ << "Each schema name should be unique (inserted once in the set)";
+
+ if (nameString == "Text") {
+ EXPECT_EQ(display.size(), 2u);
+ EXPECT_EQ(display[0u].asString(), "marker-chart");
+ EXPECT_EQ(display[1u].asString(), "marker-table");
+
+ ASSERT_EQ(data.size(), 1u);
+
+ ASSERT_TRUE(data[0u].isObject());
+ EXPECT_EQ_JSON(data[0u]["key"], String, "name");
+ EXPECT_EQ_JSON(data[0u]["label"], String, "Details");
+ EXPECT_EQ_JSON(data[0u]["format"], String, "string");
+
+ } else if (nameString == "NoPayloadUserData") {
+ // TODO: Remove this when bug 1646714 lands.
+ EXPECT_EQ(display.size(), 2u);
+ EXPECT_EQ(display[0u].asString(), "marker-chart");
+ EXPECT_EQ(display[1u].asString(), "marker-table");
+
+ ASSERT_EQ(data.size(), 0u);
+
+ } else if (nameString == "FileIO") {
+ // These are defined in ProfilerIOInterposeObserver.cpp
+
+ } else if (nameString == "tracing") {
+ EXPECT_EQ(display.size(), 3u);
+ EXPECT_EQ(display[0u].asString(), "marker-chart");
+ EXPECT_EQ(display[1u].asString(), "marker-table");
+ EXPECT_EQ(display[2u].asString(), "timeline-overview");
+
+ ASSERT_EQ(data.size(), 1u);
+
+ ASSERT_TRUE(data[0u].isObject());
+ EXPECT_EQ_JSON(data[0u]["key"], String, "category");
+ EXPECT_EQ_JSON(data[0u]["label"], String, "Type");
+ EXPECT_EQ_JSON(data[0u]["format"], String, "string");
+
+ } else if (nameString == "BHR-detected hang") {
+ EXPECT_EQ(display.size(), 2u);
+ EXPECT_EQ(display[0u].asString(), "marker-chart");
+ EXPECT_EQ(display[1u].asString(), "marker-table");
+
+ ASSERT_EQ(data.size(), 0u);
+
+ } else if (nameString == "MainThreadLongTask") {
+ EXPECT_EQ(display.size(), 2u);
+ EXPECT_EQ(display[0u].asString(), "marker-chart");
+ EXPECT_EQ(display[1u].asString(), "marker-table");
+
+ ASSERT_EQ(data.size(), 1u);
+
+ ASSERT_TRUE(data[0u].isObject());
+ EXPECT_EQ_JSON(data[0u]["key"], String, "category");
+ EXPECT_EQ_JSON(data[0u]["label"], String, "Type");
+ EXPECT_EQ_JSON(data[0u]["format"], String, "string");
+
+ } else if (nameString == "Log") {
+ EXPECT_EQ(display.size(), 1u);
+ EXPECT_EQ(display[0u].asString(), "marker-table");
+
+ ASSERT_EQ(data.size(), 2u);
+
+ ASSERT_TRUE(data[0u].isObject());
+ EXPECT_EQ_JSON(data[0u]["key"], String, "module");
+ EXPECT_EQ_JSON(data[0u]["label"], String, "Module");
+ EXPECT_EQ_JSON(data[0u]["format"], String, "string");
+
+ ASSERT_TRUE(data[1u].isObject());
+ EXPECT_EQ_JSON(data[1u]["key"], String, "name");
+ EXPECT_EQ_JSON(data[1u]["label"], String, "Name");
+ EXPECT_EQ_JSON(data[1u]["format"], String, "string");
+
+ } else if (nameString == "MediaSample") {
+ EXPECT_EQ(display.size(), 2u);
+ EXPECT_EQ(display[0u].asString(), "marker-chart");
+ EXPECT_EQ(display[1u].asString(), "marker-table");
+
+ ASSERT_EQ(data.size(), 3u);
+
+ ASSERT_TRUE(data[0u].isObject());
+ EXPECT_EQ_JSON(data[0u]["key"], String, "sampleStartTimeUs");
+ EXPECT_EQ_JSON(data[0u]["label"], String, "Sample start time");
+ EXPECT_EQ_JSON(data[0u]["format"], String, "microseconds");
+
+ ASSERT_TRUE(data[1u].isObject());
+ EXPECT_EQ_JSON(data[1u]["key"], String, "sampleEndTimeUs");
+ EXPECT_EQ_JSON(data[1u]["label"], String, "Sample end time");
+ EXPECT_EQ_JSON(data[1u]["format"], String, "microseconds");
+
+ ASSERT_TRUE(data[2u].isObject());
+ EXPECT_EQ_JSON(data[2u]["key"], String, "queueLength");
+ EXPECT_EQ_JSON(data[2u]["label"], String, "Queue length");
+ EXPECT_EQ_JSON(data[2u]["format"], String, "integer");
+
+ } else if (nameString == "VideoFallingBehind") {
+ EXPECT_EQ(display.size(), 2u);
+ EXPECT_EQ(display[0u].asString(), "marker-chart");
+ EXPECT_EQ(display[1u].asString(), "marker-table");
+
+ ASSERT_EQ(data.size(), 2u);
+
+ ASSERT_TRUE(data[0u].isObject());
+ EXPECT_EQ_JSON(data[0u]["key"], String, "videoFrameStartTimeUs");
+ EXPECT_EQ_JSON(data[0u]["label"], String, "Video frame start time");
+ EXPECT_EQ_JSON(data[0u]["format"], String, "microseconds");
+
+ ASSERT_TRUE(data[1u].isObject());
+ EXPECT_EQ_JSON(data[1u]["key"], String, "mediaCurrentTimeUs");
+ EXPECT_EQ_JSON(data[1u]["label"], String, "Media current time");
+ EXPECT_EQ_JSON(data[1u]["format"], String, "microseconds");
+
+ } else if (nameString == "Budget") {
+ EXPECT_EQ(display.size(), 2u);
+ EXPECT_EQ(display[0u].asString(), "marker-chart");
+ EXPECT_EQ(display[1u].asString(), "marker-table");
+
+ ASSERT_EQ(data.size(), 0u);
+
+ } else if (nameString == "markers-gtest") {
+ EXPECT_EQ(display.size(), 7u);
+ EXPECT_EQ(display[0u].asString(), "marker-chart");
+ EXPECT_EQ(display[1u].asString(), "marker-table");
+ EXPECT_EQ(display[2u].asString(), "timeline-overview");
+ EXPECT_EQ(display[3u].asString(), "timeline-memory");
+ EXPECT_EQ(display[4u].asString(), "timeline-ipc");
+ EXPECT_EQ(display[5u].asString(), "timeline-fileio");
+ EXPECT_EQ(display[6u].asString(), "stack-chart");
+
+ EXPECT_EQ_JSON(schema["chartLabel"], String, "chart label");
+ EXPECT_EQ_JSON(schema["tooltipLabel"], String, "tooltip label");
+ EXPECT_EQ_JSON(schema["tableLabel"], String, "table label");
+
+ ASSERT_EQ(data.size(), 14u);
+
+ ASSERT_TRUE(data[0u].isObject());
+ EXPECT_EQ_JSON(data[0u]["key"], String, "key with url");
+ EXPECT_TRUE(data[0u]["label"].isNull());
+ EXPECT_EQ_JSON(data[0u]["format"], String, "url");
+ EXPECT_TRUE(data[0u]["searchable"].isNull());
+
+ ASSERT_TRUE(data[1u].isObject());
+ EXPECT_EQ_JSON(data[1u]["key"], String, "key with label filePath");
+ EXPECT_EQ_JSON(data[1u]["label"], String, "label filePath");
+ EXPECT_EQ_JSON(data[1u]["format"], String, "file-path");
+ EXPECT_TRUE(data[1u]["searchable"].isNull());
+
+ ASSERT_TRUE(data[2u].isObject());
+ EXPECT_EQ_JSON(data[2u]["key"], String,
+ "key with string not-searchable");
+ EXPECT_TRUE(data[2u]["label"].isNull());
+ EXPECT_EQ_JSON(data[2u]["format"], String, "string");
+ EXPECT_EQ_JSON(data[2u]["searchable"], Bool, false);
+
+ ASSERT_TRUE(data[3u].isObject());
+ EXPECT_EQ_JSON(data[3u]["key"], String,
+ "key with label duration searchable");
+ EXPECT_TRUE(data[3u]["label duration"].isNull());
+ EXPECT_EQ_JSON(data[3u]["format"], String, "duration");
+ EXPECT_EQ_JSON(data[3u]["searchable"], Bool, true);
+
+ ASSERT_TRUE(data[4u].isObject());
+ EXPECT_EQ_JSON(data[4u]["key"], String, "key with time");
+ EXPECT_TRUE(data[4u]["label"].isNull());
+ EXPECT_EQ_JSON(data[4u]["format"], String, "time");
+ EXPECT_TRUE(data[4u]["searchable"].isNull());
+
+ ASSERT_TRUE(data[5u].isObject());
+ EXPECT_EQ_JSON(data[5u]["key"], String, "key with seconds");
+ EXPECT_TRUE(data[5u]["label"].isNull());
+ EXPECT_EQ_JSON(data[5u]["format"], String, "seconds");
+ EXPECT_TRUE(data[5u]["searchable"].isNull());
+
+ ASSERT_TRUE(data[6u].isObject());
+ EXPECT_EQ_JSON(data[6u]["key"], String, "key with milliseconds");
+ EXPECT_TRUE(data[6u]["label"].isNull());
+ EXPECT_EQ_JSON(data[6u]["format"], String, "milliseconds");
+ EXPECT_TRUE(data[6u]["searchable"].isNull());
+
+ ASSERT_TRUE(data[7u].isObject());
+ EXPECT_EQ_JSON(data[7u]["key"], String, "key with microseconds");
+ EXPECT_TRUE(data[7u]["label"].isNull());
+ EXPECT_EQ_JSON(data[7u]["format"], String, "microseconds");
+ EXPECT_TRUE(data[7u]["searchable"].isNull());
+
+ ASSERT_TRUE(data[8u].isObject());
+ EXPECT_EQ_JSON(data[8u]["key"], String, "key with nanoseconds");
+ EXPECT_TRUE(data[8u]["label"].isNull());
+ EXPECT_EQ_JSON(data[8u]["format"], String, "nanoseconds");
+ EXPECT_TRUE(data[8u]["searchable"].isNull());
+
+ ASSERT_TRUE(data[9u].isObject());
+ EXPECT_EQ_JSON(data[9u]["key"], String, "key with bytes");
+ EXPECT_TRUE(data[9u]["label"].isNull());
+ EXPECT_EQ_JSON(data[9u]["format"], String, "bytes");
+ EXPECT_TRUE(data[9u]["searchable"].isNull());
+
+ ASSERT_TRUE(data[10u].isObject());
+ EXPECT_EQ_JSON(data[10u]["key"], String, "key with percentage");
+ EXPECT_TRUE(data[10u]["label"].isNull());
+ EXPECT_EQ_JSON(data[10u]["format"], String, "percentage");
+ EXPECT_TRUE(data[10u]["searchable"].isNull());
+
+ ASSERT_TRUE(data[11u].isObject());
+ EXPECT_EQ_JSON(data[11u]["key"], String, "key with integer");
+ EXPECT_TRUE(data[11u]["label"].isNull());
+ EXPECT_EQ_JSON(data[11u]["format"], String, "integer");
+ EXPECT_TRUE(data[11u]["searchable"].isNull());
+
+ ASSERT_TRUE(data[12u].isObject());
+ EXPECT_EQ_JSON(data[12u]["key"], String, "key with decimal");
+ EXPECT_TRUE(data[12u]["label"].isNull());
+ EXPECT_EQ_JSON(data[12u]["format"], String, "decimal");
+ EXPECT_TRUE(data[12u]["searchable"].isNull());
+
+ ASSERT_TRUE(data[13u].isObject());
+ EXPECT_EQ_JSON(data[13u]["label"], String, "static label");
+ EXPECT_EQ_JSON(data[13u]["value"], String, "static value");
+
+ } else if (nameString == "markers-gtest-special") {
+ EXPECT_EQ(display.size(), 0u);
+ ASSERT_EQ(data.size(), 0u);
+
+ } else if (nameString == "markers-gtest-unused") {
+ ADD_FAILURE() << "Schema for GtestUnusedMarker should not be here";
+
+ } else {
+ printf("FYI: Unknown marker schema '%s'\n", nameString.c_str());
+ }
+ }
+
+ // Check that we've got all expected schema.
+ EXPECT_TRUE(testedSchemaNames.find("Text") != testedSchemaNames.end());
+ EXPECT_TRUE(testedSchemaNames.find("tracing") !=
+ testedSchemaNames.end());
+ EXPECT_TRUE(testedSchemaNames.find("MediaSample") !=
+ testedSchemaNames.end());
+ } // markerSchema
+ } // meta
+ });
+
+ Maybe<ProfilerBufferInfo> info = profiler_get_buffer_info();
+ EXPECT_TRUE(info.isSome());
+ printf("Profiler buffer range: %llu .. %llu (%llu bytes)\n",
+ static_cast<unsigned long long>(info->mRangeStart),
+ static_cast<unsigned long long>(info->mRangeEnd),
+ // sizeof(ProfileBufferEntry) == 9
+ (static_cast<unsigned long long>(info->mRangeEnd) -
+ static_cast<unsigned long long>(info->mRangeStart)) *
+ 9);
+ printf("Stats: min(us) .. mean(us) .. max(us) [count]\n");
+ printf("- Intervals: %7.1f .. %7.1f .. %7.1f [%u]\n",
+ info->mIntervalsUs.min, info->mIntervalsUs.sum / info->mIntervalsUs.n,
+ info->mIntervalsUs.max, info->mIntervalsUs.n);
+ printf("- Overheads: %7.1f .. %7.1f .. %7.1f [%u]\n",
+ info->mOverheadsUs.min, info->mOverheadsUs.sum / info->mOverheadsUs.n,
+ info->mOverheadsUs.max, info->mOverheadsUs.n);
+ printf(" - Locking: %7.1f .. %7.1f .. %7.1f [%u]\n",
+ info->mLockingsUs.min, info->mLockingsUs.sum / info->mLockingsUs.n,
+ info->mLockingsUs.max, info->mLockingsUs.n);
+ printf(" - Clearning: %7.1f .. %7.1f .. %7.1f [%u]\n",
+ info->mCleaningsUs.min, info->mCleaningsUs.sum / info->mCleaningsUs.n,
+ info->mCleaningsUs.max, info->mCleaningsUs.n);
+ printf(" - Counters: %7.1f .. %7.1f .. %7.1f [%u]\n",
+ info->mCountersUs.min, info->mCountersUs.sum / info->mCountersUs.n,
+ info->mCountersUs.max, info->mCountersUs.n);
+ printf(" - Threads: %7.1f .. %7.1f .. %7.1f [%u]\n",
+ info->mThreadsUs.min, info->mThreadsUs.sum / info->mThreadsUs.n,
+ info->mThreadsUs.max, info->mThreadsUs.n);
+
+ profiler_stop();
+
+ // Try to add markers while the profiler is stopped.
+ PROFILER_MARKER_UNTYPED("marker after profiler_stop", OTHER);
+
+ // Warning: this could be racy
+ profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL, features,
+ filters, MOZ_ARRAY_LENGTH(filters), 0);
+
+ // This last marker shouldn't get streamed.
+ SpliceableChunkedJSONWriter w2{FailureLatchInfallibleSource::Singleton()};
+ w2.Start();
+ EXPECT_TRUE(::profiler_stream_json_for_this_process(w2).isOk());
+ w2.End();
+ EXPECT_FALSE(w2.Failed());
+ UniquePtr<char[]> profile2 = w2.ChunkedWriteFunc().CopyData();
+ ASSERT_TRUE(!!profile2.get());
+ EXPECT_TRUE(
+ std::string_view(profile2.get()).find("marker after profiler_stop") ==
+ std::string_view::npos);
+
+ profiler_stop();
+}
+
+# define COUNTER_NAME "TestCounter"
+# define COUNTER_DESCRIPTION "Test of counters in profiles"
+# define COUNTER_NAME2 "Counter2"
+# define COUNTER_DESCRIPTION2 "Second Test of counters in profiles"
+
+PROFILER_DEFINE_COUNT_TOTAL(TestCounter, COUNTER_NAME, COUNTER_DESCRIPTION);
+PROFILER_DEFINE_COUNT_TOTAL(TestCounter2, COUNTER_NAME2, COUNTER_DESCRIPTION2);
+
+TEST(GeckoProfiler, Counters)
+{
+ uint32_t features = 0;
+ const char* filters[] = {"GeckoMain"};
+
+ // We will record some counter values, and check that they're present (and no
+ // other) when expected.
+
+ struct NumberAndCount {
+ uint64_t mNumber;
+ int64_t mCount;
+ };
+
+ int64_t testCounters[] = {10, 7, -17};
+ NumberAndCount expectedTestCounters[] = {{1u, 10}, {0u, 0}, {1u, 7},
+ {0u, 0}, {0u, 0}, {1u, -17},
+ {0u, 0}, {0u, 0}};
+ constexpr size_t expectedTestCountersCount =
+ MOZ_ARRAY_LENGTH(expectedTestCounters);
+
+ bool expectCounter2 = false;
+ int64_t testCounters2[] = {10};
+ NumberAndCount expectedTestCounters2[] = {{1u, 10}, {0u, 0}};
+ constexpr size_t expectedTestCounters2Count =
+ MOZ_ARRAY_LENGTH(expectedTestCounters2);
+
+ auto checkCountersInJSON = [&](const Json::Value& aRoot) {
+ size_t nextExpectedTestCounter = 0u;
+ size_t nextExpectedTestCounter2 = 0u;
+
+ GET_JSON(counters, aRoot["counters"], Array);
+ for (const Json::Value& counter : counters) {
+ ASSERT_TRUE(counter.isObject());
+ GET_JSON_VALUE(name, counter["name"], String);
+ if (name == "TestCounter") {
+ EXPECT_EQ_JSON(counter["category"], String, COUNTER_NAME);
+ EXPECT_EQ_JSON(counter["description"], String, COUNTER_DESCRIPTION);
+ GET_JSON(sampleGroups, counter["sample_groups"], Array);
+ for (const Json::Value& sampleGroup : sampleGroups) {
+ ASSERT_TRUE(sampleGroup.isObject());
+ EXPECT_EQ_JSON(sampleGroup["id"], UInt, 0u);
+
+ GET_JSON(samples, sampleGroup["samples"], Object);
+ GET_JSON(samplesSchema, samples["schema"], Object);
+ EXPECT_GE(samplesSchema.size(), 3u);
+ GET_JSON_VALUE(samplesNumber, samplesSchema["number"], UInt);
+ GET_JSON_VALUE(samplesCount, samplesSchema["count"], UInt);
+ GET_JSON(samplesData, samples["data"], Array);
+ for (const Json::Value& sample : samplesData) {
+ ASSERT_TRUE(sample.isArray());
+ ASSERT_LT(nextExpectedTestCounter, expectedTestCountersCount);
+ EXPECT_EQ_JSON(
+ sample[samplesNumber], UInt64,
+ expectedTestCounters[nextExpectedTestCounter].mNumber);
+ EXPECT_EQ_JSON(
+ sample[samplesCount], Int64,
+ expectedTestCounters[nextExpectedTestCounter].mCount);
+ ++nextExpectedTestCounter;
+ }
+ }
+ } else if (name == "TestCounter2") {
+ EXPECT_TRUE(expectCounter2);
+
+ EXPECT_EQ_JSON(counter["category"], String, COUNTER_NAME2);
+ EXPECT_EQ_JSON(counter["description"], String, COUNTER_DESCRIPTION2);
+ GET_JSON(sampleGroups, counter["sample_groups"], Array);
+ for (const Json::Value& sampleGroup : sampleGroups) {
+ ASSERT_TRUE(sampleGroup.isObject());
+ EXPECT_EQ_JSON(sampleGroup["id"], UInt, 0u);
+
+ GET_JSON(samples, sampleGroup["samples"], Object);
+ GET_JSON(samplesSchema, samples["schema"], Object);
+ EXPECT_GE(samplesSchema.size(), 3u);
+ GET_JSON_VALUE(samplesNumber, samplesSchema["number"], UInt);
+ GET_JSON_VALUE(samplesCount, samplesSchema["count"], UInt);
+ GET_JSON(samplesData, samples["data"], Array);
+ for (const Json::Value& sample : samplesData) {
+ ASSERT_TRUE(sample.isArray());
+ ASSERT_LT(nextExpectedTestCounter2, expectedTestCounters2Count);
+ EXPECT_EQ_JSON(
+ sample[samplesNumber], UInt64,
+ expectedTestCounters2[nextExpectedTestCounter2].mNumber);
+ EXPECT_EQ_JSON(
+ sample[samplesCount], Int64,
+ expectedTestCounters2[nextExpectedTestCounter2].mCount);
+ ++nextExpectedTestCounter2;
+ }
+ }
+ }
+ }
+
+ EXPECT_EQ(nextExpectedTestCounter, expectedTestCountersCount);
+ if (expectCounter2) {
+ EXPECT_EQ(nextExpectedTestCounter2, expectedTestCounters2Count);
+ }
+ };
+
+ // Inactive -> Active
+ profiler_ensure_started(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL,
+ features, filters, MOZ_ARRAY_LENGTH(filters), 0);
+
+ // Output all "TestCounter"s, with increasing delays (to test different
+ // number of counter samplings).
+ int samplingWaits = 2;
+ for (int64_t counter : testCounters) {
+ AUTO_PROFILER_COUNT_TOTAL(TestCounter, counter);
+ for (int i = 0; i < samplingWaits; ++i) {
+ ASSERT_EQ(WaitForSamplingState(), SamplingState::SamplingCompleted);
+ }
+ ++samplingWaits;
+ }
+
+ // Verify we got "TestCounter" in the output, but not "TestCounter2" yet.
+ UniquePtr<char[]> profile = profiler_get_profile();
+ JSONOutputCheck(profile.get(), checkCountersInJSON);
+
+ // Now introduce TestCounter2.
+ expectCounter2 = true;
+ for (int64_t counter2 : testCounters2) {
+ AUTO_PROFILER_COUNT_TOTAL(TestCounter2, counter2);
+ ASSERT_EQ(WaitForSamplingState(), SamplingState::SamplingCompleted);
+ ASSERT_EQ(WaitForSamplingState(), SamplingState::SamplingCompleted);
+ }
+
+ // Verify we got both "TestCounter" and "TestCounter2" in the output.
+ profile = profiler_get_profile();
+ JSONOutputCheck(profile.get(), checkCountersInJSON);
+
+ profiler_stop();
+}
+
+TEST(GeckoProfiler, Time)
+{
+ uint32_t features = ProfilerFeature::StackWalk;
+ const char* filters[] = {"GeckoMain"};
+
+ double t1 = profiler_time();
+ double t2 = profiler_time();
+ ASSERT_TRUE(t1 <= t2);
+
+ // profiler_start() restarts the timer used by profiler_time().
+ profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL, features,
+ filters, MOZ_ARRAY_LENGTH(filters), 0);
+
+ double t3 = profiler_time();
+ double t4 = profiler_time();
+ ASSERT_TRUE(t3 <= t4);
+
+ profiler_stop();
+
+ double t5 = profiler_time();
+ double t6 = profiler_time();
+ ASSERT_TRUE(t4 <= t5 && t1 <= t6);
+}
+
+TEST(GeckoProfiler, GetProfile)
+{
+ uint32_t features = ProfilerFeature::StackWalk;
+ const char* filters[] = {"GeckoMain"};
+
+ ASSERT_TRUE(!profiler_get_profile());
+
+ profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL, features,
+ filters, MOZ_ARRAY_LENGTH(filters), 0);
+
+ mozilla::Maybe<uint32_t> activeFeatures = profiler_features_if_active();
+ ASSERT_TRUE(activeFeatures.isSome());
+ // Not all platforms support stack-walking.
+ const bool hasStackWalk = ProfilerFeature::HasStackWalk(*activeFeatures);
+
+ UniquePtr<char[]> profile = profiler_get_profile();
+ JSONOutputCheck(profile.get(), [&](const Json::Value& aRoot) {
+ GET_JSON(meta, aRoot["meta"], Object);
+ {
+ GET_JSON(configuration, meta["configuration"], Object);
+ {
+ GET_JSON(features, configuration["features"], Array);
+ {
+ EXPECT_EQ(features.size(), (hasStackWalk ? 1u : 0u));
+ if (hasStackWalk) {
+ EXPECT_JSON_ARRAY_CONTAINS(features, String, "stackwalk");
+ }
+ }
+ GET_JSON(threads, configuration["threads"], Array);
+ {
+ EXPECT_EQ(threads.size(), 1u);
+ EXPECT_JSON_ARRAY_CONTAINS(threads, String, "GeckoMain");
+ }
+ }
+ }
+ });
+
+ profiler_stop();
+
+ ASSERT_TRUE(!profiler_get_profile());
+}
+
+TEST(GeckoProfiler, StreamJSONForThisProcess)
+{
+ uint32_t features = ProfilerFeature::StackWalk;
+ const char* filters[] = {"GeckoMain"};
+
+ SpliceableChunkedJSONWriter w{FailureLatchInfallibleSource::Singleton()};
+ MOZ_RELEASE_ASSERT(!w.ChunkedWriteFunc().Fallible());
+ MOZ_RELEASE_ASSERT(!w.ChunkedWriteFunc().Failed());
+ MOZ_RELEASE_ASSERT(!w.ChunkedWriteFunc().GetFailure());
+ MOZ_RELEASE_ASSERT(&w.ChunkedWriteFunc().SourceFailureLatch() ==
+ &mozilla::FailureLatchInfallibleSource::Singleton());
+ MOZ_RELEASE_ASSERT(
+ &std::as_const(w.ChunkedWriteFunc()).SourceFailureLatch() ==
+ &mozilla::FailureLatchInfallibleSource::Singleton());
+ MOZ_RELEASE_ASSERT(!w.Fallible());
+ MOZ_RELEASE_ASSERT(!w.Failed());
+ MOZ_RELEASE_ASSERT(!w.GetFailure());
+ MOZ_RELEASE_ASSERT(&w.SourceFailureLatch() ==
+ &mozilla::FailureLatchInfallibleSource::Singleton());
+ MOZ_RELEASE_ASSERT(&std::as_const(w).SourceFailureLatch() ==
+ &mozilla::FailureLatchInfallibleSource::Singleton());
+
+ ASSERT_TRUE(::profiler_stream_json_for_this_process(w).isErr());
+ MOZ_RELEASE_ASSERT(!w.ChunkedWriteFunc().Failed());
+ MOZ_RELEASE_ASSERT(!w.ChunkedWriteFunc().GetFailure());
+ MOZ_RELEASE_ASSERT(!w.Failed());
+ MOZ_RELEASE_ASSERT(!w.GetFailure());
+
+ profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL, features,
+ filters, MOZ_ARRAY_LENGTH(filters), 0);
+
+ w.Start();
+ ASSERT_TRUE(::profiler_stream_json_for_this_process(w).isOk());
+ w.End();
+
+ MOZ_RELEASE_ASSERT(!w.ChunkedWriteFunc().Failed());
+ MOZ_RELEASE_ASSERT(!w.ChunkedWriteFunc().GetFailure());
+ MOZ_RELEASE_ASSERT(!w.Failed());
+ MOZ_RELEASE_ASSERT(!w.GetFailure());
+
+ UniquePtr<char[]> profile = w.ChunkedWriteFunc().CopyData();
+
+ JSONOutputCheck(profile.get(), [](const Json::Value&) {});
+
+ profiler_stop();
+
+ ASSERT_TRUE(::profiler_stream_json_for_this_process(w).isErr());
+}
+
+// Internal version of profiler_stream_json_for_this_process, which allows being
+// called from a non-main thread of the parent process, at the risk of getting
+// an incomplete profile.
+ProfilerResult<ProfileGenerationAdditionalInformation>
+do_profiler_stream_json_for_this_process(
+ SpliceableJSONWriter& aWriter, double aSinceTime, bool aIsShuttingDown,
+ ProfilerCodeAddressService* aService,
+ mozilla::ProgressLogger aProgressLogger);
+
+TEST(GeckoProfiler, StreamJSONForThisProcessThreaded)
+{
+ // Same as the previous test, but calling some things on background threads.
+ nsCOMPtr<nsIThread> thread;
+ nsresult rv = NS_NewNamedThread("GeckoProfGTest", getter_AddRefs(thread));
+ ASSERT_NS_SUCCEEDED(rv);
+
+ uint32_t features = ProfilerFeature::StackWalk;
+ const char* filters[] = {"GeckoMain"};
+
+ SpliceableChunkedJSONWriter w{FailureLatchInfallibleSource::Singleton()};
+ MOZ_RELEASE_ASSERT(!w.ChunkedWriteFunc().Fallible());
+ MOZ_RELEASE_ASSERT(!w.ChunkedWriteFunc().Failed());
+ MOZ_RELEASE_ASSERT(!w.ChunkedWriteFunc().GetFailure());
+ MOZ_RELEASE_ASSERT(&w.ChunkedWriteFunc().SourceFailureLatch() ==
+ &mozilla::FailureLatchInfallibleSource::Singleton());
+ MOZ_RELEASE_ASSERT(
+ &std::as_const(w.ChunkedWriteFunc()).SourceFailureLatch() ==
+ &mozilla::FailureLatchInfallibleSource::Singleton());
+ MOZ_RELEASE_ASSERT(!w.Fallible());
+ MOZ_RELEASE_ASSERT(!w.Failed());
+ MOZ_RELEASE_ASSERT(!w.GetFailure());
+ MOZ_RELEASE_ASSERT(&w.SourceFailureLatch() ==
+ &mozilla::FailureLatchInfallibleSource::Singleton());
+ MOZ_RELEASE_ASSERT(&std::as_const(w).SourceFailureLatch() ==
+ &mozilla::FailureLatchInfallibleSource::Singleton());
+
+ ASSERT_TRUE(::profiler_stream_json_for_this_process(w).isErr());
+ MOZ_RELEASE_ASSERT(!w.ChunkedWriteFunc().Failed());
+ MOZ_RELEASE_ASSERT(!w.ChunkedWriteFunc().GetFailure());
+ MOZ_RELEASE_ASSERT(!w.Failed());
+ MOZ_RELEASE_ASSERT(!w.GetFailure());
+
+ // Start the profiler on the main thread.
+ profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL, features,
+ filters, MOZ_ARRAY_LENGTH(filters), 0);
+
+ // Call profiler_stream_json_for_this_process on a background thread.
+ NS_DispatchAndSpinEventLoopUntilComplete(
+ "GeckoProfiler_StreamJSONForThisProcessThreaded_Test::TestBody"_ns,
+ thread,
+ NS_NewRunnableFunction(
+ "GeckoProfiler_StreamJSONForThisProcessThreaded_Test::TestBody",
+ [&]() {
+ w.Start();
+ ASSERT_TRUE(::do_profiler_stream_json_for_this_process(
+ w, /* double aSinceTime */ 0.0,
+ /* bool aIsShuttingDown */ false,
+ /* ProfilerCodeAddressService* aService */ nullptr,
+ mozilla::ProgressLogger{})
+ .isOk());
+ w.End();
+ }));
+
+ MOZ_RELEASE_ASSERT(!w.ChunkedWriteFunc().Failed());
+ MOZ_RELEASE_ASSERT(!w.ChunkedWriteFunc().GetFailure());
+ MOZ_RELEASE_ASSERT(!w.Failed());
+ MOZ_RELEASE_ASSERT(!w.GetFailure());
+
+ UniquePtr<char[]> profile = w.ChunkedWriteFunc().CopyData();
+
+ JSONOutputCheck(profile.get(), [](const Json::Value&) {});
+
+ // Stop the profiler and call profiler_stream_json_for_this_process on a
+ // background thread.
+ NS_DispatchAndSpinEventLoopUntilComplete(
+ "GeckoProfiler_StreamJSONForThisProcessThreaded_Test::TestBody"_ns,
+ thread,
+ NS_NewRunnableFunction(
+ "GeckoProfiler_StreamJSONForThisProcessThreaded_Test::TestBody",
+ [&]() {
+ profiler_stop();
+ ASSERT_TRUE(::do_profiler_stream_json_for_this_process(
+ w, /* double aSinceTime */ 0.0,
+ /* bool aIsShuttingDown */ false,
+ /* ProfilerCodeAddressService* aService */ nullptr,
+ mozilla::ProgressLogger{})
+ .isErr());
+ }));
+ thread->Shutdown();
+
+ // Call profiler_stream_json_for_this_process on the main thread.
+ ASSERT_TRUE(::profiler_stream_json_for_this_process(w).isErr());
+}
+
+TEST(GeckoProfiler, ProfilingStack)
+{
+ uint32_t features = ProfilerFeature::StackWalk;
+ const char* filters[] = {"GeckoMain"};
+
+ AUTO_PROFILER_LABEL("A::B", OTHER);
+
+ UniqueFreePtr<char> dynamic(strdup("dynamic"));
+ {
+ AUTO_PROFILER_LABEL_DYNAMIC_CSTR("A::C", JS, dynamic.get());
+ AUTO_PROFILER_LABEL_DYNAMIC_NSCSTRING("A::C2", JS,
+ nsDependentCString(dynamic.get()));
+ AUTO_PROFILER_LABEL_DYNAMIC_LOSSY_NSSTRING(
+ "A::C3", JS, NS_ConvertUTF8toUTF16(dynamic.get()));
+
+ profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL,
+ features, filters, MOZ_ARRAY_LENGTH(filters), 0);
+
+ ASSERT_TRUE(profiler_get_backtrace());
+ }
+
+ AutoProfilerLabel label1("A", nullptr, JS::ProfilingCategoryPair::DOM);
+ AutoProfilerLabel label2("A", dynamic.get(),
+ JS::ProfilingCategoryPair::NETWORK);
+ ASSERT_TRUE(profiler_get_backtrace());
+
+ profiler_stop();
+
+ ASSERT_TRUE(!profiler_get_profile());
+}
+
+TEST(GeckoProfiler, Bug1355807)
+{
+ uint32_t features = ProfilerFeature::JS;
+ const char* manyThreadsFilter[] = {""};
+ const char* fewThreadsFilter[] = {"GeckoMain"};
+
+ profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL, features,
+ manyThreadsFilter, MOZ_ARRAY_LENGTH(manyThreadsFilter), 0);
+
+ profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL, features,
+ fewThreadsFilter, MOZ_ARRAY_LENGTH(fewThreadsFilter), 0);
+
+ // In bug 1355807 this caused an assertion failure in StopJSSampling().
+ profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL, features,
+ fewThreadsFilter, MOZ_ARRAY_LENGTH(fewThreadsFilter), 0);
+
+ profiler_stop();
+}
+
+class GTestStackCollector final : public ProfilerStackCollector {
+ public:
+ GTestStackCollector() : mSetIsMainThread(0), mFrames(0) {}
+
+ virtual void SetIsMainThread() { mSetIsMainThread++; }
+
+ virtual void CollectNativeLeafAddr(void* aAddr) { mFrames++; }
+ virtual void CollectJitReturnAddr(void* aAddr) { mFrames++; }
+ virtual void CollectWasmFrame(const char* aLabel) { mFrames++; }
+ virtual void CollectProfilingStackFrame(
+ const js::ProfilingStackFrame& aFrame) {
+ mFrames++;
+ }
+
+ int mSetIsMainThread;
+ int mFrames;
+};
+
+void DoSuspendAndSample(ProfilerThreadId aTidToSample,
+ nsIThread* aSamplingThread) {
+ NS_DispatchAndSpinEventLoopUntilComplete(
+ "GeckoProfiler_SuspendAndSample_Test::TestBody"_ns, aSamplingThread,
+ NS_NewRunnableFunction(
+ "GeckoProfiler_SuspendAndSample_Test::TestBody", [&]() {
+ uint32_t features = ProfilerFeature::CPUUtilization;
+ GTestStackCollector collector;
+ profiler_suspend_and_sample_thread(aTidToSample, features,
+ collector,
+ /* sampleNative = */ true);
+
+ ASSERT_TRUE(collector.mSetIsMainThread ==
+ (aTidToSample == profiler_main_thread_id()));
+ ASSERT_TRUE(collector.mFrames > 0);
+ }));
+}
+
+TEST(GeckoProfiler, SuspendAndSample)
+{
+ nsCOMPtr<nsIThread> thread;
+ nsresult rv = NS_NewNamedThread("GeckoProfGTest", getter_AddRefs(thread));
+ ASSERT_NS_SUCCEEDED(rv);
+
+ ProfilerThreadId tid = profiler_current_thread_id();
+
+ ASSERT_TRUE(!profiler_is_active());
+
+ // Suspend and sample while the profiler is inactive.
+ DoSuspendAndSample(tid, thread);
+
+ DoSuspendAndSample(ProfilerThreadId{}, thread);
+
+ uint32_t features = ProfilerFeature::JS;
+ const char* filters[] = {"GeckoMain", "Compositor"};
+
+ profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL, features,
+ filters, MOZ_ARRAY_LENGTH(filters), 0);
+
+ ASSERT_TRUE(profiler_is_active());
+
+ // Suspend and sample while the profiler is active.
+ DoSuspendAndSample(tid, thread);
+
+ DoSuspendAndSample(ProfilerThreadId{}, thread);
+
+ profiler_stop();
+
+ ASSERT_TRUE(!profiler_is_active());
+}
+
+TEST(GeckoProfiler, PostSamplingCallback)
+{
+ const char* filters[] = {"GeckoMain"};
+
+ ASSERT_TRUE(!profiler_is_active());
+ ASSERT_TRUE(!profiler_callback_after_sampling(
+ [&](SamplingState) { ASSERT_TRUE(false); }));
+
+ profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL,
+ ProfilerFeature::StackWalk, filters, MOZ_ARRAY_LENGTH(filters),
+ 0);
+ {
+ // Stack sampling -> This label should appear at least once.
+ AUTO_PROFILER_LABEL("PostSamplingCallback completed", OTHER);
+ ASSERT_EQ(WaitForSamplingState(), SamplingState::SamplingCompleted);
+ }
+ UniquePtr<char[]> profileCompleted = profiler_get_profile();
+ JSONOutputCheck(profileCompleted.get(), [](const Json::Value& aRoot) {
+ GET_JSON(threads, aRoot["threads"], Array);
+ {
+ GET_JSON(thread0, threads[0], Object);
+ {
+ EXPECT_JSON_ARRAY_CONTAINS(thread0["stringTable"], String,
+ "PostSamplingCallback completed");
+ }
+ }
+ });
+
+ profiler_pause();
+ {
+ // Paused -> This label should not appear.
+ AUTO_PROFILER_LABEL("PostSamplingCallback paused", OTHER);
+ ASSERT_EQ(WaitForSamplingState(), SamplingState::SamplingPaused);
+ }
+ UniquePtr<char[]> profilePaused = profiler_get_profile();
+ JSONOutputCheck(profilePaused.get(), [](const Json::Value& aRoot) {});
+ // This string shouldn't appear *anywhere* in the profile.
+ ASSERT_FALSE(strstr(profilePaused.get(), "PostSamplingCallback paused"));
+
+ profiler_resume();
+ {
+ // Stack sampling -> This label should appear at least once.
+ AUTO_PROFILER_LABEL("PostSamplingCallback resumed", OTHER);
+ ASSERT_EQ(WaitForSamplingState(), SamplingState::SamplingCompleted);
+ }
+ UniquePtr<char[]> profileResumed = profiler_get_profile();
+ JSONOutputCheck(profileResumed.get(), [](const Json::Value& aRoot) {
+ GET_JSON(threads, aRoot["threads"], Array);
+ {
+ GET_JSON(thread0, threads[0], Object);
+ {
+ EXPECT_JSON_ARRAY_CONTAINS(thread0["stringTable"], String,
+ "PostSamplingCallback resumed");
+ }
+ }
+ });
+
+ profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL,
+ ProfilerFeature::StackWalk | ProfilerFeature::NoStackSampling,
+ filters, MOZ_ARRAY_LENGTH(filters), 0);
+ {
+ // No stack sampling -> This label should not appear.
+ AUTO_PROFILER_LABEL("PostSamplingCallback completed (no stacks)", OTHER);
+ ASSERT_EQ(WaitForSamplingState(), SamplingState::NoStackSamplingCompleted);
+ }
+ UniquePtr<char[]> profileNoStacks = profiler_get_profile();
+ JSONOutputCheck(profileNoStacks.get(), [](const Json::Value& aRoot) {});
+ // This string shouldn't appear *anywhere* in the profile.
+ ASSERT_FALSE(strstr(profileNoStacks.get(),
+ "PostSamplingCallback completed (no stacks)"));
+
+ // Note: There is no non-racy way to test for SamplingState::JustStopped, as
+ // it would require coordination between `profiler_stop()` and another thread
+ // doing `profiler_callback_after_sampling()` at just the right moment.
+
+ profiler_stop();
+ ASSERT_TRUE(!profiler_is_active());
+ ASSERT_TRUE(!profiler_callback_after_sampling(
+ [&](SamplingState) { ASSERT_TRUE(false); }));
+}
+
+TEST(GeckoProfiler, ProfilingStateCallback)
+{
+ const char* filters[] = {"GeckoMain"};
+
+ ASSERT_TRUE(!profiler_is_active());
+
+ struct ProfilingStateAndId {
+ ProfilingState mProfilingState;
+ int mId;
+ };
+ DataMutex<Vector<ProfilingStateAndId>> states{"Profiling states"};
+ auto CreateCallback = [&states](int id) {
+ return [id, &states](ProfilingState aProfilingState) {
+ auto lockedStates = states.Lock();
+ ASSERT_TRUE(
+ lockedStates->append(ProfilingStateAndId{aProfilingState, id}));
+ };
+ };
+ auto CheckStatesIsEmpty = [&states]() {
+ auto lockedStates = states.Lock();
+ EXPECT_TRUE(lockedStates->empty());
+ };
+ auto CheckStatesOnlyContains = [&states](ProfilingState aProfilingState,
+ int aId) {
+ auto lockedStates = states.Lock();
+ EXPECT_EQ(lockedStates->length(), 1u);
+ if (lockedStates->length() >= 1u) {
+ EXPECT_EQ((*lockedStates)[0].mProfilingState, aProfilingState);
+ EXPECT_EQ((*lockedStates)[0].mId, aId);
+ }
+ lockedStates->clear();
+ };
+
+ profiler_add_state_change_callback(AllProfilingStates(), CreateCallback(1),
+ 1);
+ // This is in case of error, and it also exercises the (allowed) removal of
+ // unknown callback ids.
+ auto cleanup1 = mozilla::MakeScopeExit(
+ []() { profiler_remove_state_change_callback(1); });
+ CheckStatesIsEmpty();
+
+ profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL,
+ ProfilerFeature::StackWalk, filters, MOZ_ARRAY_LENGTH(filters),
+ 0);
+
+ CheckStatesOnlyContains(ProfilingState::Started, 1);
+
+ profiler_add_state_change_callback(AllProfilingStates(), CreateCallback(2),
+ 2);
+ // This is in case of error, and it also exercises the (allowed) removal of
+ // unknown callback ids.
+ auto cleanup2 = mozilla::MakeScopeExit(
+ []() { profiler_remove_state_change_callback(2); });
+ CheckStatesOnlyContains(ProfilingState::AlreadyActive, 2);
+
+ profiler_remove_state_change_callback(2);
+ CheckStatesOnlyContains(ProfilingState::RemovingCallback, 2);
+ // Note: The actual removal is effectively tested below, by not seeing any
+ // more invocations of the 2nd callback.
+
+ ASSERT_EQ(WaitForSamplingState(), SamplingState::SamplingCompleted);
+ UniquePtr<char[]> profileCompleted = profiler_get_profile();
+ CheckStatesOnlyContains(ProfilingState::GeneratingProfile, 1);
+ JSONOutputCheck(profileCompleted.get(), [](const Json::Value& aRoot) {});
+
+ profiler_pause();
+ CheckStatesOnlyContains(ProfilingState::Pausing, 1);
+ UniquePtr<char[]> profilePaused = profiler_get_profile();
+ CheckStatesOnlyContains(ProfilingState::GeneratingProfile, 1);
+ JSONOutputCheck(profilePaused.get(), [](const Json::Value& aRoot) {});
+
+ profiler_resume();
+ CheckStatesOnlyContains(ProfilingState::Resumed, 1);
+ ASSERT_EQ(WaitForSamplingState(), SamplingState::SamplingCompleted);
+ UniquePtr<char[]> profileResumed = profiler_get_profile();
+ CheckStatesOnlyContains(ProfilingState::GeneratingProfile, 1);
+ JSONOutputCheck(profileResumed.get(), [](const Json::Value& aRoot) {});
+
+ // This effectively stops the profiler before restarting it, but
+ // ProfilingState::Stopping is not notified. See `profiler_start` for details.
+ profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL,
+ ProfilerFeature::StackWalk | ProfilerFeature::NoStackSampling,
+ filters, MOZ_ARRAY_LENGTH(filters), 0);
+ CheckStatesOnlyContains(ProfilingState::Started, 1);
+ ASSERT_EQ(WaitForSamplingState(), SamplingState::NoStackSamplingCompleted);
+ UniquePtr<char[]> profileNoStacks = profiler_get_profile();
+ CheckStatesOnlyContains(ProfilingState::GeneratingProfile, 1);
+ JSONOutputCheck(profileNoStacks.get(), [](const Json::Value& aRoot) {});
+
+ profiler_stop();
+ CheckStatesOnlyContains(ProfilingState::Stopping, 1);
+ ASSERT_TRUE(!profiler_is_active());
+
+ profiler_remove_state_change_callback(1);
+ CheckStatesOnlyContains(ProfilingState::RemovingCallback, 1);
+
+ // Note: ProfilingState::ShuttingDown cannot be tested here, and the profiler
+ // can only be shut down once per process.
+}
+
+TEST(GeckoProfiler, BaseProfilerHandOff)
+{
+ const char* filters[] = {"GeckoMain"};
+
+ ASSERT_TRUE(!baseprofiler::profiler_is_active());
+ ASSERT_TRUE(!profiler_is_active());
+
+ BASE_PROFILER_MARKER_UNTYPED("Base marker before base profiler", OTHER, {});
+ PROFILER_MARKER_UNTYPED("Gecko marker before base profiler", OTHER, {});
+
+ // Start the Base Profiler.
+ baseprofiler::profiler_start(
+ PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL,
+ ProfilerFeature::StackWalk, filters, MOZ_ARRAY_LENGTH(filters));
+
+ ASSERT_TRUE(baseprofiler::profiler_is_active());
+ ASSERT_TRUE(!profiler_is_active());
+
+ // Add at least a marker, which should go straight into the buffer.
+ Maybe<baseprofiler::ProfilerBufferInfo> info0 =
+ baseprofiler::profiler_get_buffer_info();
+ BASE_PROFILER_MARKER_UNTYPED("Base marker during base profiler", OTHER, {});
+ Maybe<baseprofiler::ProfilerBufferInfo> info1 =
+ baseprofiler::profiler_get_buffer_info();
+ ASSERT_GT(info1->mRangeEnd, info0->mRangeEnd);
+
+ PROFILER_MARKER_UNTYPED("Gecko marker during base profiler", OTHER, {});
+
+ // Start the Gecko Profiler, which should grab the Base Profiler profile and
+ // stop it.
+ profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL,
+ ProfilerFeature::StackWalk, filters, MOZ_ARRAY_LENGTH(filters),
+ 0);
+
+ ASSERT_TRUE(!baseprofiler::profiler_is_active());
+ ASSERT_TRUE(profiler_is_active());
+
+ BASE_PROFILER_MARKER_UNTYPED("Base marker during gecko profiler", OTHER, {});
+ PROFILER_MARKER_UNTYPED("Gecko marker during gecko profiler", OTHER, {});
+
+ // Write some Gecko Profiler samples.
+ ASSERT_EQ(WaitForSamplingState(), SamplingState::SamplingCompleted);
+
+ // Check that the Gecko Profiler profile contains at least the Base Profiler
+ // main thread samples.
+ UniquePtr<char[]> profile = profiler_get_profile();
+
+ profiler_stop();
+ ASSERT_TRUE(!profiler_is_active());
+
+ BASE_PROFILER_MARKER_UNTYPED("Base marker after gecko profiler", OTHER, {});
+ PROFILER_MARKER_UNTYPED("Gecko marker after gecko profiler", OTHER, {});
+
+ JSONOutputCheck(profile.get(), [](const Json::Value& aRoot) {
+ GET_JSON(threads, aRoot["threads"], Array);
+ {
+ bool found = false;
+ for (const Json::Value& thread : threads) {
+ ASSERT_TRUE(thread.isObject());
+ GET_JSON(name, thread["name"], String);
+ if (name.asString() == "GeckoMain") {
+ found = true;
+ EXPECT_JSON_ARRAY_EXCLUDES(thread["stringTable"], String,
+ "Base marker before base profiler");
+ EXPECT_JSON_ARRAY_EXCLUDES(thread["stringTable"], String,
+ "Gecko marker before base profiler");
+ EXPECT_JSON_ARRAY_CONTAINS(thread["stringTable"], String,
+ "Base marker during base profiler");
+ EXPECT_JSON_ARRAY_EXCLUDES(thread["stringTable"], String,
+ "Gecko marker during base profiler");
+ EXPECT_JSON_ARRAY_CONTAINS(thread["stringTable"], String,
+ "Base marker during gecko profiler");
+ EXPECT_JSON_ARRAY_CONTAINS(thread["stringTable"], String,
+ "Gecko marker during gecko profiler");
+ EXPECT_JSON_ARRAY_EXCLUDES(thread["stringTable"], String,
+ "Base marker after gecko profiler");
+ EXPECT_JSON_ARRAY_EXCLUDES(thread["stringTable"], String,
+ "Gecko marker after gecko profiler");
+ break;
+ }
+ }
+ EXPECT_TRUE(found);
+ }
+ });
+}
+
+static std::string_view GetFeatureName(uint32_t feature) {
+ switch (feature) {
+# define FEATURE_NAME(n_, str_, Name_, desc_) \
+ case ProfilerFeature::Name_: \
+ return str_;
+
+ PROFILER_FOR_EACH_FEATURE(FEATURE_NAME)
+
+# undef FEATURE_NAME
+
+ default:
+ return "?";
+ }
+}
+
+TEST(GeckoProfiler, FeatureCombinations)
+{
+ // Bug 1845606
+ #ifdef XP_WIN
+ if (!IsWin8OrLater()) {
+ return;
+ }
+ #endif
+
+ const char* filters[] = {"*"};
+
+ // List of features to test. Every combination of up to 3 of them will be
+ // tested, so be careful not to add too many to keep the test run at a
+ // reasonable time.
+ uint32_t featureList[] = {ProfilerFeature::JS,
+ ProfilerFeature::Screenshots,
+ ProfilerFeature::StackWalk,
+ ProfilerFeature::NoStackSampling,
+ ProfilerFeature::NativeAllocations,
+ ProfilerFeature::CPUUtilization,
+ ProfilerFeature::CPUAllThreads,
+ ProfilerFeature::SamplingAllThreads,
+ ProfilerFeature::MarkersAllThreads,
+ ProfilerFeature::UnregisteredThreads};
+ constexpr uint32_t featureCount = uint32_t(MOZ_ARRAY_LENGTH(featureList));
+
+ auto testFeatures = [&](uint32_t features,
+ const std::string& featuresString) {
+ SCOPED_TRACE(featuresString.c_str());
+
+ ASSERT_TRUE(!profiler_is_active());
+
+ profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL,
+ features, filters, MOZ_ARRAY_LENGTH(filters), 0);
+
+ ASSERT_TRUE(profiler_is_active());
+
+ // Write some Gecko Profiler samples.
+ EXPECT_EQ(WaitForSamplingState(),
+ (((features & ProfilerFeature::NoStackSampling) != 0) &&
+ ((features & (ProfilerFeature::CPUUtilization |
+ ProfilerFeature::CPUAllThreads)) == 0))
+ ? SamplingState::NoStackSamplingCompleted
+ : SamplingState::SamplingCompleted);
+
+ // Check that the profile looks valid. Note that we don't test feature-
+ // specific changes.
+ UniquePtr<char[]> profile = profiler_get_profile();
+ JSONOutputCheck(profile.get(), [](const Json::Value& aRoot) {});
+
+ profiler_stop();
+ ASSERT_TRUE(!profiler_is_active());
+ };
+
+ testFeatures(0, "Features: (none)");
+
+ for (uint32_t f1 = 0u; f1 < featureCount; ++f1) {
+ const uint32_t features1 = featureList[f1];
+ std::string features1String = "Features: ";
+ features1String += GetFeatureName(featureList[f1]);
+
+ testFeatures(features1, features1String);
+
+ for (uint32_t f2 = f1 + 1u; f2 < featureCount; ++f2) {
+ const uint32_t features12 = f1 | featureList[f2];
+ std::string features12String = features1String + " ";
+ features12String += GetFeatureName(featureList[f2]);
+
+ testFeatures(features12, features12String);
+
+ for (uint32_t f3 = f2 + 1u; f3 < featureCount; ++f3) {
+ const uint32_t features123 = features12 | featureList[f3];
+ std::string features123String = features12String + " ";
+ features123String += GetFeatureName(featureList[f3]);
+
+ testFeatures(features123, features123String);
+ }
+ }
+ }
+}
+
+static void CountCPUDeltas(const Json::Value& aThread, size_t& aOutSamplings,
+ uint64_t& aOutCPUDeltaSum) {
+ GET_JSON(samples, aThread["samples"], Object);
+ {
+ Json::ArrayIndex threadCPUDeltaIndex = 0;
+ GET_JSON(schema, samples["schema"], Object);
+ {
+ GET_JSON(jsonThreadCPUDeltaIndex, schema["threadCPUDelta"], UInt);
+ threadCPUDeltaIndex = jsonThreadCPUDeltaIndex.asUInt();
+ }
+
+ aOutSamplings = 0;
+ aOutCPUDeltaSum = 0;
+ GET_JSON(data, samples["data"], Array);
+ aOutSamplings = data.size();
+ for (const Json::Value& sample : data) {
+ ASSERT_TRUE(sample.isArray());
+ if (sample.isValidIndex(threadCPUDeltaIndex)) {
+ if (!sample[threadCPUDeltaIndex].isNull()) {
+ GET_JSON(cpuDelta, sample[threadCPUDeltaIndex], UInt64);
+ aOutCPUDeltaSum += uint64_t(cpuDelta.asUInt64());
+ }
+ }
+ }
+ }
+}
+
+TEST(GeckoProfiler, CPUUsage)
+{
+ profiler_init_main_thread_id();
+ ASSERT_TRUE(profiler_is_main_thread())
+ << "This test assumes it runs on the main thread";
+
+ const char* filters[] = {"GeckoMain", "Idle test", "Busy test"};
+
+ enum class TestThreadsState {
+ // Initial state, while constructing and starting the idle thread.
+ STARTING,
+ // Set by the idle thread just before running its main mostly-idle loop.
+ RUNNING1,
+ RUNNING2,
+ // Set by the main thread when it wants the idle thread to stop.
+ STOPPING
+ };
+ Atomic<TestThreadsState> testThreadsState{TestThreadsState::STARTING};
+
+ std::thread idle([&]() {
+ AUTO_PROFILER_REGISTER_THREAD("Idle test");
+ // Add a label to ensure that we have a non-empty stack, even if native
+ // stack-walking is not available.
+ AUTO_PROFILER_LABEL("Idle test", PROFILER);
+ ASSERT_TRUE(testThreadsState.compareExchange(TestThreadsState::STARTING,
+ TestThreadsState::RUNNING1) ||
+ testThreadsState.compareExchange(TestThreadsState::RUNNING1,
+ TestThreadsState::RUNNING2));
+
+ while (testThreadsState != TestThreadsState::STOPPING) {
+ // Sleep for multiple profiler intervals, so the profiler should have
+ // samples with zero CPU utilization.
+ PR_Sleep(PR_MillisecondsToInterval(PROFILER_DEFAULT_INTERVAL * 10));
+ }
+ });
+
+ std::thread busy([&]() {
+ AUTO_PROFILER_REGISTER_THREAD("Busy test");
+ // Add a label to ensure that we have a non-empty stack, even if native
+ // stack-walking is not available.
+ AUTO_PROFILER_LABEL("Busy test", PROFILER);
+ ASSERT_TRUE(testThreadsState.compareExchange(TestThreadsState::STARTING,
+ TestThreadsState::RUNNING1) ||
+ testThreadsState.compareExchange(TestThreadsState::RUNNING1,
+ TestThreadsState::RUNNING2));
+
+ while (testThreadsState != TestThreadsState::STOPPING) {
+ // Stay busy!
+ }
+ });
+
+ // Wait for idle thread to start running its main loop.
+ while (testThreadsState != TestThreadsState::RUNNING2) {
+ PR_Sleep(PR_MillisecondsToInterval(1));
+ }
+
+ // We want to ensure that CPU usage numbers are present whether or not we are
+ // collecting stack samples.
+ static constexpr bool scTestsWithOrWithoutStackSampling[] = {false, true};
+ for (const bool testWithNoStackSampling : scTestsWithOrWithoutStackSampling) {
+ ASSERT_TRUE(!profiler_is_active());
+ ASSERT_TRUE(!profiler_callback_after_sampling(
+ [&](SamplingState) { ASSERT_TRUE(false); }));
+
+ profiler_start(
+ PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL,
+ ProfilerFeature::StackWalk | ProfilerFeature::CPUUtilization |
+ (testWithNoStackSampling ? ProfilerFeature::NoStackSampling : 0),
+ filters, MOZ_ARRAY_LENGTH(filters), 0);
+ // Grab a few samples, each with a different label on the stack.
+# define SAMPLE_LABEL_PREFIX "CPUUsage sample label "
+ static constexpr const char* scSampleLabels[] = {
+ SAMPLE_LABEL_PREFIX "0", SAMPLE_LABEL_PREFIX "1",
+ SAMPLE_LABEL_PREFIX "2", SAMPLE_LABEL_PREFIX "3",
+ SAMPLE_LABEL_PREFIX "4", SAMPLE_LABEL_PREFIX "5",
+ SAMPLE_LABEL_PREFIX "6", SAMPLE_LABEL_PREFIX "7",
+ SAMPLE_LABEL_PREFIX "8", SAMPLE_LABEL_PREFIX "9"};
+ static constexpr size_t scSampleLabelCount =
+ (sizeof(scSampleLabels) / sizeof(scSampleLabels[0]));
+ // We'll do two samplings for each label.
+ static constexpr size_t scMinSamplings = scSampleLabelCount * 2;
+
+ for (const char* sampleLabel : scSampleLabels) {
+ AUTO_PROFILER_LABEL(sampleLabel, OTHER);
+ ASSERT_EQ(WaitForSamplingState(), SamplingState::SamplingCompleted);
+ // Note: There could have been a delay before this label above, where the
+ // profiler could have sampled the stack and missed the label. By forcing
+ // another sampling now, the label is guaranteed to be present.
+ ASSERT_EQ(WaitForSamplingState(), SamplingState::SamplingCompleted);
+ }
+
+ UniquePtr<char[]> profile = profiler_get_profile();
+
+ if (testWithNoStackSampling) {
+ // If we are testing nostacksampling, we shouldn't find this label prefix
+ // in the profile.
+ EXPECT_FALSE(strstr(profile.get(), SAMPLE_LABEL_PREFIX));
+ } else {
+ // In normal sampling mode, we should find all labels.
+ for (const char* sampleLabel : scSampleLabels) {
+ EXPECT_TRUE(strstr(profile.get(), sampleLabel));
+ }
+ }
+
+ JSONOutputCheck(profile.get(), [testWithNoStackSampling](
+ const Json::Value& aRoot) {
+ // Check that the "cpu" feature is present.
+ GET_JSON(meta, aRoot["meta"], Object);
+ {
+ GET_JSON(configuration, meta["configuration"], Object);
+ {
+ GET_JSON(features, configuration["features"], Array);
+ { EXPECT_JSON_ARRAY_CONTAINS(features, String, "cpu"); }
+ }
+ }
+
+ {
+ GET_JSON(sampleUnits, meta["sampleUnits"], Object);
+ {
+ EXPECT_EQ_JSON(sampleUnits["time"], String, "ms");
+ EXPECT_EQ_JSON(sampleUnits["eventDelay"], String, "ms");
+# if defined(GP_OS_windows) || defined(GP_OS_darwin) || \
+ defined(GP_OS_linux) || defined(GP_OS_android) || defined(GP_OS_freebsd)
+ // Note: The exact string is not important here.
+ EXPECT_TRUE(sampleUnits["threadCPUDelta"].isString())
+ << "There should be a sampleUnits.threadCPUDelta on this "
+ "platform";
+# else
+ EXPECT_FALSE(sampleUnits.isMember("threadCPUDelta"))
+ << "Unexpected sampleUnits.threadCPUDelta on this platform";;
+# endif
+ }
+ }
+
+ bool foundMain = false;
+ bool foundIdle = false;
+ uint64_t idleThreadCPUDeltaSum = 0u;
+ bool foundBusy = false;
+ uint64_t busyThreadCPUDeltaSum = 0u;
+
+ // Check that the sample schema contains "threadCPUDelta".
+ GET_JSON(threads, aRoot["threads"], Array);
+ for (const Json::Value& thread : threads) {
+ ASSERT_TRUE(thread.isObject());
+ GET_JSON(name, thread["name"], String);
+ if (name.asString() == "GeckoMain") {
+ foundMain = true;
+ GET_JSON(samples, thread["samples"], Object);
+ {
+ Json::ArrayIndex stackIndex = 0;
+ Json::ArrayIndex threadCPUDeltaIndex = 0;
+ GET_JSON(schema, samples["schema"], Object);
+ {
+ GET_JSON(jsonStackIndex, schema["stack"], UInt);
+ stackIndex = jsonStackIndex.asUInt();
+ GET_JSON(jsonThreadCPUDeltaIndex, schema["threadCPUDelta"], UInt);
+ threadCPUDeltaIndex = jsonThreadCPUDeltaIndex.asUInt();
+ }
+
+ std::set<uint64_t> stackLeaves; // To count distinct leaves.
+ unsigned threadCPUDeltaCount = 0;
+ GET_JSON(data, samples["data"], Array);
+ if (testWithNoStackSampling) {
+ // When not sampling stacks, the first sampling loop will have no
+ // running times, so it won't output anything.
+ EXPECT_GE(data.size(), scMinSamplings - 1);
+ } else {
+ EXPECT_GE(data.size(), scMinSamplings);
+ }
+ for (const Json::Value& sample : data) {
+ ASSERT_TRUE(sample.isArray());
+ if (sample.isValidIndex(stackIndex)) {
+ if (!sample[stackIndex].isNull()) {
+ GET_JSON(stack, sample[stackIndex], UInt64);
+ stackLeaves.insert(stack.asUInt64());
+ }
+ }
+ if (sample.isValidIndex(threadCPUDeltaIndex)) {
+ if (!sample[threadCPUDeltaIndex].isNull()) {
+ EXPECT_TRUE(sample[threadCPUDeltaIndex].isUInt64());
+ ++threadCPUDeltaCount;
+ }
+ }
+ }
+
+ if (testWithNoStackSampling) {
+ // in nostacksampling mode, there should only be one kind of stack
+ // leaf (the root).
+ EXPECT_EQ(stackLeaves.size(), 1u);
+ } else {
+ // in normal sampling mode, there should be at least one kind of
+ // stack leaf for each distinct label.
+ EXPECT_GE(stackLeaves.size(), scSampleLabelCount);
+ }
+
+# if defined(GP_OS_windows) || defined(GP_OS_darwin) || \
+ defined(GP_OS_linux) || defined(GP_OS_android) || defined(GP_OS_freebsd)
+ EXPECT_GE(threadCPUDeltaCount, data.size() - 1u)
+ << "There should be 'threadCPUDelta' values in all but 1 "
+ "samples";
+# else
+ // All "threadCPUDelta" data should be absent or null on unsupported
+ // platforms.
+ EXPECT_EQ(threadCPUDeltaCount, 0u);
+# endif
+ }
+ } else if (name.asString() == "Idle test") {
+ foundIdle = true;
+ size_t samplings;
+ CountCPUDeltas(thread, samplings, idleThreadCPUDeltaSum);
+ if (testWithNoStackSampling) {
+ // When not sampling stacks, the first sampling loop will have no
+ // running times, so it won't output anything.
+ EXPECT_GE(samplings, scMinSamplings - 1);
+ } else {
+ EXPECT_GE(samplings, scMinSamplings);
+ }
+# if !(defined(GP_OS_windows) || defined(GP_OS_darwin) || \
+ defined(GP_OS_linux) || defined(GP_OS_android) || \
+ defined(GP_OS_freebsd))
+ // All "threadCPUDelta" data should be absent or null on unsupported
+ // platforms.
+ EXPECT_EQ(idleThreadCPUDeltaSum, 0u);
+# endif
+ } else if (name.asString() == "Busy test") {
+ foundBusy = true;
+ size_t samplings;
+ CountCPUDeltas(thread, samplings, busyThreadCPUDeltaSum);
+ if (testWithNoStackSampling) {
+ // When not sampling stacks, the first sampling loop will have no
+ // running times, so it won't output anything.
+ EXPECT_GE(samplings, scMinSamplings - 1);
+ } else {
+ EXPECT_GE(samplings, scMinSamplings);
+ }
+# if !(defined(GP_OS_windows) || defined(GP_OS_darwin) || \
+ defined(GP_OS_linux) || defined(GP_OS_android) || \
+ defined(GP_OS_freebsd))
+ // All "threadCPUDelta" data should be absent or null on unsupported
+ // platforms.
+ EXPECT_EQ(busyThreadCPUDeltaSum, 0u);
+# endif
+ }
+ }
+
+ EXPECT_TRUE(foundMain);
+ EXPECT_TRUE(foundIdle);
+ EXPECT_TRUE(foundBusy);
+ EXPECT_LE(idleThreadCPUDeltaSum, busyThreadCPUDeltaSum);
+ });
+
+ // Note: There is no non-racy way to test for SamplingState::JustStopped, as
+ // it would require coordination between `profiler_stop()` and another
+ // thread doing `profiler_callback_after_sampling()` at just the right
+ // moment.
+
+ profiler_stop();
+ ASSERT_TRUE(!profiler_is_active());
+ ASSERT_TRUE(!profiler_callback_after_sampling(
+ [&](SamplingState) { ASSERT_TRUE(false); }));
+ }
+
+ testThreadsState = TestThreadsState::STOPPING;
+ busy.join();
+ idle.join();
+}
+
+TEST(GeckoProfiler, AllThreads)
+{
+ // Bug 1845606
+ #ifdef XP_WIN
+ if (!IsWin8OrLater()) {
+ return;
+ }
+ #endif
+
+ profiler_init_main_thread_id();
+ ASSERT_TRUE(profiler_is_main_thread())
+ << "This test assumes it runs on the main thread";
+
+ ASSERT_EQ(static_cast<uint32_t>(ThreadProfilingFeatures::Any), 1u + 2u + 4u)
+ << "This test assumes that there are 3 binary choices 1+2+4; "
+ "Is this test up to date?";
+
+ for (uint32_t threadFeaturesBinary = 0u;
+ threadFeaturesBinary <=
+ static_cast<uint32_t>(ThreadProfilingFeatures::Any);
+ ++threadFeaturesBinary) {
+ ThreadProfilingFeatures threadFeatures =
+ static_cast<ThreadProfilingFeatures>(threadFeaturesBinary);
+ const bool threadCPU = DoFeaturesIntersect(
+ threadFeatures, ThreadProfilingFeatures::CPUUtilization);
+ const bool threadSampling =
+ DoFeaturesIntersect(threadFeatures, ThreadProfilingFeatures::Sampling);
+ const bool threadMarkers =
+ DoFeaturesIntersect(threadFeatures, ThreadProfilingFeatures::Markers);
+
+ ASSERT_TRUE(!profiler_is_active());
+
+ uint32_t features = ProfilerFeature::StackWalk;
+ std::string featuresString = "Features: StackWalk Threads";
+ if (threadCPU) {
+ features |= ProfilerFeature::CPUAllThreads;
+ featuresString += " CPUAllThreads";
+ }
+ if (threadSampling) {
+ features |= ProfilerFeature::SamplingAllThreads;
+ featuresString += " SamplingAllThreads";
+ }
+ if (threadMarkers) {
+ features |= ProfilerFeature::MarkersAllThreads;
+ featuresString += " MarkersAllThreads";
+ }
+
+ SCOPED_TRACE(featuresString.c_str());
+
+ const char* filters[] = {"GeckoMain", "Selected"};
+
+ EXPECT_FALSE(profiler_thread_is_being_profiled(
+ ThreadProfilingFeatures::CPUUtilization));
+ EXPECT_FALSE(
+ profiler_thread_is_being_profiled(ThreadProfilingFeatures::Sampling));
+ EXPECT_FALSE(
+ profiler_thread_is_being_profiled(ThreadProfilingFeatures::Markers));
+ EXPECT_FALSE(profiler_thread_is_being_profiled_for_markers());
+
+ profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL,
+ features, filters, MOZ_ARRAY_LENGTH(filters), 0);
+
+ EXPECT_TRUE(profiler_thread_is_being_profiled(
+ ThreadProfilingFeatures::CPUUtilization));
+ EXPECT_TRUE(
+ profiler_thread_is_being_profiled(ThreadProfilingFeatures::Sampling));
+ EXPECT_TRUE(
+ profiler_thread_is_being_profiled(ThreadProfilingFeatures::Markers));
+ EXPECT_TRUE(profiler_thread_is_being_profiled_for_markers());
+
+ // This will signal all threads to stop spinning.
+ Atomic<bool> stopThreads{false};
+
+ Atomic<int> selectedThreadSpins{0};
+ std::thread selectedThread([&]() {
+ AUTO_PROFILER_REGISTER_THREAD("Selected test thread");
+ // Add a label to ensure that we have a non-empty stack, even if native
+ // stack-walking is not available.
+ AUTO_PROFILER_LABEL("Selected test thread", PROFILER);
+ EXPECT_TRUE(profiler_thread_is_being_profiled(
+ ThreadProfilingFeatures::CPUUtilization));
+ EXPECT_TRUE(
+ profiler_thread_is_being_profiled(ThreadProfilingFeatures::Sampling));
+ EXPECT_TRUE(
+ profiler_thread_is_being_profiled(ThreadProfilingFeatures::Markers));
+ EXPECT_TRUE(profiler_thread_is_being_profiled_for_markers());
+ while (!stopThreads) {
+ PROFILER_MARKER_UNTYPED("Spinning Selected!", PROFILER);
+ ++selectedThreadSpins;
+ PR_Sleep(PR_MillisecondsToInterval(1));
+ }
+ });
+
+ Atomic<int> unselectedThreadSpins{0};
+ std::thread unselectedThread([&]() {
+ AUTO_PROFILER_REGISTER_THREAD("Registered test thread");
+ // Add a label to ensure that we have a non-empty stack, even if native
+ // stack-walking is not available.
+ AUTO_PROFILER_LABEL("Registered test thread", PROFILER);
+ // This thread is *not* selected for full profiling, but it may still be
+ // profiled depending on the -allthreads features.
+ EXPECT_EQ(profiler_thread_is_being_profiled(
+ ThreadProfilingFeatures::CPUUtilization),
+ threadCPU);
+ EXPECT_EQ(
+ profiler_thread_is_being_profiled(ThreadProfilingFeatures::Sampling),
+ threadSampling);
+ EXPECT_EQ(
+ profiler_thread_is_being_profiled(ThreadProfilingFeatures::Markers),
+ threadMarkers);
+ EXPECT_EQ(profiler_thread_is_being_profiled_for_markers(), threadMarkers);
+ while (!stopThreads) {
+ PROFILER_MARKER_UNTYPED("Spinning Registered!", PROFILER);
+ ++unselectedThreadSpins;
+ PR_Sleep(PR_MillisecondsToInterval(1));
+ }
+ });
+
+ Atomic<int> unregisteredThreadSpins{0};
+ std::thread unregisteredThread([&]() {
+ // No `AUTO_PROFILER_REGISTER_THREAD` here.
+ EXPECT_FALSE(profiler_thread_is_being_profiled(
+ ThreadProfilingFeatures::CPUUtilization));
+ EXPECT_FALSE(
+ profiler_thread_is_being_profiled(ThreadProfilingFeatures::Sampling));
+ EXPECT_FALSE(
+ profiler_thread_is_being_profiled(ThreadProfilingFeatures::Markers));
+ EXPECT_FALSE(profiler_thread_is_being_profiled_for_markers());
+ while (!stopThreads) {
+ PROFILER_MARKER_UNTYPED("Spinning Unregistered!", PROFILER);
+ ++unregisteredThreadSpins;
+ PR_Sleep(PR_MillisecondsToInterval(1));
+ }
+ });
+
+ // Wait for all threads to have started at least one spin.
+ while (selectedThreadSpins == 0 || unselectedThreadSpins == 0 ||
+ unregisteredThreadSpins == 0) {
+ PR_Sleep(PR_MillisecondsToInterval(1));
+ }
+
+ // Wait until the sampler has done at least one loop.
+ ASSERT_EQ(WaitForSamplingState(), SamplingState::SamplingCompleted);
+
+ // Restart the spin counts, and ensure each threads will do at least one
+ // more spin each. Since spins are increased after PROFILER_MARKER calls, in
+ // the worst case, each thread will have attempted to record at least one
+ // marker.
+ selectedThreadSpins = 0;
+ unselectedThreadSpins = 0;
+ unregisteredThreadSpins = 0;
+ while (selectedThreadSpins < 1 && unselectedThreadSpins < 1 &&
+ unregisteredThreadSpins < 1) {
+ ASSERT_EQ(WaitForSamplingState(), SamplingState::SamplingCompleted);
+ }
+
+ profiler_pause();
+ UniquePtr<char[]> profile = profiler_get_profile();
+
+ profiler_stop();
+ stopThreads = true;
+ unregisteredThread.join();
+ unselectedThread.join();
+ selectedThread.join();
+
+ JSONOutputCheck(profile.get(), [&](const Json::Value& aRoot) {
+ GET_JSON(threads, aRoot["threads"], Array);
+ int foundMain = 0;
+ int foundSelected = 0;
+ int foundSelectedMarker = 0;
+ int foundUnselected = 0;
+ int foundUnselectedMarker = 0;
+ for (const Json::Value& thread : threads) {
+ ASSERT_TRUE(thread.isObject());
+ GET_JSON(stringTable, thread["stringTable"], Array);
+ GET_JSON(name, thread["name"], String);
+ if (name.asString() == "GeckoMain") {
+ ++foundMain;
+ // Don't check the main thread further in this test.
+
+ } else if (name.asString() == "Selected test thread") {
+ ++foundSelected;
+
+ GET_JSON(samples, thread["samples"], Object);
+ GET_JSON(samplesData, samples["data"], Array);
+ EXPECT_GT(samplesData.size(), 0u);
+
+ GET_JSON(markers, thread["markers"], Object);
+ GET_JSON(markersData, markers["data"], Array);
+ for (const Json::Value& marker : markersData) {
+ const unsigned int NAME = 0u;
+ ASSERT_TRUE(marker[NAME].isUInt()); // name id
+ GET_JSON(name, stringTable[marker[NAME].asUInt()], String);
+ if (name == "Spinning Selected!") {
+ ++foundSelectedMarker;
+ }
+ }
+ } else if (name.asString() == "Registered test thread") {
+ ++foundUnselected;
+
+ GET_JSON(samples, thread["samples"], Object);
+ GET_JSON(samplesData, samples["data"], Array);
+ if (threadCPU || threadSampling) {
+ EXPECT_GT(samplesData.size(), 0u);
+ } else {
+ EXPECT_EQ(samplesData.size(), 0u);
+ }
+
+ GET_JSON(markers, thread["markers"], Object);
+ GET_JSON(markersData, markers["data"], Array);
+ for (const Json::Value& marker : markersData) {
+ const unsigned int NAME = 0u;
+ ASSERT_TRUE(marker[NAME].isUInt()); // name id
+ GET_JSON(name, stringTable[marker[NAME].asUInt()], String);
+ if (name == "Spinning Registered!") {
+ ++foundUnselectedMarker;
+ }
+ }
+
+ } else {
+ EXPECT_STRNE(name.asString().c_str(),
+ "Unregistered test thread label");
+ }
+ }
+ EXPECT_EQ(foundMain, 1);
+ EXPECT_EQ(foundSelected, 1);
+ EXPECT_GT(foundSelectedMarker, 0);
+ EXPECT_EQ(foundUnselected,
+ (threadCPU || threadSampling || threadMarkers) ? 1 : 0)
+ << "Unselected thread should only be present if at least one of the "
+ "allthreads feature is on";
+ if (threadMarkers) {
+ EXPECT_GT(foundUnselectedMarker, 0);
+ } else {
+ EXPECT_EQ(foundUnselectedMarker, 0);
+ }
+ });
+ }
+}
+
+TEST(GeckoProfiler, FailureHandling)
+{
+ profiler_init_main_thread_id();
+ ASSERT_TRUE(profiler_is_main_thread())
+ << "This test assumes it runs on the main thread";
+
+ uint32_t features = ProfilerFeature::StackWalk;
+ const char* filters[] = {"GeckoMain"};
+ profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL, features,
+ filters, MOZ_ARRAY_LENGTH(filters), 0);
+
+ ASSERT_EQ(WaitForSamplingState(), SamplingState::SamplingCompleted);
+
+ // User-defined marker type that generates a failure when streaming JSON.
+ struct GtestFailingMarker {
+ static constexpr Span<const char> MarkerTypeName() {
+ return MakeStringSpan("markers-gtest-failing");
+ }
+ static void StreamJSONMarkerData(
+ mozilla::baseprofiler::SpliceableJSONWriter& aWriter) {
+ aWriter.SetFailure("boom!");
+ }
+ static mozilla::MarkerSchema MarkerTypeDisplay() {
+ return mozilla::MarkerSchema::SpecialFrontendLocation{};
+ }
+ };
+ EXPECT_TRUE(profiler_add_marker("Gtest failing marker",
+ geckoprofiler::category::OTHER, {},
+ GtestFailingMarker{}));
+
+ ASSERT_EQ(WaitForSamplingState(), SamplingState::SamplingCompleted);
+ profiler_pause();
+
+ FailureLatchSource failureLatch;
+ SpliceableChunkedJSONWriter w{failureLatch};
+ EXPECT_FALSE(w.Failed());
+ ASSERT_FALSE(w.GetFailure());
+
+ w.Start();
+ EXPECT_FALSE(w.Failed());
+ ASSERT_FALSE(w.GetFailure());
+
+ // The marker will cause a failure during this function call.
+ EXPECT_FALSE(::profiler_stream_json_for_this_process(w).isOk());
+ EXPECT_TRUE(w.Failed());
+ ASSERT_TRUE(w.GetFailure());
+ EXPECT_EQ(strcmp(w.GetFailure(), "boom!"), 0);
+
+ // Already failed, check that we don't crash or reset the failure.
+ EXPECT_FALSE(::profiler_stream_json_for_this_process(w).isOk());
+ EXPECT_TRUE(w.Failed());
+ ASSERT_TRUE(w.GetFailure());
+ EXPECT_EQ(strcmp(w.GetFailure(), "boom!"), 0);
+
+ w.End();
+
+ profiler_stop();
+
+ EXPECT_TRUE(w.Failed());
+ ASSERT_TRUE(w.GetFailure());
+ EXPECT_EQ(strcmp(w.GetFailure(), "boom!"), 0);
+
+ UniquePtr<char[]> profile = w.ChunkedWriteFunc().CopyData();
+ ASSERT_EQ(profile.get(), nullptr);
+}
+
+TEST(GeckoProfiler, NoMarkerStacks)
+{
+ uint32_t features = ProfilerFeature::NoMarkerStacks;
+ const char* filters[] = {"GeckoMain"};
+
+ ASSERT_TRUE(!profiler_get_profile());
+
+ // Make sure that profiler_capture_backtrace returns nullptr when the profiler
+ // is not active.
+ ASSERT_TRUE(!profiler_capture_backtrace());
+
+ {
+ // Start the profiler without the NoMarkerStacks feature and make sure we
+ // capture stacks.
+ profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL,
+ /* features */ 0, filters, MOZ_ARRAY_LENGTH(filters), 0);
+
+ ASSERT_TRUE(profiler_capture_backtrace());
+ profiler_stop();
+ }
+
+ // Start the profiler without the NoMarkerStacks feature and make sure we
+ // don't capture stacks.
+ profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL, features,
+ filters, MOZ_ARRAY_LENGTH(filters), 0);
+
+ // Make sure that the active features has the NoMarkerStacks feature.
+ mozilla::Maybe<uint32_t> activeFeatures = profiler_features_if_active();
+ ASSERT_TRUE(activeFeatures.isSome());
+ ASSERT_TRUE(ProfilerFeature::HasNoMarkerStacks(*activeFeatures));
+
+ // Make sure we don't capture stacks.
+ ASSERT_TRUE(!profiler_capture_backtrace());
+
+ // Add a marker with a stack to test.
+ EXPECT_TRUE(profiler_add_marker(
+ "Text with stack", geckoprofiler::category::OTHER, MarkerStack::Capture(),
+ geckoprofiler::markers::TextMarker{}, ""));
+
+ UniquePtr<char[]> profile = profiler_get_profile();
+ JSONOutputCheck(profile.get(), [&](const Json::Value& aRoot) {
+ // Check that the meta.configuration.features array contains
+ // "nomarkerstacks".
+ GET_JSON(meta, aRoot["meta"], Object);
+ {
+ GET_JSON(configuration, meta["configuration"], Object);
+ {
+ GET_JSON(features, configuration["features"], Array);
+ {
+ EXPECT_EQ(features.size(), 1u);
+ EXPECT_JSON_ARRAY_CONTAINS(features, String, "nomarkerstacks");
+ }
+ }
+ }
+
+ // Make sure that the marker we captured doesn't have a stack.
+ GET_JSON(threads, aRoot["threads"], Array);
+ {
+ ASSERT_EQ(threads.size(), 1u);
+ GET_JSON(thread0, threads[0], Object);
+ {
+ GET_JSON(markers, thread0["markers"], Object);
+ {
+ GET_JSON(data, markers["data"], Array);
+ {
+ const unsigned int NAME = 0u;
+ const unsigned int PAYLOAD = 5u;
+ bool foundMarker = false;
+ GET_JSON(stringTable, thread0["stringTable"], Array);
+
+ for (const Json::Value& marker : data) {
+ // Even though we only added one marker, some markers like
+ // NotifyObservers are being added as well. Let's iterate over
+ // them and make sure that we have the one we added explicitly and
+ // check its stack doesn't exist.
+ GET_JSON(name, stringTable[marker[NAME].asUInt()], String);
+ std::string nameString = name.asString();
+
+ if (nameString == "Text with stack") {
+ // Make sure that the marker doesn't have a stack.
+ foundMarker = true;
+ EXPECT_FALSE(marker[PAYLOAD].isNull());
+ EXPECT_TRUE(marker[PAYLOAD]["stack"].isNull());
+ }
+ }
+
+ EXPECT_TRUE(foundMarker);
+ }
+ }
+ }
+ }
+ });
+
+ profiler_stop();
+
+ ASSERT_TRUE(!profiler_get_profile());
+}
+
+#endif // MOZ_GECKO_PROFILER