/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// This file tests a lot of the profiler_*() functions in GeckoProfiler.h.
// Most of the tests just check that nothing untoward (e.g. crashes, deadlocks)
// happens when calling these functions. They don't do much inspection of
// profiler internals.
#include "mozilla/ProfilerThreadPlatformData.h"
#include "mozilla/ProfilerThreadRegistration.h"
#include "mozilla/ProfilerThreadRegistrationInfo.h"
#include "mozilla/ProfilerThreadRegistry.h"
#include "mozilla/ProfilerUtils.h"
#include "mozilla/ProgressLogger.h"
#include "mozilla/UniquePtrExtensions.h"
#include "nsIThread.h"
#include "nsThreadUtils.h"
#include "prthread.h"
#include "nsHttp.h"
#include "nsIClassOfService.h"
#include "gtest/gtest.h"
#include "mozilla/gtest/MozAssertions.h"
#include <thread>
#if defined(_MSC_VER) ||
defined(__MINGW32__)
# include <processthreadsapi.h>
# include <realtimeapiset.h>
#elif defined(__APPLE__)
# include <mach/thread_act.h>
#endif
#ifdef MOZ_GECKO_PROFILER
# include
"GeckoProfiler.h"
# include
"mozilla/ProfilerMarkerTypes.h"
# include
"mozilla/ProfilerMarkers.h"
# include
"NetworkMarker.h"
# include
"platform.h"
# include
"ProfileBuffer.h"
# include
"ProfilerControl.h"
# include
"js/Initialization.h"
# include
"js/Printf.h"
# include
"jsapi.h"
# include
"json/json.h"
# include
"mozilla/Atomics.h"
# include
"mozilla/DataMutex.h"
# include
"mozilla/ProfileBufferEntrySerializationGeckoExtensions.h"
# include
"mozilla/ProfileJSONWriter.h"
# include
"mozilla/ScopeExit.h"
# include
"mozilla/net/HttpBaseChannel.h"
# include
"nsIChannelEventSink.h"
# include
"nsIThread.h"
# include
"nsThreadUtils.h"
# include <cstring>
# include <set>
#endif // MOZ_GECKO_PROFILER
// Note: profiler_init() has already been called in XRE_main(), so we can't
// test it here. Likewise for profiler_shutdown(), and AutoProfilerInit
// (which is just an RAII wrapper for profiler_init() and profiler_shutdown()).
using namespace mozilla;
TEST(GeckoProfiler, ProfilerUtils)
{
profiler_init_main_thread_id();
static_assert(std::is_same_v<decltype(profiler_current_process_id()),
ProfilerProcessId>);
static_assert(
std::is_same_v<decltype(profiler_current_process_id()),
decltype(baseprofiler::profiler_current_process_id())>);
ProfilerProcessId processId = profiler_current_process_id();
EXPECT_TRUE(processId.IsSpecified());
EXPECT_EQ(processId, baseprofiler::profiler_current_process_id());
static_assert(
std::is_same_v<decltype(profiler_current_thread_id()), ProfilerThreadId>);
static_assert(
std::is_same_v<decltype(profiler_current_thread_id()),
decltype(baseprofiler::profiler_current_thread_id())>);
EXPECT_EQ(profiler_current_thread_id(),
baseprofiler::profiler_current_thread_id());
ProfilerThreadId mainTestThreadId = profiler_current_thread_id();
EXPECT_TRUE(mainTestThreadId.IsSpecified());
ProfilerThreadId mainThreadId = profiler_main_thread_id();
EXPECT_TRUE(mainThreadId.IsSpecified());
EXPECT_EQ(mainThreadId, mainTestThreadId)
<<
"Test should run on the main thread";
EXPECT_TRUE(profiler_is_main_thread());
std::thread testThread([&]() {
EXPECT_EQ(profiler_current_process_id(), processId);
const ProfilerThreadId testThreadId = profiler_current_thread_id();
EXPECT_TRUE(testThreadId.IsSpecified());
EXPECT_NE(testThreadId, mainThreadId);
EXPECT_FALSE(profiler_is_main_thread());
EXPECT_EQ(baseprofiler::profiler_current_process_id(), processId);
EXPECT_EQ(baseprofiler::profiler_current_thread_id(), testThreadId);
EXPECT_EQ(baseprofiler::profiler_main_thread_id(), mainThreadId);
EXPECT_FALSE(baseprofiler::profiler_is_main_thread());
});
testThread.join();
}
TEST(GeckoProfiler, ThreadRegistrationInfo)
{
profiler_init_main_thread_id();
TimeStamp ts = TimeStamp::Now();
{
profiler::ThreadRegistrationInfo trInfo{
"name", ProfilerThreadId::FromNumber(123),
false, ts};
EXPECT_STREQ(trInfo.Name(),
"name");
EXPECT_NE(trInfo.Name(),
"name")
<<
"ThreadRegistrationInfo should keep its own copy of the name";
EXPECT_EQ(trInfo.RegisterTime(), ts);
EXPECT_EQ(trInfo.ThreadId(), ProfilerThreadId::FromNumber(123));
EXPECT_EQ(trInfo.IsMainThread(),
false);
}
// Make sure the next timestamp will be different from `ts`.
while (TimeStamp::Now() == ts) {
}
{
profiler::ThreadRegistrationInfo trInfoHere{
"Here"};
EXPECT_STREQ(trInfoHere.Name(),
"Here");
EXPECT_NE(trInfoHere.Name(),
"Here")
<<
"ThreadRegistrationInfo should keep its own copy of the name";
TimeStamp baseRegistrationTime;
#ifdef MOZ_GECKO_PROFILER
baseRegistrationTime = baseprofiler::detail::GetThreadRegistrationTime();
#endif
if (baseRegistrationTime) {
EXPECT_EQ(trInfoHere.RegisterTime(), baseRegistrationTime);
}
else {
EXPECT_GT(trInfoHere.RegisterTime(), ts);
}
EXPECT_EQ(trInfoHere.ThreadId(), profiler_current_thread_id());
EXPECT_EQ(trInfoHere.ThreadId(), profiler_main_thread_id())
<<
"Gtests are assumed to run on the main thread";
EXPECT_EQ(trInfoHere.IsMainThread(),
true)
<<
"Gtests are assumed to run on the main thread";
}
{
// Sub-thread test.
// These will receive sub-thread data (to test move at thread end).
TimeStamp tsThread;
ProfilerThreadId threadThreadId;
UniquePtr<profiler::ThreadRegistrationInfo> trInfoThreadPtr;
std::thread testThread([&]() {
profiler::ThreadRegistrationInfo trInfoThread{
"Thread"};
EXPECT_STREQ(trInfoThread.Name(),
"Thread");
EXPECT_NE(trInfoThread.Name(),
"Thread")
<<
"ThreadRegistrationInfo should keep its own copy of the name";
EXPECT_GT(trInfoThread.RegisterTime(), ts);
EXPECT_EQ(trInfoThread.ThreadId(), profiler_current_thread_id());
EXPECT_NE(trInfoThread.ThreadId(), profiler_main_thread_id());
EXPECT_EQ(trInfoThread.IsMainThread(),
false);
tsThread = trInfoThread.RegisterTime();
threadThreadId = trInfoThread.ThreadId();
trInfoThreadPtr =
MakeUnique<profiler::ThreadRegistrationInfo>(std::move(trInfoThread));
});
testThread.join();
ASSERT_NE(trInfoThreadPtr, nullptr);
EXPECT_STREQ(trInfoThreadPtr->Name(),
"Thread");
EXPECT_EQ(trInfoThreadPtr->RegisterTime(), tsThread);
EXPECT_EQ(trInfoThreadPtr->ThreadId(), threadThreadId);
EXPECT_EQ(trInfoThreadPtr->IsMainThread(),
false)
<<
"Gtests are assumed to run on the main thread";
}
}
static constexpr ThreadProfilingFeatures scEachAndAnyThreadProfilingFeatures[] =
{ThreadProfilingFeatures::CPUUtilization, ThreadProfilingFeatures::Sampling,
ThreadProfilingFeatures::Markers, ThreadProfilingFeatures::Any};
TEST(GeckoProfiler, ThreadProfilingFeaturesType)
{
ASSERT_EQ(
static_cast<uint32_t>(ThreadProfilingFeatures::Any), 1u + 2u + 4u)
<<
"This test assumes that there are 3 binary choices 1+2+4; "
"Is this test up to date?";
EXPECT_EQ(Combine(ThreadProfilingFeatures::CPUUtilization,
ThreadProfilingFeatures::Sampling,
ThreadProfilingFeatures::Markers),
ThreadProfilingFeatures::Any);
constexpr ThreadProfilingFeatures allThreadProfilingFeatures[] = {
ThreadProfilingFeatures::NotProfiled,
ThreadProfilingFeatures::CPUUtilization,
ThreadProfilingFeatures::Sampling, ThreadProfilingFeatures::Markers,
ThreadProfilingFeatures::Any};
for (ThreadProfilingFeatures f1 : allThreadProfilingFeatures) {
// Combine and Intersect are commutative.
for (ThreadProfilingFeatures f2 : allThreadProfilingFeatures) {
EXPECT_EQ(Combine(f1, f2), Combine(f2, f1));
EXPECT_EQ(Intersect(f1, f2), Intersect(f2, f1));
}
// Combine works like OR.
EXPECT_EQ(Combine(f1, f1), f1);
EXPECT_EQ(Combine(f1, f1, f1), f1);
// 'OR NotProfiled' doesn't change anything.
EXPECT_EQ(Combine(f1, ThreadProfilingFeatures::NotProfiled), f1);
// 'OR Any' makes Any.
EXPECT_EQ(Combine(f1, ThreadProfilingFeatures::Any),
ThreadProfilingFeatures::Any);
// Intersect works like AND.
EXPECT_EQ(Intersect(f1, f1), f1);
EXPECT_EQ(Intersect(f1, f1, f1), f1);
// 'AND NotProfiled' erases anything.
EXPECT_EQ(Intersect(f1, ThreadProfilingFeatures::NotProfiled),
ThreadProfilingFeatures::NotProfiled);
// 'AND Any' doesn't change anything.
EXPECT_EQ(Intersect(f1, ThreadProfilingFeatures::Any), f1);
}
for (ThreadProfilingFeatures f1 : scEachAndAnyThreadProfilingFeatures) {
EXPECT_TRUE(DoFeaturesIntersect(f1, f1));
// NotProfiled doesn't intersect with any feature.
EXPECT_FALSE(DoFeaturesIntersect(f1, ThreadProfilingFeatures::NotProfiled));
// Any intersects with any feature.
EXPECT_TRUE(DoFeaturesIntersect(f1, ThreadProfilingFeatures::Any));
}
}
static void TestConstUnlockedConstReader(
const profiler::ThreadRegistration::UnlockedConstReader& aData,
const TimeStamp& aBeforeRegistration,
const TimeStamp& aAfterRegistration,
const void* aOnStackObject,
ProfilerThreadId aThreadId = profiler_current_thread_id()) {
EXPECT_STREQ(aData.Info().Name(),
"Test thread");
EXPECT_GE(aData.Info().RegisterTime(), aBeforeRegistration);
EXPECT_LE(aData.Info().RegisterTime(), aAfterRegistration);
EXPECT_EQ(aData.Info().ThreadId(), aThreadId);
EXPECT_FALSE(aData.Info().IsMainThread());
#if (
defined(_MSC_VER) ||
defined(__MINGW32__)) &&
defined(MOZ_GECKO_PROFILER)
HANDLE threadHandle = aData.PlatformDataCRef().ProfiledThread();
EXPECT_NE(threadHandle, nullptr);
EXPECT_EQ(ProfilerThreadId::FromNumber(::GetThreadId(threadHandle)),
aThreadId);
// Test calling QueryThreadCycleTime, we cannot assume that it will always
// work, but at least it shouldn't crash.
ULONG64 cycles;
(
void)QueryThreadCycleTime(threadHandle, &cycles);
#elif defined(__APPLE__) &&
defined(MOZ_GECKO_PROFILER)
// Test calling thread_info, we cannot assume that it will always work, but at
// least it shouldn't crash.
thread_basic_info_data_t threadBasicInfo;
mach_msg_type_number_t basicCount = THREAD_BASIC_INFO_COUNT;
(
void)thread_info(
aData.PlatformDataCRef().ProfiledThread(), THREAD_BASIC_INFO,
reinterpret_cast<thread_info_t>(&threadBasicInfo), &basicCount);
#elif (
defined(__linux__) ||
defined(__ANDROID__) ||
defined(__FreeBSD__)) && \
defined(MOZ_GECKO_PROFILER)
// Test calling GetClockId, we cannot assume that it will always work, but at
// least it shouldn't crash.
Maybe<clockid_t> maybeClockId = aData.PlatformDataCRef().GetClockId();
if (maybeClockId) {
// Test calling clock_gettime, we cannot assume that it will always work,
// but at least it shouldn't crash.
timespec ts;
(
void)clock_gettime(*maybeClockId, &ts);
}
#else
(
void)aData.PlatformDataCRef();
#endif
EXPECT_GE(aData.StackTop(), aOnStackObject)
<<
"StackTop should be at &onStackChar, or higher on some "
"platforms";
};
static void TestConstUnlockedConstReaderAndAtomicRW(
const profiler::ThreadRegistration::UnlockedConstReaderAndAtomicRW& aData,
const TimeStamp& aBeforeRegistration,
const TimeStamp& aAfterRegistration,
const void* aOnStackObject,
ProfilerThreadId aThreadId = profiler_current_thread_id()) {
TestConstUnlockedConstReader(aData, aBeforeRegistration, aAfterRegistration,
aOnStackObject, aThreadId);
(
void)aData.ProfilingStackCRef();
EXPECT_EQ(aData.ProfilingFeatures(), ThreadProfilingFeatures::NotProfiled);
EXPECT_FALSE(aData.IsSleeping());
};
static void TestUnlockedConstReaderAndAtomicRW(
profiler::ThreadRegistration::UnlockedConstReaderAndAtomicRW& aData,
const TimeStamp& aBeforeRegistration,
const TimeStamp& aAfterRegistration,
const void* aOnStackObject,
ProfilerThreadId aThreadId = profiler_current_thread_id()) {
TestConstUnlockedConstReaderAndAtomicRW(aData, aBeforeRegistration,
aAfterRegistration, aOnStackObject,
aThreadId);
(
void)aData.ProfilingStackRef();
EXPECT_FALSE(aData.IsSleeping());
aData.SetSleeping();
EXPECT_TRUE(aData.IsSleeping());
aData.SetAwake();
EXPECT_FALSE(aData.IsSleeping());
aData.ReinitializeOnResume();
EXPECT_FALSE(aData.CanDuplicateLastSampleDueToSleep());
EXPECT_FALSE(aData.CanDuplicateLastSampleDueToSleep());
aData.SetSleeping();
// After sleeping, the 2nd+ calls can duplicate.
EXPECT_FALSE(aData.CanDuplicateLastSampleDueToSleep());
EXPECT_TRUE(aData.CanDuplicateLastSampleDueToSleep());
EXPECT_TRUE(aData.CanDuplicateLastSampleDueToSleep());
aData.ReinitializeOnResume();
// After reinit (and sleeping), the 2nd+ calls can duplicate.
EXPECT_FALSE(aData.CanDuplicateLastSampleDueToSleep());
EXPECT_TRUE(aData.CanDuplicateLastSampleDueToSleep());
EXPECT_TRUE(aData.CanDuplicateLastSampleDueToSleep());
aData.SetAwake();
EXPECT_FALSE(aData.CanDuplicateLastSampleDueToSleep());
EXPECT_FALSE(aData.CanDuplicateLastSampleDueToSleep());
};
static void TestConstUnlockedRWForLockedProfiler(
const profiler::ThreadRegistration::UnlockedRWForLockedProfiler& aData,
const TimeStamp& aBeforeRegistration,
const TimeStamp& aAfterRegistration,
const void* aOnStackObject,
ProfilerThreadId aThreadId = profiler_current_thread_id()) {
TestConstUnlockedConstReaderAndAtomicRW(aData, aBeforeRegistration,
aAfterRegistration, aOnStackObject,
aThreadId);
// We can't create a PSAutoLock here, so just verify that the call would
// compile and return the expected type.
static_assert(std::is_same_v<decltype(aData.GetProfiledThreadData(
std::declval<PSAutoLock>())),
const ProfiledThreadData*>);
};
static void TestConstUnlockedReaderAndAtomicRWOnThread(
const profiler::ThreadRegistration::UnlockedReaderAndAtomicRWOnThread&
aData,
const TimeStamp& aBeforeRegistration,
const TimeStamp& aAfterRegistration,
const void* aOnStackObject,
ProfilerThreadId aThreadId = profiler_current_thread_id()) {
TestConstUnlockedRWForLockedProfiler(aData, aBeforeRegistration,
aAfterRegistration, aOnStackObject,
aThreadId);
EXPECT_EQ(aData.GetJSContext(), nullptr);
};
static void TestUnlockedRWForLockedProfiler(
profiler::ThreadRegistration::UnlockedRWForLockedProfiler& aData,
const TimeStamp& aBeforeRegistration,
const TimeStamp& aAfterRegistration,
const void* aOnStackObject,
ProfilerThreadId aThreadId = profiler_current_thread_id()) {
TestConstUnlockedRWForLockedProfiler(aData, aBeforeRegistration,
aAfterRegistration, aOnStackObject,
aThreadId);
TestUnlockedConstReaderAndAtomicRW(aData, aBeforeRegistration,
aAfterRegistration, aOnStackObject,
aThreadId);
// No functions to test here.
};
static void TestUnlockedReaderAndAtomicRWOnThread(
profiler::ThreadRegistration::UnlockedReaderAndAtomicRWOnThread& aData,
const TimeStamp& aBeforeRegistration,
const TimeStamp& aAfterRegistration,
const void* aOnStackObject,
ProfilerThreadId aThreadId = profiler_current_thread_id()) {
TestConstUnlockedReaderAndAtomicRWOnThread(aData, aBeforeRegistration,
aAfterRegistration, aOnStackObject,
aThreadId);
TestUnlockedRWForLockedProfiler(aData, aBeforeRegistration,
aAfterRegistration, aOnStackObject,
aThreadId);
// No functions to test here.
};
static void TestConstLockedRWFromAnyThread(
const profiler::ThreadRegistration::LockedRWFromAnyThread& aData,
const TimeStamp& aBeforeRegistration,
const TimeStamp& aAfterRegistration,
const void* aOnStackObject,
ProfilerThreadId aThreadId = profiler_current_thread_id()) {
TestConstUnlockedReaderAndAtomicRWOnThread(aData, aBeforeRegistration,
aAfterRegistration, aOnStackObject,
aThreadId);
EXPECT_EQ(aData.GetJsFrameBuffer(), nullptr);
EXPECT_EQ(aData.GetEventTarget(), nullptr);
};
static void TestLockedRWFromAnyThread(
profiler::ThreadRegistration::LockedRWFromAnyThread& aData,
const TimeStamp& aBeforeRegistration,
const TimeStamp& aAfterRegistration,
const void* aOnStackObject,
ProfilerThreadId aThreadId = profiler_current_thread_id()) {
TestConstLockedRWFromAnyThread(aData, aBeforeRegistration, aAfterRegistration,
aOnStackObject, aThreadId);
TestUnlockedReaderAndAtomicRWOnThread(aData, aBeforeRegistration,
aAfterRegistration, aOnStackObject,
aThreadId);
// We can't create a ProfiledThreadData nor PSAutoLock here, so just verify
// that the call would compile and return the expected type.
static_assert(std::is_same_v<decltype(aData.SetProfilingFeaturesAndData(
std::declval<ThreadProfilingFeatures>(),
std::declval<ProfiledThreadData*>(),
std::declval<PSAutoLock>())),
void>);
aData.ResetMainThread(nullptr);
TimeDuration delay = TimeDuration::FromSeconds(1);
TimeDuration running = TimeDuration::FromSeconds(1);
aData.GetRunningEventDelay(TimeStamp::Now(), delay, running);
EXPECT_TRUE(delay.IsZero());
EXPECT_TRUE(running.IsZero());
aData.StartJSSampling(123u);
aData.StopJSSampling();
};
static void TestConstLockedRWOnThread(
const profiler::ThreadRegistration::LockedRWOnThread& aData,
const TimeStamp& aBeforeRegistration,
const TimeStamp& aAfterRegistration,
const void* aOnStackObject,
ProfilerThreadId aThreadId = profiler_current_thread_id()) {
TestConstLockedRWFromAnyThread(aData, aBeforeRegistration, aAfterRegistration,
aOnStackObject, aThreadId);
// No functions to test here.
};
static void TestLockedRWOnThread(
profiler::ThreadRegistration::LockedRWOnThread& aData,
const TimeStamp& aBeforeRegistration,
const TimeStamp& aAfterRegistration,
const void* aOnStackObject,
ProfilerThreadId aThreadId = profiler_current_thread_id()) {
TestConstLockedRWOnThread(aData, aBeforeRegistration, aAfterRegistration,
aOnStackObject, aThreadId);
TestLockedRWFromAnyThread(aData, aBeforeRegistration, aAfterRegistration,
aOnStackObject, aThreadId);
// We don't want to really call SetCycleCollectedJSContext here, so just
// verify that the call would compile and return the expected type.
static_assert(std::is_same_v<decltype(aData.SetCycleCollectedJSContext(
std::declval<CycleCollectedJSContext*>())),
void>);
aData.ClearCycleCollectedJSContext();
aData.PollJSSampling();
};
TEST(GeckoProfiler, ThreadRegistration_DataAccess)
{
using TR = profiler::ThreadRegistration;
profiler_init_main_thread_id();
ASSERT_TRUE(profiler_is_main_thread())
<<
"This test assumes it runs on the main thread";
// Note that the main thread could already be registered, so we work in a new
// thread to test an actual registration that we control.
std::thread testThread([&]() {
ASSERT_FALSE(TR::IsRegistered())
<<
"A new std::thread should not start registered";
EXPECT_FALSE(TR::GetOnThreadPtr());
EXPECT_FALSE(TR::WithOnThreadRefOr([&](
auto) {
return true; },
false));
char onStackChar;
TimeStamp beforeRegistration = TimeStamp::Now();
TR tr{
"Test thread", &onStackChar};
TimeStamp afterRegistration = TimeStamp::Now();
ASSERT_TRUE(TR::IsRegistered());
// Note: This test will mostly be about checking the correct access to
// thread data, depending on how it's obtained. Not all the functionality
// related to that data is tested (e.g., because it involves JS or other
// external dependencies that would be difficult to control here.)
auto TestOnThreadRef = [&](TR::OnThreadRef aOnThreadRef) {
// To test const-qualified member functions.
const TR::OnThreadRef& onThreadCRef = aOnThreadRef;
// const UnlockedConstReader (always const)
TestConstUnlockedConstReader(onThreadCRef.UnlockedConstReaderCRef(),
beforeRegistration, afterRegistration,
&onStackChar);
onThreadCRef.WithUnlockedConstReader(
[&](
const TR::UnlockedConstReader& aData) {
TestConstUnlockedConstReader(aData, beforeRegistration,
afterRegistration, &onStackChar);
});
// const UnlockedConstReaderAndAtomicRW
TestConstUnlockedConstReaderAndAtomicRW(
onThreadCRef.UnlockedConstReaderAndAtomicRWCRef(), beforeRegistration,
afterRegistration, &onStackChar);
onThreadCRef.WithUnlockedConstReaderAndAtomicRW(
[&](
const TR::UnlockedConstReaderAndAtomicRW& aData) {
TestConstUnlockedConstReaderAndAtomicRW(
aData, beforeRegistration, afterRegistration, &onStackChar);
});
// non-const UnlockedConstReaderAndAtomicRW
TestUnlockedConstReaderAndAtomicRW(
aOnThreadRef.UnlockedConstReaderAndAtomicRWRef(), beforeRegistration,
afterRegistration, &onStackChar);
aOnThreadRef.WithUnlockedConstReaderAndAtomicRW(
[&](TR::UnlockedConstReaderAndAtomicRW& aData) {
TestUnlockedConstReaderAndAtomicRW(aData, beforeRegistration,
afterRegistration, &onStackChar);
});
// const UnlockedRWForLockedProfiler
TestConstUnlockedRWForLockedProfiler(
onThreadCRef.UnlockedRWForLockedProfilerCRef(), beforeRegistration,
afterRegistration, &onStackChar);
onThreadCRef.WithUnlockedRWForLockedProfiler(
[&](
const TR::UnlockedRWForLockedProfiler& aData) {
TestConstUnlockedRWForLockedProfiler(
aData, beforeRegistration, afterRegistration, &onStackChar);
});
// non-const UnlockedRWForLockedProfiler
TestUnlockedRWForLockedProfiler(
aOnThreadRef.UnlockedRWForLockedProfilerRef(), beforeRegistration,
afterRegistration, &onStackChar);
aOnThreadRef.WithUnlockedRWForLockedProfiler(
[&](TR::UnlockedRWForLockedProfiler& aData) {
TestUnlockedRWForLockedProfiler(aData, beforeRegistration,
afterRegistration, &onStackChar);
});
// const UnlockedReaderAndAtomicRWOnThread
TestConstUnlockedReaderAndAtomicRWOnThread(
onThreadCRef.UnlockedReaderAndAtomicRWOnThreadCRef(),
beforeRegistration, afterRegistration, &onStackChar);
onThreadCRef.WithUnlockedReaderAndAtomicRWOnThread(
[&](
const TR::UnlockedReaderAndAtomicRWOnThread& aData) {
TestConstUnlockedReaderAndAtomicRWOnThread(
aData, beforeRegistration, afterRegistration, &onStackChar);
});
// non-const UnlockedReaderAndAtomicRWOnThread
TestUnlockedReaderAndAtomicRWOnThread(
aOnThreadRef.UnlockedReaderAndAtomicRWOnThreadRef(),
beforeRegistration, afterRegistration, &onStackChar);
aOnThreadRef.WithUnlockedReaderAndAtomicRWOnThread(
[&](TR::UnlockedReaderAndAtomicRWOnThread& aData) {
TestUnlockedReaderAndAtomicRWOnThread(
aData, beforeRegistration, afterRegistration, &onStackChar);
});
// LockedRWFromAnyThread
// Note: It cannot directly be accessed on the thread, this will be
// tested through LockedRWOnThread.
// const LockedRWOnThread
EXPECT_FALSE(TR::IsDataMutexLockedOnCurrentThread());
{
TR::OnThreadRef::ConstRWOnThreadWithLock constRWOnThreadWithLock =
onThreadCRef.ConstLockedRWOnThread();
EXPECT_TRUE(TR::IsDataMutexLockedOnCurrentThread());
TestConstLockedRWOnThread(constRWOnThreadWithLock.DataCRef(),
beforeRegistration, afterRegistration,
&onStackChar);
}
EXPECT_FALSE(TR::IsDataMutexLockedOnCurrentThread());
onThreadCRef.WithConstLockedRWOnThread(
[&](
const TR::LockedRWOnThread& aData) {
EXPECT_TRUE(TR::IsDataMutexLockedOnCurrentThread());
TestConstLockedRWOnThread(aData, beforeRegistration,
afterRegistration, &onStackChar);
});
EXPECT_FALSE(TR::IsDataMutexLockedOnCurrentThread());
// non-const LockedRWOnThread
EXPECT_FALSE(TR::IsDataMutexLockedOnCurrentThread());
{
TR::OnThreadRef::RWOnThreadWithLock rwOnThreadWithLock =
aOnThreadRef.GetLockedRWOnThread();
EXPECT_TRUE(TR::IsDataMutexLockedOnCurrentThread());
TestConstLockedRWOnThread(rwOnThreadWithLock.DataCRef(),
beforeRegistration, afterRegistration,
&onStackChar);
TestLockedRWOnThread(rwOnThreadWithLock.DataRef(), beforeRegistration,
afterRegistration, &onStackChar);
}
EXPECT_FALSE(TR::IsDataMutexLockedOnCurrentThread());
aOnThreadRef.WithLockedRWOnThread([&](TR::LockedRWOnThread& aData) {
EXPECT_TRUE(TR::IsDataMutexLockedOnCurrentThread());
TestLockedRWOnThread(aData, beforeRegistration, afterRegistration,
&onStackChar);
});
EXPECT_FALSE(TR::IsDataMutexLockedOnCurrentThread());
};
TR::OnThreadPtr onThreadPtr = TR::GetOnThreadPtr();
ASSERT_TRUE(onThreadPtr);
TestOnThreadRef(*onThreadPtr);
TR::WithOnThreadRef(
[&](TR::OnThreadRef aOnThreadRef) { TestOnThreadRef(aOnThreadRef); });
EXPECT_TRUE(TR::WithOnThreadRefOr(
[&](TR::OnThreadRef aOnThreadRef) {
TestOnThreadRef(aOnThreadRef);
return true;
},
false));
});
testThread.join();
}
// Thread name if registered, nullptr otherwise.
static const char* GetThreadName() {
return profiler::ThreadRegistration::WithOnThreadRefOr(
[](profiler::ThreadRegistration::OnThreadRef onThreadRef) {
return onThreadRef.WithUnlockedConstReader(
[](
const profiler::ThreadRegistration::UnlockedConstReader& aData) {
return aData.Info().Name();
});
},
nullptr);
}
// Get the thread name, as registered in the PRThread, nullptr on failure.
static const char* GetPRThreadName() {
nsIThread* nsThread = NS_GetCurrentThread();
if (!nsThread) {
return nullptr;
}
PRThread* prThread = nullptr;
if (NS_FAILED(nsThread->GetPRThread(&prThread))) {
return nullptr;
}
if (!prThread) {
return nullptr;
}
return PR_GetThreadName(prThread);
}
TEST(GeckoProfiler, ThreadRegistration_MainThreadName)
{
EXPECT_TRUE(profiler::ThreadRegistration::IsRegistered());
EXPECT_STREQ(GetThreadName(),
"GeckoMain");
// Check that the real thread name (outside the profiler) is *not* GeckoMain.
EXPECT_STRNE(GetPRThreadName(),
"GeckoMain");
}
TEST(GeckoProfiler, ThreadRegistration_NestedRegistrations)
{
using TR = profiler::ThreadRegistration;
profiler_init_main_thread_id();
ASSERT_TRUE(profiler_is_main_thread())
<<
"This test assumes it runs on the main thread";
// Note that the main thread could already be registered, so we work in a new
// thread to test actual registrations that we control.
std::thread testThread([&]() {
ASSERT_FALSE(TR::IsRegistered())
<<
"A new std::thread should not start registered";
char onStackChar;
// Blocks {} are mostly for clarity, but some control on-stack registration
// lifetimes.
// On-stack registration.
{
TR rt{
"Test thread #1", &onStackChar};
ASSERT_TRUE(TR::IsRegistered());
EXPECT_STREQ(GetThreadName(),
"Test thread #1");
EXPECT_STREQ(GetPRThreadName(),
"Test thread #1");
}
ASSERT_FALSE(TR::IsRegistered());
// Off-stack registration.
{
TR::RegisterThread(
"Test thread #2", &onStackChar);
ASSERT_TRUE(TR::IsRegistered());
EXPECT_STREQ(GetThreadName(),
"Test thread #2");
EXPECT_STREQ(GetPRThreadName(),
"Test thread #2");
TR::UnregisterThread();
ASSERT_FALSE(TR::IsRegistered());
}
// Extra un-registration should be ignored.
TR::UnregisterThread();
ASSERT_FALSE(TR::IsRegistered());
// Nested on-stack.
{
TR rt2{
"Test thread #3", &onStackChar};
ASSERT_TRUE(TR::IsRegistered());
EXPECT_STREQ(GetThreadName(),
"Test thread #3");
EXPECT_STREQ(GetPRThreadName(),
"Test thread #3");
{
TR rt3{
"Test thread #4", &onStackChar};
ASSERT_TRUE(TR::IsRegistered());
EXPECT_STREQ(GetThreadName(),
"Test thread #3")
<<
"Nested registration shouldn't change the name";
EXPECT_STREQ(GetPRThreadName(),
"Test thread #3")
<<
"Nested registration shouldn't change the PRThread name";
}
ASSERT_TRUE(TR::IsRegistered())
<<
"Thread should still be registered after nested un-registration";
EXPECT_STREQ(GetThreadName(),
"Test thread #3")
<<
"Thread should still be registered after nested un-registration";
EXPECT_STREQ(GetPRThreadName(),
"Test thread #3");
}
ASSERT_FALSE(TR::IsRegistered());
// Nested off-stack.
{
TR::RegisterThread(
"Test thread #5", &onStackChar);
ASSERT_TRUE(TR::IsRegistered());
EXPECT_STREQ(GetThreadName(),
"Test thread #5");
EXPECT_STREQ(GetPRThreadName(),
"Test thread #5");
{
TR::RegisterThread(
"Test thread #6", &onStackChar);
ASSERT_TRUE(TR::IsRegistered());
EXPECT_STREQ(GetThreadName(),
"Test thread #5")
<<
"Nested registration shouldn't change the name";
EXPECT_STREQ(GetPRThreadName(),
"Test thread #5")
<<
"Nested registration shouldn't change the PRThread name";
TR::UnregisterThread();
ASSERT_TRUE(TR::IsRegistered())
<<
"Thread should still be registered after nested un-registration";
EXPECT_STREQ(GetThreadName(),
"Test thread #5")
<<
"Thread should still be registered after nested un-registration";
EXPECT_STREQ(GetPRThreadName(),
"Test thread #5");
}
TR::UnregisterThread();
ASSERT_FALSE(TR::IsRegistered());
}
// Nested on- and off-stack.
{
TR rt2{
"Test thread #7", &onStackChar};
ASSERT_TRUE(TR::IsRegistered());
EXPECT_STREQ(GetThreadName(),
"Test thread #7");
EXPECT_STREQ(GetPRThreadName(),
"Test thread #7");
{
TR::RegisterThread(
"Test thread #8", &onStackChar);
ASSERT_TRUE(TR::IsRegistered());
EXPECT_STREQ(GetThreadName(),
"Test thread #7")
<<
"Nested registration shouldn't change the name";
EXPECT_STREQ(GetPRThreadName(),
"Test thread #7")
<<
"Nested registration shouldn't change the PRThread name";
TR::UnregisterThread();
ASSERT_TRUE(TR::IsRegistered())
<<
"Thread should still be registered after nested un-registration";
EXPECT_STREQ(GetThreadName(),
"Test thread #7")
<<
"Thread should still be registered after nested un-registration";
EXPECT_STREQ(GetPRThreadName(),
"Test thread #7");
}
}
ASSERT_FALSE(TR::IsRegistered());
// Nested off- and on-stack.
{
TR::RegisterThread(
"Test thread #9", &onStackChar);
ASSERT_TRUE(TR::IsRegistered());
EXPECT_STREQ(GetThreadName(),
"Test thread #9");
EXPECT_STREQ(GetPRThreadName(),
"Test thread #9");
{
TR rt3{
"Test thread #10", &onStackChar};
ASSERT_TRUE(TR::IsRegistered());
EXPECT_STREQ(GetThreadName(),
"Test thread #9")
<<
"Nested registration shouldn't change the name";
EXPECT_STREQ(GetPRThreadName(),
"Test thread #9")
<<
"Nested registration shouldn't change the PRThread name";
}
ASSERT_TRUE(TR::IsRegistered())
<<
"Thread should still be registered after nested un-registration";
EXPECT_STREQ(GetThreadName(),
"Test thread #9")
<<
"Thread should still be registered after nested un-registration";
EXPECT_STREQ(GetPRThreadName(),
"Test thread #9");
TR::UnregisterThread();
ASSERT_FALSE(TR::IsRegistered());
}
// Excess UnregisterThread with on-stack TR.
{
TR rt2{
"Test thread #11", &onStackChar};
ASSERT_TRUE(TR::IsRegistered());
EXPECT_STREQ(GetThreadName(),
"Test thread #11");
EXPECT_STREQ(GetPRThreadName(),
"Test thread #11");
TR::UnregisterThread();
ASSERT_TRUE(TR::IsRegistered())
<<
"On-stack thread should still be registered after off-stack "
"un-registration";
EXPECT_STREQ(GetThreadName(),
"Test thread #11")
<<
"On-stack thread should still be registered after off-stack "
"un-registration";
EXPECT_STREQ(GetPRThreadName(),
"Test thread #11");
}
ASSERT_FALSE(TR::IsRegistered());
// Excess on-thread TR destruction with already-unregistered root off-thread
// registration.
{
TR::RegisterThread(
"Test thread #12", &onStackChar);
ASSERT_TRUE(TR::IsRegistered());
EXPECT_STREQ(GetThreadName(),
"Test thread #12");
EXPECT_STREQ(GetPRThreadName(),
"Test thread #12");
{
TR rt3{
"Test thread #13", &onStackChar};
ASSERT_TRUE(TR::IsRegistered());
EXPECT_STREQ(GetThreadName(),
"Test thread #12")
<<
"Nested registration shouldn't change the name";
EXPECT_STREQ(GetPRThreadName(),
"Test thread #12")
<<
"Nested registration shouldn't change the PRThread name";
// Note that we unregister the root registration, while nested `rt3` is
// still alive.
TR::UnregisterThread();
ASSERT_FALSE(TR::IsRegistered())
<<
"UnregisterThread() of the root RegisterThread() should always work";
// At this end of this block, `rt3` will be destroyed, but nothing
// should happen.
}
ASSERT_FALSE(TR::IsRegistered());
}
ASSERT_FALSE(TR::IsRegistered());
});
testThread.join();
}
TEST(GeckoProfiler, ThreadRegistry_DataAccess)
{
using TR = profiler::ThreadRegistration;
using TRy = profiler::ThreadRegistry;
profiler_init_main_thread_id();
ASSERT_TRUE(profiler_is_main_thread())
<<
"This test assumes it runs on the main thread";
// Note that the main thread could already be registered, so we work in a new
// thread to test an actual registration that we control.
std::thread testThread([&]() {
ASSERT_FALSE(TR::IsRegistered())
<<
"A new std::thread should not start registered";
EXPECT_FALSE(TR::GetOnThreadPtr());
EXPECT_FALSE(TR::WithOnThreadRefOr([&](
auto) {
return true; },
false));
char onStackChar;
TimeStamp beforeRegistration = TimeStamp::Now();
TR tr{
"Test thread", &onStackChar};
TimeStamp afterRegistration = TimeStamp::Now();
ASSERT_TRUE(TR::IsRegistered());
// Note: This test will mostly be about checking the correct access to
// thread data, depending on how it's obtained. Not all the functionality
// related to that data is tested (e.g., because it involves JS or other
// external dependencies that would be difficult to control here.)
const ProfilerThreadId testThreadId = profiler_current_thread_id();
auto testThroughRegistry = [&]() {
auto TestOffThreadRef = [&](
TRy::OffThreadRef aOffThreadRef) {
// To test const-qualified member functions.
const TRy::OffThreadRef& offThreadCRef = aOffThreadRef;
// const UnlockedConstReader (always const)
TestConstUnlockedConstReader(offThreadCRef.UnlockedConstReaderCRef(),
beforeRegistration, afterRegistration,
&onStackChar, testThreadId);
offThreadCRef.WithUnlockedConstReader(
[&](
const TR::UnlockedConstReader& aData) {
TestConstUnlockedConstReader(aData, beforeRegistration,
afterRegistration, &onStackChar,
testThreadId);
});
// const UnlockedConstReaderAndAtomicRW
TestConstUnlockedConstReaderAndAtomicRW(
offThreadCRef.UnlockedConstReaderAndAtomicRWCRef(),
beforeRegistration, afterRegistration, &onStackChar, testThreadId);
offThreadCRef.WithUnlockedConstReaderAndAtomicRW(
[&](
const TR::UnlockedConstReaderAndAtomicRW& aData) {
TestConstUnlockedConstReaderAndAtomicRW(
aData, beforeRegistration, afterRegistration, &onStackChar,
testThreadId);
});
// non-const UnlockedConstReaderAndAtomicRW
TestUnlockedConstReaderAndAtomicRW(
aOffThreadRef.UnlockedConstReaderAndAtomicRWRef(),
beforeRegistration, afterRegistration, &onStackChar, testThreadId);
aOffThreadRef.WithUnlockedConstReaderAndAtomicRW(
[&](TR::UnlockedConstReaderAndAtomicRW& aData) {
TestUnlockedConstReaderAndAtomicRW(aData, beforeRegistration,
afterRegistration,
&onStackChar, testThreadId);
});
// const UnlockedRWForLockedProfiler
TestConstUnlockedRWForLockedProfiler(
offThreadCRef.UnlockedRWForLockedProfilerCRef(), beforeRegistration,
afterRegistration, &onStackChar, testThreadId);
offThreadCRef.WithUnlockedRWForLockedProfiler(
[&](
const TR::UnlockedRWForLockedProfiler& aData) {
TestConstUnlockedRWForLockedProfiler(aData, beforeRegistration,
afterRegistration,
&onStackChar, testThreadId);
});
// non-const UnlockedRWForLockedProfiler
TestUnlockedRWForLockedProfiler(
aOffThreadRef.UnlockedRWForLockedProfilerRef(), beforeRegistration,
afterRegistration, &onStackChar, testThreadId);
aOffThreadRef.WithUnlockedRWForLockedProfiler(
[&](TR::UnlockedRWForLockedProfiler& aData) {
TestUnlockedRWForLockedProfiler(aData, beforeRegistration,
afterRegistration, &onStackChar,
testThreadId);
});
// UnlockedReaderAndAtomicRWOnThread
// Note: It cannot directly be accessed off the thread, this will be
// tested through LockedRWFromAnyThread.
// const LockedRWFromAnyThread
EXPECT_FALSE(TR::IsDataMutexLockedOnCurrentThread());
{
TRy::OffThreadRef::ConstRWFromAnyThreadWithLock
constRWFromAnyThreadWithLock =
offThreadCRef.ConstLockedRWFromAnyThread();
if (profiler_current_thread_id() == testThreadId) {
EXPECT_TRUE(TR::IsDataMutexLockedOnCurrentThread());
}
TestConstLockedRWFromAnyThread(
constRWFromAnyThreadWithLock.DataCRef(), beforeRegistration,
afterRegistration, &onStackChar, testThreadId);
}
EXPECT_FALSE(TR::IsDataMutexLockedOnCurrentThread());
offThreadCRef.WithConstLockedRWFromAnyThread(
[&](
const TR::LockedRWFromAnyThread& aData) {
if (profiler_current_thread_id() == testThreadId) {
EXPECT_TRUE(TR::IsDataMutexLockedOnCurrentThread());
}
TestConstLockedRWFromAnyThread(aData, beforeRegistration,
afterRegistration, &onStackChar,
testThreadId);
});
EXPECT_FALSE(TR::IsDataMutexLockedOnCurrentThread());
// non-const LockedRWFromAnyThread
EXPECT_FALSE(TR::IsDataMutexLockedOnCurrentThread());
{
TRy::OffThreadRef::RWFromAnyThreadWithLock rwFromAnyThreadWithLock =
aOffThreadRef.GetLockedRWFromAnyThread();
if (profiler_current_thread_id() == testThreadId) {
EXPECT_TRUE(TR::IsDataMutexLockedOnCurrentThread());
}
TestLockedRWFromAnyThread(rwFromAnyThreadWithLock.DataRef(),
beforeRegistration, afterRegistration,
&onStackChar, testThreadId);
}
EXPECT_FALSE(TR::IsDataMutexLockedOnCurrentThread());
aOffThreadRef.WithLockedRWFromAnyThread(
[&](TR::LockedRWFromAnyThread& aData) {
if (profiler_current_thread_id() == testThreadId) {
EXPECT_TRUE(TR::IsDataMutexLockedOnCurrentThread());
}
TestLockedRWFromAnyThread(aData, beforeRegistration,
afterRegistration, &onStackChar,
testThreadId);
});
EXPECT_FALSE(TR::IsDataMutexLockedOnCurrentThread());
// LockedRWOnThread
// Note: It can never be accessed off the thread.
};
int ranTest = 0;
TRy::WithOffThreadRef(testThreadId, [&](
TRy::OffThreadRef aOff
ThreadRef) {
TestOffThreadRef(aOffThreadRef);
++ranTest;
});
EXPECT_EQ(ranTest, 1);
EXPECT_TRUE(TRy::WithOffThreadRefOr(
testThreadId,
[&](TRy::OffThreadRef aOffThreadRef) {
TestOffThreadRef(aOffThreadRef);
return true;
},
false));
ranTest = 0;
EXPECT_FALSE(TRy::IsRegistryMutexLockedOnCurrentThread());
for (TRy::OffThreadRef offThreadRef : TRy::LockedRegistry{}) {
EXPECT_TRUE(TRy::IsRegistryMutexLockedOnCurrentThread() ||
!TR::IsRegistered());
if (offThreadRef.UnlockedConstReaderCRef().Info().ThreadId() ==
testThreadId) {
TestOffThreadRef(offThreadRef);
++ranTest;
}
}
EXPECT_EQ(ranTest, 1);
EXPECT_FALSE(TRy::IsRegistryMutexLockedOnCurrentThread());
{
ranTest = 0;
EXPECT_FALSE(TRy::IsRegistryMutexLockedOnCurrentThread());
TRy::LockedRegistry lockedRegistry{};
EXPECT_TRUE(TRy::IsRegistryMutexLockedOnCurrentThread() ||
!TR::IsRegistered());
for (TRy::OffThreadRef offThreadRef : lockedRegistry) {
if (offThreadRef.UnlockedConstReaderCRef().Info().ThreadId() ==
testThreadId) {
TestOffThreadRef(offThreadRef);
++ranTest;
}
}
EXPECT_EQ(ranTest, 1);
}
EXPECT_FALSE(TRy::IsRegistryMutexLockedOnCurrentThread());
};
// Test on the current thread.
testThroughRegistry();
// Test from another thread.
std::thread otherThread([&]() {
ASSERT_NE(profiler_current_thread_id(), testThreadId);
testThroughRegistry();
// Test that this unregistered thread is really not registered.
int ranTest = 0;
TRy::WithOffThreadRef(
profiler_current_thread_id(),
[&](TRy::OffThreadRef aOffThreadRef) { ++ranTest; });
EXPECT_EQ(ranTest, 0);
EXPECT_FALSE(TRy::WithOffThreadRefOr(
profiler_current_thread_id(),
[&](TRy::OffThreadRef aOffThreadRef) {
++ranTest;
return true;
},
false));
EXPECT_EQ(ranTest, 0);
});
otherThread.join();
});
testThread.join();
}
TEST(GeckoProfiler, ThreadRegistration_RegistrationEdgeCases)
{
using TR = profiler::ThreadRegistration;
using TRy = profiler::ThreadRegistry;
profiler_init_main_thread_id();
ASSERT_TRUE(profiler_is_main_thread())
<< "This test assumes it runs on the main thread";
// Note that the main thread could already be registered, so we work in a new
// thread to test an actual registration that we control.
int registrationCount = 0;
int otherThreadLoops = 0;
int otherThreadReads = 0;
// This thread will register and unregister in a loop, with some pauses.
// Another thread will attempty to access the test thread, and lock its data.
// The main goal is to check edges cases around (un)registrations.
std::thread testThread([&]() {
const ProfilerThreadId testThreadId = profiler_current_thread_id();
const TimeStamp endTestAt = TimeStamp::Now() + TimeDuration::FromSeconds(1);
std::thread otherThread([&]() {
// Initial sleep so that testThread can start its loop.
PR_Sleep(PR_MillisecondsToInterval(1));
while (TimeStamp::Now() < endTestAt) {
++otherThreadLoops;
TRy::WithOffThreadRef(testThreadId, [&](TRy::OffThreadRef
aOffThreadRef) {
if (otherThreadLoops % 1000 == 0) {
PR_Sleep(PR_MillisecondsToInterval(1));
}
TRy::OffThreadRef::RWFromAnyThreadWithLock rwFromAnyThreadWithLock =
aOffThreadRef.GetLockedRWFromAnyThread();
++otherThreadReads;
if (otherThreadReads % 1000 == 0) {
PR_Sleep(PR_MillisecondsToInterval(1));
}
});
}
});
while (TimeStamp::Now() < endTestAt) {
ASSERT_FALSE(TR::IsRegistered())
<< "A new std::thread should not start registered";
EXPECT_FALSE(TR::GetOnThreadPtr());
EXPECT_FALSE(TR::WithOnThreadRefOr([&](auto) { return true; }, false));
char onStackChar;
TR tr{"Test thread", &onStackChar};
++registrationCount;
ASSERT_TRUE(TR::IsRegistered());
int ranTest = 0;
TRy::WithOffThreadRef(testThreadId, [&](TRy::OffThreadRef aOffThreadRef) {
if (registrationCount % 2000 == 0) {
PR_Sleep(PR_MillisecondsToInterval(1));
}
++ranTest;
});
EXPECT_EQ(ranTest, 1);
if (registrationCount % 1000 == 0) {
PR_Sleep(PR_MillisecondsToInterval(1));
}
}
otherThread.join();
});
testThread.join();
// It's difficult to guess what these numbers should be, but they definitely
// should be non-zero. The main goal was to test that nothing goes wrong.
EXPECT_GT(registrationCount, 0);
EXPECT_GT(otherThreadLoops, 0);
EXPECT_GT(otherThreadReads, 0);
}
#ifdef MOZ_GECKO_PROFILER
// Common JSON checks.
// Check that the given JSON string include no JSON whitespace characters
// (excluding those in property names and strings).
void JSONWhitespaceCheck(const char* aOutput) {
ASSERT_NE(aOutput, nullptr);
enum class State { Data, String, StringEscaped };
State state = State::Data;
size_t length = 0;
size_t whitespaces = 0;
for (const char* p = aOutput; *p != '\0'; ++p) {
++length;
const char c = *p;
switch (state) {
case State::Data:
if (c == '\n' || c == '\r' || c == ' ' || c == '\t') {
++whitespaces;
} else if (c == '"') {
state = State::String;
}
break;
case State::String:
if (c == '"') {
state = State::Data;
} else if (c == '\\') {
state = State::StringEscaped;
}
break;
case State::StringEscaped:
state = State::String;
break;
}
}
EXPECT_EQ(whitespaces, 0u);
EXPECT_GT(length, 0u);
}
// Does the GETTER return a non-null TYPE? (Non-critical)
# define EXPECT_HAS_JSON(GETTER, TYPE) \
do { \
if ((GETTER).isNull()) { \
EXPECT_FALSE((GETTER).isNull()) \
<< #GETTER " doesn't exist or is null"; \
} else if (!(GETTER).is##TYPE()) { \
EXPECT_TRUE((GETTER).is##TYPE()) \
<< #GETTER " didn't return type " #TYPE; \
} \
} while (false)
// Does the GETTER return a non-null TYPE? (Critical)
# define ASSERT_HAS_JSON(GETTER, TYPE) \
do { \
ASSERT_FALSE((GETTER).isNull()); \
ASSERT_TRUE((GETTER).is##TYPE()); \
} while (false)
// Does the GETTER return a non-null TYPE? (Critical)
// If yes, store the reference to Json::Value into VARIABLE.
# define GET_JSON(VARIABLE, GETTER, TYPE) \
ASSERT_HAS_JSON(GETTER, TYPE); \
const Json::Value& VARIABLE = (GETTER)
// Does the GETTER return a non-null TYPE? (Critical)
// If yes, store the value as `const TYPE` into VARIABLE.
# define GET_JSON_VALUE(VARIABLE, GETTER, TYPE) \
ASSERT_HAS_JSON(GETTER, TYPE); \
const auto VARIABLE = (GETTER).as##TYPE()
// Non-const GET_JSON_VALUE.
# define GET_JSON_MUTABLE_VALUE(VARIABLE, GETTER, TYPE) \
ASSERT_HAS_JSON(GETTER, TYPE); \
auto VARIABLE = (GETTER).as##TYPE()
// Checks that the GETTER's value is present, is of the expected TYPE, and has
// the expected VALUE. (Non-critical)
# define EXPECT_EQ_JSON(GETTER, TYPE, VALUE) \
do { \
if ((GETTER).isNull()) { \
EXPECT_FALSE((GETTER).isNull()) \
<< #GETTER " doesn't exist or is null"; \
} else if (!(GETTER).is##TYPE()) { \
EXPECT_TRUE((GETTER).is##TYPE()) \
<< #GETTER " didn't return type " #TYPE; \
} else { \
EXPECT_EQ((GETTER).as##TYPE(), (VALUE)); \
} \
} while (false)
// Checks that the GETTER's value is present, and is a valid index into the
// STRINGTABLE array, pointing at the expected STRING.
# define EXPECT_EQ_STRINGTABLE(GETTER, STRINGTABLE, STRING) \
do { \
if ((GETTER).isNull()) { \
EXPECT_FALSE((GETTER).isNull()) \
<< #GETTER " doesn't exist or is null"; \
} else if (!(GETTER).isUInt()) { \
EXPECT_TRUE((GETTER).isUInt()) << #GETTER " didn't return an index"; \
} else { \
EXPECT_LT((GETTER).asUInt(), (STRINGTABLE).size()); \
EXPECT_EQ_JSON((STRINGTABLE)[(GETTER).asUInt()], String, (STRING)); \
} \
} while (false)
# define EXPECT_JSON_ARRAY_CONTAINS(GETTER, TYPE, VALUE) \
do { \
if ((GETTER).isNull()) { \
EXPECT_FALSE((GETTER).isNull()) \
<< #GETTER " doesn't exist or is null"; \
} else if (!(GETTER).isArray()) { \
EXPECT_TRUE((GETTER).is##TYPE()) << #GETTER " is not an array"; \
} else if (const Json::ArrayIndex size = (GETTER).size(); size == 0u) { \
EXPECT_NE(size, 0u) << #GETTER " is an empty array"; \
} else { \
bool found = false; \
for (Json::ArrayIndex i = 0; i < size; ++i) { \
if (!(GETTER)[i].is##TYPE()) { \
EXPECT_TRUE((GETTER)[i].is##TYPE()) \
<< #GETTER "[" << i << "] is not " #TYPE; \
break; \
} \
if ((GETTER)[i].as##TYPE() == (VALUE)) { \
found = true; \
break; \
} \
} \
EXPECT_TRUE(found) << #GETTER " doesn't contain " #VALUE; \
} \
} while (false)
# define EXPECT_JSON_ARRAY_EXCLUDES(GETTER, TYPE, VALUE) \
do { \
if ((GETTER).isNull()) { \
EXPECT_FALSE((GETTER).isNull()) \
<< #GETTER " doesn't exist or is null"; \
} else if (!(GETTER).isArray()) { \
EXPECT_TRUE((GETTER).is##TYPE()) << #GETTER " is not an array"; \
} else { \
const Json::ArrayIndex size = (GETTER).size(); \
for (Json::ArrayIndex i = 0; i < size; ++i) { \
if (!(GETTER)[i].is##TYPE()) { \
EXPECT_TRUE((GETTER)[i].is##TYPE()) \
<< #GETTER "[" << i << "] is not " #TYPE; \
break; \
} \
if ((GETTER)[i].as##TYPE() == (VALUE)) { \
EXPECT_TRUE((GETTER)[i].as##TYPE() != (VALUE)) \
<< #GETTER " contains " #VALUE; \
break; \
} \
} \
} \
} while (false)
// Check that the given process root contains all the expected properties.
static void JSONRootCheck(const Json::Value& aRoot,
bool aWithMainThread = true) {
ASSERT_TRUE(aRoot.isObject());
EXPECT_HAS_JSON(aRoot["libs"], Array);
GET_JSON(meta, aRoot["meta"], Object);
EXPECT_HAS_JSON(meta["version"], UInt);
EXPECT_HAS_JSON(meta["startTime"], Double);
EXPECT_HAS_JSON(meta["profilingStartTime"], Double);
EXPECT_HAS_JSON(meta["contentEarliestTime"], Double);
EXPECT_HAS_JSON(meta["profilingEndTime"], Double);
EXPECT_HAS_JSON(aRoot["pages"], Array);
// "counters" is only present if there is any data to report.
// Test that expect "counters" should test for its presence first.
if (aRoot.isMember("counters")) {
// We have "counters", test their overall validity.
GET_JSON(counters, aRoot["counters"], Array);
for (const Json::Value& counter : counters) {
ASSERT_TRUE(counter.isObject());
EXPECT_HAS_JSON(counter["name"], String);
EXPECT_HAS_JSON(counter["category"], String);
EXPECT_HAS_JSON(counter["description"], String);
GET_JSON(samples, counter["samples"], Object);
GET_JSON(samplesSchema, samples["schema"], Object);
EXPECT_GE(samplesSchema.size(), 3u);
GET_JSON_VALUE(samplesTime, samplesSchema["time"], UInt);
GET_JSON_VALUE(samplesNumber, samplesSchema["number"], UInt);
GET_JSON_VALUE(samplesCount, samplesSchema["count"], UInt);
GET_JSON(samplesData, samples["data"], Array);
double previousTime = 0.0;
for (const Json::Value& sample : samplesData) {
ASSERT_TRUE(sample.isArray());
GET_JSON_VALUE(time, sample[samplesTime], Double);
EXPECT_GE(time, previousTime);
previousTime = time;
if (sample.isValidIndex(samplesNumber)) {
EXPECT_HAS_JSON(sample[samplesNumber], UInt64);
}
if (sample.isValidIndex(samplesCount)) {
EXPECT_HAS_JSON(sample[samplesCount], Int64);
}
}
}
}
GET_JSON(threads, aRoot["threads"], Array);
const Json::ArrayIndex threadCount = threads.size();
for (Json::ArrayIndex i = 0; i < threadCount; ++i) {
GET_JSON(thread, threads[i], Object);
EXPECT_HAS_JSON(thread["processType"], String);
EXPECT_HAS_JSON(thread["name"], String);
EXPECT_HAS_JSON(thread["registerTime"], Double);
GET_JSON(samples, thread["samples"], Object);
EXPECT_HAS_JSON(thread["markers"], Object);
EXPECT_HAS_JSON(thread["pid"], Int64);
EXPECT_HAS_JSON(thread["tid"], Int64);
GET_JSON(stackTable, thread["stackTable"], Object);
GET_JSON(frameTable, thread["frameTable"], Object);
GET_JSON(stringTable, thread["stringTable"], Array);
GET_JSON(stackTableSchema, stackTable["schema"], Object);
EXPECT_GE(stackTableSchema.size(), 2u);
GET_JSON_VALUE(stackTablePrefix, stackTableSchema["prefix"], UInt);
GET_JSON_VALUE(stackTableFrame, stackTableSchema["frame"], UInt);
GET_JSON(stackTableData, stackTable["data"], Array);
GET_JSON(frameTableSchema, frameTable["schema"], Object);
EXPECT_GE(frameTableSchema.size(), 1u);
GET_JSON_VALUE(frameTableLocation, frameTableSchema["location"], UInt);
GET_JSON(frameTableData, frameTable["data"], Array);
GET_JSON(samplesSchema, samples["schema"], Object);
GET_JSON_VALUE(sampleStackIndex, samplesSchema["stack"], UInt);
GET_JSON(samplesData, samples["data"], Array);
for (const Json::Value& sample : samplesData) {
ASSERT_TRUE(sample.isArray());
if (sample.isValidIndex(sampleStackIndex)) {
if (!sample[sampleStackIndex].isNull()) {
GET_JSON_MUTABLE_VALUE(stack, sample[sampleStackIndex], UInt);
EXPECT_TRUE(stackTableData.isValidIndex(stack));
for (;;) {
// `stack` (from the sample, or from the callee frame's "prefix" in
// the previous loop) points into the stackTable.
GET_JSON(stackTableEntry, stackTableData[stack], Array);
GET_JSON_VALUE(frame, stackTableEntry[stackTableFrame], UInt);
// The stackTable entry's "frame" points into the frameTable.
EXPECT_TRUE(frameTableData.isValidIndex(frame));
GET_JSON(frameTableEntry, frameTableData[frame], Array);
GET_JSON_VALUE(location, frameTableEntry[frameTableLocation], UInt);
// The frameTable entry's "location" points at a string.
EXPECT_TRUE(stringTable.isValidIndex(location));
// The stackTable entry's "prefix" is null for the root frame.
if (stackTableEntry[stackTablePrefix].isNull()) {
break;
}
// Otherwise it recursively points at the caller in the stackTable.
GET_JSON_VALUE(prefix, stackTableEntry[stackTablePrefix], UInt);
EXPECT_TRUE(stackTableData.isValidIndex(prefix));
stack = prefix;
}
}
}
}
}
if (aWithMainThread) {
ASSERT_GT(threadCount, 0u);
GET_JSON(thread0, threads[0], Object);
EXPECT_EQ_JSON(thread0["name"], String, "GeckoMain");
}
EXPECT_HAS_JSON(aRoot["pausedRanges"], Array);
const Json::Value& processes = aRoot["processes"];
if (!processes.isNull()) {
ASSERT_TRUE(processes.isArray());
const Json::ArrayIndex processCount = processes.size();
for (Json::ArrayIndex i = 0; i < processCount; ++i) {
GET_JSON(process, processes[i], Object);
JSONRootCheck(process, aWithMainThread);
}
}
GET_JSON(profilingLog, aRoot["profilingLog"], Object);
EXPECT_EQ(profilingLog.size(), 1u);
for (auto it = profilingLog.begin(); it != profilingLog.end(); ++it) {
// The key should be a pid.
const auto key = it.name();
for (const auto letter : key) {
EXPECT_GE(letter, '0');
EXPECT_LE(letter, '9');
}
// And the value should be an object.
GET_JSON(logForPid, profilingLog[key], Object);
// Its content is not defined, but we expect at least these:
EXPECT_HAS_JSON(logForPid["profilingLogBegin_TSms"], Double);
EXPECT_HAS_JSON(logForPid["profilingLogEnd_TSms"], Double);
}
}
// Check that various expected top properties are in the JSON, and then call the
// provided `aJSONCheckFunction` with the JSON root object.
template <typename JSONCheckFunction>
void JSONOutputCheck(const char* aOutput,
JSONCheckFunction&& aJSONCheckFunction) {
ASSERT_NE(aOutput, nullptr);
JSONWhitespaceCheck(aOutput);
// Extract JSON.
Json::Value parsedRoot;
Json::CharReaderBuilder builder;
const std::unique_ptr<Json::CharReader> reader(builder.newCharReader());
ASSERT_TRUE(
reader->parse(aOutput, strchr(aOutput, '\0'), &parsedRoot, nullptr));
JSONRootCheck(parsedRoot);
std::forward<JSONCheckFunction>(aJSONCheckFunction)(parsedRoot);
}
// Returns `static_cast<SamplingState>(-1)` if callback could not be installed.
static SamplingState WaitForSamplingState() {
Atomic<int> samplingState{-1};
if (!profiler_callback_after_sampling([&](SamplingState aSamplingState) {
samplingState = static_cast<int>(aSamplingState);
})) {
return static_cast<SamplingState>(-1);
}
while (samplingState == -1) {
}
return static_cast<SamplingState>(static_cast<int>(samplingState));
}
typedef Vector<const char*> StrVec;
static void InactiveFeaturesAndParamsCheck() {
int entries;
Maybe<double> duration;
double interval;
uint32_t features;
StrVec filters;
uint64_t activeTabID;
ASSERT_TRUE(!profiler_is_active());
ASSERT_TRUE(!profiler_feature_active(ProfilerFeature::MainThreadIO));
ASSERT_TRUE(!profiler_feature_active(ProfilerFeature::NativeAllocations));
profiler_get_start_params(&entries, &duration, &interval, &features, &filters,
&activeTabID);
ASSERT_TRUE(entries == 0);
ASSERT_TRUE(duration == Nothing());
ASSERT_TRUE(interval == 0);
ASSERT_TRUE(features == 0);
ASSERT_TRUE(filters.empty());
ASSERT_TRUE(activeTabID == 0);
}
static void ActiveParamsCheck(int aEntries, double aInterval,
uint32_t aFeatures, const char** aFilters,
size_t aFiltersLen, uint64_t aActiveTabID,
const Maybe<double>& aDuration = Nothing()) {
int entries;
Maybe<double> duration;
double interval;
uint32_t features;
StrVec filters;
uint64_t activeTabID;
profiler_get_start_params(&entries, &duration, &interval, &features, &filters,
&activeTabID);
ASSERT_TRUE(entries == aEntries);
ASSERT_TRUE(duration == aDuration);
ASSERT_TRUE(interval == aInterval);
ASSERT_TRUE(features == aFeatures);
ASSERT_TRUE(filters.length() == aFiltersLen);
ASSERT_TRUE(activeTabID == aActiveTabID);
for (size_t i = 0; i < aFiltersLen; i++) {
ASSERT_TRUE(strcmp(filters[i], aFilters[i]) == 0);
}
}
TEST(GeckoProfiler, FeaturesAndParams)
{
InactiveFeaturesAndParamsCheck();
// Try a couple of features and filters.
{
uint32_t features = ProfilerFeature::JS;
const char* filters[] = {"GeckoMain", "Compositor"};
# define PROFILER_DEFAULT_DURATION 20 /* seconds, for tests only */
profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL,
features, filters, std::size(filters), 100,
Some(PROFILER_DEFAULT_DURATION));
ASSERT_TRUE(profiler_is_active());
ASSERT_TRUE(!profiler_feature_active(ProfilerFeature::MainThreadIO));
ASSERT_TRUE(!profiler_feature_active(ProfilerFeature::IPCMessages));
ActiveParamsCheck(PROFILER_DEFAULT_ENTRIES.Value(),
PROFILER_DEFAULT_INTERVAL, features, filters,
std::size(filters), 100, Some(PROFILER_DEFAULT_DURATION));
profiler_stop();
InactiveFeaturesAndParamsCheck();
}
// Try some different features and filters.
{
uint32_t features =
ProfilerFeature::MainThreadIO | ProfilerFeature::IPCMessages;
const char* filters[] = {"GeckoMain", "Foo", "Bar"};
// Testing with some arbitrary buffer size (as could be provided by
// external code), which we convert to the appropriate power of 2.
--> --------------------
--> maximum size reached
--> --------------------