/* 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/. */ "use strict"; const ProcessTools = Cc["@mozilla.org/processtools-service;1"].getService( Ci.nsIProcessToolsService ); let getStatsReports = async (filter = "") => { let { reports } = await new Promise(r => WebrtcGlobalInformation.getAllStats(r, filter) ); ok(Array.isArray(reports), "|reports| is an array"); let sanityCheckReport = report => { isnot(report.pcid, "", "pcid is non-empty"); if (filter.length) { is(report.pcid, filter, "pcid matches filter"); } // Check for duplicates const checkForDuplicateId = statsArray => { ok(Array.isArray(statsArray), "|statsArray| is an array"); const ids = new Set(); statsArray.forEach(stat => { is(typeof stat.id, "string", "|stat.id| is a string"); ok( !ids.has(stat.id), `Id ${stat.id} should appear only once. Stat was ${JSON.stringify( stat )}` ); ids.add(stat.id); }); }; checkForDuplicateId(report.inboundRtpStreamStats); checkForDuplicateId(report.outboundRtpStreamStats); checkForDuplicateId(report.remoteInboundRtpStreamStats); checkForDuplicateId(report.remoteOutboundRtpStreamStats); checkForDuplicateId(report.rtpContributingSourceStats); checkForDuplicateId(report.iceCandidatePairStats); checkForDuplicateId(report.iceCandidateStats); checkForDuplicateId(report.trickledIceCandidateStats); checkForDuplicateId(report.dataChannelStats); checkForDuplicateId(report.codecStats); }; reports.forEach(sanityCheckReport); return reports; }; const getStatsHistoryPcIds = async () => { return new Promise(r => WebrtcGlobalInformation.getStatsHistoryPcIds(r)); }; const getStatsHistorySince = async (pcid, after, sdpAfter) => { return new Promise(r => WebrtcGlobalInformation.getStatsHistorySince(r, pcid, after, sdpAfter) ); }; let getLogging = async () => { let logs = await new Promise(r => WebrtcGlobalInformation.getLogging("", r)); ok(Array.isArray(logs), "|logs| is an array"); return logs; }; let checkStatsReportCount = async (count, filter = "") => { let reports = await getStatsReports(filter); is(reports.length, count, `|reports| should have length ${count}`); if (reports.length != count) { info(`reports = ${JSON.stringify(reports)}`); } return reports; }; let checkLoggingEmpty = async () => { let logs = await getLogging(); is(logs.length, 0, "Logging is empty"); if (logs.length) { info(`logs = ${JSON.stringify(logs)}`); } return logs; }; let checkLoggingNonEmpty = async () => { let logs = await getLogging(); isnot(logs.length, 0, "Logging is not empty"); return logs; }; let clearAndCheck = async () => { WebrtcGlobalInformation.clearAllStats(); WebrtcGlobalInformation.clearLogging(); await checkStatsReportCount(0); await checkLoggingEmpty(); }; let openTabInNewProcess = async file => { let rootDir = getRootDirectory(gTestPath); rootDir = rootDir.replace( "chrome://mochitests/content/", "https://example.com/" ); let absoluteURI = rootDir + file; return BrowserTestUtils.openNewForegroundTab({ gBrowser, opening: absoluteURI, forceNewProcess: true, }); }; let killTabProcess = async tab => { await SpecialPowers.spawn(tab.linkedBrowser, [], () => { ChromeUtils.privateNoteIntentionalCrash(); }); ProcessTools.kill(tab.linkedBrowser.frameLoader.remoteTab.osPid); }; add_task(async () => { info("Test that clearAllStats is callable"); WebrtcGlobalInformation.clearAllStats(); ok(true, "clearAllStats returns"); }); add_task(async () => { info("Test that clearLogging is callable"); WebrtcGlobalInformation.clearLogging(); ok(true, "clearLogging returns"); }); add_task(async () => { info( "Test that getAllStats is callable, and returns 0 results when no RTCPeerConnections have existed" ); await checkStatsReportCount(0); }); add_task(async () => { info( "Test that getLogging is callable, and returns 0 results when no RTCPeerConnections have existed" ); await checkLoggingEmpty(); }); add_task(async () => { info("Test that we can get stats/logging for a PC on the parent process"); await clearAndCheck(); let pc = new RTCPeerConnection(); await pc.setLocalDescription( await pc.createOffer({ offerToReceiveAudio: true }) ); // Let ICE stack go quiescent await new Promise(r => { pc.onicegatheringstatechange = () => { if (pc.iceGatheringState == "complete") { r(); } }; }); await checkStatsReportCount(1); await checkLoggingNonEmpty(); pc.close(); pc = null; // Closing a PC should not do anything to the ICE logging await checkLoggingNonEmpty(); // There's just no way to get a signal that the ICE stack has stopped logging // eslint-disable-next-line mozilla/no-arbitrary-setTimeout await new Promise(r => setTimeout(r, 2000)); await clearAndCheck(); }); add_task(async () => { info("Test that we can get stats/logging for a PC on a content process"); await clearAndCheck(); let tab = await openTabInNewProcess("single_peerconnection.html"); await checkStatsReportCount(1); await checkLoggingNonEmpty(); await killTabProcess(tab); BrowserTestUtils.removeTab(tab); await clearAndCheck(); }); add_task(async () => { info( "Test that we can get stats/logging for two connected PCs on a content process" ); await clearAndCheck(); let tab = await openTabInNewProcess("peerconnection_connect.html"); await checkStatsReportCount(2); await checkLoggingNonEmpty(); await killTabProcess(tab); BrowserTestUtils.removeTab(tab); await clearAndCheck(); }); add_task(async () => { info("Test filtering for stats reports (parent process)"); await clearAndCheck(); let pc1 = new RTCPeerConnection(); let pc2 = new RTCPeerConnection(); let allReports = await checkStatsReportCount(2); await checkStatsReportCount(1, allReports[0].pcid); pc1.close(); pc2.close(); pc1 = null; pc2 = null; await checkStatsReportCount(1, allReports[0].pcid); await clearAndCheck(); }); add_task(async () => { info("Test filtering for stats reports (content process)"); await clearAndCheck(); let tab1 = await openTabInNewProcess("single_peerconnection.html"); let tab2 = await openTabInNewProcess("single_peerconnection.html"); let allReports = await checkStatsReportCount(2); await checkStatsReportCount(1, allReports[0].pcid); await killTabProcess(tab1); BrowserTestUtils.removeTab(tab1); await killTabProcess(tab2); BrowserTestUtils.removeTab(tab2); await checkStatsReportCount(1, allReports[0].pcid); await clearAndCheck(); }); add_task(async () => { info("Test that stats/logging persists when PC is closed (parent process)"); await clearAndCheck(); let pc = new RTCPeerConnection(); // This stuff will generate logging await pc.setLocalDescription( await pc.createOffer({ offerToReceiveAudio: true }) ); // Once gathering is done, the ICE stack should go quiescent await new Promise(r => { pc.onicegatheringstatechange = () => { if (pc.iceGatheringState == "complete") { r(); } }; }); let reports = await checkStatsReportCount(1); isnot( window.browsingContext.browserId, undefined, "browserId is defined for parent process" ); is( reports[0].browserId, window.browsingContext.browserId, "browserId for stats report matches parent process" ); await checkLoggingNonEmpty(); pc.close(); pc = null; await checkStatsReportCount(1); await checkLoggingNonEmpty(); await clearAndCheck(); }); add_task(async () => { info("Test that stats/logging persists when PC is closed (content process)"); await clearAndCheck(); let tab = await openTabInNewProcess("single_peerconnection.html"); let { browserId } = tab.linkedBrowser; let reports = await checkStatsReportCount(1); is(reports[0].browserId, browserId, "browserId for stats report matches tab"); isnot( browserId, window.browsingContext.browserId, "tab browser id is not the same as parent process browser id" ); await checkLoggingNonEmpty(); await killTabProcess(tab); BrowserTestUtils.removeTab(tab); await checkStatsReportCount(1); await checkLoggingNonEmpty(); await clearAndCheck(); }); const set_int_pref_returning_unsetter = (pref, num) => { const value = Services.prefs.getIntPref(pref); Services.prefs.setIntPref(pref, num); return () => Services.prefs.setIntPref(pref, value); }; const stats_history_is_enabled = () => { return Services.prefs.getBoolPref("media.aboutwebrtc.hist.enabled"); }; const set_max_histories_to_retain = num => set_int_pref_returning_unsetter( "media.aboutwebrtc.hist.closed_stats_to_retain", num ); const set_history_storage_window_s = num => set_int_pref_returning_unsetter( "media.aboutwebrtc.hist.storage_window_s", num ); add_task(async () => { if (!stats_history_is_enabled()) { return; } info( "Test that stats history is available after close until clearLongTermStats is called" ); await clearAndCheck(); const pc = new RTCPeerConnection(); const ids = await getStatsHistoryPcIds(); is(ids.length, 1, "There is a single PeerConnection Id for stats history."); let firstLen = 0; // I "don't love" this but we don't have a anything we can await on ... yet. // eslint-disable-next-line mozilla/no-arbitrary-setTimeout await new Promise(r => setTimeout(r, 2000)); { const history = await getStatsHistorySince(ids[0]); firstLen = history.reports.length; ok( history.reports.length, "There is at least a single PeerConnection stats history before close." ); } // I "don't love" this but we don't have a anything we can await on ... yet. // eslint-disable-next-line mozilla/no-arbitrary-setTimeout await new Promise(r => setTimeout(r, 2000)); { const history = await getStatsHistorySince(ids[0]); const secondLen = history.reports.length; ok( secondLen > firstLen, "After waiting there are more history entries available." ); } pc.close(); // After close for final stats and pc teardown to settle // I "don't love" this but we don't have a anything we can await on ... yet. // eslint-disable-next-line mozilla/no-arbitrary-setTimeout await new Promise(r => setTimeout(r, 2000)); { const history = await getStatsHistorySince(ids[0]); ok( history.reports.length, "There is at least a single PeerConnection stats history after close." ); } await clearAndCheck(); { const history = await getStatsHistorySince(ids[0]); is( history.reports.length, 0, "After PC.close and clearing the stats there are no history reports" ); } { const ids1 = await getStatsHistoryPcIds(); is( ids1.length, 0, "After PC.close and clearing the stats there are no history pcids" ); } { const pc2 = new RTCPeerConnection(); const pc3 = new RTCPeerConnection(); let idsN = await getStatsHistoryPcIds(); is( idsN.length, 2, "There are two pcIds after creating two PeerConnections" ); pc2.close(); // After close for final stats and pc teardown to settle // I "don't love" this but we don't have a anything we can await on ... yet. // eslint-disable-next-line mozilla/no-arbitrary-setTimeout await new Promise(r => setTimeout(r, 2000)); await WebrtcGlobalInformation.clearAllStats(); idsN = await getStatsHistoryPcIds(); is( idsN.length, 1, "There is one pcIds after closing one of two PeerConnections and clearing stats" ); pc3.close(); // After close for final stats and pc teardown to settle // I "don't love" this but we don't have a anything we can await on ... yet. // eslint-disable-next-line mozilla/no-arbitrary-setTimeout await new Promise(r => setTimeout(r, 2000)); } }); add_task(async () => { if (!stats_history_is_enabled()) { return; } const restoreHistRetainPref = set_max_histories_to_retain(7); info("Test that the proper number of pcIds are available"); await clearAndCheck(); const pc01 = new RTCPeerConnection(); const pc02 = new RTCPeerConnection(); const pc03 = new RTCPeerConnection(); const pc04 = new RTCPeerConnection(); const pc05 = new RTCPeerConnection(); const pc06 = new RTCPeerConnection(); const pc07 = new RTCPeerConnection(); const pc08 = new RTCPeerConnection(); const pc09 = new RTCPeerConnection(); const pc10 = new RTCPeerConnection(); const pc11 = new RTCPeerConnection(); const pc12 = new RTCPeerConnection(); const pc13 = new RTCPeerConnection(); // I "don't love" this but we don't have a anything we can await on ... yet. // eslint-disable-next-line mozilla/no-arbitrary-setTimeout await new Promise(r => setTimeout(r, 2000)); { const ids = await getStatsHistoryPcIds(); is(ids.length, 13, "There is are 13 PeerConnection Ids for stats history."); } pc01.close(); pc02.close(); pc03.close(); pc04.close(); pc05.close(); pc06.close(); pc07.close(); pc08.close(); pc09.close(); pc10.close(); pc11.close(); pc12.close(); pc13.close(); // I "don't love" this but we don't have a anything we can await on ... yet. // eslint-disable-next-line mozilla/no-arbitrary-setTimeout await new Promise(r => setTimeout(r, 5000)); { const ids = await getStatsHistoryPcIds(); is( ids.length, 7, "After closing 13 PCs there are no more than the max closed (7) PeerConnection Ids for stats history." ); } restoreHistRetainPref(); await clearAndCheck(); }); add_task(async () => { if (!stats_history_is_enabled()) { return; } // If you change this, please check if the setTimeout should be updated. // NOTE: the unit here is _integer_ seconds. const STORAGE_WINDOW_S = 1; const restoreStorageWindowPref = set_history_storage_window_s(STORAGE_WINDOW_S); info("Test that history items are being aged out"); await clearAndCheck(); const pc = new RTCPeerConnection(); // I "don't love" this but we don't have a anything we can await on ... yet. // eslint-disable-next-line mozilla/no-arbitrary-setTimeout await new Promise(r => setTimeout(r, STORAGE_WINDOW_S * 2 * 1000)); const ids = await getStatsHistoryPcIds(); const { reports } = await getStatsHistorySince(ids[0]); const first = reports[0]; const last = reports.at(-1); ok( last.timestamp - first.timestamp <= STORAGE_WINDOW_S * 1000, "History reports should be aging out according to the storage window pref" ); pc.close(); restoreStorageWindowPref(); await clearAndCheck(); });