Bug 1674476 - ChromeUtils.addProfilerMarker should support capturing a stack or setting the category, r=gerald,gregtatum.

Differential Revision: https://phabricator.services.mozilla.com/D95406
This commit is contained in:
Florian Quèze 2020-11-20 15:41:16 +00:00
parent 4db97d1029
commit 0d164d3a9f
6 changed files with 257 additions and 12 deletions

View File

@ -193,12 +193,37 @@ void ChromeUtils::ReleaseAssert(GlobalObject& aGlobal, bool aCondition,
/* static */
void ChromeUtils::AddProfilerMarker(
GlobalObject& aGlobal, const nsACString& aName,
const Optional<DOMHighResTimeStamp>& aStartTime,
const ProfilerMarkerOptionsOrDouble& aOptions,
const Optional<nsACString>& aText) {
#ifdef MOZ_GECKO_PROFILER
MarkerOptions options;
MarkerCategory category = ::geckoprofiler::category::JS;
if (aStartTime.WasPassed()) {
DOMHighResTimeStamp startTime = 0;
if (aOptions.IsDouble()) {
startTime = aOptions.GetAsDouble();
} else {
const ProfilerMarkerOptions& opt = aOptions.GetAsProfilerMarkerOptions();
startTime = opt.mStartTime;
if (opt.mCaptureStack) {
options.Set(MarkerStack::Capture());
}
# define BEGIN_CATEGORY(name, labelAsString, color) \
if (opt.mCategory.Equals(labelAsString)) { \
category = ::geckoprofiler::category::name; \
} else
# define SUBCATEGORY(supercategory, name, labelAsString)
# define END_CATEGORY
MOZ_PROFILING_CATEGORY_LIST(BEGIN_CATEGORY, SUBCATEGORY, END_CATEGORY)
# undef BEGIN_CATEGORY
# undef SUBCATEGORY
# undef END_CATEGORY
{
category = ::geckoprofiler::category::OTHER;
}
}
if (startTime) {
RefPtr<Performance> performance;
if (NS_IsMainThread()) {
@ -218,18 +243,22 @@ void ChromeUtils::AddProfilerMarker(
if (performance) {
options.Set(MarkerTiming::IntervalUntilNowFrom(
performance->CreationTimeStamp() +
TimeDuration::FromMilliseconds(aStartTime.Value())));
TimeDuration::FromMilliseconds(startTime)));
} else {
options.Set(MarkerTiming::IntervalUntilNowFrom(
TimeStamp::ProcessCreation() +
TimeDuration::FromMilliseconds(aStartTime.Value())));
TimeDuration::FromMilliseconds(startTime)));
}
}
{
AUTO_PROFILER_STATS(ChromeUtils::AddProfilerMarker);
if (aText.WasPassed()) {
PROFILER_MARKER_TEXT(aName, JS, std::move(options), aText.Value());
profiler_add_marker(aName, category, std::move(options),
::geckoprofiler::markers::Text{}, aText.Value());
} else {
PROFILER_MARKER_UNTYPED(aName, JS, std::move(options));
profiler_add_marker(aName, category, std::move(options));
}
}
#endif // MOZ_GECKO_PROFILER
}

View File

@ -83,7 +83,7 @@ class ChromeUtils {
const nsAString& aMessage);
static void AddProfilerMarker(GlobalObject& aGlobal, const nsACString& aName,
const Optional<DOMHighResTimeStamp>& aStartTime,
const ProfilerMarkerOptionsOrDouble& aOptions,
const Optional<nsACString>& text);
static void OriginAttributesToSuffix(

View File

@ -24,6 +24,30 @@ interface MozQueryInterface {
legacycaller any (any aIID);
};
/**
* Options for a marker created with the addProfilerMarker method.
* All fields are optional.
*/
dictionary ProfilerMarkerOptions {
// A timestamp to use as the start time of the marker.
// If no start time is provided, the marker will have no duration.
// In window and ChromeWorker contexts, use a timestamp from
// `performance.now()`.
// In JS modules, use `Cu.now()` to get a timestamp.
DOMHighResTimeStamp startTime = 0;
// If captureStack is true, a profiler stack will be captured and associated
// with the marker.
boolean captureStack = false;
// Markers are created by default in the JavaScript category, but this can be
// overridden.
// Examples of correct values: "JavaScript", "Test", "Other", ...
// See ProfilingCategoryList.h for the complete list of valid values.
// Using an unrecognized value will set the category to "Other".
ByteString category = "JavaScript";
};
/**
* A collection of static utility methods that are only exposed to system code.
* This is exposed in all the system globals where we can expose stuff by
@ -171,15 +195,18 @@ namespace ChromeUtils {
* add a marker for the current thread. No-op otherwise.
*
* @param name The name of the marker.
* @param startTime The timestamp to use as the start of the marker.
* If omitted, the marker will have no duration.
* @param options Either a timestamp to use as the start time of the
* marker, or a ProfilerMarkerOptions object that can
* contain startTime, captureStack or category fields.
* If this parameter is omitted, the marker will have
* no duration.
* In window and ChromeWorker contexts, use a
* timestamp from `performance.now()`.
* In JS modules, use `Cu.now()` to get a timestamp.
* @param text Text to associate with the marker.
*/
void addProfilerMarker(UTF8String name,
optional DOMHighResTimeStamp startTime,
optional (ProfilerMarkerOptions or DOMHighResTimeStamp) options = {},
optional UTF8String text);
/**

View File

@ -36,6 +36,9 @@
SUBCATEGORY(OTHER, OTHER_PreferenceRead, "Preference Read") \
SUBCATEGORY(OTHER, OTHER_Profiling, "Profiling") \
END_CATEGORY \
BEGIN_CATEGORY(TEST, "Test", "darkgray") \
SUBCATEGORY(TEST, TEST, "Test") \
END_CATEGORY \
BEGIN_CATEGORY(LAYOUT, "Layout", "purple") \
SUBCATEGORY(LAYOUT, LAYOUT, "Other") \
SUBCATEGORY(LAYOUT, LAYOUT_FrameConstruction, "Frame construction") \
@ -103,6 +106,7 @@
SUBCATEGORY(IPC, IPC, "Other") \
END_CATEGORY \
BEGIN_CATEGORY(MEDIA, "Media", "orange") \
SUBCATEGORY(MEDIA, MEDIA, "Other") \
SUBCATEGORY(MEDIA, MEDIA_CUBEB, "Cubeb") \
SUBCATEGORY(MEDIA, MEDIA_PLAYBACK, "Playback") \
SUBCATEGORY(MEDIA, MEDIA_RT, "Real-time rendering") \

View File

@ -0,0 +1,184 @@
/* 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/. */
/**
* Test that ChromeUtils.addProfilerMarker is working correctly.
*/
const markerNamePrefix = "test_addProfilerMarker";
const markerText = "Text payload";
// The same startTime will be used for all markers with a duration,
// and we store this value globally so that expectDuration and
// expectNoDuration can access it. The value isn't set here as we
// want a start time after the profiler has started
var startTime;
function expectNoDuration(marker) {
Assert.equal(
typeof marker.startTime,
"number",
"startTime should be a number"
);
Assert.greater(
marker.startTime,
startTime,
"startTime should be after the begining of the test"
);
Assert.equal(typeof marker.endTime, "number", "endTime should be a number");
Assert.equal(marker.endTime, 0, "endTime should be 0");
}
function expectDuration(marker) {
Assert.equal(
typeof marker.startTime,
"number",
"startTime should be a number"
);
Assert.equal(
Math.round(marker.startTime * 10 ** 6) / 10 ** 6,
startTime,
"startTime should be the expected time"
);
Assert.equal(typeof marker.endTime, "number", "endTime should be a number");
Assert.greater(
marker.endTime,
startTime,
"endTime should be after startTime"
);
}
function expectNoData(marker) {
Assert.equal(
typeof marker.data,
"undefined",
"The data property should be undefined"
);
}
function expectText(marker) {
Assert.equal(
typeof marker.data,
"object",
"The data property should be an object"
);
Assert.equal(marker.data.type, "Text", "Should be a Text marker");
Assert.equal(
marker.data.name,
markerText,
"The payload should contain the expected text"
);
}
function expectNoStack(marker) {
Assert.ok(!marker.data || !marker.data.stack, "There should be no stack");
}
function expectStack(marker) {
Assert.ok(marker.data.stack, "There should be a stack");
}
add_task(async () => {
if (!AppConstants.MOZ_GECKO_PROFILER) {
return;
}
startProfiler();
startTime = Math.round(Cu.now() * 10 ** 6) / 10 ** 6;
info("startTime used for markers with durations: " + startTime);
/* Each call to testMarker will record a marker with a unique name.
* The testFunctions and testCases objects contain respectively test
* functions to verify that the marker found in the captured profile
* matches expectations, and a string that can be printed to describe
* in which way ChromeUtils.addProfilerMarker was called. */
let testFunctions = {};
let testCases = {};
let markerId = 0;
function testMarker(args, checks) {
let name = markerNamePrefix + markerId++;
ChromeUtils.addProfilerMarker(name, ...args);
testFunctions[name] = checks;
testCases[name] = `ChromeUtils.addProfilerMarker(${[name, ...args]
.toSource()
.slice(1, -1)})`;
}
info("Record markers without options object.");
testMarker([], m => {
expectNoDuration(m);
expectNoData(m);
});
testMarker([startTime], m => {
expectDuration(m);
expectNoData(m);
});
testMarker([undefined, markerText], m => {
expectNoDuration(m);
expectText(m);
});
testMarker([startTime, markerText], m => {
expectDuration(m);
expectText(m);
});
info("Record markers providing the duration as the startTime property.");
testMarker([{ startTime }], m => {
expectDuration(m);
expectNoData(m);
});
testMarker([{}, markerText], m => {
expectNoDuration(m);
expectText(m);
});
testMarker([{ startTime }, markerText], m => {
expectDuration(m);
expectText(m);
});
info("Record markers to test the captureStack property.");
const captureStack = true;
testMarker([], expectNoStack);
testMarker([startTime, markerText], expectNoStack);
testMarker([{ captureStack: false }], expectNoStack);
testMarker([{ captureStack }], expectStack);
testMarker([{ startTime, captureStack }], expectStack);
testMarker([{ captureStack }, markerText], expectStack);
testMarker([{ startTime, captureStack }, markerText], expectStack);
info("Record markers to test the category property");
function testCategory(args, expectedCategory) {
testMarker(args, marker => {
Assert.equal(marker.category, expectedCategory);
});
}
testCategory([], "JavaScript");
testCategory([{ category: "Test" }], "Test");
testCategory([{ category: "Test" }, markerText], "Test");
testCategory([{ category: "JavaScript" }], "JavaScript");
testCategory([{ category: "Other" }], "Other");
testCategory([{ category: "DOM" }], "DOM");
testCategory([{ category: "does not exist" }], "Other");
info("Capture the profile");
const profile = await stopAndGetProfile();
const mainThread = profile.threads.find(({ name }) => name === "GeckoMain");
const markers = getInflatedMarkerData(mainThread).filter(m =>
m.name.startsWith(markerNamePrefix)
);
Assert.equal(
markers.length,
Object.keys(testFunctions).length,
`Found ${markers.length} test markers in the captured profile`
);
for (let marker of markers) {
marker.category = profile.meta.categories[marker.category].name;
info(`${testCases[marker.name]} -> ${marker.toSource()}`);
testFunctions[marker.name](marker);
delete testFunctions[marker.name];
}
Assert.equal(0, Object.keys(testFunctions).length, "all markers were found");
});

View File

@ -5,6 +5,7 @@ support-files =
skip-if = toolkit == 'android'
[test_active_configuration.js]
[test_addProfilerMarker.js]
[test_start.js]
skip-if = true
[test_get_features.js]