/* -*- 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.
#ifdefined(_MSC_VER) || defined(__MINGW32__) # include <processthreadsapi.h> # include <realtimeapiset.h> #elifdefined(__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()).
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);
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";
}
}
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?";
#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); #elifdefined(__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";
};
// 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*>);
};
// 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>);
// 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) { returntrue; }, false));
// 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;
// Get the thread name, as registered in the PRThread, nullptr on failure. staticconstchar* 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);
}
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_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());
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());
// 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());
}
TEST(GeckoProfiler, ThreadRegistry_DataAccess)
{ using TR = profiler::ThreadRegistration; usingTRy = 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) { returntrue; }, false));
// 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 testThroughRegistry = [&]() { auto TestOffThreadRef = [&](TRy::OffThreadRef aOffThreadRef) { // To test const-qualified member functions. constTRy::OffThreadRef& offThreadCRef = aOffThreadRef;
// 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);
TEST(GeckoProfiler, ThreadRegistration_RegistrationEdgeCases)
{ using TR = profiler::ThreadRegistration; usingTRy = 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();
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) { returntrue; }, false));
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(constchar* aOutput) {
ASSERT_NE(aOutput, nullptr);
enumclass State { Data, String, StringEscaped };
State state = State::Data;
size_t length = 0;
size_t whitespaces = 0; for (constchar* p = aOutput; *p != '\0'; ++p) {
++length; constchar c = *p;
switch (state) { case State::Data: if (c == '\n' || c == '\r' || c == ' ' || c == '\t') {
++whitespaces;
} elseif (c == '"') {
state = State::String;
} break;
case State::String: if (c == '"') {
state = State::Data;
} elseif (c == '\\') {
state = State::StringEscaped;
} break;
case State::StringEscaped:
state = State::String; break;
}
}
// 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"; \
} elseif (!(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); \ constauto 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"; \
} elseif (!(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"; \
} elseif (!(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"; \
} elseif (!(GETTER).isArray()) { \
EXPECT_TRUE((GETTER).is##TYPE()) << #GETTER" is not an array"; \
} elseif (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"; \
} elseif (!(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. staticvoid JSONRootCheck(const Json::Value& aRoot, bool aWithMainThread = true) {
ASSERT_TRUE(aRoot.isObject());
// "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(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;
}
}
}
}
}
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. constauto key = it.name(); for (constauto 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(constchar* aOutput,
JSONCheckFunction&& aJSONCheckFunction) {
ASSERT_NE(aOutput, nullptr);
// Returns `static_cast<SamplingState>(-1)` if callback could not be installed. static SamplingState WaitForSamplingState() {
Atomic<int> samplingState{-1};
// Try some different features and filters.
{
uint32_t features =
ProfilerFeature::MainThreadIO | ProfilerFeature::IPCMessages; constchar* filters[] = {"GeckoMain", "Foo", "Bar"};
// Testing with some arbitrary buffer size (as could be provided by // external code), which we convert to the appropriate power of 2.
profiler_start(PowerOfTwo32(999999), 3, features, filters,
std::size(filters), 123, Some(25.0));
// Try all supported features, and filters that match all threads.
{
uint32_t availableFeatures = profiler_get_available_features(); constchar* filters[] = {""};
// Turn off tracing because it mucks with other features
availableFeatures &= ~ProfilerFeature::Tracing;
// Try no features, and filters that match no threads.
{
uint32_t features = 0; constchar* filters[] = {"NoThreadWillMatchThis"};
// Second profiler_start() call in a row without an intervening // profiler_stop(); this will do an implicit profiler_stop() and restart.
profiler_start(PowerOfTwo32(0), 0, features, filters, std::size(filters), 0,
Some(0.0));
// 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();
// 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));
// 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);
}
// 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);
// 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.
// Control the profiler on a background thread and verify flags on the // main thread.
{
uint32_t features = ProfilerFeature::JS; constchar* filters[] = {"GeckoMain", "Compositor"};
// Control the profiler on the main thread and verify flags on a // background thread.
{
uint32_t features = ProfilerFeature::JS; constchar* filters[] = {"GeckoMain", "Compositor"};
// These will be destroyed while the profiler is active. staticconstint 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; constchar* 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();
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);
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();
}
// Create three strings: two that are the maximum allowed length, and one that // is one char longer. staticconst 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<constchar> MarkerTypeName() { return MakeStringSpan("markers-gtest");
} staticvoid StreamJSONMarkerData(
mozilla::baseprofiler::SpliceableJSONWriter& aWriter, int aInt, double aDouble, const mozilla::ProfilerString8View& aText, const mozilla::ProfilerString8View& aUniqueText, const mozilla::TimeStamp& aTime) {
aWriter.NullProperty("null");
aWriter.BoolProperty("bool-false", false);
aWriter.BoolProperty("bool-true", true);
aWriter.IntProperty("int", aInt);
aWriter.DoubleProperty("double", aDouble);
aWriter.StringProperty("text", aText);
aWriter.UniqueStringProperty("unique text", aUniqueText);
aWriter.UniqueStringProperty("unique text again", aUniqueText);
aWriter.TimeProperty("time", aTime);
} static mozilla::MarkerSchema MarkerTypeDisplay() { // Note: This is an test function that is not intended to actually output // that correctly matches StreamJSONMarkerData data above! Instead we only // test that it outputs the expected JSON at the end. using MS = mozilla::MarkerSchema;
MS schema{MS::Location::MarkerChart, MS::Location::MarkerTable,
MS::Location::TimelineOverview, MS::Location::TimelineMemory,
MS::Location::TimelineIPC, MS::Location::TimelineFileIO,
MS::Location::StackChart}; // All label functions.
schema.SetChartLabel("chart label");
schema.SetTooltipLabel("tooltip label");
schema.SetTableLabel("table label"); // All data functions, all formats, all "searchable" values.
schema.AddKeyFormat("key with url", MS::Format::Url);
schema.AddKeyLabelFormat("key with label filePath", "label filePath",
MS::Format::FilePath);
schema.AddKeyFormatSearchable("key with string not-searchable",
MS::Format::String,
MS::Searchable::NotSearchable);
schema.AddKeyLabelFormatSearchable("key with label duration searchable", "label duration", MS::Format::Duration,
MS::Searchable::Searchable);
schema.AddKeyFormat("key with time", MS::Format::Time);
schema.AddKeyFormat("key with seconds", MS::Format::Seconds);
schema.AddKeyFormat("key with milliseconds", MS::Format::Milliseconds);
schema.AddKeyFormat("key with microseconds", MS::Format::Microseconds);
schema.AddKeyFormat("key with nanoseconds", MS::Format::Nanoseconds);
schema.AddKeyFormat("key with bytes", MS::Format::Bytes);
schema.AddKeyFormat("key with percentage", MS::Format::Percentage);
schema.AddKeyFormat("key with integer", MS::Format::Integer);
schema.AddKeyFormat("key with decimal", MS::Format::Decimal);
schema.AddStaticLabelValue("static label", "static value");
schema.AddKeyFormat("key with unique string", MS::Format::UniqueString);
schema.AddKeyFormatSearchable("key with sanitized string",
MS::Format::SanitizedString,
MS::Searchable::Searchable); return schema;
}
};
EXPECT_TRUE(profiler_add_marker_impl( "Gtest custom marker", geckoprofiler::category::OTHER,
MarkerTiming::Interval(ts1, ts2), GtestMarker{}, 42, 43.0, "gtest text", "gtest unique text", ts1));
// User-defined marker type with no data, special frontend schema. struct GtestSpecialMarker { static constexpr Span<constchar> MarkerTypeName() { return MakeStringSpan("markers-gtest-special");
} staticvoid 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<constchar> MarkerTypeName() { return MakeStringSpan("markers-gtest-unused");
} staticvoid StreamJSONMarkerData(
mozilla::baseprofiler::SpliceableJSONWriter& aWriter) {} static mozilla::MarkerSchema MarkerTypeDisplay() { return mozilla::MarkerSchema::SpecialFrontendLocation{};
}
};
// Make sure the compiler doesn't complain about this unused struct.
mozilla::Unused << GtestUnusedMarker{};
// Other markers in alphabetical order of payload class names.
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();
// 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')));
// 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;
// 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());
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();
if (marker.size() == SIZE_WITHOUT_PAYLOAD) { // root.threads[0].markers.data[i] is an array with 5 elements, // so there is no payload. if (nameString == "M1") {
ASSERT_EQ(state, S_M1);
state = State(state + 1);
} elseif (nameString == "M3") {
ASSERT_EQ(state, S_M3);
state = State(state + 1);
} elseif (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
} elseif (nameString == "default-templated markers 2.0 with option") {
EXPECT_EQ(state, S_Markers2DefaultWithOptions);
state = State(S_Markers2DefaultWithOptions + 1); # endif
} elseif (nameString == "explicitly-default-templated markers 2.0 with " "empty " "options") {
EXPECT_EQ(state, S_Markers2ExplicitDefaultEmptyOptions);
state = State(S_Markers2ExplicitDefaultEmptyOptions + 1);
} elseif (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());
} elseif (nameString == "auto tracing") { switch (state) { case S_tracing_auto_tracing_start:
state = State(S_tracing_auto_tracing_start + 1);
EXPECT_EQ(typeString, "tracing");
EXPECT_TIMING_START;
EXPECT_EQ_JSON(payload["category"], String, "C");
EXPECT_TRUE(payload["stack"].isNull()); break; case S_tracing_auto_tracing_end:
state = State(S_tracing_auto_tracing_end + 1);
EXPECT_EQ(typeString, "tracing");
EXPECT_TIMING_END;
EXPECT_EQ_JSON(payload["category"], String, "C");
ASSERT_TRUE(payload["stack"].isNull()); break; default:
EXPECT_TRUE(state == S_tracing_auto_tracing_start ||
state == S_tracing_auto_tracing_end); break;
}
} elseif (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());
} elseif (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");
} elseif (nameString == "Gtest custom marker") {
EXPECT_EQ(state, S_CustomMarker);
state = State(S_CustomMarker + 1);
EXPECT_EQ(typeString, "markers-gtest");
EXPECT_EQ(payload.size(), 1u + 9u);
EXPECT_TRUE(payload["null"].isNull());
EXPECT_EQ_JSON(payload["bool-false"], Bool, false);
EXPECT_EQ_JSON(payload["bool-true"], Bool, true);
EXPECT_EQ_JSON(payload["int"], Int64, 42);
EXPECT_EQ_JSON(payload["double"], Double, 43.0);
EXPECT_EQ_JSON(payload["text"], String, "gtest text"); // Unique strings can be fetched from the string table.
ASSERT_TRUE(payload["unique text"].isUInt()); auto textIndex = payload["unique text"].asUInt();
GET_JSON(uniqueText, stringTable[textIndex], String);
ASSERT_TRUE(uniqueText.isString());
ASSERT_EQ(uniqueText.asString(), "gtest unique text"); // The duplicate unique text should have the exact same index.
EXPECT_EQ_JSON(payload["unique text again"], UInt, textIndex);
EXPECT_EQ_JSON(payload["time"], Double, ts1Double);
} elseif (nameString == "Gtest special marker") {
EXPECT_EQ(state, S_SpecialMarker);
state = State(S_SpecialMarker + 1);
EXPECT_EQ(typeString, "markers-gtest-special");
EXPECT_EQ(payload.size(), 1u) << "Only 'type' in the payload";
EXPECT_TRUE(
testedSchemaNames
.insert(std::string(nameString.data(), nameString.size()))
.second)
<< "Each schema name should be unique (inserted once in the set)";
// 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"
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_start() restarts the timer used by profiler_time().
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. constbool hasStackWalk = ProfilerFeature::HasStackWalk(*activeFeatures);
// 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; constchar* filters[] = {"GeckoMain"};
// Start the profiler on the main thread.
profiler_start(PROFILER_DEFAULT_ENTRIES, PROFILER_DEFAULT_INTERVAL, features,
filters, std::size(filters), 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) {}
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.
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_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.
// 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);
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();
// 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());
// 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) {});
TEST(GeckoProfiler, CPUUsage)
{
profiler_init_main_thread_id();
ASSERT_TRUE(profiler_is_main_thread())
<< "This test assumes it runs on the main thread";
enumclass 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 (constbool 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 constchar* 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 (constchar* 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);
}
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 (constchar* sampleLabel : scSampleLabels) {
EXPECT_TRUE(strstr(profile.get(), sampleLabel));
}
}
{
GET_JSON(sampleUnits, meta["sampleUnits"], Object);
{
EXPECT_EQ_JSON(sampleUnits["time"], String, "ms");
EXPECT_EQ_JSON(sampleUnits["eventDelay"], String, "ms"); # ifdefined(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
}
}
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);
}
# ifdefined(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
}
} elseif (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
} elseif (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
}
}
// 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.
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?";
// 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));
}
});
// 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);
}
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.
} elseif (name.asString() == "Selected test thread") {
++foundSelected;
GET_JSON(markers, thread["markers"], Object);
GET_JSON(markersData, markers["data"], Array); for (const Json::Value& marker : markersData) { constunsignedint 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";
// 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);
// 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);
// 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{}, ""));
// 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);
{ constunsignedint NAME = 0u; constunsignedint PAYLOAD = 5u; bool foundMarker = false;
GET_JSON(stringTable, thread0["stringTable"], Array);
for (const Json::Value& marker : data) { // Even though we only added one marker, some markers like // NotifyObservers are being added as well. Let's iterate over // them and make sure that we have the one we added explicitly and // check its stack doesn't exist.
GET_JSON(name, stringTable[marker[NAME].asUInt()], String);
std::string nameString = name.asString();
if (nameString == "Text with stack") { // Make sure that the marker doesn't have a stack.
foundMarker = true;
EXPECT_FALSE(marker[PAYLOAD].isNull());
EXPECT_TRUE(marker[PAYLOAD]["stack"].isNull());
}
}
EXPECT_TRUE(foundMarker);
}
}
}
}
});
profiler_stop();
ASSERT_TRUE(!profiler_get_profile());
}
#endif// MOZ_GECKO_PROFILER
Messung V0.5 in Prozent
¤ Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.0.122Bemerkung:
(vorverarbeitet am 2026-05-08)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.