browser_WebrtcGlobalInformation.js (14984B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 const ProcessTools = Cc["@mozilla.org/processtools-service;1"].getService( 8 Ci.nsIProcessToolsService 9 ); 10 11 let getStatsReports = async (filter = "") => { 12 let { reports } = await new Promise(r => 13 WebrtcGlobalInformation.getAllStats(r, filter) 14 ); 15 16 ok(Array.isArray(reports), "|reports| is an array"); 17 18 let sanityCheckReport = report => { 19 isnot(report.pcid, "", "pcid is non-empty"); 20 if (filter.length) { 21 is(report.pcid, filter, "pcid matches filter"); 22 } 23 24 // Check for duplicates 25 const checkForDuplicateId = statsArray => { 26 ok(Array.isArray(statsArray), "|statsArray| is an array"); 27 const ids = new Set(); 28 statsArray.forEach(stat => { 29 is(typeof stat.id, "string", "|stat.id| is a string"); 30 ok( 31 !ids.has(stat.id), 32 `Id ${stat.id} should appear only once. Stat was ${JSON.stringify( 33 stat 34 )}` 35 ); 36 ids.add(stat.id); 37 }); 38 }; 39 40 checkForDuplicateId(report.inboundRtpStreamStats); 41 checkForDuplicateId(report.outboundRtpStreamStats); 42 checkForDuplicateId(report.remoteInboundRtpStreamStats); 43 checkForDuplicateId(report.remoteOutboundRtpStreamStats); 44 checkForDuplicateId(report.rtpContributingSourceStats); 45 checkForDuplicateId(report.iceCandidatePairStats); 46 checkForDuplicateId(report.iceCandidateStats); 47 checkForDuplicateId(report.trickledIceCandidateStats); 48 checkForDuplicateId(report.dataChannelStats); 49 checkForDuplicateId(report.codecStats); 50 }; 51 52 reports.forEach(sanityCheckReport); 53 return reports; 54 }; 55 56 const getStatsHistoryPcIds = async () => { 57 return new Promise(r => WebrtcGlobalInformation.getStatsHistoryPcIds(r)); 58 }; 59 60 const getStatsHistorySince = async (pcid, after, sdpAfter) => { 61 return new Promise(r => 62 WebrtcGlobalInformation.getStatsHistorySince(r, pcid, after, sdpAfter) 63 ); 64 }; 65 66 let getLogging = async () => { 67 let logs = await new Promise(r => WebrtcGlobalInformation.getLogging("", r)); 68 ok(Array.isArray(logs), "|logs| is an array"); 69 return logs; 70 }; 71 72 let checkStatsReportCount = async (count, filter = "") => { 73 let reports = await getStatsReports(filter); 74 is(reports.length, count, `|reports| should have length ${count}`); 75 if (reports.length != count) { 76 info(`reports = ${JSON.stringify(reports)}`); 77 } 78 return reports; 79 }; 80 81 let checkLoggingEmpty = async () => { 82 let logs = await getLogging(); 83 is(logs.length, 0, "Logging is empty"); 84 if (logs.length) { 85 info(`logs = ${JSON.stringify(logs)}`); 86 } 87 return logs; 88 }; 89 90 let checkLoggingNonEmpty = async () => { 91 let logs = await getLogging(); 92 isnot(logs.length, 0, "Logging is not empty"); 93 return logs; 94 }; 95 96 let clearAndCheck = async () => { 97 WebrtcGlobalInformation.clearAllStats(); 98 WebrtcGlobalInformation.clearLogging(); 99 await checkStatsReportCount(0); 100 await checkLoggingEmpty(); 101 }; 102 103 let openTabInNewProcess = async file => { 104 let rootDir = getRootDirectory(gTestPath); 105 rootDir = rootDir.replace( 106 "chrome://mochitests/content/", 107 "https://example.com/" 108 ); 109 let absoluteURI = rootDir + file; 110 111 return BrowserTestUtils.openNewForegroundTab({ 112 gBrowser, 113 opening: absoluteURI, 114 forceNewProcess: true, 115 }); 116 }; 117 118 let killTabProcess = async tab => { 119 await SpecialPowers.spawn(tab.linkedBrowser, [], () => { 120 ChromeUtils.privateNoteIntentionalCrash(); 121 }); 122 ProcessTools.kill(tab.linkedBrowser.frameLoader.remoteTab.osPid); 123 }; 124 125 add_task(async () => { 126 info("Test that clearAllStats is callable"); 127 WebrtcGlobalInformation.clearAllStats(); 128 ok(true, "clearAllStats returns"); 129 }); 130 131 add_task(async () => { 132 info("Test that clearLogging is callable"); 133 WebrtcGlobalInformation.clearLogging(); 134 ok(true, "clearLogging returns"); 135 }); 136 137 add_task(async () => { 138 info( 139 "Test that getAllStats is callable, and returns 0 results when no RTCPeerConnections have existed" 140 ); 141 await checkStatsReportCount(0); 142 }); 143 144 add_task(async () => { 145 info( 146 "Test that getLogging is callable, and returns 0 results when no RTCPeerConnections have existed" 147 ); 148 await checkLoggingEmpty(); 149 }); 150 151 add_task(async () => { 152 info("Test that we can get stats/logging for a PC on the parent process"); 153 await clearAndCheck(); 154 let pc = new RTCPeerConnection(); 155 await pc.setLocalDescription( 156 await pc.createOffer({ offerToReceiveAudio: true }) 157 ); 158 // Let ICE stack go quiescent 159 await new Promise(r => { 160 pc.onicegatheringstatechange = () => { 161 if (pc.iceGatheringState == "complete") { 162 r(); 163 } 164 }; 165 }); 166 await checkStatsReportCount(1); 167 await checkLoggingNonEmpty(); 168 pc.close(); 169 pc = null; 170 // Closing a PC should not do anything to the ICE logging 171 await checkLoggingNonEmpty(); 172 // There's just no way to get a signal that the ICE stack has stopped logging 173 // eslint-disable-next-line mozilla/no-arbitrary-setTimeout 174 await new Promise(r => setTimeout(r, 2000)); 175 await clearAndCheck(); 176 }); 177 178 add_task(async () => { 179 info("Test that we can get stats/logging for a PC on a content process"); 180 await clearAndCheck(); 181 let tab = await openTabInNewProcess("single_peerconnection.html"); 182 await checkStatsReportCount(1); 183 await checkLoggingNonEmpty(); 184 await killTabProcess(tab); 185 BrowserTestUtils.removeTab(tab); 186 await clearAndCheck(); 187 }); 188 189 add_task(async () => { 190 info( 191 "Test that we can get stats/logging for two connected PCs on a content process" 192 ); 193 await clearAndCheck(); 194 let tab = await openTabInNewProcess("peerconnection_connect.html"); 195 await checkStatsReportCount(2); 196 await checkLoggingNonEmpty(); 197 await killTabProcess(tab); 198 BrowserTestUtils.removeTab(tab); 199 await clearAndCheck(); 200 }); 201 202 add_task(async () => { 203 info("Test filtering for stats reports (parent process)"); 204 await clearAndCheck(); 205 let pc1 = new RTCPeerConnection(); 206 let pc2 = new RTCPeerConnection(); 207 let allReports = await checkStatsReportCount(2); 208 await checkStatsReportCount(1, allReports[0].pcid); 209 pc1.close(); 210 pc2.close(); 211 pc1 = null; 212 pc2 = null; 213 await checkStatsReportCount(1, allReports[0].pcid); 214 await clearAndCheck(); 215 }); 216 217 add_task(async () => { 218 info("Test filtering for stats reports (content process)"); 219 await clearAndCheck(); 220 let tab1 = await openTabInNewProcess("single_peerconnection.html"); 221 let tab2 = await openTabInNewProcess("single_peerconnection.html"); 222 let allReports = await checkStatsReportCount(2); 223 await checkStatsReportCount(1, allReports[0].pcid); 224 await killTabProcess(tab1); 225 BrowserTestUtils.removeTab(tab1); 226 await killTabProcess(tab2); 227 BrowserTestUtils.removeTab(tab2); 228 await checkStatsReportCount(1, allReports[0].pcid); 229 await clearAndCheck(); 230 }); 231 232 add_task(async () => { 233 info("Test that stats/logging persists when PC is closed (parent process)"); 234 await clearAndCheck(); 235 let pc = new RTCPeerConnection(); 236 // This stuff will generate logging 237 await pc.setLocalDescription( 238 await pc.createOffer({ offerToReceiveAudio: true }) 239 ); 240 // Once gathering is done, the ICE stack should go quiescent 241 await new Promise(r => { 242 pc.onicegatheringstatechange = () => { 243 if (pc.iceGatheringState == "complete") { 244 r(); 245 } 246 }; 247 }); 248 let reports = await checkStatsReportCount(1); 249 isnot( 250 window.browsingContext.browserId, 251 undefined, 252 "browserId is defined for parent process" 253 ); 254 is( 255 reports[0].browserId, 256 window.browsingContext.browserId, 257 "browserId for stats report matches parent process" 258 ); 259 await checkLoggingNonEmpty(); 260 pc.close(); 261 pc = null; 262 await checkStatsReportCount(1); 263 await checkLoggingNonEmpty(); 264 await clearAndCheck(); 265 }); 266 267 add_task(async () => { 268 info("Test that stats/logging persists when PC is closed (content process)"); 269 await clearAndCheck(); 270 let tab = await openTabInNewProcess("single_peerconnection.html"); 271 let { browserId } = tab.linkedBrowser; 272 let reports = await checkStatsReportCount(1); 273 is(reports[0].browserId, browserId, "browserId for stats report matches tab"); 274 isnot( 275 browserId, 276 window.browsingContext.browserId, 277 "tab browser id is not the same as parent process browser id" 278 ); 279 await checkLoggingNonEmpty(); 280 await killTabProcess(tab); 281 BrowserTestUtils.removeTab(tab); 282 await checkStatsReportCount(1); 283 await checkLoggingNonEmpty(); 284 await clearAndCheck(); 285 }); 286 287 const set_int_pref_returning_unsetter = (pref, num) => { 288 const value = Services.prefs.getIntPref(pref); 289 Services.prefs.setIntPref(pref, num); 290 return () => Services.prefs.setIntPref(pref, value); 291 }; 292 293 const stats_history_is_enabled = () => { 294 return Services.prefs.getBoolPref("media.aboutwebrtc.hist.enabled"); 295 }; 296 297 const set_max_histories_to_retain = num => 298 set_int_pref_returning_unsetter( 299 "media.aboutwebrtc.hist.closed_stats_to_retain", 300 num 301 ); 302 303 const set_history_storage_window_s = num => 304 set_int_pref_returning_unsetter( 305 "media.aboutwebrtc.hist.storage_window_s", 306 num 307 ); 308 309 add_task(async () => { 310 if (!stats_history_is_enabled()) { 311 return; 312 } 313 info( 314 "Test that stats history is available after close until clearLongTermStats is called" 315 ); 316 await clearAndCheck(); 317 const pc = new RTCPeerConnection(); 318 319 const ids = await getStatsHistoryPcIds(); 320 is(ids.length, 1, "There is a single PeerConnection Id for stats history."); 321 322 let firstLen = 0; 323 // I "don't love" this but we don't have a anything we can await on ... yet. 324 // eslint-disable-next-line mozilla/no-arbitrary-setTimeout 325 await new Promise(r => setTimeout(r, 2000)); 326 { 327 const history = await getStatsHistorySince(ids[0]); 328 firstLen = history.reports.length; 329 ok( 330 history.reports.length, 331 "There is at least a single PeerConnection stats history before close." 332 ); 333 } 334 // I "don't love" this but we don't have a anything we can await on ... yet. 335 // eslint-disable-next-line mozilla/no-arbitrary-setTimeout 336 await new Promise(r => setTimeout(r, 2000)); 337 { 338 const history = await getStatsHistorySince(ids[0]); 339 const secondLen = history.reports.length; 340 Assert.greater( 341 secondLen, 342 firstLen, 343 "After waiting there are more history entries available." 344 ); 345 } 346 pc.close(); 347 // After close for final stats and pc teardown to settle 348 // I "don't love" this but we don't have a anything we can await on ... yet. 349 // eslint-disable-next-line mozilla/no-arbitrary-setTimeout 350 await new Promise(r => setTimeout(r, 2000)); 351 { 352 const history = await getStatsHistorySince(ids[0]); 353 ok( 354 history.reports.length, 355 "There is at least a single PeerConnection stats history after close." 356 ); 357 } 358 await clearAndCheck(); 359 { 360 const history = await getStatsHistorySince(ids[0]); 361 is( 362 history.reports.length, 363 0, 364 "After PC.close and clearing the stats there are no history reports" 365 ); 366 } 367 { 368 const ids1 = await getStatsHistoryPcIds(); 369 is( 370 ids1.length, 371 0, 372 "After PC.close and clearing the stats there are no history pcids" 373 ); 374 } 375 { 376 const pc2 = new RTCPeerConnection(); 377 const pc3 = new RTCPeerConnection(); 378 let idsN = await getStatsHistoryPcIds(); 379 is( 380 idsN.length, 381 2, 382 "There are two pcIds after creating two PeerConnections" 383 ); 384 pc2.close(); 385 // After close for final stats and pc teardown to settle 386 // I "don't love" this but we don't have a anything we can await on ... yet. 387 // eslint-disable-next-line mozilla/no-arbitrary-setTimeout 388 await new Promise(r => setTimeout(r, 2000)); 389 await WebrtcGlobalInformation.clearAllStats(); 390 idsN = await getStatsHistoryPcIds(); 391 is( 392 idsN.length, 393 1, 394 "There is one pcIds after closing one of two PeerConnections and clearing stats" 395 ); 396 pc3.close(); 397 // After close for final stats and pc teardown to settle 398 // I "don't love" this but we don't have a anything we can await on ... yet. 399 // eslint-disable-next-line mozilla/no-arbitrary-setTimeout 400 await new Promise(r => setTimeout(r, 2000)); 401 } 402 }); 403 404 add_task(async () => { 405 if (!stats_history_is_enabled()) { 406 return; 407 } 408 const restoreHistRetainPref = set_max_histories_to_retain(7); 409 info("Test that the proper number of pcIds are available"); 410 await clearAndCheck(); 411 const pc01 = new RTCPeerConnection(); 412 const pc02 = new RTCPeerConnection(); 413 const pc03 = new RTCPeerConnection(); 414 const pc04 = new RTCPeerConnection(); 415 const pc05 = new RTCPeerConnection(); 416 const pc06 = new RTCPeerConnection(); 417 const pc07 = new RTCPeerConnection(); 418 const pc08 = new RTCPeerConnection(); 419 const pc09 = new RTCPeerConnection(); 420 const pc10 = new RTCPeerConnection(); 421 const pc11 = new RTCPeerConnection(); 422 const pc12 = new RTCPeerConnection(); 423 const pc13 = new RTCPeerConnection(); 424 // I "don't love" this but we don't have a anything we can await on ... yet. 425 // eslint-disable-next-line mozilla/no-arbitrary-setTimeout 426 await new Promise(r => setTimeout(r, 2000)); 427 { 428 const ids = await getStatsHistoryPcIds(); 429 is(ids.length, 13, "There is are 13 PeerConnection Ids for stats history."); 430 } 431 pc01.close(); 432 pc02.close(); 433 pc03.close(); 434 pc04.close(); 435 pc05.close(); 436 pc06.close(); 437 pc07.close(); 438 pc08.close(); 439 pc09.close(); 440 pc10.close(); 441 pc11.close(); 442 pc12.close(); 443 pc13.close(); 444 // I "don't love" this but we don't have a anything we can await on ... yet. 445 // eslint-disable-next-line mozilla/no-arbitrary-setTimeout 446 await new Promise(r => setTimeout(r, 5000)); 447 { 448 const ids = await getStatsHistoryPcIds(); 449 is( 450 ids.length, 451 7, 452 "After closing 13 PCs there are no more than the max closed (7) PeerConnection Ids for stats history." 453 ); 454 } 455 restoreHistRetainPref(); 456 await clearAndCheck(); 457 }); 458 459 add_task(async () => { 460 if (!stats_history_is_enabled()) { 461 return; 462 } 463 // If you change this, please check if the setTimeout should be updated. 464 // NOTE: the unit here is _integer_ seconds. 465 const STORAGE_WINDOW_S = 1; 466 const restoreStorageWindowPref = 467 set_history_storage_window_s(STORAGE_WINDOW_S); 468 info("Test that history items are being aged out"); 469 await clearAndCheck(); 470 const pc = new RTCPeerConnection(); 471 // I "don't love" this but we don't have a anything we can await on ... yet. 472 // eslint-disable-next-line mozilla/no-arbitrary-setTimeout 473 await new Promise(r => setTimeout(r, STORAGE_WINDOW_S * 2 * 1000)); 474 const ids = await getStatsHistoryPcIds(); 475 const { reports } = await getStatsHistorySince(ids[0]); 476 const first = reports[0]; 477 const last = reports.at(-1); 478 Assert.lessOrEqual( 479 last.timestamp - first.timestamp, 480 STORAGE_WINDOW_S * 1000, 481 "History reports should be aging out according to the storage window pref" 482 ); 483 pc.close(); 484 restoreStorageWindowPref(); 485 await clearAndCheck(); 486 });