diff --git a/devtools/client/performance-new/components/Settings.js b/devtools/client/performance-new/components/Settings.js index 1bde609eba47..e85a341005ac 100644 --- a/devtools/client/performance-new/components/Settings.js +++ b/devtools/client/performance-new/components/Settings.js @@ -181,6 +181,11 @@ const featureCheckboxes = [ value: "jstracer", title: "Trace JS engine (Experimental, requires custom build.)", }, + { + name: "Preference Read", + value: "preferencereads", + title: "Track Preference Reads", + }, ]; /** diff --git a/devtools/client/performance-new/popup/background.jsm b/devtools/client/performance-new/popup/background.jsm index 4fb446fe013d..8b16849fbbec 100644 --- a/devtools/client/performance-new/popup/background.jsm +++ b/devtools/client/performance-new/popup/background.jsm @@ -245,6 +245,7 @@ function intializeState() { tasktracer: false, trackopts: false, jstracer: false, + preferencereads: false, jsallocations: false, }; diff --git a/devtools/client/performance-new/popup/popup.html b/devtools/client/performance-new/popup/popup.html index c3f4bf99b440..ac84b372c9c0 100644 --- a/devtools/client/performance-new/popup/popup.html +++ b/devtools/client/performance-new/popup/popup.html @@ -168,7 +168,12 @@ id="perf-settings-feature-checkbox-jsallocations" type="checkbox" value="jsallocations" />
JS Allocations
Track JavaScript allocations (Experimental.)
+ + diff --git a/devtools/client/performance-new/popup/popup.js b/devtools/client/performance-new/popup/popup.js index 36a4ef10bfef..28fef235114a 100644 --- a/devtools/client/performance-new/popup/popup.js +++ b/devtools/client/performance-new/popup/popup.js @@ -33,6 +33,7 @@ const features = [ "tasktracer", "jstracer", "jsallocations", + "preferencereads", "trackopts", ]; const threadPrefix = "perf-settings-thread-checkbox-"; diff --git a/js/public/ProfilingCategory.h b/js/public/ProfilingCategory.h index 91c7179e3e9f..5935adb65402 100644 --- a/js/public/ProfilingCategory.h +++ b/js/public/ProfilingCategory.h @@ -35,6 +35,7 @@ SUBCATEGORY(IDLE, IDLE, "Other") \ END_CATEGORY \ BEGIN_CATEGORY(OTHER, "Other", "grey") \ + SUBCATEGORY(OTHER, OTHER_PreferenceRead, "Preference Read") \ SUBCATEGORY(OTHER, OTHER, "Other") \ END_CATEGORY \ BEGIN_CATEGORY(LAYOUT, "Layout", "purple") \ diff --git a/modules/libpref/Preferences.cpp b/modules/libpref/Preferences.cpp index 01b5d56d550e..50e49cefc51b 100644 --- a/modules/libpref/Preferences.cpp +++ b/modules/libpref/Preferences.cpp @@ -14,6 +14,7 @@ #include "base/basictypes.h" #include "GeckoProfiler.h" +#include "ProfilerMarkerPayload.h" #include "MainThreadUtils.h" #include "mozilla/ArenaAllocatorExtensions.h" #include "mozilla/ArenaAllocator.h" @@ -4324,6 +4325,22 @@ static nsresult pref_ReadDefaultPrefs(const RefPtr jarReader, return NS_OK; } +static void PrefValueToString(const bool* b, nsCString& value) { + value = nsCString(*b ? "true" : "false"); +} +static void PrefValueToString(const int* i, nsCString& value) { + value = nsPrintfCString("%d", *i); +} +static void PrefValueToString(const uint32_t* u, nsCString& value) { + value = nsPrintfCString("%d", *u); +} +static void PrefValueToString(const float* f, nsCString& value) { + value = nsPrintfCString("%f", *f); +} +static void PrefValueToString(const nsACString& s, nsCString& value) { + value = s; +} + // These preference getter wrappers allow us to look up the value for static // preferences based on their native types, rather than manually mapping them to // the appropriate Preferences::Get* functions. @@ -4333,20 +4350,54 @@ struct Internals { template static nsresult GetPrefValue(const char* aPrefName, T&& aResult, PrefValueKind aKind) { + nsresult rv = NS_ERROR_UNEXPECTED; NS_ENSURE_TRUE(Preferences::InitStaticMembers(), NS_ERROR_NOT_AVAILABLE); + TimeStamp prefAccessTime = TimeStamp::Now(); + Maybe prefType = Nothing(); + nsCString prefValue{""}; + if (Maybe pref = pref_Lookup(aPrefName)) { - return pref->GetValue(aKind, std::forward(aResult)); + rv = pref->GetValue(aKind, std::forward(aResult)); + + if (profiler_feature_active(ProfilerFeature::PreferenceReads)) { + prefType = Some(pref->Type()); + PrefValueToString(aResult, prefValue); + } } - return NS_ERROR_UNEXPECTED; + if (profiler_feature_active(ProfilerFeature::PreferenceReads)) { + profiler_add_marker( + "PreferenceRead", JS::ProfilingCategoryPair::OTHER_PreferenceRead, + MakeUnique(aPrefName, Some(aKind), prefType, + prefValue, prefAccessTime)); + } + return rv; } template static nsresult GetSharedPrefValue(const char* aName, T* aResult) { + nsresult rv = NS_ERROR_UNEXPECTED; + + TimeStamp prefAccessTime = TimeStamp::Now(); + Maybe prefType = Nothing(); + nsCString prefValue{""}; + if (Maybe pref = pref_SharedLookup(aName)) { - return pref->GetValue(PrefValueKind::User, aResult); + rv = pref->GetValue(PrefValueKind::User, aResult); + + if (profiler_feature_active(ProfilerFeature::PreferenceReads)) { + prefType = Some(pref->Type()); + PrefValueToString(aResult, prefValue); + } } - return NS_ERROR_UNEXPECTED; + + if (profiler_feature_active(ProfilerFeature::PreferenceReads)) { + profiler_add_marker( + "PreferenceRead", JS::ProfilingCategoryPair::OTHER_PreferenceRead, + MakeUnique(aName, Nothing() /* indicates Shared */, + prefType, prefValue, prefAccessTime)); + } + return rv; } template diff --git a/toolkit/components/extensions/schemas/geckoProfiler.json b/toolkit/components/extensions/schemas/geckoProfiler.json index 3a6b35f004e2..a04bf1e5613f 100644 --- a/toolkit/components/extensions/schemas/geckoProfiler.json +++ b/toolkit/components/extensions/schemas/geckoProfiler.json @@ -36,7 +36,8 @@ "threads", "trackopts", "jstracer", - "jsallocations" + "jsallocations", + "preferencereads" ] }, { diff --git a/tools/profiler/core/ProfilerMarkerPayload.cpp b/tools/profiler/core/ProfilerMarkerPayload.cpp index 63cd83a4a703..ca39c5b5bfa7 100644 --- a/tools/profiler/core/ProfilerMarkerPayload.cpp +++ b/tools/profiler/core/ProfilerMarkerPayload.cpp @@ -14,6 +14,7 @@ #include "Layers.h" #include "mozilla/Maybe.h" #include "mozilla/net/HttpBaseChannel.h" +#include "mozilla/Preferences.h" #include "mozilla/Sprintf.h" #include @@ -127,6 +128,44 @@ void DOMEventMarkerPayload::StreamPayload(SpliceableJSONWriter& aWriter, aWriter.StringProperty("eventType", NS_ConvertUTF16toUTF8(mEventType).get()); } +static const char* PrefValueKindToString( + const mozilla::Maybe& aKind) { + if (aKind) { + return *aKind == PrefValueKind::Default ? "Default" : "User"; + } + return "Shared"; +} + +static const char* PrefTypeToString(const mozilla::Maybe& type) { + if (type) { + switch (*type) { + case PrefType::None: + return "None"; + case PrefType::Int: + return "Int"; + case PrefType::Bool: + return "Bool"; + case PrefType::String: + return "String"; + default: + MOZ_ASSERT_UNREACHABLE("Unknown preference type."); + } + } + return "Preference not found"; +} + +void PrefMarkerPayload::StreamPayload(SpliceableJSONWriter& aWriter, + const TimeStamp& aProcessStartTime, + UniqueStacks& aUniqueStacks) { + StreamCommonProps("PreferenceRead", aWriter, aProcessStartTime, + aUniqueStacks); + WriteTime(aWriter, aProcessStartTime, mPrefAccessTime, "prefAccessTime"); + aWriter.StringProperty("prefName", mPrefName.get()); + aWriter.StringProperty("prefKind", PrefValueKindToString(mPrefKind)); + aWriter.StringProperty("prefType", PrefTypeToString(mPrefType)); + aWriter.StringProperty("prefValue", mPrefValue.get()); +} + void LayerTranslationMarkerPayload::StreamPayload( SpliceableJSONWriter& aWriter, const TimeStamp& aProcessStartTime, UniqueStacks& aUniqueStacks) { diff --git a/tools/profiler/public/GeckoProfiler.h b/tools/profiler/public/GeckoProfiler.h index 235eb5da9fb8..2395f8cb2a3e 100644 --- a/tools/profiler/public/GeckoProfiler.h +++ b/tools/profiler/public/GeckoProfiler.h @@ -159,7 +159,10 @@ class Vector; MACRO(12, "jstracer", JSTracer, "Enable tracing of the JavaScript engine") \ \ MACRO(13, "jsallocations", JSAllocations, \ - "Have the JavaScript engine track allocations") + "Have the JavaScript engine track allocations") \ + \ + MACRO(15, "preferencereads", PreferenceReads, \ + "Track when preferences are read") struct ProfilerFeature { # define DECLARE(n_, str_, Name_, desc_) \ diff --git a/tools/profiler/public/ProfilerMarkerPayload.h b/tools/profiler/public/ProfilerMarkerPayload.h index 098c5c3e11d8..7d655711b36a 100644 --- a/tools/profiler/public/ProfilerMarkerPayload.h +++ b/tools/profiler/public/ProfilerMarkerPayload.h @@ -12,6 +12,7 @@ #include "mozilla/RefPtr.h" #include "mozilla/TimeStamp.h" #include "mozilla/UniquePtr.h" +#include "mozilla/Preferences.h" #include "mozilla/UniquePtrExtensions.h" #include "mozilla/net/TimingStruct.h" @@ -149,6 +150,35 @@ class DOMEventMarkerPayload : public TracingMarkerPayload { nsString mEventType; }; +class PrefMarkerPayload : public ProfilerMarkerPayload { + public: + PrefMarkerPayload(const char* aPrefName, + const mozilla::Maybe& aPrefKind, + const mozilla::Maybe& aPrefType, + const nsCString& aPrefValue, + const mozilla::TimeStamp& aPrefAccessTime) + : ProfilerMarkerPayload(aPrefAccessTime, aPrefAccessTime), + mPrefAccessTime(aPrefAccessTime), + mPrefName(aPrefName), + mPrefKind(aPrefKind), + mPrefType(aPrefType), + mPrefValue(aPrefValue) {} + + DECL_STREAM_PAYLOAD + + private: + mozilla::TimeStamp mPrefAccessTime; + nsCString mPrefName; + // Nothing means this is a shared preference. Something, on the other hand, + // holds an actual PrefValueKind indicating either a Default or User + // preference. + mozilla::Maybe mPrefKind; + // Nothing means that the mPrefName preference was not found. Something + // contains the type of the preference. + mozilla::Maybe mPrefType; + nsCString mPrefValue; +}; + class UserTimingMarkerPayload : public ProfilerMarkerPayload { public: UserTimingMarkerPayload(const nsAString& aName, diff --git a/tools/profiler/tests/browser/browser.ini b/tools/profiler/tests/browser/browser.ini index c0e22d88e20d..87d70562423e 100644 --- a/tools/profiler/tests/browser/browser.ini +++ b/tools/profiler/tests/browser/browser.ini @@ -2,12 +2,14 @@ support-files = head.js do_work_500ms.html + fixed_height.html multi_frame.html single_frame.html single_frame_pushstate.html single_frame_replacestate.html [browser_test_feature_jsallocations.js] +[browser_test_feature_preferencereads.js] [browser_test_profile_single_frame_page_info.js] [browser_test_profile_multi_frame_page_info.js] [browser_test_profile_pushstate_page_info.js] diff --git a/tools/profiler/tests/browser/browser_test_feature_jsallocations.js b/tools/profiler/tests/browser/browser_test_feature_jsallocations.js index b92ad761b8c2..56b3f77c723f 100644 --- a/tools/profiler/tests/browser/browser_test_feature_jsallocations.js +++ b/tools/profiler/tests/browser/browser_test_feature_jsallocations.js @@ -80,48 +80,3 @@ add_task(async function test_profile_feature_jsallocations() { } }); }); - -/** - * Markers are collected only after a periodic sample. This function ensures that - * at least one periodic sample has been done. - */ -async function doAtLeastOnePeriodicSample() { - async function getProfileSampleCount() { - const profile = await Services.profiler.getProfileDataAsync(); - return profile.threads[0].samples.data.length; - } - - const sampleCount = await getProfileSampleCount(); - // Create an infinite loop until a sample has been collected. - while (true) { - if (sampleCount < (await getProfileSampleCount())) { - return; - } - } -} - -async function stopProfilerAndGetThreads(contentPid) { - await doAtLeastOnePeriodicSample(); - - const profile = await Services.profiler.getProfileDataAsync(); - Services.profiler.StopProfiler(); - - const parentThread = profile.threads[0]; - const contentProcess = profile.processes.find( - p => p.threads[0].pid == contentPid - ); - if (!contentProcess) { - throw new Error("Could not find the content process."); - } - const contentThread = contentProcess.threads[0]; - - if (!parentThread) { - throw new Error("The parent thread was not found in the profile."); - } - - if (!contentThread) { - throw new Error("The content thread was not found in the profile."); - } - - return { parentThread, contentThread }; -} diff --git a/tools/profiler/tests/browser/browser_test_feature_preferencereads.js b/tools/profiler/tests/browser/browser_test_feature_preferencereads.js new file mode 100644 index 000000000000..7e5809712c0e --- /dev/null +++ b/tools/profiler/tests/browser/browser_test_feature_preferencereads.js @@ -0,0 +1,81 @@ +/* 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/. */ + +function countDpiPrefReadsInThread(thread) { + let count = 0; + for (let payload of getPayloadsOfType(thread, "PreferenceRead")) { + if (payload.prefName === "layout.css.dpi") { + count++; + } + } + return count; +} + +/** + * Test the PreferenceRead feature. + */ +add_task(async function test_profile_feature_preferencereads() { + if (!AppConstants.MOZ_GECKO_PROFILER) { + return; + } + Assert.ok( + !Services.profiler.IsActive(), + "The profiler is not currently active" + ); + + startProfiler({ features: ["threads", "preferencereads"] }); + + const url = BASE_URL + "fixed_height.html"; + await BrowserTestUtils.withNewTab(url, async contentBrowser => { + const contentPid = await ContentTask.spawn( + contentBrowser, + null, + () => Services.appinfo.processID + ); + + // Wait 100ms so that the tab finishes executing. + await wait(100); + + // Check that some PreferenceRead profile markers were generated when the + // feature is enabled. + { + const { contentThread } = await stopProfilerAndGetThreads(contentPid); + + const timesReadDpiInContent = countDpiPrefReadsInThread(contentThread); + + Assert.greater( + timesReadDpiInContent, + 0, + "PreferenceRead profile markers for layout.css.dpi were recorded " + + "when the PreferenceRead feature was turned on." + ); + } + + startProfiler({ features: ["threads"] }); + // Now reload the tab with a clean run. + gBrowser.reload(); + await wait(100); + + // Check that no PreferenceRead markers were recorded when the feature + // is turned off. + { + const { parentThread, contentThread } = await stopProfilerAndGetThreads( + contentPid + ); + Assert.equal( + getPayloadsOfType(parentThread, "PreferenceRead").length, + 0, + "No PreferenceRead profile markers for layout.css.dpi were recorded " + + "when the PreferenceRead feature was turned on." + ); + + Assert.equal( + getPayloadsOfType(contentThread, "PreferenceRead").length, + 0, + "No PreferenceRead profile markers for layout.css.dpi were recorded " + + "when the PreferenceRead feature was turned on." + ); + } + }); +}); diff --git a/tools/profiler/tests/browser/fixed_height.html b/tools/profiler/tests/browser/fixed_height.html new file mode 100644 index 000000000000..7d21f3b74632 --- /dev/null +++ b/tools/profiler/tests/browser/fixed_height.html @@ -0,0 +1,18 @@ + + + + + + + + +
Testing
+ + diff --git a/tools/profiler/tests/browser/head.js b/tools/profiler/tests/browser/head.js index 559d7c8d54f3..e8b65f5c2d95 100644 --- a/tools/profiler/tests/browser/head.js +++ b/tools/profiler/tests/browser/head.js @@ -21,6 +21,60 @@ function startProfiler(callersSettings) { settings.duration ); } +/** + * This function spins on a while loop until at least one + * periodic sample is taken. Use this function to ensure + * that markers are properly collected for a test or that + * at least one sample in which we are interested is collected. + */ +async function doAtLeastOnePeriodicSample() { + async function getProfileSampleCount() { + const profile = await Services.profiler.getProfileDataAsync(); + return profile.threads[0].samples.data.length; + } + + const sampleCount = await getProfileSampleCount(); + // Create an infinite loop until a sample has been collected. + while (true) { + if (sampleCount < (await getProfileSampleCount())) { + return; + } + } +} + +/** + * This is a helper function that will stop the profiler of the browser + * running with PID contentPid. The profiler in that PID + * will not stop until there is at least one periodic sample taken, though. + * + * @param {number} contentPid + * @returns {Promise} + */ +async function stopProfilerAndGetThreads(contentPid) { + await doAtLeastOnePeriodicSample(); + + const profile = await Services.profiler.getProfileDataAsync(); + Services.profiler.StopProfiler(); + + const parentThread = profile.threads[0]; + const contentProcess = profile.processes.find( + p => p.threads[0].pid == contentPid + ); + if (!contentProcess) { + throw new Error("Could not find the content process."); + } + const contentThread = contentProcess.threads[0]; + + if (!parentThread) { + throw new Error("The parent thread was not found in the profile."); + } + + if (!contentThread) { + throw new Error("The content thread was not found in the profile."); + } + + return { parentThread, contentThread }; +} /** * This is a helper function be able to run `await wait(500)`. Unfortunately this