GeckoViewProcessHangMonitor.sys.mjs (5359B)
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 import { GeckoViewModule } from "resource://gre/modules/GeckoViewModule.sys.mjs"; 6 7 export class GeckoViewProcessHangMonitor extends GeckoViewModule { 8 constructor(aModuleInfo) { 9 super(aModuleInfo); 10 11 /** 12 * Collection of hang reports that haven't expired or been dismissed 13 * by the user. These are nsIHangReports. 14 */ 15 this._activeReports = new Set(); 16 17 /** 18 * Collection of hang reports that have been suppressed for a short 19 * period of time. Keys are nsIHangReports. Values are timeouts for 20 * when the wait time expires. 21 */ 22 this._pausedReports = new Map(); 23 24 /** 25 * Simple index used for report identification 26 */ 27 this._nextIndex = 0; 28 29 /** 30 * Map of report IDs to report objects. 31 * Keys are numbers. Values are nsIHangReports. 32 */ 33 this._reportIndex = new Map(); 34 35 /** 36 * Map of report objects to report IDs. 37 * Keys are nsIHangReports. Values are numbers. 38 */ 39 this._reportLookupIndex = new Map(); 40 } 41 42 onInit() { 43 debug`onInit`; 44 Services.obs.addObserver(this, "process-hang-report"); 45 Services.obs.addObserver(this, "clear-hang-report"); 46 } 47 48 onDestroy() { 49 debug`onDestroy`; 50 Services.obs.removeObserver(this, "process-hang-report"); 51 Services.obs.removeObserver(this, "clear-hang-report"); 52 } 53 54 onEnable() { 55 debug`onEnable`; 56 this.registerListener([ 57 "GeckoView:HangReportStop", 58 "GeckoView:HangReportWait", 59 ]); 60 } 61 62 onDisable() { 63 debug`onDisable`; 64 this.unregisterListener(); 65 } 66 67 // Bundle event handler. 68 onEvent(aEvent, aData) { 69 debug`onEvent: event=${aEvent}, data=${aData}`; 70 71 if (this._reportIndex.has(aData.hangId)) { 72 const report = this._reportIndex.get(aData.hangId); 73 switch (aEvent) { 74 case "GeckoView:HangReportStop": 75 this.stopHang(report); 76 break; 77 case "GeckoView:HangReportWait": 78 this.pauseHang(report); 79 break; 80 } 81 } else { 82 debug`Report not found: reportIndex=${this._reportIndex}`; 83 } 84 } 85 86 // nsIObserver event handler 87 observe(aSubject, aTopic) { 88 debug`observe(aTopic=${aTopic})`; 89 aSubject.QueryInterface(Ci.nsIHangReport); 90 if (!aSubject.isReportForBrowserOrChildren(this.browser.frameLoader)) { 91 return; 92 } 93 94 switch (aTopic) { 95 case "process-hang-report": { 96 this.reportHang(aSubject); 97 break; 98 } 99 case "clear-hang-report": { 100 this.clearHang(aSubject); 101 break; 102 } 103 } 104 } 105 106 /** 107 * This timeout is the wait period applied after a user selects "Wait" in 108 * an existing notification. 109 */ 110 get WAIT_EXPIRATION_TIME() { 111 try { 112 return Services.prefs.getIntPref("browser.hangNotification.waitPeriod"); 113 } catch (ex) { 114 return 10000; 115 } 116 } 117 118 /** 119 * Terminate whatever is causing this report, be it an add-on or page script. 120 * This is done without updating any report notifications. 121 */ 122 stopHang(report) { 123 report.terminateScript(); 124 } 125 126 /** 127 * 128 */ 129 pauseHang(report) { 130 this._activeReports.delete(report); 131 132 // Create a new timeout with notify callback 133 const timer = this.window.setTimeout(() => { 134 for (const [stashedReport, otherTimer] of this._pausedReports) { 135 if (otherTimer === timer) { 136 this._pausedReports.delete(stashedReport); 137 138 // We're still hung, so move the report back to the active 139 // list. 140 this._activeReports.add(report); 141 break; 142 } 143 } 144 }, this.WAIT_EXPIRATION_TIME); 145 146 this._pausedReports.set(report, timer); 147 } 148 149 /** 150 * construct an information bundle 151 */ 152 notifyReport(report) { 153 this.eventDispatcher.sendRequest({ 154 type: "GeckoView:HangReport", 155 hangId: this._reportLookupIndex.get(report), 156 scriptFileName: report.scriptFileName, 157 }); 158 } 159 160 /** 161 * Handle a potentially new hang report. 162 */ 163 reportHang(report) { 164 // if we aren't enabled then default to stopping the script 165 if (!this.enabled) { 166 this.stopHang(report); 167 return; 168 } 169 170 // if we have already notified, remind 171 if (this._activeReports.has(report)) { 172 this.notifyReport(report); 173 return; 174 } 175 176 // If this hang was already reported and paused by the user then ignore it. 177 if (this._pausedReports.has(report)) { 178 return; 179 } 180 181 const index = this._nextIndex++; 182 this._reportLookupIndex.set(report, index); 183 this._reportIndex.set(index, report); 184 this._activeReports.add(report); 185 186 // Actually notify the new report 187 this.notifyReport(report); 188 } 189 190 clearHang(report) { 191 this._activeReports.delete(report); 192 193 const timer = this._pausedReports.get(report); 194 if (timer) { 195 this.window.clearTimeout(timer); 196 } 197 this._pausedReports.delete(report); 198 199 if (this._reportLookupIndex.has(report)) { 200 const index = this._reportLookupIndex.get(report); 201 this._reportIndex.delete(index); 202 } 203 this._reportLookupIndex.delete(report); 204 report.userCanceled(); 205 } 206 } 207 208 const { debug, warn } = GeckoViewProcessHangMonitor.initLogging( 209 "GeckoViewProcessHangMonitor" 210 );