tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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:
Mtoolkit/crashreporter/test/unit/crasher_subprocess_head.js | 1+
Mtoolkit/crashreporter/test/unit/crasher_subprocess_tail.js | 2+-
Atoolkit/crashreporter/test/unit/test_crash_psi_annotation.js | 215+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtoolkit/crashreporter/test/unit/xpcshell.toml | 4++++
Mxpcom/base/AvailableMemoryWatcherLinux.cpp | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Mxpcom/base/moz.build | 5+++++
Axpcom/base/nsIAvailableMemoryWatcherTestingLinux.idl | 23+++++++++++++++++++++++
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); +};