/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// This file tests a lot of the profiler_*() functions in GeckoProfiler.h.
// Most of the tests just check that nothing untoward (e.g. crashes, deadlocks)
// happens when calling these functions. They don't do much inspection of
// profiler internals.
#include "mozilla/ProfilerPlatformMacros.h"
#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(GP_OS_windows)
# include <processthreadsapi.h>
# include <realtimeapiset.h>
#elif defined(GP_OS_darwin)
# include <mach/thread_act.h>
#endif
#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>
// 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;
baseRegistrationTime = baseprofiler::detail::GetThreadRegistrationTime();
if (baseRegistrationTime) {
EXPECT_EQ(trInfoHere.RegisterTime(), baseRegistrationTime);
}
else {
EXPECT_GT(trInfoHere.RegisterTime(), ts);
}
EXPECT_EQ(trInfoHere.ThreadId(), profiler_current_thread_id());
EXPECT_EQ(trInfoHere.ThreadId(), profiler_main_thread_id())
<<
"Gtests are assumed to run on the main thread";
EXPECT_EQ(trInfoHere.IsMainThread(), true)
<<
"Gtests are assumed to run on the main thread";
}
{
// Sub-thread test.
// These will receive sub-thread data (to test move at thread end).
TimeStamp tsThread;
ProfilerThreadId threadThreadId;
UniquePtr<profiler::ThreadRegistrationInfo> trInfoThreadPtr;
std::thread testThread([&]() {
profiler::ThreadRegistrationInfo trInfoThread{
"Thread"};
EXPECT_STREQ(trInfoThread.Name(),
"Thread");
EXPECT_NE(trInfoThread.Name(),
"Thread")
<<
"ThreadRegistrationInfo should keep its own copy of the name";
EXPECT_GT(trInfoThread.RegisterTime(), ts);
EXPECT_EQ(trInfoThread.ThreadId(), profiler_current_thread_id());
EXPECT_NE(trInfoThread.ThreadId(), profiler_main_thread_id());
EXPECT_EQ(trInfoThread.IsMainThread(),
false);
tsThread = trInfoThread.RegisterTime();
threadThreadId = trInfoThread.ThreadId();
trInfoThreadPtr =
MakeUnique<profiler::ThreadRegistrationInfo>(std::move(trInfoThread));
});
testThread.join();
ASSERT_NE(trInfoThreadPtr, nullptr);
EXPECT_STREQ(trInfoThreadPtr->Name(),
"Thread");
EXPECT_EQ(trInfoThreadPtr->RegisterTime(), tsThread);
EXPECT_EQ(trInfoThreadPtr->ThreadId(), threadThreadId);
EXPECT_EQ(trInfoThreadPtr->IsMainThread(),
false)
<<
"Gtests are assumed to run on the main thread";
}
}
static constexpr ThreadProfilingFeatures scEachAndAnyThreadProfilingFeatures[] =
{ThreadProfilingFeatures::CPUUtilization, ThreadProfilingFeatures::Sampling,
ThreadProfilingFeatures::Markers, ThreadProfilingFeatures::Any};
TEST(GeckoProfiler, ThreadProfilingFeaturesType)
{
ASSERT_EQ(static_cast<uint32_t>(ThreadProfilingFeatures::Any),
1u +
2u +
4u)
<<
"This test assumes that there are 3 binary choices 1+2+4; "
"Is this test up to date?";
EXPECT_EQ(Combine(ThreadProfilingFeatures::CPUUtilization,
ThreadProfilingFeatures::Sampling,
ThreadProfilingFeatures::Markers),
ThreadProfilingFeatures::Any);
constexpr ThreadProfilingFeatures allThreadProfilingFeatures[] = {
ThreadProfilingFeatures::NotProfiled,
ThreadProfilingFeatures::CPUUtilization,
ThreadProfilingFeatures::Sampling, ThreadProfilingFeatures::Markers,
ThreadProfilingFeatures::Any};
for (ThreadProfilingFeatures f1 : allThreadProfilingFeatures) {
// Combine and Intersect are commutative.
for (ThreadProfilingFeatures f2 : allThreadProfilingFeatures) {
EXPECT_EQ(Combine(f1, f2), Combine(f2, f1));
EXPECT_EQ(Intersect(f1, f2), Intersect(f2, f1));
}
// Combine works like OR.
EXPECT_EQ(Combine(f1, f1), f1);
EXPECT_EQ(Combine(f1, f1, f1), f1);
// 'OR NotProfiled' doesn't change anything.
EXPECT_EQ(Combine(f1, ThreadProfilingFeatures::NotProfiled), f1);
// 'OR Any' makes Any.
EXPECT_EQ(Combine(f1, ThreadProfilingFeatures::Any),
ThreadProfilingFeatures::Any);
// Intersect works like AND.
EXPECT_EQ(Intersect(f1, f1), f1);
EXPECT_EQ(Intersect(f1, f1, f1), f1);
// 'AND NotProfiled' erases anything.
EXPECT_EQ(Intersect(f1, ThreadProfilingFeatures::NotProfiled),
ThreadProfilingFeatures::NotProfiled);
// 'AND Any' doesn't change anything.
EXPECT_EQ(Intersect(f1, ThreadProfilingFeatures::Any), f1);
}
for (ThreadProfilingFeatures f1 : scEachAndAnyThreadProfilingFeatures) {
EXPECT_TRUE(DoFeaturesIntersect(f1, f1));
// NotProfiled doesn't intersect with any feature.
EXPECT_FALSE(DoFeaturesIntersect(f1, ThreadProfilingFeatures::NotProfiled));
// Any intersects with any feature.
EXPECT_TRUE(DoFeaturesIntersect(f1, ThreadProfilingFeatures::Any));
}
}
static void TestConstUnlockedConstReader(
const profiler::ThreadRegistration::UnlockedConstReader& aData,
const TimeStamp& aBeforeRegistration,
const TimeStamp& aAfterRegistration,
const void* aOnStackObject,
ProfilerThreadId aThreadId = profiler_current_thread_id()) {
EXPECT_STREQ(aData.Info().Name(),
"Test thread");
EXPECT_GE(aData.Info().RegisterTime(), aBeforeRegistration);
EXPECT_LE(aData.Info().RegisterTime(), aAfterRegistration);
EXPECT_EQ(aData.Info().ThreadId(), aThreadId);
EXPECT_FALSE(aData.Info().IsMainThread());
#if defined(GP_OS_windows)
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(GP_OS_darwin)
// 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(GP_OS_linux) ||
defined(GP_OS_android) ||
defined(GP_OS_freebsd))
// 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
#if defined(MOZ_ASAN)
// When ASan is enabled, onStackChar will be located on ASan's fake stack
// instead of the thread's native stack, thus interfering with the following
// check. See <https://bugzil.la/2040038>.
(
void)aOnStackObject;
#else
EXPECT_GE(aData.StackTop(), aOnStackObject)
<<
"StackTop should be at &onStackChar, or higher on some "
"platforms";
#endif // defined(MOZ_ASAN)
};
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);
}
// 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(
999999u),
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(
999999u).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(
999999u),
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(
999999u).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(
88888u),
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(
88888u).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(
0u),
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();
}
// Mock nsIClassOfService for network marker tests
class MockClassOfService final : public nsIClassOfService {
public:
NS_DECL_ISUPPORTS
explicit MockClassOfService(uint32_t aClassFlags,
bool aIncremental =
false,
nsIClassOfService::FetchPriority aFetchPriority =
nsIClassOfService::FETCHPRIORITY_UNSET)
: mClassFlags(aClassFlags),
mIncremental(aIncremental),
mFetchPriority(aFetchPriority) {}
NS_IMETHOD GetClassFlags(uint32_t* aFlags) override {
*aFlags = mClassFlags;
return NS_OK;
}
NS_IMETHOD SetClassFlags(uint32_t aFlags) override {
mClassFlags = aFlags;
return NS_OK;
}
NS_IMETHOD ClearClassFlags(uint32_t aFlags) override {
mClassFlags &= ~aFlags;
return NS_OK;
}
NS_IMETHOD AddClassFlags(uint32_t aFlags) override {
mClassFlags |= aFlags;
return NS_OK;
}
NS_IMETHOD GetIncremental(
bool* aIncremental) override {
*aIncremental = mIncremental;
return NS_OK;
}
NS_IMETHOD SetIncremental(
bool aIncremental) override {
mIncremental = aIncremental;
return NS_OK;
}
NS_IMETHOD GetFetchPriority(
nsIClassOfService::FetchPriority* aFetchPriority) override {
*aFetchPriority = mFetchPriority;
return NS_OK;
}
NS_IMETHOD SetFetchPriority(
nsIClassOfService::FetchPriority aFetchPriority) override {
mFetchPriority = aFetchPriority;
return NS_OK;
}
NS_IMETHOD SetClassOfService(mozilla::net::ClassOfService s) override {
return NS_ERROR_NOT_IMPLEMENTED;
}
NS_IMETHOD_(
void)
SetFetchPriorityDOM(mozilla::dom::FetchPriority aPriority) override {}
private:
~MockClassOfService() =
default;
uint32_t mClassFlags;
bool mIncremental;
nsIClassOfService::FetchPriority mFetchPriority;
};
NS_IMPL_ISUPPORTS(MockClassOfService, nsIClassOfService)
// BaseMarkerType-based marker with Format::UniqueString payload fields.
// Must be at file scope since local structs cannot have static data members.
struct GtestBaseMarkerTypeUniqueString
: public mozilla::BaseMarkerType<GtestBaseMarkerTypeUniqueString> {
static constexpr
const char* Name =
"markers-gtest-base-unique-string";
using MS = mozilla::MarkerSchema;
static constexpr MS::Location Locations[] = {MS::Location::MarkerChart,
MS::Location::MarkerTable};
static constexpr MS::PayloadField PayloadFields[] = {
{
"uniqueField", MS::InputType::CString,
"Unique Field",
MS::Format::UniqueString},
{
"plainField", MS::InputType::CString,
"Plain Field",
MS::Format::String}};
static void StreamJSONMarkerData(
mozilla::baseprofiler::SpliceableJSONWriter& aWriter,
const mozilla::ProfilerString8View& aUniqueText,
const mozilla::ProfilerString8View& aPlainText) {
StreamJSONMarkerDataImpl(aWriter, aUniqueText, aPlainText);
}
};
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_MARKER(
"auto tracing", OTHER);
}
{
AUTO_PROFILER_MARKER_UNTYPED(
"D", 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);
aWriter.StringProperty(
"color",
"green");
}
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");
schema.SetColorField(
"color");
// All data functions, all formats.
schema.AddKeyFormat(
"key with url", MS::Format::Url);
schema.AddKeyLabelFormat(
"key with label filePath",
"label filePath",
MS::Format::FilePath);
schema.AddKeyFormat(
"key with string", MS::Format::String);
schema.AddKeyLabelFormat(
"key with label duration",
"label duration",
MS::Format::Duration);
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.AddKeyFormat(
"key with sanitized string",
MS::Format::SanitizedString);
schema.AddKeyLabelFormat(
"key with label hidden",
"label",
MS::Format::String, MS::PayloadFlags::Hidden);
schema.AddKeyFormat(
"key hidden", MS::Format::String,
MS::PayloadFlags::Hidden);
schema.AddKeyFormat(
"color", MS::Format::String,
MS::PayloadFlags::Hidden);
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.
(
void)GtestUnusedMarker{};
EXPECT_TRUE(profiler_add_marker_impl(
"Gtest base marker type unique string", geckoprofiler::category::OTHER,
{}, GtestBaseMarkerTypeUniqueString{},
"gtest unique field value",
"gtest plain field value"));
// Test PROFILER_MARKER_SIMPLE_PAYLOAD with various data types.
int testInt =
42;
double testDouble =
3.
14;
bool testBool = true;
nsCString testString(
"test_string");
PROFILER_MARKER_SIMPLE_PAYLOAD(
"SimplePayload with int", OTHER, testInt);
PROFILER_MARKER_SIMPLE_PAYLOAD(
"SimplePayload with double", OTHER,
testDouble);
PROFILER_MARKER_SIMPLE_PAYLOAD(
"SimplePayload with bool", OTHER, testBool);
PROFILER_MARKER_SIMPLE_PAYLOAD(
"SimplePayload with string", OTHER,
testString);
PROFILER_MARKER_SIMPLE_PAYLOAD(
"SimplePayload with multiple", OTHER, testInt,
testDouble, testBool);
// Other markers in alphabetical order of payload class names.
nsCOMPtr<nsIURI> uri;
ASSERT_TRUE(
NS_SUCCEEDED(NS_NewURI(getter_AddRefs(uri),
"http://mozilla.org/"_ns)));
// The marker name will be "Load <aChannelId>: <aURI>".
RefPtr<MockClassOfService> classOfService1 =
new MockClassOfService(nsIClassOfService::Leader);
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,
/* nsICacheInfoChannel::CacheDisposition aCacheDisposition */
nsICacheInfoChannel::kCacheHit,
/* uint64_t aInnerWindowID */ 78,
/* bool aIsPrivateBrowsing */ false,
/* nsIClassOfService* aClassOfService */ classOfService1,
/* nsresult aRequestStatus */ NS_OK
/* const mozilla::net::TimingStruct* aTimings = nullptr */
/* mozilla::UniquePtr<mozilla::ProfileChunkedBuffer> aSource =
nullptr */
/* const mozilla::Maybe<mozilla::net::HttpVersion> aHttpVersion =
mozilla::Nothing() */
/* const mozilla::Maybe<uint32_t> aResponseStatus = mozilla::Nothing() */
/* const mozilla::Maybe<nsDependentCString>& aContentType =
mozilla::Nothing() */
/* nsIURI* aRedirectURI = nullptr */
/* uint64_t aRedirectChannelId = 0 */
);
RefPtr<MockClassOfService> classOfService2 =
new MockClassOfService(nsIClassOfService::Follower);
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,
/* nsICacheInfoChannel::CacheDisposition aCacheDisposition */
nsICacheInfoChannel::kCacheUnresolved,
/* uint64_t aInnerWindowID */ 78,
/* bool aIsPrivateBrowsing */ false,
/* nsIClassOfService* aClassOfService */ classOfService2,
/* nsresult aRequestStatus */ NS_BINDING_ABORTED,
/* const mozilla::net::TimingStruct* aTimings = nullptr */ nullptr,
/* mozilla::UniquePtr<mozilla::ProfileChunkedBuffer> aSource =
nullptr */
nullptr,
/* const mozilla::Maybe<mozilla::net::HttpVersion> aHttpVersion =
mozilla::Nothing() */
Some(net::HttpVersion::v3_0),
/* const mozilla::Maybe<uint32_t> aResponseStatus = mozilla::Nothing() */
Some(
200),
/* const mozilla::Maybe<nsDependentCString>& aContentType =
mozilla::Nothing() */
Some(nsDependentCString(
"text/html")),
/* nsIURI* aRedirectURI = nullptr */ nullptr,
/* uint64_t aRedirectChannelId = 0 */ 0);
nsCOMPtr<nsIURI> redirectURI;
ASSERT_TRUE(NS_SUCCEEDED(
NS_NewURI(getter_AddRefs(redirectURI),
"http://example.com/"_ns)));
RefPtr<MockClassOfService> classOfService3 =
new MockClassOfService(nsIClassOfService::Speculative);
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,
/* nsICacheInfoChannel::CacheDisposition aCacheDisposition */
nsICacheInfoChannel::kCacheUnresolved,
/* uint64_t aInnerWindowID */ 78,
/* bool aIsPrivateBrowsing */ false,
/* nsIClassOfService* aClassOfService */ classOfService3,
/* nsresult aRequestStatus */ NS_ERROR_UNEXPECTED,
/* const mozilla::net::TimingStruct* aTimings = nullptr */ nullptr,
/* mozilla::UniquePtr<mozilla::ProfileChunkedBuffer> aSource =
nullptr */
nullptr,
/* const mozilla::Maybe<mozilla::net::HttpVersion> aHttpVersion =
mozilla::Nothing() */
mozilla::Nothing(),
/* const mozilla::Maybe<uint32_t> aResponseStatus = mozilla::Nothing() */
Some(
0),
/* const mozilla::Maybe<nsDependentCString>& aContentType =
mozilla::Nothing() */
mozilla::Nothing(),
/* nsIURI* aRedirectURI = nullptr */ redirectURI,
/* uint32_t aRedirectFlags = 0 */
nsIChannelEventSink::REDIRECT_TEMPORARY,
/* uint64_t aRedirectChannelId = 0 */ 103);
RefPtr<MockClassOfService> classOfService4 =
new MockClassOfService(nsIClassOfService::Background);
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,
/* nsICacheInfoChannel::CacheDisposition aCacheDisposition */
nsICacheInfoChannel::kCacheUnresolved,
/* uint64_t aInnerWindowID */ 78,
/* bool aIsPrivateBrowsing */ false,
/* nsIClassOfService* aClassOfService */ classOfService4,
/* nsresult aRequestStatus */ NS_ERROR_DOCSHELL_DYING,
/* const mozilla::net::TimingStruct* aTimings = nullptr */ nullptr,
/* mozilla::UniquePtr<mozilla::ProfileChunkedBuffer> aSource =
nullptr */
nullptr,
/* const mozilla::Maybe<mozilla::net::HttpVersion> aHttpVersion =
mozilla::Nothing() */
mozilla::Nothing(),
/* const mozilla::Maybe<uint32_t> aResponseStatus = mozilla::Nothing() */
mozilla::Nothing(),
/* const mozilla::Maybe<nsDependentCString>& aContentType =
mozilla::Nothing() */
mozilla::Nothing(),
/* nsIURI* aRedirectURI = nullptr */ redirectURI,
/* uint32_t aRedirectFlags = 0 */
nsIChannelEventSink::REDIRECT_PERMANENT,
/* uint64_t aRedirectChannelId = 0 */ 104);
RefPtr<MockClassOfService> classOfService5 =
new MockClassOfService(
nsIClassOfService::Unblocked | nsIClassOfService::TailForbidden);
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,
/* nsICacheInfoChannel::CacheDisposition aCacheDisposition */
nsICacheInfoChannel::kCacheUnresolved,
/* uint64_t aInnerWindowID */ 78,
/* bool aIsPrivateBrowsing */ false,
/* nsIClassOfService* aClassOfService */ classOfService5,
/* nsresult aRequestStatus */ NS_ERROR_DOM_CORP_FAILED,
/* const mozilla::net::TimingStruct* aTimings = nullptr */ nullptr,
/* mozilla::UniquePtr<mozilla::ProfileChunkedBuffer> aSource =
nullptr */
nullptr,
/* const mozilla::Maybe<mozilla::net::HttpVersion> aHttpVersion =
mozilla::Nothing() */
mozilla::Nothing(),
/* const mozilla::Maybe<uint32_t> aResponseStatus = mozilla::Nothing() */
mozilla::Nothing(),
/* const mozilla::Maybe<nsDependentCString>& aContentType =
mozilla::Nothing() */
mozilla::Nothing(),
/* nsIURI* aRedirectURI = nullptr */ redirectURI,
/* uint32_t aRedirectFlags = 0 */ nsIChannelEventSink::REDIRECT_INTERNAL,
/* uint64_t aRedirectChannelId = 0 */ 105);
RefPtr<MockClassOfService> classOfService6 =
new MockClassOfService(
nsIClassOfService::Unblocked | nsIClassOfService::Throttleable |
nsIClassOfService::TailForbidden);
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,
/* nsICacheInfoChannel::CacheDisposition aCacheDisposition */
nsICacheInfoChannel::kCacheUnresolved,
/* uint64_t aInnerWindowID */ 78,
/* bool aIsPrivateBrowsing */ false,
/* nsIClassOfService* aClassOfService */ classOfService6,
/* nsresult aRequestStatus */ NS_ERROR_BLOCKED_BY_POLICY,
/* const mozilla::net::TimingStruct* aTimings = nullptr */ nullptr,
/* mozilla::UniquePtr<mozilla::ProfileChunkedBuffer> aSource =
nullptr */
nullptr,
/* const mozilla::Maybe<mozilla::net::HttpVersion> aHttpVersion =
mozilla::Nothing() */
mozilla::Nothing(),
/* const mozilla::Maybe<uint32_t> aResponseStatus = mozilla::Nothing() */
mozilla::Nothing(),
/* const mozilla::Maybe<nsDependentCString>& aContentType =
mozilla::Nothing() */
mozilla::Nothing(),
/* nsIURI* aRedirectURI = nullptr */ redirectURI,
/* uint32_t aRedirectFlags = 0 */ nsIChannelEventSink::REDIRECT_INTERNAL |
nsIChannelEventSink::REDIRECT_STS_UPGRADE,
/* uint64_t aRedirectChannelId = 0 */ 106);
RefPtr<MockClassOfService> classOfService7 =
new MockClassOfService(nsIClassOfService::Tail);
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,
/* nsICacheInfoChannel::CacheDisposition aCacheDisposition */
nsICacheInfoChannel::kCacheUnresolved,
/* uint64_t aInnerWindowID */ 78,
/* bool aIsPrivateBrowsing */ true,
/* nsIClassOfService* aClassOfService */ classOfService7,
/* nsresult aRequestStatus */ NS_BINDING_REDIRECTED
/* const mozilla::net::TimingStruct* aTimings = nullptr */
/* mozilla::UniquePtr<mozilla::ProfileChunkedBuffer> aSource =
nullptr */
/* const mozilla::Maybe<mozilla::net::HttpVersion> aHttpVersion =
mozilla::Nothing() */
/* const mozilla::Maybe<uint32_t> aResponseStatus = mozilla::Nothing() */
/* const mozilla::Maybe<nsDependentCString>& aContentType =
mozilla::Nothing() */
/* nsIURI* aRedirectURI = nullptr */
/* uint64_t aRedirectChannelId = 0 */
);
// Test network marker with FetchPriority to verify priorityHeader
RefPtr<MockClassOfService> classOfService8 =
new MockClassOfService(nsIClassOfService::Leader,
/* incremental */ true,
nsIClassOfService::FETCHPRIORITY_HIGH);
profiler_add_network_marker(
/* nsIURI* aURI */ uri,
/* const nsACString& aRequestMethod */ "GET"_ns,
/* int32_t aPriority */ 34,
/* uint64_t aChannelId */ 8,
/* NetworkLoadType aType */ net::NetworkLoadType::LOAD_START,
/* mozilla::TimeStamp aStart */ ts1,
/* mozilla::TimeStamp aEnd */ ts2,
/* int64_t aCount */ 56,
/* nsICacheInfoChannel::CacheDisposition aCacheDisposition */
nsICacheInfoChannel::kCacheHit,
/* uint64_t aInnerWindowID */ 78,
/* bool aIsPrivateBrowsing */ false,
/* nsIClassOfService* aClassOfService */ classOfService8,
/* nsresult aRequestStatus */ NS_OK);
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_D,
S_M1,
S_M3,
S_Markers2DefaultEmptyOptions,
S_Markers2DefaultWithOptions,
S_Markers2ExplicitDefaultEmptyOptions,
S_Markers2ExplicitDefaultWithOptions,
S_FirstMarker,
S_CustomMarker,
S_SpecialMarker,
S_BaseMarkerTypeUniqueString,
S_SimplePayload_int,
S_SimplePayload_double,
S_SimplePayload_bool,
S_SimplePayload_string,
S_SimplePayload_multiple,
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_NetworkMarkerPayload_priorityHeader,
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 == "D") {
ASSERT_EQ(state, S_D);
state = State(state + 1);
EXPECT_TIMING_INTERVAL;
} else if (nameString == "M1") {
ASSERT_EQ(state, S_M1);
state = State(state + 1);
} else if (nameString == "M3") {
ASSERT_EQ(state, S_M3);
state = State(state + 1);
} else if (nameString ==
"default-templated markers 2.0 with empty options") {
EXPECT_EQ(state, S_Markers2DefaultEmptyOptions);
state = State(S_Markers2DefaultEmptyOptions + 1);
// TODO: Re-enable this when bug 1646714 lands, and check for stack.
#if 0
} else if (nameString ==
"default-templated markers 2.0 with option") {
EXPECT_EQ(state, S_Markers2DefaultWithOptions);
state = State(S_Markers2DefaultWithOptions + 1);
#endif
} else if (nameString ==
"explicitly-default-templated markers 2.0 with "
"empty "
"options") {
EXPECT_EQ(state, S_Markers2ExplicitDefaultEmptyOptions);
state = State(S_Markers2ExplicitDefaultEmptyOptions + 1);
} else if (nameString ==
"explicitly-default-templated markers 2.0 with "
"option") {
EXPECT_EQ(state, S_Markers2ExplicitDefaultWithOptions);
state = State(S_Markers2ExplicitDefaultWithOptions + 1);
}
} else {
// root.threads[0].markers.data[i] is an array with 6 elements,
// so there is a payload.
GET_JSON(payload, marker[PAYLOAD], Object);
// root.threads[0].markers.data[i][PAYLOAD] is an object
// (payload).
// It should at least have a "type" string.
GET_JSON(type, payload["type"], String);
std::string typeString = type.asString();
if (nameString == "tracing event") {
EXPECT_EQ(state, S_tracing_event);
state = State(S_tracing_event + 1);
EXPECT_EQ(typeString, "tracing");
EXPECT_TIMING_INSTANT;
EXPECT_EQ_JSON(payload["category"], String, "A");
EXPECT_TRUE(payload["stack"].isNull());
} else if (nameString == "tracing start") {
EXPECT_EQ(state, S_tracing_start);
state = State(S_tracing_start + 1);
EXPECT_EQ(typeString, "tracing");
EXPECT_TIMING_START;
EXPECT_EQ_JSON(payload["category"], String, "A");
EXPECT_TRUE(payload["stack"].isNull());
} else if (nameString == "tracing end") {
EXPECT_EQ(state, S_tracing_end);
state = State(S_tracing_end + 1);
EXPECT_EQ(typeString, "tracing");
EXPECT_TIMING_END;
EXPECT_EQ_JSON(payload["category"], String, "A");
EXPECT_TRUE(payload["stack"].isNull());
} else if (nameString == "tracing event with stack") {
EXPECT_EQ(state, S_tracing_event_with_stack);
state = State(S_tracing_event_with_stack + 1);
EXPECT_EQ(typeString, "tracing");
EXPECT_TIMING_INSTANT;
EXPECT_EQ_JSON(payload["category"], String, "B");
EXPECT_TRUE(payload["stack"].isObject());
} else if (nameString == "auto tracing") {
switch (state) {
case S_tracing_auto_tracing_start:
state = State(S_tracing_auto_tracing_start + 1);
EXPECT_EQ(typeString, "StackMarker");
EXPECT_TIMING_START;
EXPECT_TRUE(payload["stack"].isNull());
break;
case S_tracing_auto_tracing_end:
state = State(S_tracing_auto_tracing_end + 1);
EXPECT_EQ(typeString, "StackMarker");
EXPECT_TIMING_END;
ASSERT_TRUE(payload["stack"].isNull());
break;
default:
EXPECT_TRUE(state == S_tracing_auto_tracing_start ||
state == S_tracing_auto_tracing_end);
break;
}
} else if (nameString ==
"default-templated markers 2.0 with option") {
// TODO: Remove this when bug 1646714 lands.
EXPECT_EQ(state, S_Markers2DefaultWithOptions);
state = State(S_Markers2DefaultWithOptions + 1);
EXPECT_EQ(typeString, "NoPayloadUserData");
EXPECT_FALSE(payload["stack"].isNull());
} else if (nameString == "FirstMarker") {
// Record start and end times, to compare with timestamps in
// following markers.
EXPECT_EQ(state, S_FirstMarker);
ts1Double = marker[START_TIME].asDouble();
ts2Double = marker[END_TIME].asDouble();
state = State(S_FirstMarker + 1);
EXPECT_EQ(typeString, "Text");
EXPECT_EQ_JSON(payload["name"], String, "First Marker");
} else if (nameString == "Gtest custom marker") {
EXPECT_EQ(state, S_CustomMarker);
state = State(S_CustomMarker + 1);
EXPECT_EQ(typeString, "markers-gtest");
EXPECT_EQ(payload.size(), 1u + 10u);
EXPECT_TRUE(payload["null"].isNull());
EXPECT_EQ_JSON(payload["bool-false"], Bool, false);
EXPECT_EQ_JSON(payload["bool-true"], Bool, true);
EXPECT_EQ_JSON(payload["int"], Int64, 42);
EXPECT_EQ_JSON(payload["double"], Double, 43.0);
EXPECT_EQ_JSON(payload["text"], String, "gtest text");
// Unique strings can be fetched from the string table.
ASSERT_TRUE(payload["unique text"].isUInt());
auto textIndex = payload["unique text"].asUInt();
GET_JSON(uniqueText, stringTable[textIndex], String);
ASSERT_TRUE(uniqueText.isString());
ASSERT_EQ(uniqueText.asString(), "gtest unique text");
// The duplicate unique text should have the exact same index.
EXPECT_EQ_JSON(payload["unique text again"], UInt, textIndex);
EXPECT_EQ_JSON(payload["time"], Double, ts1Double);
} else if (nameString == "Gtest special marker") {
EXPECT_EQ(state, S_SpecialMarker);
state = State(S_SpecialMarker + 1);
EXPECT_EQ(typeString, "markers-gtest-special");
EXPECT_EQ(payload.size(), 1u) << "Only 'type' in the payload";
} else if (nameString ==
"Gtest base marker type unique string") {
EXPECT_EQ(state, S_BaseMarkerTypeUniqueString);
state = State(S_BaseMarkerTypeUniqueString + 1);
EXPECT_EQ(typeString, "markers-gtest-base-unique-string");
// uniqueField should be stored as a unique-string index.
ASSERT_TRUE(payload["uniqueField"].isUInt());
auto uniqueIndex = payload["uniqueField"].asUInt();
GET_JSON(uniqueValue, stringTable[uniqueIndex], String);
ASSERT_TRUE(uniqueValue.isString());
EXPECT_EQ(uniqueValue.asString(), "gtest unique field value");
// plainField should be stored as a regular string.
EXPECT_EQ_JSON(payload["plainField"], String,
"gtest plain field value");
} else if (nameString == "SimplePayload with int") {
EXPECT_EQ(state, S_SimplePayload_int);
state = State(S_SimplePayload_int + 1);
EXPECT_EQ(typeString, "SimplePayload with int");
EXPECT_TIMING_INSTANT;
EXPECT_EQ_JSON(payload["testInt"], Int64, 42);
} else if (nameString == "SimplePayload with double") {
EXPECT_EQ(state, S_SimplePayload_double);
state = State(S_SimplePayload_double + 1);
EXPECT_EQ(typeString, "SimplePayload with double");
EXPECT_TIMING_INSTANT;
EXPECT_EQ_JSON(payload["testDouble"], Double, 3.14);
} else if (nameString == "SimplePayload with bool") {
EXPECT_EQ(state, S_SimplePayload_bool);
state = State(S_SimplePayload_bool + 1);
EXPECT_EQ(typeString, "SimplePayload with bool");
EXPECT_TIMING_INSTANT;
EXPECT_EQ_JSON(payload["testBool"], Bool, true);
} else if (nameString == "SimplePayload with string") {
EXPECT_EQ(state, S_SimplePayload_string);
state = State(S_SimplePayload_string + 1);
EXPECT_EQ(typeString, "SimplePayload with string");
EXPECT_TIMING_INSTANT;
EXPECT_EQ_JSON(payload["testString"], String, "test_string");
} else if (nameString == "SimplePayload with multiple") {
EXPECT_EQ(state, S_SimplePayload_multiple);
state = State(S_SimplePayload_multiple + 1);
EXPECT_EQ(typeString, "SimplePayload with multiple");
EXPECT_TIMING_INSTANT;
EXPECT_EQ_JSON(payload["testInt"], Int64, 42);
EXPECT_EQ_JSON(payload["testDouble"], Double, 3.14);
EXPECT_EQ_JSON(payload["testBool"], Bool, true);
} else if (nameString == "Load 1: http://mozilla.org/") {
EXPECT_EQ(state, S_NetworkMarkerPayload_start);
state = State(S_NetworkMarkerPayload_start + 1);
EXPECT_EQ(typeString, "Network");
EXPECT_EQ_JSON(payload["startTime"], Double, ts1Double);
EXPECT_EQ_JSON(payload["endTime"], Double, ts2Double);
EXPECT_EQ_JSON(payload["id"], Int64, 1);
EXPECT_EQ_JSON(payload["URI"], String, "http://mozilla.org/");
EXPECT_EQ_JSON(payload["requestMethod"], String, "GET");
EXPECT_EQ_JSON(payload["pri"], Int64, 34);
EXPECT_EQ_JSON(payload["count"], Int64, 56);
EXPECT_EQ_JSON(payload["cache"], String, "Hit");
EXPECT_TRUE(payload["isPrivateBrowsing"].isNull());
EXPECT_EQ_JSON(payload["classOfService"], String, "Leader");
EXPECT_EQ_JSON(payload["requestStatus"], String, "NS_OK");
EXPECT_TRUE(payload["RedirectURI"].isNull());
EXPECT_TRUE(payload["redirectType"].isNull());
EXPECT_TRUE(payload["isHttpToHttpsRedirect"].isNull());
EXPECT_TRUE(payload["redirectId"].isNull());
EXPECT_TRUE(payload["contentType"].isNull());
} else if (nameString == "Load 2: http://mozilla.org/") {
EXPECT_EQ(state, S_NetworkMarkerPayload_stop);
state = State(S_NetworkMarkerPayload_stop + 1);
EXPECT_EQ(typeString, "Network");
EXPECT_EQ_JSON(payload["startTime"], Double, ts1Double);
EXPECT_EQ_JSON(payload["endTime"], Double, ts2Double);
EXPECT_EQ_JSON(payload["id"], Int64, 2);
EXPECT_EQ_JSON(payload["URI"], String, "http://mozilla.org/");
EXPECT_EQ_JSON(payload["requestMethod"], String, "GET");
EXPECT_EQ_JSON(payload["pri"], Int64, 34);
EXPECT_EQ_JSON(payload["count"], Int64, 56);
EXPECT_EQ_JSON(payload["cache"], String, "Unresolved");
EXPECT_TRUE(payload["isPrivateBrowsing"].isNull());
EXPECT_EQ_JSON(payload["httpVersion"], String, "h3");
EXPECT_EQ_JSON(payload["classOfService"], String, "Follower");
EXPECT_EQ_JSON(payload["requestStatus"], String,
"NS_BINDING_ABORTED");
EXPECT_TRUE(payload["RedirectURI"].isNull());
EXPECT_TRUE(payload["redirectType"].isNull());
EXPECT_TRUE(payload["isHttpToHttpsRedirect"].isNull());
EXPECT_TRUE(payload["redirectId"].isNull());
EXPECT_EQ_JSON(payload["responseStatus"], Int64, 200);
EXPECT_EQ_JSON(payload["contentType"], String, "text/html");
} else if (nameString == "Load 3: http://mozilla.org/") {
EXPECT_EQ(state, S_NetworkMarkerPayload_redirect_temporary);
state = State(S_NetworkMarkerPayload_redirect_temporary + 1);
EXPECT_EQ(typeString, "Network");
EXPECT_EQ_JSON(payload["startTime"], Double, ts1Double);
EXPECT_EQ_JSON(payload["endTime"], Double, ts2Double);
EXPECT_EQ_JSON(payload["id"], Int64, 3);
EXPECT_EQ_JSON(payload["URI"], String, "http://mozilla.org/");
EXPECT_EQ_JSON(payload["requestMethod"], String, "GET");
EXPECT_EQ_JSON(payload["pri"], Int64, 34);
EXPECT_EQ_JSON(payload["count"], Int64, 56);
EXPECT_EQ_JSON(payload["cache"], String, "Unresolved");
EXPECT_TRUE(payload["isPrivateBrowsing"].isNull());
EXPECT_EQ_JSON(payload["classOfService"], String,
"Speculative");
EXPECT_EQ_JSON(payload["requestStatus"], String,
"NS_ERROR_UNEXPECTED");
EXPECT_EQ_JSON(payload["RedirectURI"], String,
"http://example.com/");
EXPECT_EQ_JSON(payload["redirectType"], String, "Temporary");
EXPECT_EQ_JSON(payload["isHttpToHttpsRedirect"], Bool, false);
EXPECT_EQ_JSON(payload["redirectId"], Int64, 103);
EXPECT_TRUE(payload["contentType"].isNull());
} else if (nameString == "Load 4: http://mozilla.org/") {
EXPECT_EQ(state, S_NetworkMarkerPayload_redirect_permanent);
state = State(S_NetworkMarkerPayload_redirect_permanent + 1);
EXPECT_EQ(typeString, "Network");
EXPECT_EQ_JSON(payload["startTime"], Double, ts1Double);
EXPECT_EQ_JSON(payload["endTime"], Double, ts2Double);
EXPECT_EQ_JSON(payload["id"], Int64, 4);
EXPECT_EQ_JSON(payload["URI"], String, "http://mozilla.org/");
EXPECT_EQ_JSON(payload["requestMethod"], String, "GET");
EXPECT_EQ_JSON(payload["pri"], Int64, 34);
EXPECT_EQ_JSON(payload["count"], Int64, 56);
EXPECT_EQ_JSON(payload["cache"], String, "Unresolved");
EXPECT_TRUE(payload["isPrivateBrowsing"].isNull());
EXPECT_EQ_JSON(payload["classOfService"], String,
"Background");
EXPECT_EQ_JSON(payload["requestStatus"], String,
"NS_ERROR_DOCSHELL_DYING");
EXPECT_EQ_JSON(payload["RedirectURI"], String,
"http://example.com/");
EXPECT_EQ_JSON(payload["redirectType"], String, "Permanent");
EXPECT_EQ_JSON(payload["isHttpToHttpsRedirect"], Bool, false);
EXPECT_EQ_JSON(payload["redirectId"], Int64, 104);
EXPECT_TRUE(payload["contentType"].isNull());
} else if (nameString == "Load 5: http://mozilla.org/") {
EXPECT_EQ(state, S_NetworkMarkerPayload_redirect_internal);
state = State(S_NetworkMarkerPayload_redirect_internal + 1);
EXPECT_EQ(typeString, "Network");
EXPECT_EQ_JSON(payload["startTime"], Double, ts1Double);
EXPECT_EQ_JSON(payload["endTime"], Double, ts2Double);
EXPECT_EQ_JSON(payload["id"], Int64, 5);
EXPECT_EQ_JSON(payload["URI"], String, "http://mozilla.org/");
EXPECT_EQ_JSON(payload["requestMethod"], String, "GET");
EXPECT_EQ_JSON(payload["pri"], Int64, 34);
EXPECT_EQ_JSON(payload["count"], Int64, 56);
EXPECT_EQ_JSON(payload["cache"], String, "Unresolved");
EXPECT_TRUE(payload["isPrivateBrowsing"].isNull());
EXPECT_EQ_JSON(payload["classOfService"], String,
"Unblocked | TailForbidden");
EXPECT_EQ_JSON(payload["requestStatus"], String,
"NS_ERROR_DOM_CORP_FAILED");
EXPECT_EQ_JSON(payload["RedirectURI"], String,
"http://example.com/");
EXPECT_EQ_JSON(payload["redirectType"], String, "Internal");
EXPECT_EQ_JSON(payload["isHttpToHttpsRedirect"], Bool, false);
EXPECT_EQ_JSON(payload["redirectId"], Int64, 105);
EXPECT_TRUE(payload["contentType"].isNull());
} else if (nameString == "Load 6: http://mozilla.org/") {
EXPECT_EQ(state,
S_NetworkMarkerPayload_redirect_internal_sts);
state =
State(S_NetworkMarkerPayload_redirect_internal_sts + 1);
EXPECT_EQ(typeString, "Network");
EXPECT_EQ_JSON(payload["startTime"], Double, ts1Double);
EXPECT_EQ_JSON(payload["endTime"], Double, ts2Double);
EXPECT_EQ_JSON(payload["id"], Int64, 6);
EXPECT_EQ_JSON(payload["URI"], String, "http://mozilla.org/");
EXPECT_EQ_JSON(payload["requestMethod"], String, "GET");
EXPECT_EQ_JSON(payload["pri"], Int64, 34);
EXPECT_EQ_JSON(payload["count"], Int64, 56);
EXPECT_EQ_JSON(payload["cache"], String, "Unresolved");
EXPECT_TRUE(payload["isPrivateBrowsing"].isNull());
EXPECT_EQ_JSON(payload["classOfService"], String,
"Unblocked | Throttleable | TailForbidden");
EXPECT_EQ_JSON(payload["requestStatus"], String,
"NS_ERROR_BLOCKED_BY_POLICY");
EXPECT_EQ_JSON(payload["RedirectURI"], String,
"http://example.com/");
EXPECT_EQ_JSON(payload["redirectType"], String, "Internal");
EXPECT_EQ_JSON(payload["isHttpToHttpsRedirect"], Bool, true);
EXPECT_EQ_JSON(payload["redirectId"], Int64, 106);
EXPECT_TRUE(payload["contentType"].isNull());
} else if (nameString == "Load 7: http://mozilla.org/") {
EXPECT_EQ(state, S_NetworkMarkerPayload_private_browsing);
state = State(S_NetworkMarkerPayload_private_browsing + 1);
EXPECT_EQ(typeString, "Network");
EXPECT_EQ_JSON(payload["startTime"], Double, ts1Double);
EXPECT_EQ_JSON(payload["endTime"], Double, ts2Double);
EXPECT_EQ_JSON(payload["id"], Int64, 7);
EXPECT_EQ_JSON(payload["URI"], String, "http://mozilla.org/");
EXPECT_EQ_JSON(payload["requestMethod"], String, "GET");
EXPECT_EQ_JSON(payload["pri"], Int64, 34);
EXPECT_EQ_JSON(payload["count"], Int64, 56);
EXPECT_EQ_JSON(payload["cache"], String, "Unresolved");
EXPECT_EQ_JSON(payload["isPrivateBrowsing"], Bool, true);
EXPECT_EQ_JSON(payload["classOfService"], String, "Tail");
EXPECT_EQ_JSON(payload["requestStatus"], String,
"NS_BINDING_REDIRECTED");
EXPECT_TRUE(payload["RedirectURI"].isNull());
EXPECT_TRUE(payload["redirectType"].isNull());
EXPECT_TRUE(payload["isHttpToHttpsRedirect"].isNull());
EXPECT_TRUE(payload["redirectId"].isNull());
EXPECT_TRUE(payload["contentType"].isNull());
} else if (nameString == "Load 8: http://mozilla.org/") {
EXPECT_EQ(state, S_NetworkMarkerPayload_priorityHeader);
state = State(S_NetworkMarkerPayload_priorityHeader + 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, 8);
EXPECT_EQ_JSON(payload["URI"], String, "http://mozilla.org/");
EXPECT_EQ_JSON(payload["requestMethod"], String, "GET");
EXPECT_EQ_JSON(payload["pri"], Int64, 34);
EXPECT_EQ_JSON(payload["count"], Int64, 56);
EXPECT_EQ_JSON(payload["cache"], String, "Hit");
EXPECT_TRUE(payload["isPrivateBrowsing"].isNull());
EXPECT_EQ_JSON(payload["classOfService"], String, "Leader");
EXPECT_EQ_JSON(payload["requestStatus"], String, "NS_OK");
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_FALSE(payload["priorityHeader"].isNull());
EXPECT_EQ_JSON(payload["priorityHeader"], String, "u=4, i");
} else if (nameString == "Text in main thread with stack") {
EXPECT_EQ(state, S_TextWithStack);
state = State(S_TextWithStack + 1);
EXPECT_EQ(typeString, "Text");
EXPECT_FALSE(payload["stack"].isNull());
EXPECT_TIMING_INTERVAL_AT(ts1Double, ts2Double);
EXPECT_EQ_JSON(payload["name"], String, "");
} else if (nameString == "Text from main thread with stack") {
EXPECT_EQ(state, S_TextToMTWithStack);
state = State(S_TextToMTWithStack + 1);
EXPECT_EQ(typeString, "Text");
EXPECT_FALSE(payload["stack"].isNull());
EXPECT_EQ_JSON(payload["name"], String, "");
} else if (nameString ==
"Text in registered thread with stack") {
ADD_FAILURE()
<< "Unexpected 'Text in registered thread with stack'";
} else if (nameString ==
"Text from registered thread with stack") {
EXPECT_EQ(state, S_RegThread_TextToMTWithStack);
state = State(S_RegThread_TextToMTWithStack + 1);
EXPECT_EQ(typeString, "Text");
EXPECT_FALSE(payload["stack"].isNull());
EXPECT_EQ_JSON(payload["name"], String, "");
} else if (nameString ==
"Text in unregistered thread with stack") {
ADD_FAILURE()
<< "Unexpected 'Text in unregistered thread with stack'";
} else if (nameString ==
"Text from unregistered thread with stack") {
EXPECT_EQ(state, S_UnregThread_TextToMTWithStack);
state = State(S_UnregThread_TextToMTWithStack + 1);
EXPECT_EQ(typeString, "Text");
EXPECT_TRUE(payload["stack"].isNull());
EXPECT_EQ_JSON(payload["name"], String, "");
}
} // marker with payload
} // for (marker : data)
} // markers.data
} // markers
} // thread0
} // threads
// We should have read all expected markers.
EXPECT_EQ(state, S_LAST);
{
GET_JSON(meta, root["meta"], Object);
{
GET_JSON(markerSchema, meta["markerSchema"], Array);
std::set<std::string> testedSchemaNames;
for (const Json::Value& schema : markerSchema) {
GET_JSON(name, schema["name"], String);
const std::string nameString = name.asString();
GET_JSON(display, schema["display"], Array);
GET_JSON(data, schema["data"], Array);
EXPECT_TRUE(
testedSchemaNames
.insert(std::string(nameString.data(), nameString.size()))
.second)
<< "Each schema name should be unique (inserted once in the set)";
if (nameString == "Text") {
EXPECT_EQ(display.size(), 2u);
EXPECT_EQ(display[0u].asString(), "marker-chart");
EXPECT_EQ(display[1u].asString(), "marker-table");
ASSERT_EQ(data.size(), 1u);
ASSERT_TRUE(data[0u].isObject());
EXPECT_EQ_JSON(data[0u]["key"], String, "name");
EXPECT_EQ_JSON(data[0u]["label"], String, "Details");
EXPECT_EQ_JSON(data[0u]["format"], String, "string");
} else if (nameString == "NoPayloadUserData") {
// TODO: Remove this when bug 1646714 lands.
EXPECT_EQ(display.size(), 2u);
EXPECT_EQ(display[0u].asString(), "marker-chart");
EXPECT_EQ(display[1u].asString(), "marker-table");
ASSERT_EQ(data.size(), 0u);
} else if (nameString == "FileIO") {
// These are defined in ProfilerIOInterposeObserver.cpp
} else if (nameString == "tracing") {
EXPECT_EQ(display.size(), 3u);
EXPECT_EQ(display[0u].asString(), "marker-chart");
EXPECT_EQ(display[1u].asString(), "marker-table");
EXPECT_EQ(display[2u].asString(), "timeline-overview");
ASSERT_EQ(data.size(), 1u);
ASSERT_TRUE(data[0u].isObject());
EXPECT_EQ_JSON(data[0u]["key"], String, "category");
EXPECT_EQ_JSON(data[0u]["label"], String, "Type");
EXPECT_EQ_JSON(data[0u]["format"], String, "string");
} else if (nameString == "BHR-detected hang") {
EXPECT_EQ(display.size(), 2u);
EXPECT_EQ(display[0u].asString(), "marker-chart");
EXPECT_EQ(display[1u].asString(), "marker-table");
ASSERT_EQ(data.size(), 0u);
} else if (nameString == "MainThreadLongTask") {
EXPECT_EQ(display.size(), 2u);
EXPECT_EQ(display[0u].asString(), "marker-chart");
EXPECT_EQ(display[1u].asString(), "marker-table");
ASSERT_EQ(data.size(), 1u);
ASSERT_TRUE(data[0u].isObject());
EXPECT_EQ_JSON(data[0u]["key"], String, "category");
EXPECT_EQ_JSON(data[0u]["label"], String, "Type");
EXPECT_EQ_JSON(data[0u]["format"], String, "string");
} else if (nameString == "Log") {
EXPECT_EQ(display.size(), 1u);
EXPECT_EQ(display[0u].asString(), "marker-table");
ASSERT_EQ(data.size(), 2u);
ASSERT_TRUE(data[0u].isObject());
EXPECT_EQ_JSON(data[0u]["key"], String, "module");
EXPECT_EQ_JSON(data[0u]["label"], String, "Module");
EXPECT_EQ_JSON(data[0u]["format"], String, "string");
ASSERT_TRUE(data[1u].isObject());
EXPECT_EQ_JSON(data[1u]["key"], String, "name");
EXPECT_EQ_JSON(data[1u]["label"], String, "Name");
EXPECT_EQ_JSON(data[1u]["format"], String, "string");
} else if (nameString == "MediaSample") {
EXPECT_EQ(display.size(), 2u);
EXPECT_EQ(display[0u].asString(), "marker-chart");
EXPECT_EQ(display[1u].asString(), "marker-table");
ASSERT_EQ(data.size(), 3u);
ASSERT_TRUE(data[0u].isObject());
EXPECT_EQ_JSON(data[0u]["key"], String, "sampleStartTimeUs");
EXPECT_EQ_JSON(data[0u]["label"], String, "Sample start time");
EXPECT_EQ_JSON(data[0u]["format"], String, "microseconds");
ASSERT_TRUE(data[1u].isObject());
EXPECT_EQ_JSON(data[1u]["key"], String, "sampleEndTimeUs");
EXPECT_EQ_JSON(data[1u]["label"], String, "Sample end time");
EXPECT_EQ_JSON(data[1u]["format"], String, "microseconds");
ASSERT_TRUE(data[2u].isObject());
EXPECT_EQ_JSON(data[2u]["key"], String, "queueLength");
EXPECT_EQ_JSON(data[2u]["label"], String, "Queue length");
EXPECT_EQ_JSON(data[2u]["format"], String, "integer");
} else if (nameString == "VideoFallingBehind") {
EXPECT_EQ(display.size(), 2u);
EXPECT_EQ(display[0u].asString(), "marker-chart");
EXPECT_EQ(display[1u].asString(), "marker-table");
ASSERT_EQ(data.size(), 2u);
ASSERT_TRUE(data[0u].isObject());
EXPECT_EQ_JSON(data[0u]["key"], String, "videoFrameStartTimeUs");
EXPECT_EQ_JSON(data[0u]["label"], String, "Video frame start time");
EXPECT_EQ_JSON(data[0u]["format"], String, "microseconds");
ASSERT_TRUE(data[1u].isObject());
EXPECT_EQ_JSON(data[1u]["key"], String, "mediaCurrentTimeUs");
EXPECT_EQ_JSON(data[1u]["label"], String, "Media current time");
EXPECT_EQ_JSON(data[1u]["format"], String, "microseconds");
} else if (nameString == "Budget") {
EXPECT_EQ(display.size(), 2u);
EXPECT_EQ(display[0u].asString(), "marker-chart");
EXPECT_EQ(display[1u].asString(), "marker-table");
ASSERT_EQ(data.size(), 0u);
} else if (nameString == "markers-gtest") {
EXPECT_EQ(display.size(), 7u);
EXPECT_EQ(display[0u].asString(), "marker-chart");
EXPECT_EQ(display[1u].asString(), "marker-table");
EXPECT_EQ(display[2u].asString(), "timeline-overview");
EXPECT_EQ(display[3u].asString(), "timeline-memory");
EXPECT_EQ(display[4u].asString(), "timeline-ipc");
EXPECT_EQ(display[5u].asString(), "timeline-fileio");
EXPECT_EQ(display[6u].asString(), "stack-chart");
EXPECT_EQ_JSON(schema["chartLabel"], String, "chart label");
EXPECT_EQ_JSON(schema["tooltipLabel"], String, "tooltip label");
EXPECT_EQ_JSON(schema["tableLabel"], String, "table label");
EXPECT_EQ_JSON(schema["colorField"], String, "color");
ASSERT_EQ(data.size(), 19u);
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");
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");
ASSERT_TRUE(data[2u].isObject());
EXPECT_EQ_JSON(data[2u]["key"], String, "key with string");
EXPECT_TRUE(data[2u]["label"].isNull());
EXPECT_EQ_JSON(data[2u]["format"], String, "string");
ASSERT_TRUE(data[3u].isObject());
EXPECT_EQ_JSON(data[3u]["key"], String, "key with label duration");
EXPECT_TRUE(data[3u]["label duration"].isNull());
EXPECT_EQ_JSON(data[3u]["format"], String, "duration");
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");
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");
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");
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");
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");
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");
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");
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");
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");
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");
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");
ASSERT_TRUE(data[16u].isObject());
EXPECT_EQ_JSON(data[16u]["key"], String, "key with label hidden");
EXPECT_EQ_JSON(data[16u]["label"], String, "label");
EXPECT_EQ_JSON(data[16u]["format"], String, "string");
EXPECT_EQ_JSON(data[16u]["hidden"], Bool, true);
ASSERT_TRUE(data[17u].isObject());
EXPECT_EQ_JSON(data[17u]["key"], String, "key hidden");
EXPECT_TRUE(data[17u]["label"].isNull());
EXPECT_EQ_JSON(data[17u]["format"], String, "string");
EXPECT_EQ_JSON(data[17u]["hidden"], Bool, true);
ASSERT_TRUE(data[18u].isObject());
EXPECT_EQ_JSON(data[18u]["key"], String, "color");
EXPECT_TRUE(data[18u]["label"].isNull());
EXPECT_EQ_JSON(data[18u]["format"], String, "string");
EXPECT_EQ_JSON(data[18u]["hidden"], Bool, true);
} else if (nameString == "markers-gtest-base-unique-string") {
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, "uniqueField");
EXPECT_EQ_JSON(data[0u]["label"], String, "Unique Field");
EXPECT_EQ_JSON(data[0u]["format"], String, "unique-string");
ASSERT_TRUE(data[1u].isObject());
EXPECT_EQ_JSON(data[1u]["key"], String, "plainField");
EXPECT_EQ_JSON(data[1u]["label"], String, "Plain Field");
EXPECT_EQ_JSON(data[1u]["format"], String, "string");
} else if (nameString == "markers-gtest-special") {
EXPECT_EQ(display.size(), 0u);
ASSERT_EQ(data.size(), 0u);
} else if (nameString == "markers-gtest-unused") {
ADD_FAILURE() << "Schema for GtestUnusedMarker should not be here";
} else {
printf("FYI: Unknown marker schema '%s'\n", nameString.c_str());
}
}
// Check that we've got all expected schema.
EXPECT_TRUE(testedSchemaNames.find("Text") != testedSchemaNames.end());
EXPECT_TRUE(testedSchemaNames.find("tracing") !=
testedSchemaNames.end());
EXPECT_TRUE(testedSchemaNames.find("MediaSample") !=
testedSchemaNames.end());
} // markerSchema
} // meta
});
Maybe<ProfilerBufferInfo> info = profiler_get_buffer_info();
EXPECT_TRUE(info.isSome());
printf("Profiler buffer range: %llu .. %llu (%llu bytes)\n",
static_cast<unsigned long long>(info->mRangeStart),
static_cast<unsigned long long>(info->mRangeEnd),
// sizeof(ProfileBufferEntry) == 9
(static_cast<unsigned long long>(info->mRangeEnd) -
static_cast<unsigned long long>(info->mRangeStart)) *
9);
printf("Stats: min(us) .. mean(us) .. max(us) [count]\n");
printf("- Intervals: %7.1f .. %7.1f .. %7.1f [%u]\n",
info->mIntervalsUs.min, info->mIntervalsUs.sum / info->mIntervalsUs.n,
info->mIntervalsUs.max, info->mIntervalsUs.n);
printf("- Overheads: %7.1f .. %7.1f .. %7.1f [%u]\n",
info->mOverheadsUs.min, info->mOverheadsUs.sum / info->mOverheadsUs.n,
info->mOverheadsUs.max, info->mOverheadsUs.n);
printf(" - Locking: %7.1f .. %7.1f .. %7.1f [%u]\n",
info->mLockingsUs.min, info->mLockingsUs.sum / info->mLockingsUs.n,
info->mLockingsUs.max, info->mLockingsUs.n);
printf(" - Clearning: %7.1f .. %7.1f .. %7.1f [%u]\n",
info->mCleaningsUs.min, info->mCleaningsUs.sum / info->mCleaningsUs.n,
info->mCleaningsUs.max, info->mCleaningsUs.n);
printf(" - Counters: %7.1f .. %7.1f .. %7.1f [%u]\n",
info->mCountersUs.min, info->mCountersUs.sum / info->mCountersUs.n,
info->mCountersUs.max, info->mCountersUs.n);
printf(" - Threads: %7.1f .. %7.1f .. %7.1f [%u]\n",
info->mThreadsUs.min, info->mThreadsUs.sum / info->mThreadsUs.n,
info->mThreadsUs.max, info->mThreadsUs.n);
profiler_stop();
// Try to add markers while the profiler is stopped.
PROFILER_MARKER_UNTYPED("marker after profiler_stop", OTHER);
// Warning: this could be racy
profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL, features,
filters, std::size(filters), 0);
// This last marker shouldn't get streamed.
SpliceableChunkedJSONWriter w2{FailureLatchInfallibleSource::Singleton()};
w2.Start();
EXPECT_TRUE(::profiler_stream_json_for_this_process(w2).isOk());
w2.End();
EXPECT_FALSE(w2.Failed());
UniquePtr<char[]> profile2 = w2.ChunkedWriteFunc().CopyData();
ASSERT_TRUE(!!profile2.get());
EXPECT_TRUE(
std::string_view(profile2.get()).find("marker after profiler_stop") ==
std::string_view::npos);
profiler_stop();
}
#define COUNTER_NAME "TestCounter"
#define COUNTER_DESCRIPTION "Test of counters in profiles"
#define COUNTER_NAME2 "Counter2"
#define COUNTER_DESCRIPTION2 "Second Test of counters in profiles"
PROFILER_DEFINE_COUNT_TOTAL(TestCounter, COUNTER_NAME, COUNTER_DESCRIPTION);
PROFILER_DEFINE_COUNT_TOTAL(TestCounter2, COUNTER_NAME2, COUNTER_DESCRIPTION2);
TEST(GeckoProfiler, Counters)
{
uint32_t features = 0;
const char* filters[] = {"GeckoMain"};
// We will record some counter values, and check that they're present (and no
// other) when expected.
struct NumberAndCount {
uint64_t mNumber;
int64_t mCount;
};
int64_t testCounters[] = {10, 7, -17};
NumberAndCount expectedTestCounters[] = {{1u, 10}, {0u, 0}, {1u, 7},
{0u, 0}, {0u, 0}, {1u, -17},
{0u, 0}, {0u, 0}};
constexpr size_t expectedTestCountersCount = std::size(expectedTestCounters);
bool expectCounter2 = false;
int64_t testCounters2[] = {10};
NumberAndCount expectedTestCounters2[] = {{1u, 10}, {0u, 0}};
constexpr size_t expectedTestCounters2Count =
std::size(expectedTestCounters2);
auto checkCountersInJSON = [&](const Json::Value& aRoot) {
size_t nextExpectedTestCounter = 0u;
size_t nextExpectedTestCounter2 = 0u;
GET_JSON(counters, aRoot["counters"], Array);
for (const Json::Value& counter : counters) {
ASSERT_TRUE(counter.isObject());
GET_JSON_VALUE(name, counter["name"], String);
if (name == "TestCounter") {
EXPECT_EQ_JSON(counter["category"], String, COUNTER_NAME);
EXPECT_EQ_JSON(counter["description"], String, COUNTER_DESCRIPTION);
GET_JSON(samples, counter["samples"], Object);
GET_JSON(samplesSchema, samples["schema"], Object);
EXPECT_GE(samplesSchema.size(), 3u);
GET_JSON_VALUE(samplesNumber, samplesSchema["number"], UInt);
GET_JSON_VALUE(samplesCount, samplesSchema["count"], UInt);
GET_JSON(samplesData, samples["data"], Array);
for (const Json::Value& sample : samplesData) {
ASSERT_TRUE(sample.isArray());
ASSERT_LT(nextExpectedTestCounter, expectedTestCountersCount);
EXPECT_EQ_JSON(sample[samplesNumber], UInt64,
expectedTestCounters[nextExpectedTestCounter].mNumber);
EXPECT_EQ_JSON(sample[samplesCount], Int64,
expectedTestCounters[nextExpectedTestCounter].mCount);
++nextExpectedTestCounter;
}
} else if (name == "TestCounter2") {
EXPECT_TRUE(expectCounter2);
EXPECT_EQ_JSON(counter["category"], String, COUNTER_NAME2);
EXPECT_EQ_JSON(counter["description"], String, COUNTER_DESCRIPTION2);
GET_JSON(samples, counter["samples"], Object);
GET_JSON(samplesSchema, samples["schema"], Object);
EXPECT_GE(samplesSchema.size(), 3u);
GET_JSON_VALUE(samplesNumber, samplesSchema["number"], UInt);
GET_JSON_VALUE(samplesCount, samplesSchema["count"], UInt);
GET_JSON(samplesData, samples["data"], Array);
for (const Json::Value& sample : samplesData) {
ASSERT_TRUE(sample.isArray());
ASSERT_LT(nextExpectedTestCounter2, expectedTestCounters2Count);
EXPECT_EQ_JSON(
sample[samplesNumber], UInt64,
expectedTestCounters2[nextExpectedTestCounter2].mNumber);
EXPECT_EQ_JSON(
sample[samplesCount], Int64,
expectedTestCounters2[nextExpectedTestCounter2].mCount);
++nextExpectedTestCounter2;
}
}
}
EXPECT_EQ(nextExpectedTestCounter, expectedTestCountersCount);
if (expectCounter2) {
EXPECT_EQ(nextExpectedTestCounter2, expectedTestCounters2Count);
}
};
// Inactive -> Active
profiler_ensure_started(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL,
features, filters, std::size(filters), 0);
// Output all "TestCounter"s, with increasing delays (to test different
// number of counter samplings).
int samplingWaits = 2;
for (int64_t counter : testCounters) {
AUTO_PROFILER_COUNT_TOTAL(TestCounter, counter);
for (int i = 0; i < samplingWaits; ++i) {
ASSERT_EQ(WaitForSamplingState(), SamplingState::SamplingCompleted);
}
++samplingWaits;
}
// Verify we got "TestCounter" in the output, but not "TestCounter2" yet.
UniquePtr<char[]> profile = profiler_get_profile();
JSONOutputCheck(profile.get(), checkCountersInJSON);
// Now introduce TestCounter2.
expectCounter2 = true;
for (int64_t counter2 : testCounters2) {
AUTO_PROFILER_COUNT_TOTAL(TestCounter2, counter2);
ASSERT_EQ(WaitForSamplingState(), SamplingState::SamplingCompleted);
ASSERT_EQ(WaitForSamplingState(), SamplingState::SamplingCompleted);
}
// Verify we got both "TestCounter" and "TestCounter2" in the output.
profile = profiler_get_profile();
JSONOutputCheck(profile.get(), checkCountersInJSON);
profiler_stop();
}
TEST(GeckoProfiler, Time)
{
uint32_t features = ProfilerFeature::StackWalk;
const char* filters[] = {"GeckoMain"};
double t1 = profiler_time();
double t2 = profiler_time();
ASSERT_TRUE(t1 <= t2);
// profiler_start() restarts the timer used by profiler_time().
profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL, features,
filters, std::size(filters), 0);
double t3 = profiler_time();
double t4 = profiler_time();
ASSERT_TRUE(t3 <= t4);
profiler_stop();
double t5 = profiler_time();
double t6 = profiler_time();
ASSERT_TRUE(t4 <= t5 && t1 <= t6);
}
TEST(GeckoProfiler, GetProfile)
{
uint32_t features = ProfilerFeature::StackWalk;
const char* filters[] = {"GeckoMain"};
ASSERT_TRUE(!profiler_get_profile());
profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL, features,
filters, std::size(filters), 0);
mozilla::Maybe<uint32_t> activeFeatures = profiler_features_if_active();
ASSERT_TRUE(activeFeatures.isSome());
// Not all platforms support stack-walking.
const bool hasStackWalk = ProfilerFeature::HasStackWalk(*activeFeatures);
UniquePtr<char[]> profile = profiler_get_profile();
JSONOutputCheck(profile.get(), [&](const Json::Value& aRoot) {
GET_JSON(meta, aRoot["meta"], Object);
{
GET_JSON(configuration, meta["configuration"], Object);
{
GET_JSON(features, configuration["features"], Array);
{
EXPECT_EQ(features.size(), (hasStackWalk ? 1u : 0u));
if (hasStackWalk) {
EXPECT_JSON_ARRAY_CONTAINS(features, String, "stackwalk");
}
}
GET_JSON(threads, configuration["threads"], Array);
{
EXPECT_EQ(threads.size(), 1u);
EXPECT_JSON_ARRAY_CONTAINS(threads, String, "GeckoMain");
}
}
}
});
profiler_stop();
ASSERT_TRUE(!profiler_get_profile());
}
TEST(GeckoProfiler, StreamJSONForThisProcess)
{
uint32_t features = ProfilerFeature::StackWalk;
const char* filters[] = {"GeckoMain"};
SpliceableChunkedJSONWriter w{FailureLatchInfallibleSource::Singleton()};
MOZ_RELEASE_ASSERT(!w.ChunkedWriteFunc().Fallible());
MOZ_RELEASE_ASSERT(!w.ChunkedWriteFunc().Failed());
MOZ_RELEASE_ASSERT(!w.ChunkedWriteFunc().GetFailure());
MOZ_RELEASE_ASSERT(&w.ChunkedWriteFunc().SourceFailureLatch() ==
&mozilla::FailureLatchInfallibleSource::Singleton());
MOZ_RELEASE_ASSERT(
&std::as_const(w.ChunkedWriteFunc()).SourceFailureLatch() ==
&mozilla::FailureLatchInfallibleSource::Singleton());
MOZ_RELEASE_ASSERT(!w.Fallible());
MOZ_RELEASE_ASSERT(!w.Failed());
MOZ_RELEASE_ASSERT(!w.GetFailure());
MOZ_RELEASE_ASSERT(&w.SourceFailureLatch() ==
&mozilla::FailureLatchInfallibleSource::Singleton());
MOZ_RELEASE_ASSERT(&std::as_const(w).SourceFailureLatch() ==
&mozilla::FailureLatchInfallibleSource::Singleton());
ASSERT_TRUE(::profiler_stream_json_for_this_process(w).isErr());
MOZ_RELEASE_ASSERT(!w.ChunkedWriteFunc().Failed());
MOZ_RELEASE_ASSERT(!w.ChunkedWriteFunc().GetFailure());
MOZ_RELEASE_ASSERT(!w.Failed());
MOZ_RELEASE_ASSERT(!w.GetFailure());
profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL, features,
filters, std::size(filters), 0);
w.Start();
ASSERT_TRUE(::profiler_stream_json_for_this_process(w).isOk());
w.End();
MOZ_RELEASE_ASSERT(!w.ChunkedWriteFunc().Failed());
MOZ_RELEASE_ASSERT(!w.ChunkedWriteFunc().GetFailure());
MOZ_RELEASE_ASSERT(!w.Failed());
MOZ_RELEASE_ASSERT(!w.GetFailure());
UniquePtr<char[]> profile = w.ChunkedWriteFunc().CopyData();
JSONOutputCheck(profile.get(), [](const Json::Value&) {});
profiler_stop();
ASSERT_TRUE(::profiler_stream_json_for_this_process(w).isErr());
}
// Internal version of profiler_stream_json_for_this_process, which allows being
// called from a non-main thread of the parent process, at the risk of getting
// an incomplete profile.
ProfilerResult<ProfileGenerationAdditionalInformation>
do_profiler_stream_json_for_this_process(
SpliceableJSONWriter& aWriter, double aSinceTime, bool aIsShuttingDown,
ProfilerCodeAddressService* aService,
mozilla::ProgressLogger aProgressLogger);
TEST(GeckoProfiler, StreamJSONForThisProcessThreaded)
{
// Same as the previous test, but calling some things on background threads.
nsCOMPtr<nsIThread> thread;
nsresult rv = NS_NewNamedThread("GeckoProfGTest", getter_AddRefs(thread));
ASSERT_NS_SUCCEEDED(rv);
uint32_t features = ProfilerFeature::StackWalk;
const char* filters[] = {"GeckoMain"};
SpliceableChunkedJSONWriter w{FailureLatchInfallibleSource::Singleton()};
MOZ_RELEASE_ASSERT(!w.ChunkedWriteFunc().Fallible());
MOZ_RELEASE_ASSERT(!w.ChunkedWriteFunc().Failed());
MOZ_RELEASE_ASSERT(!w.ChunkedWriteFunc().GetFailure());
MOZ_RELEASE_ASSERT(&w.ChunkedWriteFunc().SourceFailureLatch() ==
&mozilla::FailureLatchInfallibleSource::Singleton());
MOZ_RELEASE_ASSERT(
&std::as_const(w.ChunkedWriteFunc()).SourceFailureLatch() ==
&mozilla::FailureLatchInfallibleSource::Singleton());
MOZ_RELEASE_ASSERT(!w.Fallible());
MOZ_RELEASE_ASSERT(!w.Failed());
MOZ_RELEASE_ASSERT(!w.GetFailure());
MOZ_RELEASE_ASSERT(&w.SourceFailureLatch() ==
&mozilla::FailureLatchInfallibleSource::Singleton());
MOZ_RELEASE_ASSERT(&std::as_const(w).SourceFailureLatch() ==
&mozilla::FailureLatchInfallibleSource::Singleton());
ASSERT_TRUE(::profiler_stream_json_for_this_process(w).isErr());
MOZ_RELEASE_ASSERT(!w.ChunkedWriteFunc().Failed());
MOZ_RELEASE_ASSERT(!w.ChunkedWriteFunc().GetFailure());
MOZ_RELEASE_ASSERT(!w.Failed());
MOZ_RELEASE_ASSERT(!w.GetFailure());
// Start the profiler on the main thread.
profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL, features,
filters, std::size(filters), 0);
// Call profiler_stream_json_for_this_process on a background thread.
NS_DispatchAndSpinEventLoopUntilComplete(
"GeckoProfiler_StreamJSONForThisProcessThreaded_Test::TestBody"_ns,
thread,
NS_NewRunnableFunction(
"GeckoProfiler_StreamJSONForThisProcessThreaded_Test::TestBody",
[&]() {
w.Start();
ASSERT_TRUE(::do_profiler_stream_json_for_this_process(
w, /* double aSinceTime */ 0.0,
/* bool aIsShuttingDown */ false,
/* ProfilerCodeAddressService* aService */ nullptr,
mozilla::ProgressLogger{})
.isOk());
w.End();
}));
MOZ_RELEASE_ASSERT(!w.ChunkedWriteFunc().Failed());
MOZ_RELEASE_ASSERT(!w.ChunkedWriteFunc().GetFailure());
MOZ_RELEASE_ASSERT(!w.Failed());
MOZ_RELEASE_ASSERT(!w.GetFailure());
UniquePtr<char[]> profile = w.ChunkedWriteFunc().CopyData();
JSONOutputCheck(profile.get(), [](const Json::Value&) {});
// Stop the profiler and call profiler_stream_json_for_this_process on a
// background thread.
NS_DispatchAndSpinEventLoopUntilComplete(
"GeckoProfiler_StreamJSONForThisProcessThreaded_Test::TestBody"_ns,
thread,
NS_NewRunnableFunction(
"GeckoProfiler_StreamJSONForThisProcessThreaded_Test::TestBody",
[&]() {
profiler_stop();
ASSERT_TRUE(::do_profiler_stream_json_for_this_process(
w, /* double aSinceTime */ 0.0,
/* bool aIsShuttingDown */ false,
/* ProfilerCodeAddressService* aService */ nullptr,
mozilla::ProgressLogger{})
.isErr());
}));
thread->Shutdown();
// Call profiler_stream_json_for_this_process on the main thread.
ASSERT_TRUE(::profiler_stream_json_for_this_process(w).isErr());
}
TEST(GeckoProfiler, ProfilingStack)
{
uint32_t features = ProfilerFeature::StackWalk;
const char* filters[] = {"GeckoMain"};
AUTO_PROFILER_LABEL("A::B", OTHER);
UniqueFreePtr<char> dynamic(strdup("dynamic"));
{
AUTO_PROFILER_LABEL_DYNAMIC_CSTR("A::C", JS, dynamic.get());
AUTO_PROFILER_LABEL_DYNAMIC_NSCSTRING("A::C2", JS,
nsDependentCString(dynamic.get()));
AUTO_PROFILER_LABEL_DYNAMIC_LOSSY_NSSTRING(
"A::C3", JS, NS_ConvertUTF8toUTF16(dynamic.get()));
profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL,
features, filters, std::size(filters), 0);
ASSERT_TRUE(profiler_get_backtrace());
}
AutoProfilerLabel label1("A", nullptr, JS::ProfilingCategoryPair::DOM);
AutoProfilerLabel label2("A", dynamic.get(),
JS::ProfilingCategoryPair::NETWORK);
ASSERT_TRUE(profiler_get_backtrace());
profiler_stop();
ASSERT_TRUE(!profiler_get_profile());
}
TEST(GeckoProfiler, Bug1355807)
{
uint32_t features = ProfilerFeature::JS;
const char* manyThreadsFilter[] = {""};
const char* fewThreadsFilter[] = {"GeckoMain"};
profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL, features,
manyThreadsFilter, std::size(manyThreadsFilter), 0);
profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL, features,
fewThreadsFilter, std::size(fewThreadsFilter), 0);
// In bug 1355807 this caused an assertion failure in StopJSSampling().
profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL, features,
fewThreadsFilter, std::size(fewThreadsFilter), 0);
profiler_stop();
}
class GTestStackCollector final : public ProfilerStackCollector {
public:
GTestStackCollector() : mSetIsMainThread(0), mFrames(0) {}
virtual void SetIsMainThread() { mSetIsMainThread++; }
virtual void CollectNativeLeafAddr(void* aAddr) { mFrames++; }
virtual void CollectJitReturnAddr(void* aAddr) { mFrames++; }
virtual void CollectWasmOrSyncJITFrame(JS::ProfilingCategoryPair aCategory,
const char* aLabel,
uint32_t aSourceId) {
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);
}
});
}
// Bug 1953108: Windows ASan builds frequently time out on
// GeckoProfiler.FeatureCombinations.
#if !defined(XP_WIN) || !defined(MOZ_ASAN)
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);
}
}
}
}
#endif // if !defined(XP_WIN) || !defined(MOZ_ASAN)
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());
}