Source code
Revision control
Copy as Markdown
Other Tools
/* -*- 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
// 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(
// 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,
/* mozilla::net::HttpVersion aHttpVersion */ net::HttpVersion::v3_0,
/* unsigned long aClassOfServiceFlag */ nsIClassOfService::Leader
/* const mozilla::net::TimingStruct* aTimings = nullptr */
/* mozilla::UniquePtr<mozilla::ProfileChunkedBuffer> aSource =
nullptr */
/* const mozilla::Maybe<nsDependentCString>& aContentType =
mozilla::Nothing() */
/* nsIURI* aRedirectURI = nullptr */
/* uint64_t aRedirectChannelId = 0 */
);
profiler_add_network_marker(
/* nsIURI* aURI */ uri,
/* const nsACString& aRequestMethod */ "GET"_ns,
/* int32_t aPriority */ 34,
/* uint64_t aChannelId */ 2,
/* NetworkLoadType aType */ net::NetworkLoadType::LOAD_STOP,
/* mozilla::TimeStamp aStart */ ts1,
/* mozilla::TimeStamp aEnd */ ts2,
/* int64_t aCount */ 56,
/* mozilla::net::CacheDisposition aCacheDisposition */
net::kCacheUnresolved,
/* uint64_t aInnerWindowID */ 78,
/* bool aIsPrivateBrowsing */ false,
/* mozilla::net::HttpVersion aHttpVersion */ net::HttpVersion::v3_0,
/* unsigned long aClassOfServiceFlag */ nsIClassOfService::Follower,
/* const mozilla::net::TimingStruct* aTimings = nullptr */ nullptr,
/* mozilla::UniquePtr<mozilla::ProfileChunkedBuffer> aSource =
nullptr */
nullptr,
/* const mozilla::Maybe<nsDependentCString>& aContentType =
mozilla::Nothing() */
Some(nsDependentCString("text/html")),
/* nsIURI* aRedirectURI = nullptr */ nullptr,
/* uint64_t aRedirectChannelId = 0 */ 0);
nsCOMPtr<nsIURI> redirectURI;
ASSERT_TRUE(NS_SUCCEEDED(
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,
/* mozilla::net::HttpVersion aHttpVersion */ net::HttpVersion::v3_0,
/* unsigned long aClassOfServiceFlag */ nsIClassOfService::Speculative,
/* const mozilla::net::TimingStruct* aTimings = nullptr */ nullptr,
/* mozilla::UniquePtr<mozilla::ProfileChunkedBuffer> aSource =
nullptr */
nullptr,
/* const mozilla::Maybe<nsDependentCString>& aContentType =
mozilla::Nothing() */
mozilla::Nothing(),
/* nsIURI* aRedirectURI = nullptr */ redirectURI,
/* uint32_t aRedirectFlags = 0 */
nsIChannelEventSink::REDIRECT_TEMPORARY,
/* uint64_t aRedirectChannelId = 0 */ 103);
profiler_add_network_marker(
/* nsIURI* aURI */ uri,
/* const nsACString& aRequestMethod */ "GET"_ns,
/* int32_t aPriority */ 34,
/* uint64_t aChannelId */ 4,
/* NetworkLoadType aType */ net::NetworkLoadType::LOAD_REDIRECT,
/* mozilla::TimeStamp aStart */ ts1,
/* mozilla::TimeStamp aEnd */ ts2,
/* int64_t aCount */ 56,
/* mozilla::net::CacheDisposition aCacheDisposition */
net::kCacheUnresolved,
/* uint64_t aInnerWindowID */ 78,
/* bool aIsPrivateBrowsing */ false,
/* mozilla::net::HttpVersion aHttpVersion */ net::HttpVersion::v3_0,
/* unsigned long aClassOfServiceFlag */ nsIClassOfService::Background,
/* const mozilla::net::TimingStruct* aTimings = nullptr */ nullptr,
/* mozilla::UniquePtr<mozilla::ProfileChunkedBuffer> aSource =
nullptr */
nullptr,
/* const mozilla::Maybe<nsDependentCString>& aContentType =
mozilla::Nothing() */
mozilla::Nothing(),
/* nsIURI* aRedirectURI = nullptr */ redirectURI,
/* uint32_t aRedirectFlags = 0 */
nsIChannelEventSink::REDIRECT_PERMANENT,
/* uint64_t aRedirectChannelId = 0 */ 104);
profiler_add_network_marker(
/* nsIURI* aURI */ uri,
/* const nsACString& aRequestMethod */ "GET"_ns,
/* int32_t aPriority */ 34,
/* uint64_t aChannelId */ 5,
/* NetworkLoadType aType */ net::NetworkLoadType::LOAD_REDIRECT,
/* mozilla::TimeStamp aStart */ ts1,
/* mozilla::TimeStamp aEnd */ ts2,
/* int64_t aCount */ 56,
/* mozilla::net::CacheDisposition aCacheDisposition */
net::kCacheUnresolved,
/* uint64_t aInnerWindowID */ 78,
/* bool aIsPrivateBrowsing */ false,
/* mozilla::net::HttpVersion aHttpVersion */ net::HttpVersion::v3_0,
/* unsigned long aClassOfServiceFlag */ nsIClassOfService::Unblocked |
nsIClassOfService::TailForbidden,
/* const mozilla::net::TimingStruct* aTimings = nullptr */ nullptr,
/* mozilla::UniquePtr<mozilla::ProfileChunkedBuffer> aSource =
nullptr */
nullptr,
/* const mozilla::Maybe<nsDependentCString>& aContentType =
mozilla::Nothing() */
mozilla::Nothing(),
/* nsIURI* aRedirectURI = nullptr */ redirectURI,
/* uint32_t aRedirectFlags = 0 */ nsIChannelEventSink::REDIRECT_INTERNAL,
/* uint64_t aRedirectChannelId = 0 */ 105);
profiler_add_network_marker(
/* nsIURI* aURI */ uri,
/* const nsACString& aRequestMethod */ "GET"_ns,
/* int32_t aPriority */ 34,
/* uint64_t aChannelId */ 6,
/* NetworkLoadType aType */ net::NetworkLoadType::LOAD_REDIRECT,
/* mozilla::TimeStamp aStart */ ts1,
/* mozilla::TimeStamp aEnd */ ts2,
/* int64_t aCount */ 56,
/* mozilla::net::CacheDisposition aCacheDisposition */
net::kCacheUnresolved,
/* uint64_t aInnerWindowID */ 78,
/* bool aIsPrivateBrowsing */ false,
/* mozilla::net::HttpVersion aHttpVersion */ net::HttpVersion::v3_0,
/* 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<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,
/* mozilla::net::HttpVersion aHttpVersion */ net::HttpVersion::v3_0,
/* unsigned long aClassOfServiceFlag */ nsIClassOfService::Tail
/* const mozilla::net::TimingStruct* aTimings = nullptr */
/* mozilla::UniquePtr<mozilla::ProfileChunkedBuffer> aSource =
nullptr */
/* const mozilla::Maybe<nsDependentCString>& aContentType =
mozilla::Nothing() */
/* nsIURI* aRedirectURI = nullptr */
/* uint64_t aRedirectChannelId = 0 */
);
EXPECT_TRUE(profiler_add_marker_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);
# 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") {
EXPECT_EQ(state, S_Markers2DefaultWithOptions);
state = State(S_Markers2DefaultWithOptions + 1);
EXPECT_EQ(typeString, "NoPayloadUserData");
EXPECT_FALSE(payload["stack"].isNull());
} else if (nameString == "FirstMarker") {
// Record start and end times, to compare with timestamps in
// following markers.
EXPECT_EQ(state, S_FirstMarker);
ts1Double = marker[START_TIME].asDouble();
ts2Double = marker[END_TIME].asDouble();
state = State(S_FirstMarker + 1);
EXPECT_EQ(typeString, "Text");
EXPECT_EQ_JSON(payload["name"], String, "First Marker");
} else if (nameString == "Gtest custom marker") {
EXPECT_EQ(state, S_CustomMarker);
state = State(S_CustomMarker + 1);
EXPECT_EQ(typeString, "markers-gtest");
EXPECT_EQ(payload.size(), 1u + 9u);
EXPECT_TRUE(payload["null"].isNull());
EXPECT_EQ_JSON(payload["bool-false"], Bool, false);
EXPECT_EQ_JSON(payload["bool-true"], Bool, true);
EXPECT_EQ_JSON(payload["int"], Int64, 42);
EXPECT_EQ_JSON(payload["double"], Double, 43.0);
EXPECT_EQ_JSON(payload["text"], String, "gtest text");
// Unique strings can be fetched from the string table.
ASSERT_TRUE(payload["unique text"].isUInt());
auto textIndex = payload["unique text"].asUInt();
GET_JSON(uniqueText, stringTable[textIndex], String);
ASSERT_TRUE(uniqueText.isString());
ASSERT_EQ(uniqueText.asString(), "gtest unique text");
// The duplicate unique text should have the exact same index.
EXPECT_EQ_JSON(payload["unique text again"], UInt, textIndex);
EXPECT_EQ_JSON(payload["time"], Double, ts1Double);
} else if (nameString == "Gtest special marker") {
EXPECT_EQ(state, S_SpecialMarker);
state = State(S_SpecialMarker + 1);
EXPECT_EQ(typeString, "markers-gtest-special");
EXPECT_EQ(payload.size(), 1u) << "Only 'type' in the payload";
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["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["httpVersion"], String, "h3");
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());
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["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["contentType"], String, "text/html");
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["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,
"Speculative");
EXPECT_EQ_JSON(payload["RedirectURI"], String,
EXPECT_EQ_JSON(payload["redirectType"], String, "Temporary");
EXPECT_EQ_JSON(payload["isHttpToHttpsRedirect"], Bool, false);
EXPECT_EQ_JSON(payload["redirectId"], Int64, 103);
EXPECT_TRUE(payload["contentType"].isNull());
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["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,
"Background");
EXPECT_EQ_JSON(payload["RedirectURI"], String,
EXPECT_EQ_JSON(payload["redirectType"], String, "Permanent");
EXPECT_EQ_JSON(payload["isHttpToHttpsRedirect"], Bool, false);
EXPECT_EQ_JSON(payload["redirectId"], Int64, 104);
EXPECT_TRUE(payload["contentType"].isNull());
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["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,
"Unblocked | TailForbidden");
EXPECT_EQ_JSON(payload["RedirectURI"], String,
EXPECT_EQ_JSON(payload["redirectType"], String, "Internal");
EXPECT_EQ_JSON(payload["isHttpToHttpsRedirect"], Bool, false);
EXPECT_EQ_JSON(payload["redirectId"], Int64, 105);
EXPECT_TRUE(payload["contentType"].isNull());
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["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,
"Unblocked | Throttleable | TailForbidden");
EXPECT_EQ_JSON(payload["RedirectURI"], String,
EXPECT_EQ_JSON(payload["redirectType"], String, "Internal");
EXPECT_EQ_JSON(payload["isHttpToHttpsRedirect"], Bool, true);
EXPECT_EQ_JSON(payload["redirectId"], Int64, 106);
EXPECT_TRUE(payload["contentType"].isNull());
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["requestMethod"], String, "GET");
EXPECT_EQ_JSON(payload["pri"], Int64, 34);
EXPECT_EQ_JSON(payload["count"], Int64, 56);
EXPECT_EQ_JSON(payload["cache"], String, "Unresolved");
EXPECT_EQ_JSON(payload["isPrivateBrowsing"], Bool, true);
EXPECT_EQ_JSON(payload["httpVersion"], String, "h3");
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") {
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"], Bool, false);
ASSERT_TRUE(data[3u].isObject());
EXPECT_EQ_JSON(data[3u]["key"], String,
"key with label duration searchable");
EXPECT_TRUE(data[3u]["label duration"].isNull());
EXPECT_EQ_JSON(data[3u]["format"], String, "duration");
EXPECT_EQ_JSON(data[3u]["searchable"], Bool, true);
ASSERT_TRUE(data[4u].isObject());
EXPECT_EQ_JSON(data[4u]["key"], String, "key with time");
EXPECT_TRUE(data[4u]["label"].isNull());
EXPECT_EQ_JSON(data[4u]["format"], String, "time");
EXPECT_TRUE(data[4u]["searchable"].isNull());
ASSERT_TRUE(data[5u].isObject());
EXPECT_EQ_JSON(data[5u]["key"], String, "key with seconds");
EXPECT_TRUE(data[5u]["label"].isNull());
EXPECT_EQ_JSON(data[5u]["format"], String, "seconds");
EXPECT_TRUE(data[5u]["searchable"].isNull());
ASSERT_TRUE(data[6u].isObject());
EXPECT_EQ_JSON(data[6u]["key"], String, "key with milliseconds");
EXPECT_TRUE(data[6u]["label"].isNull());
EXPECT_EQ_JSON(data[6u]["format"], String, "milliseconds");
EXPECT_TRUE(data[6u]["searchable"].isNull());
ASSERT_TRUE(data[7u].isObject());
EXPECT_EQ_JSON(data[7u]["key"], String, "key with microseconds");
EXPECT_TRUE(data[7u]["label"].isNull());
EXPECT_EQ_JSON(data[7u]["format"], String, "microseconds");
EXPECT_TRUE(data[7u]["searchable"].isNull());
ASSERT_TRUE(data[8u].isObject());
EXPECT_EQ_JSON(data[8u]["key"], String, "key with nanoseconds");
EXPECT_TRUE(data[8u]["label"].isNull());
EXPECT_EQ_JSON(data[8u]["format"], String, "nanoseconds");
EXPECT_TRUE(data[8u]["searchable"].isNull());
ASSERT_TRUE(data[9u].isObject());
EXPECT_EQ_JSON(data[9u]["key"], String, "key with bytes");
EXPECT_TRUE(data[9u]["label"].isNull());
EXPECT_EQ_JSON(data[9u]["format"], String, "bytes");
EXPECT_TRUE(data[9u]["searchable"].isNull());
ASSERT_TRUE(data[10u].isObject());
EXPECT_EQ_JSON(data[10u]["key"], String, "key with percentage");
EXPECT_TRUE(data[10u]["label"].isNull());
EXPECT_EQ_JSON(data[10u]["format"], String, "percentage");
EXPECT_TRUE(data[10u]["searchable"].isNull());
ASSERT_TRUE(data[11u].isObject());
EXPECT_EQ_JSON(data[11u]["key"], String, "key with integer");
EXPECT_TRUE(data[11u]["label"].isNull());
EXPECT_EQ_JSON(data[11u]["format"], String, "integer");
EXPECT_TRUE(data[11u]["searchable"].isNull());
ASSERT_TRUE(data[12u].isObject());
EXPECT_EQ_JSON(data[12u]["key"], String, "key with decimal");
EXPECT_TRUE(data[12u]["label"].isNull());
EXPECT_EQ_JSON(data[12u]["format"], String, "decimal");
EXPECT_TRUE(data[12u]["searchable"].isNull());
ASSERT_TRUE(data[13u].isObject());
EXPECT_EQ_JSON(data[13u]["label"], String, "static label");
EXPECT_EQ_JSON(data[13u]["value"], String, "static value");
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"], Bool, true);
} 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);
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[] = {false, true};
for (const bool testWithNoStackSampling : scTestsWithOrWithoutStackSampling) {
ASSERT_TRUE(!profiler_is_active());
ASSERT_TRUE(!profiler_callback_after_sampling(
[&](SamplingState) { ASSERT_TRUE(false); }));
profiler_start(
PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL,
ProfilerFeature::StackWalk | ProfilerFeature::CPUUtilization |
(testWithNoStackSampling ? ProfilerFeature::NoStackSampling : 0),
filters, 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