Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/C/Firefox/tools/profiler/tests/gtest/   (Browser von der Mozilla Stiftung Version 136.0.1©)  Datei vom 10.2.2025 mit Größe 204 kB image not shown  

Quelle  GeckoProfiler.cpp

  Sprache: C
 

/* -*- 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 "nsHttp.h"
#include "nsIClassOfService.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 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/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;
#ifdef MOZ_GECKO_PROFILER
    baseRegistrationTime = baseprofiler::detail::GetThreadRegistrationTime();
#endif
    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 SetCycleCollectedJSContext here, so just
  // verify that the call would compile and return the expected type.
  static_assert(std::is_same_v<decltype(aData.SetCycleCollectedJSContext(
                                   std::declval<CycleCollectedJSContext*>())),
                               void>);
  aData.ClearCycleCollectedJSContext();
  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

// 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);

  // "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(samples, counter["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, std::size(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,
                      std::size(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,
                   std::size(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,
                      std::size(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,
                   std::size(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,
                      std::size(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[] = {""};

    // Turn off tracing because it mucks with other features
    availableFeatures &= ~ProfilerFeature::Tracing;

    profiler_start(PowerOfTwo32(88888), 10, availableFeatures, filters,
                   std::size(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, std::size(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, std::size(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,
                      std::size(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, std::size(filters), 0,
                            Some(PROFILER_DEFAULT_DURATION));

    ActiveParamsCheck(PROFILER_DEFAULT_ENTRIES.Value(),
                      PROFILER_DEFAULT_INTERVAL, features, filters,
                      std::size(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, std::size(filters), 0,
                            Some(PROFILER_DEFAULT_DURATION));

    ActiveParamsCheck(PROFILER_DEFAULT_ENTRIES.Value(),
                      PROFILER_DEFAULT_INTERVAL, features, filters,
                      std::size(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, std::size(filters), 0);

    ActiveParamsCheck(PROFILER_DEFAULT_ENTRIES.Value(),
                      PROFILER_DEFAULT_INTERVAL, differentFeatures, filters,
                      std::size(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,
                             std::size(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,
                      std::size(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, std::size(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,
                                std::size(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, std::size(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, std::size(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, std::size(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_impl(
      "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_impl(
      "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_impl(
      "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");
      schema.AddKeyFormat("key with unique string", MS::Format::UniqueString);
      schema.AddKeyFormatSearchable("key with sanitized string",
                                    MS::Format::SanitizedString,
                                    MS::Searchable::Searchable);
      return schema;
    }
  };
  EXPECT_TRUE(profiler_add_marker_impl(
      "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_impl("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,
      /* unsigned long aClassOfServiceFlag */ nsIClassOfService::Leader
      /* const mozilla::net::TimingStruct* aTimings = nullptr */
      /* mozilla::UniquePtr<mozilla::ProfileChunkedBuffer> aSource =
         nullptr */

      /* const mozilla::Maybe<mozilla::net::HttpVersion> aHttpVersion =
         mozilla::Nothing() */

      /* const mozilla::Maybe<uint32_t> aResponseStatus = mozilla::Nothing() */
      /* 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,
      /* unsigned long aClassOfServiceFlag */ nsIClassOfService::Follower,
      /* const mozilla::net::TimingStruct* aTimings = nullptr */ nullptr,
      /* mozilla::UniquePtr<mozilla::ProfileChunkedBuffer> aSource =
         nullptr */

      nullptr,
      /* const mozilla::Maybe<mozilla::net::HttpVersion> aHttpVersion =
         mozilla::Nothing() */

      Some(net::HttpVersion::v3_0),
      /* const mozilla::Maybe<uint32_t> aResponseStatus = mozilla::Nothing() */
      Some(200),
      /* 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,
      /* unsigned long aClassOfServiceFlag */ nsIClassOfService::Speculative,
      /* const mozilla::net::TimingStruct* aTimings = nullptr */ nullptr,
      /* mozilla::UniquePtr<mozilla::ProfileChunkedBuffer> aSource =
         nullptr */

      nullptr,
      /* const mozilla::Maybe<mozilla::net::HttpVersion> aHttpVersion =
         mozilla::Nothing() */

      mozilla::Nothing(),
      /* const mozilla::Maybe<uint32_t> aResponseStatus = mozilla::Nothing() */
      Some(0),
      /* 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,
      /* unsigned long aClassOfServiceFlag */ nsIClassOfService::Background,
      /* const mozilla::net::TimingStruct* aTimings = nullptr */ nullptr,
      /* mozilla::UniquePtr<mozilla::ProfileChunkedBuffer> aSource =
         nullptr */

      nullptr,
      /* const mozilla::Maybe<mozilla::net::HttpVersion> aHttpVersion =
         mozilla::Nothing() */

      mozilla::Nothing(),
      /* const mozilla::Maybe<uint32_t> aResponseStatus = mozilla::Nothing() */
      mozilla::Nothing(),
      /* 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,
      /* unsigned long aClassOfServiceFlag */ nsIClassOfService::Unblocked |
          nsIClassOfService::TailForbidden,
      /* const mozilla::net::TimingStruct* aTimings = nullptr */ nullptr,
      /* mozilla::UniquePtr<mozilla::ProfileChunkedBuffer> aSource =
         nullptr */

      nullptr,
      /* const mozilla::Maybe<mozilla::net::HttpVersion> aHttpVersion =
         mozilla::Nothing() */

      mozilla::Nothing(),
      /* const mozilla::Maybe<uint32_t> aResponseStatus = mozilla::Nothing() */
      mozilla::Nothing(),
      /* 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,
      /* unsigned long aClassOfServiceFlag */ nsIClassOfService::Unblocked |
          nsIClassOfService::Throttleable | nsIClassOfService::TailForbidden,
      /* const mozilla::net::TimingStruct* aTimings = nullptr */ nullptr,
      /* mozilla::UniquePtr<mozilla::ProfileChunkedBuffer> aSource =
         nullptr */

      nullptr,
      /* const mozilla::Maybe<mozilla::net::HttpVersion> aHttpVersion =
         mozilla::Nothing() */

      mozilla::Nothing(),
      /* const mozilla::Maybe<uint32_t> aResponseStatus = mozilla::Nothing() */
      mozilla::Nothing(),
      /* 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,
      /* unsigned long aClassOfServiceFlag */ nsIClassOfService::Tail
      /* const mozilla::net::TimingStruct* aTimings = nullptr */
      /* mozilla::UniquePtr<mozilla::ProfileChunkedBuffer> aSource =
         nullptr */

      /* const mozilla::Maybe<mozilla::net::HttpVersion> aHttpVersion =
         mozilla::Nothing() */

      /* const mozilla::Maybe<uint32_t> aResponseStatus = mozilla::Nothing() */
      /* const mozilla::Maybe<nsDependentCString>& aContentType =
         mozilla::Nothing() */

      /* nsIURI* aRedirectURI = nullptr */
      /* uint64_t aRedirectChannelId = 0 */
  );

  EXPECT_TRUE(profiler_add_marker_impl(
      "Text in main thread with stack", geckoprofiler::category::OTHER,
      {MarkerStack::Capture(), MarkerTiming::Interval(ts1, ts2)},
      geckoprofiler::markers::TextMarker{}, ""));
  EXPECT_TRUE(profiler_add_marker_impl(
      "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_impl(
        "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_impl(
        "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_impl(
        "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_impl(
        "Text from unregistered thread with stack",
        geckoprofiler::category::OTHER,
        MarkerOptions(MarkerThreadId::MainThread(), MarkerStack::Capture()),
        geckoprofiler::markers::TextMarker{}, ""));
  });
  unregisteredThread.join();

  EXPECT_TRUE(
      profiler_add_marker_impl("Tracing", geckoprofiler::category::OTHER, {},
                               geckoprofiler::markers::Tracing{}, "category"));

  EXPECT_TRUE(profiler_add_marker_impl("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_impl(
      "Text", geckoprofiler::category::OTHER, {},
      geckoprofiler::markers::TextMarker{}, std::string(3 * 1024 * 1024, 'x')));

  EXPECT_TRUE(profiler_add_marker_impl(
      "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"], Boolfalse);
                  EXPECT_EQ_JSON(payload["bool-true"], Booltrue);
                  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_EQ_JSON(payload["classOfService"], String, "Leader");
                  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_EQ_JSON(payload["httpVersion"], String, "h3");
                  EXPECT_EQ_JSON(payload["classOfService"], String, "Follower");
                  EXPECT_TRUE(payload["RedirectURI"].isNull());
                  EXPECT_TRUE(payload["redirectType"].isNull());
                  EXPECT_TRUE(payload["isHttpToHttpsRedirect"].isNull());
                  EXPECT_TRUE(payload["redirectId"].isNull());
                  EXPECT_EQ_JSON(payload["responseStatus"], Int64, 200);
                  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["classOfService"], String,
                                 "Speculative");
                  EXPECT_EQ_JSON(payload["RedirectURI"], String,
                                 "http://example.com/");
                  EXPECT_EQ_JSON(payload["redirectType"], String, "Temporary");
                  EXPECT_EQ_JSON(payload["isHttpToHttpsRedirect"], Boolfalse);
                  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["classOfService"], String,
                                 "Background");
                  EXPECT_EQ_JSON(payload["RedirectURI"], String,
                                 "http://example.com/");
                  EXPECT_EQ_JSON(payload["redirectType"], String, "Permanent");
                  EXPECT_EQ_JSON(payload["isHttpToHttpsRedirect"], Boolfalse);
                  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["classOfService"], String,
                                 "Unblocked | TailForbidden");
                  EXPECT_EQ_JSON(payload["RedirectURI"], String,
                                 "http://example.com/");
                  EXPECT_EQ_JSON(payload["redirectType"], String, "Internal");
                  EXPECT_EQ_JSON(payload["isHttpToHttpsRedirect"], Boolfalse);
                  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["classOfService"], String,
                                 "Unblocked | Throttleable | TailForbidden");
                  EXPECT_EQ_JSON(payload["RedirectURI"], String,
                                 "http://example.com/");
                  EXPECT_EQ_JSON(payload["redirectType"], String, "Internal");
                  EXPECT_EQ_JSON(payload["isHttpToHttpsRedirect"], Booltrue);
                  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"], Booltrue);
                  EXPECT_EQ_JSON(payload["classOfService"], String, "Tail");
                  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(), 16u);

            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"], Boolfalse);

            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"], Booltrue);

            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");

            ASSERT_TRUE(data[14u].isObject());
            EXPECT_EQ_JSON(data[14u]["key"], String, "key with unique string");
            EXPECT_TRUE(data[14u]["label"].isNull());
            EXPECT_EQ_JSON(data[14u]["format"], String, "unique-string");
            EXPECT_TRUE(data[14u]["searchable"].isNull());

            ASSERT_TRUE(data[15u].isObject());
            EXPECT_EQ_JSON(data[15u]["key"], String,
                           "key with sanitized string");
            EXPECT_TRUE(data[15u]["label"].isNull());
            EXPECT_EQ_JSON(data[15u]["format"], String, "sanitized-string");
            EXPECT_EQ_JSON(data[15u]["searchable"], Booltrue);
          } 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, std::size(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 = std::size(expectedTestCounters);

  bool expectCounter2 = false;
  int64_t testCounters2[] = {10};
  NumberAndCount expectedTestCounters2[] = {{1u, 10}, {0u, 0}};
  constexpr size_t expectedTestCounters2Count =
      std::size(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(samples, counter["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(samples, counter["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, std::size(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, std::size(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, std::size(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, std::size(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, std::size(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, std::size(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, std::size(manyThreadsFilter), 0);

  profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL, features,
                 fewThreadsFilter, std::size(fewThreadsFilter), 0);

  // In bug 1355807 this caused an assertion failure in StopJSSampling().
  profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL, features,
                 fewThreadsFilter, std::size(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(JS::ProfilingCategoryPair aCategory,
                                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, std::size(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, std::size(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, std::size(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, std::size(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, std::size(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, std::size(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, std::size(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)
{
  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(std::size(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, std::size(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[] = {falsetrue};
  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, std::size(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)
{
  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, std::size(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, std::size(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_impl("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, std::size(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, std::size(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_impl(
      "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

Messung V0.5 in Prozent
C=93 H=91 G=91

¤ Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.0.119Bemerkung:  (vorverarbeitet am  2026-05-08) ¤

*Bot Zugriff






Wurzel

Suchen

Beweissystem der NASA

Beweissystem Isabelle

NIST Cobol Testsuite

Cephes Mathematical Library

Wiener Entwicklungsmethode

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.