commit 9acc6af34eb1a547dbedebe679170c24c162bed0
parent 1c8cafec146a9fa38c2b9cab7bc4208dd30e6ed3
Author: Kui-Feng Lee <thinker.li@gmail.com>
Date: Wed, 1 Oct 2025 16:37:12 +0000
Bug 1982963 - Test case to make sure LinuxMemoryPSI is in the crash report. r=xpcom-reviewers,nika
test_crash_psi_annotation.js comprise two test cases. One is for PSI
available, the other one is for PSI unavailable. It make sure
LinuxMemoryPSI works normal with or without PSI.
Differential Revision: https://phabricator.services.mozilla.com/D261057
Diffstat:
7 files changed, 302 insertions(+), 15 deletions(-)
diff --git a/toolkit/crashreporter/test/unit/crasher_subprocess_head.js b/toolkit/crashreporter/test/unit/crasher_subprocess_head.js
@@ -27,6 +27,7 @@ const { CrashTestUtils } = ChromeUtils.importESModule(
);
var crashType = CrashTestUtils.CRASH_INVALID_POINTER_DEREF;
var shouldDelay = false;
+var shouldWaitSetup = false;
// Turn PHC on so that the PHC tests work.
CrashTestUtils.enablePHC();
diff --git a/toolkit/crashreporter/test/unit/crasher_subprocess_tail.js b/toolkit/crashreporter/test/unit/crasher_subprocess_tail.js
@@ -12,7 +12,7 @@ if (shouldDelay) {
Services.tm.spinEventLoopUntil(
"Test(crasher_subprocess_tail.js:shouldDelay)",
- () => shouldCrashNow
+ () => shouldCrashNow && !shouldWaitSetup
);
}
diff --git a/toolkit/crashreporter/test/unit/test_crash_psi_annotation.js b/toolkit/crashreporter/test/unit/test_crash_psi_annotation.js
@@ -0,0 +1,215 @@
+add_task(async function test_psi_annotation_in_crash_report() {
+ if (!("@mozilla.org/toolkit/crash-reporter;1" in Cc)) {
+ dump(
+ "INFO | test_crash_psi_annotation.js | Can't test crashreporter in a non-libxul build.\n"
+ );
+ return;
+ }
+
+ // Create a mock PSI file with known values for testing
+ const mockPSIContent =
+ "some avg10=10.00 avg60=8.00 avg300=6.00 total=1000\n" +
+ "full avg10=2.00 avg60=1.50 avg300=1.00 total=500\n";
+
+ // Create mock PSI file as a temporary file
+ const psiFile = do_get_tempdir();
+ psiFile.append("test_psi_memory");
+ psiFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
+
+ // Write mock PSI data
+ await IOUtils.writeUTF8(psiFile.path, mockPSIContent);
+
+ // Register cleanup to remove the test file
+ registerCleanupFunction(async function () {
+ if (psiFile.exists()) {
+ try {
+ psiFile.remove(false);
+ } catch (e) {
+ // Ignore cleanup errors
+ }
+ }
+ });
+
+ // Set the PSI path for testing through environment variable
+ // The callback runs in a separate process, so we need to pass the path via env
+ Services.env.set("MOZ_TEST_PSI_PATH", psiFile.path);
+
+ // Test that PSI annotation is recorded in crash report
+ await do_crash(
+ async function () {
+ shouldWaitSetup = true;
+ shouldDelay = true;
+
+ // Read the PSI path from environment variable in the separate process
+ const psiPath = Services.env.get("MOZ_TEST_PSI_PATH");
+ const watcher = Cc["@mozilla.org/xpcom/memory-watcher;1"].getService(
+ Ci.nsIAvailableMemoryWatcherBase
+ );
+ if (psiPath) {
+ const testingWatcher = watcher.QueryInterface(
+ Ci.nsIAvailableMemoryWatcherTestingLinux
+ );
+ testingWatcher.setPSIPathForTesting(psiPath);
+ }
+
+ // Set up tab unloader and wait for it to be called
+ let tabUnloaderCalled = false;
+ const tabUnloaderPromise = new Promise(resolve => {
+ const mockTabUnloader = {
+ queryInterface: ChromeUtils.generateQI(["nsITabUnloader"]),
+ unloadTabAsync() {
+ tabUnloaderCalled = true;
+ resolve();
+ },
+ };
+
+ // Register our mock tab unloader through the service
+ watcher.registerTabUnloader(mockTabUnloader);
+ });
+
+ // Set memory threshold to 100% to ensure memory pressure is detected
+ Services.prefs.setIntPref(
+ "browser.low_commit_space_threshold_percent",
+ 100
+ );
+
+ // Start user interaction to begin polling
+ Services.obs.notifyObservers(null, "user-interaction-active");
+
+ // Wait for the tab unloader to be called
+ await tabUnloaderPromise;
+
+ // Verify that the tab unloader was actually called
+ if (!tabUnloaderCalled) {
+ throw new Error("Tab unloader was not called");
+ }
+
+ // Trigger the crash now that PSI data has been processed
+ crashType = CrashTestUtils.CRASH_PURE_VIRTUAL_CALL;
+ shouldWaitSetup = false;
+ },
+ function (mdump, extra) {
+ Assert.ok(
+ "LinuxMemoryPSI" in extra,
+ "LinuxMemoryPSI annotation should be present"
+ );
+
+ // Verify the format is correct (comma-separated values)
+ const psiValues = extra.LinuxMemoryPSI;
+ Assert.strictEqual(
+ typeof psiValues,
+ "string",
+ "PSI values should be a string"
+ );
+
+ // Parse the comma-separated values
+ const values = psiValues.split(",");
+ Assert.equal(
+ values.length,
+ 8,
+ "PSI annotation should have 8 comma-separated values"
+ );
+
+ // Verify the expected values from our mock PSI file
+ // Format: some_avg10,some_avg60,some_avg300,some_total,full_avg10,full_avg60,full_avg300,full_total
+ Assert.equal(values[0], "10", "some_avg10 should be 10");
+ Assert.equal(values[1], "8", "some_avg60 should be 8");
+ Assert.equal(values[2], "6", "some_avg300 should be 6");
+ Assert.equal(values[3], "1000", "some_total should be 1000");
+ Assert.equal(values[4], "2", "full_avg10 should be 2");
+ Assert.equal(values[5], "1", "full_avg60 should be 1");
+ Assert.equal(values[6], "1", "full_avg300 should be 1");
+ Assert.equal(values[7], "500", "full_total should be 500");
+
+ dump("INFO | PSI annotation test passed: " + psiValues + "\n");
+ },
+ // process will exit with a zero exit status
+ true
+ );
+});
+
+// Test PSI annotation when PSI file is not available
+add_task(async function test_psi_annotation_no_psi_file() {
+ if (!("@mozilla.org/toolkit/crash-reporter;1" in Cc)) {
+ dump(
+ "INFO | test_crash_psi_annotation.js | Can't test crashreporter in a non-libxul build.\n"
+ );
+ return;
+ }
+
+ await do_crash(
+ async function () {
+ shouldWaitSetup = true;
+ shouldDelay = true;
+
+ // Set the PSI path for testing (point to a unique non-existent file)
+ const watcher = Cc["@mozilla.org/xpcom/memory-watcher;1"].getService(
+ Ci.nsIAvailableMemoryWatcherBase
+ );
+ const testingWatcher = watcher.QueryInterface(
+ Ci.nsIAvailableMemoryWatcherTestingLinux
+ );
+
+ // Build a unique path by appending PID and timestamp to the base name.
+ // If a file happens to exist at that path, append a sequence number.
+ const basePath =
+ Services.env.get("XPCSHELL_TEST_TEMP_DIR") + "/non_existent_psi_file";
+ const pid = Services.appinfo.processID;
+ const timestamp = Date.now();
+ let sequence = 0;
+ let psiPath = `${basePath}_${pid}_${timestamp}`;
+ while (await IOUtils.exists(psiPath)) {
+ sequence++;
+ psiPath = `${basePath}_${pid}_${timestamp}_${sequence}`;
+ }
+
+ testingWatcher.setPSIPathForTesting(psiPath);
+
+ // Set memory threshold to 100% to ensure memory pressure is detected
+ Services.prefs.setIntPref(
+ "browser.low_commit_space_threshold_percent",
+ 100
+ );
+
+ // Wait for PSI data to be processed
+ await new Promise(resolve => {
+ Services.obs.addObserver(function observer(_subject, _topic) {
+ Services.obs.removeObserver(observer, "memory-poller-sync");
+ resolve();
+ }, "memory-poller-sync");
+
+ Services.obs.notifyObservers(null, "user-interaction-active");
+ });
+
+ // Trigger the crash now that PSI data has been processed
+ crashType = CrashTestUtils.CRASH_PURE_VIRTUAL_CALL;
+ shouldWaitSetup = false;
+ },
+ function (mdump, extra) {
+ Assert.ok(
+ "LinuxMemoryPSI" in extra,
+ "LinuxMemoryPSI annotation should be present even when PSI file is unavailable"
+ );
+
+ const psiValues = extra.LinuxMemoryPSI;
+ const values = psiValues.split(",");
+ Assert.equal(
+ values.length,
+ 8,
+ "PSI annotation should have 8 values even when PSI file is unavailable"
+ );
+
+ // All values should be zero when PSI file is not available
+ for (let i = 0; i < 8; i++) {
+ Assert.equal(
+ values[i],
+ "0",
+ `PSI value ${i} should be 0 when PSI file is unavailable`
+ );
+ }
+
+ dump("INFO | PSI annotation test (no file) passed: " + psiValues + "\n");
+ },
+ true
+ );
+});
diff --git a/toolkit/crashreporter/test/unit/xpcshell.toml b/toolkit/crashreporter/test/unit/xpcshell.toml
@@ -47,6 +47,10 @@ reason = "Test covering Linux-specific module handling"
["test_crash_oom.js"]
+["test_crash_psi_annotation.js"]
+run-if = ["os == 'linux'"]
+reason = "Test covering Linux-specific PSI (Pressure Stall Information) annotation"
+
["test_crash_purevirtual.js"]
["test_crash_rust_panic.js"]
diff --git a/xpcom/base/AvailableMemoryWatcherLinux.cpp b/xpcom/base/AvailableMemoryWatcherLinux.cpp
@@ -10,6 +10,7 @@
#include "mozilla/StaticPrefs_browser.h"
#include "mozilla/Unused.h"
#include "nsAppRunner.h"
+#include "nsIAvailableMemoryWatcherTestingLinux.h"
#include "nsIObserverService.h"
#include "nsISupports.h"
#include "nsITimer.h"
@@ -103,14 +104,17 @@ static nsresult ReadPSIFile(const char* aPSIPath, PSIInfo& aResult) {
// Linux has no native low memory detection. This class creates a timer that
// polls for low memory and sends a low memory notification if it notices a
// memory pressure event.
-class nsAvailableMemoryWatcher final : public nsITimerCallback,
- public nsINamed,
- public nsAvailableMemoryWatcherBase {
+class nsAvailableMemoryWatcher final
+ : public nsITimerCallback,
+ public nsINamed,
+ public nsAvailableMemoryWatcherBase,
+ public nsIAvailableMemoryWatcherTestingLinux {
public:
NS_DECL_ISUPPORTS_INHERITED
NS_DECL_NSITIMERCALLBACK
NS_DECL_NSIOBSERVER
NS_DECL_NSINAMED
+ NS_DECL_NSIAVAILABLEMEMORYWATCHERTESTINGLINUX
nsresult Init() override;
nsAvailableMemoryWatcher();
@@ -119,7 +123,7 @@ class nsAvailableMemoryWatcher final : public nsITimerCallback,
void MaybeHandleHighMemory();
private:
- ~nsAvailableMemoryWatcher() = default;
+ ~nsAvailableMemoryWatcher();
void StartPolling(const MutexAutoLock&);
void StopPolling(const MutexAutoLock&);
void ShutDown();
@@ -134,6 +138,12 @@ class nsAvailableMemoryWatcher final : public nsITimerCallback,
bool mUnderMemoryPressure MOZ_GUARDED_BY(mMutex);
PSIInfo mPSIInfo MOZ_GUARDED_BY(mMutex);
+ // PSI file path - can be overridden for testing
+ nsCString mPSIPath MOZ_GUARDED_BY(mMutex);
+
+ // Flag to track if SetPSIPathForTesting has been called
+ bool mIsTesting MOZ_GUARDED_BY(mMutex);
+
// Polling interval to check for low memory. In high memory scenarios,
// default to 5000 ms between each check.
static const uint32_t kHighMemoryPollingIntervalMS = 5000;
@@ -148,12 +158,16 @@ class nsAvailableMemoryWatcher final : public nsITimerCallback,
static const char* kMeminfoPath = "/proc/meminfo";
// Linux memory PSI (Pressure Stall Information) path
-static const char* kPSIPath = "/proc/pressure/memory";
+static const auto kPSIPath = "/proc/pressure/memory"_ns;
nsAvailableMemoryWatcher::nsAvailableMemoryWatcher()
: mPolling(false),
mUnderMemoryPressure(false),
- mPSIInfo{} {}
+ mPSIInfo{},
+ mPSIPath(kPSIPath),
+ mIsTesting(false) {}
+
+nsAvailableMemoryWatcher::~nsAvailableMemoryWatcher() {}
nsresult nsAvailableMemoryWatcher::Init() {
nsresult rv = nsAvailableMemoryWatcherBase::Init();
@@ -195,7 +209,8 @@ already_AddRefed<nsAvailableMemoryWatcherBase> CreateAvailableMemoryWatcher() {
NS_IMPL_ISUPPORTS_INHERITED(nsAvailableMemoryWatcher,
nsAvailableMemoryWatcherBase, nsITimerCallback,
- nsIObserver, nsINamed);
+ nsIObserver, nsINamed,
+ nsIAvailableMemoryWatcherTestingLinux);
void nsAvailableMemoryWatcher::StopPolling(const MutexAutoLock&)
MOZ_REQUIRES(mMutex) {
@@ -257,13 +272,25 @@ nsAvailableMemoryWatcher::Notify(nsITimer* aTimer) {
MOZ_ASSERT(mThread);
return NS_ERROR_FAILURE;
}
+ bool isTesting = mIsTesting;
nsresult rv = mThread->Dispatch(
- NS_NewRunnableFunction("MemoryPoller", [self = RefPtr{this}]() {
+ NS_NewRunnableFunction("MemoryPoller", [self = RefPtr{this}, isTesting]() {
if (self->IsMemoryLow()) {
self->HandleLowMemory();
} else {
self->MaybeHandleHighMemory();
}
+ if (isTesting) {
+ NS_DispatchToMainThread(
+ NS_NewRunnableFunction("MemoryPollerSync", [self]() {
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (observerService) {
+ observerService->NotifyObservers(
+ nullptr, "memory-poller-sync", nullptr);
+ }
+ }));
+ }
}));
if NS_FAILED (rv) {
@@ -315,7 +342,7 @@ void nsAvailableMemoryWatcher::UpdateCrashAnnotation(const MutexAutoLock&)
void nsAvailableMemoryWatcher::UpdatePSIInfo(const MutexAutoLock&)
MOZ_REQUIRES(mMutex) {
- nsresult rv = ReadPSIFile(kPSIPath, mPSIInfo);
+ nsresult rv = ReadPSIFile(mPSIPath.get(), mPSIInfo);
if (NS_FAILED(rv)) {
mPSIInfo = {};
}
@@ -344,19 +371,23 @@ void nsAvailableMemoryWatcher::MaybeHandleHighMemory() {
// on the new interval.
void nsAvailableMemoryWatcher::StartPolling(const MutexAutoLock& aLock)
MOZ_REQUIRES(mMutex) {
+ // Determine the effective polling interval up-front.
uint32_t pollingInterval = mUnderMemoryPressure
? kLowMemoryPollingIntervalMS
: kHighMemoryPollingIntervalMS;
+ // For tests, enforce a very small interval to speed up polling.
+ if (gIsGtest || mIsTesting) {
+ pollingInterval = 10;
+ }
+
if (!mPolling) {
// Restart the timer with the new interval if it has stopped.
- // For testing, use a small polling interval.
- if (NS_SUCCEEDED(
- mTimer->InitWithCallback(this, gIsGtest ? 10 : pollingInterval,
- nsITimer::TYPE_REPEATING_SLACK))) {
+ if (NS_SUCCEEDED(mTimer->InitWithCallback(
+ this, pollingInterval, nsITimer::TYPE_REPEATING_SLACK))) {
mPolling = true;
}
} else {
- mTimer->SetDelay(gIsGtest ? 10 : pollingInterval);
+ mTimer->SetDelay(pollingInterval);
}
}
@@ -390,4 +421,12 @@ NS_IMETHODIMP nsAvailableMemoryWatcher::GetName(nsACString& aName) {
return NS_OK;
}
+NS_IMETHODIMP nsAvailableMemoryWatcher::SetPSIPathForTesting(
+ const nsACString& aPSIPath) {
+ MutexAutoLock lock(mMutex);
+ mPSIPath.Assign(aPSIPath);
+ mIsTesting = true;
+ return NS_OK;
+}
+
} // namespace mozilla
diff --git a/xpcom/base/moz.build b/xpcom/base/moz.build
@@ -23,6 +23,11 @@ XPIDL_SOURCES += [
"nsrootidl.idl",
]
+if CONFIG["OS_TARGET"] == "Linux":
+ XPIDL_SOURCES += [
+ "nsIAvailableMemoryWatcherTestingLinux.idl",
+ ]
+
if CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa":
XPIDL_SOURCES += [
"nsIMacPreferencesReader.idl",
diff --git a/xpcom/base/nsIAvailableMemoryWatcherTestingLinux.idl b/xpcom/base/nsIAvailableMemoryWatcherTestingLinux.idl
@@ -0,0 +1,23 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+/**
+ * nsIAvailableMemoryWatcherTestingLinux: Linux-only testing interface for
+ * methods related to the available memory watcher. This interface is kept
+ * separate from nsIAvailableMemoryWatcherBase so that testing methods remain
+ * isolated from production code.
+ */
+
+[builtinclass, scriptable, uuid(88c3f4a6-6f9a-4f6b-9a4b-5c1b1b0c2ad3)]
+interface nsIAvailableMemoryWatcherTestingLinux : nsISupports
+{
+ /**
+ * Set the PSI file path for testing purposes.
+ * Only meaningful on Linux in testing builds.
+ */
+ void setPSIPathForTesting(in ACString aPSIPath);
+};