mirror of
				https://github.com/mozilla/gecko-dev.git
				synced 2025-10-31 16:28:05 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			177 lines
		
	
	
	
		
			5.2 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			177 lines
		
	
	
	
		
			5.2 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* 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/. */
 | |
| 
 | |
| const lazy = {};
 | |
| 
 | |
| ChromeUtils.defineESModuleGetters(lazy, {
 | |
|   TelemetryController: "resource://gre/modules/TelemetryController.sys.mjs",
 | |
| });
 | |
| 
 | |
| export function BHRTelemetryService() {
 | |
|   // Allow tests to get access to this object to verify it works correctly.
 | |
|   this.wrappedJSObject = this;
 | |
| 
 | |
|   Services.obs.addObserver(this, "profile-before-change");
 | |
|   Services.obs.addObserver(this, "bhr-thread-hang");
 | |
|   Services.obs.addObserver(this, "idle-daily");
 | |
| 
 | |
|   this.resetPayload();
 | |
| }
 | |
| 
 | |
| BHRTelemetryService.prototype = Object.freeze({
 | |
|   classID: Components.ID("{117c8cdf-69e6-4f31-a439-b8a654c67127}"),
 | |
|   QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
 | |
| 
 | |
|   TRANSMIT_HANG_COUNT: 50,
 | |
| 
 | |
|   resetPayload() {
 | |
|     this.startTime = +new Date();
 | |
|     this.payload = {
 | |
|       modules: [],
 | |
|       hangs: [],
 | |
|     };
 | |
|     this.clearPermahangFile = false;
 | |
|   },
 | |
| 
 | |
|   recordHang({
 | |
|     duration,
 | |
|     thread,
 | |
|     runnableName,
 | |
|     process,
 | |
|     stack,
 | |
|     remoteType,
 | |
|     modules,
 | |
|     annotations,
 | |
|     wasPersisted,
 | |
|   }) {
 | |
|     if (!Services.telemetry.canRecordExtended) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // Create a mapping from module indicies in the original nsIHangDetails
 | |
|     // object to this.payload.modules indicies.
 | |
|     let moduleIdxs = modules.map(module => {
 | |
|       let idx = this.payload.modules.findIndex(m => {
 | |
|         return m[0] === module[0] && m[1] === module[1];
 | |
|       });
 | |
|       if (idx === -1) {
 | |
|         idx = this.payload.modules.length;
 | |
|         this.payload.modules.push(module);
 | |
|       }
 | |
|       return idx;
 | |
|     });
 | |
| 
 | |
|     // Native stack frames are [modIdx, offset] arrays. If we have a valid
 | |
|     // module index, we want to map it to the this.payload.modules array.
 | |
|     for (let i = 0; i < stack.length; ++i) {
 | |
|       if (Array.isArray(stack[i]) && stack[i][0] !== -1) {
 | |
|         stack[i][0] = moduleIdxs[stack[i][0]];
 | |
|       } else if (typeof stack[i] == "string") {
 | |
|         // This is just a precaution - we don't currently know of sensitive
 | |
|         // URLs being included in label frames' dynamic strings which we
 | |
|         // include here, but this is just an added guard. Here we strip any
 | |
|         // string with a :// in it that isn't a chrome:// or resource://
 | |
|         // URL. This is not completely robust, but we are already trying to
 | |
|         // protect against this by only including dynamic strings from the
 | |
|         // opt-in AUTO_PROFILER_..._NONSENSITIVE macros.
 | |
|         let match = /[^\s]+:\/\/.*/.exec(stack[i]);
 | |
|         if (
 | |
|           match &&
 | |
|           !match[0].startsWith("chrome://") &&
 | |
|           !match[0].startsWith("resource://")
 | |
|         ) {
 | |
|           stack[i] = stack[i].replace(match[0], "(excluded)");
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // Create the hang object to record in the payload.
 | |
|     this.payload.hangs.push({
 | |
|       duration,
 | |
|       thread,
 | |
|       runnableName,
 | |
|       process,
 | |
|       remoteType,
 | |
|       annotations,
 | |
|       stack,
 | |
|     });
 | |
| 
 | |
|     if (wasPersisted) {
 | |
|       this.clearPermahangFile = true;
 | |
|     }
 | |
| 
 | |
|     // If we have collected enough hangs, we can submit the hangs we have
 | |
|     // collected to telemetry.
 | |
|     if (this.payload.hangs.length > this.TRANSMIT_HANG_COUNT) {
 | |
|       this.submit();
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   submit() {
 | |
|     if (this.clearPermahangFile) {
 | |
|       // NB: This is async but it is called from an Observer callback.
 | |
|       IOUtils.remove(
 | |
|         PathUtils.join(PathUtils.profileDir, "last_permahang.bin")
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     if (!Services.telemetry.canRecordExtended) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // NOTE: We check a separate bhrPing.enabled pref here. This pref is unset
 | |
|     // when running tests so that we run as much of BHR as possible (to catch
 | |
|     // errors) while avoiding timeouts caused by invoking `pingsender` during
 | |
|     // testing.
 | |
|     if (
 | |
|       Services.prefs.getBoolPref("toolkit.telemetry.bhrPing.enabled", false) &&
 | |
|       this.payload.hangs.length
 | |
|     ) {
 | |
|       this.payload.timeSinceLastPing = new Date() - this.startTime;
 | |
|       lazy.TelemetryController.submitExternalPing("bhr", this.payload, {
 | |
|         addEnvironment: true,
 | |
|       });
 | |
| 
 | |
|       Glean.hangs.modules.set(this.payload.modules);
 | |
|       let gleanHangs = this.payload.hangs.map(
 | |
|         ({ stack, duration, ...restOfHang }) => ({
 | |
|           stack: stack.map(frame =>
 | |
|             typeof frame == "string"
 | |
|               ? { frame }
 | |
|               : { module: frame[0], frame: frame[1] }
 | |
|           ),
 | |
|           duration: Math.round(duration),
 | |
|           ...restOfHang,
 | |
|         })
 | |
|       );
 | |
|       Glean.hangs.reports.set(gleanHangs);
 | |
|       GleanPings.hangReport.submit();
 | |
|     }
 | |
|     this.resetPayload();
 | |
|   },
 | |
| 
 | |
|   shutdown() {
 | |
|     Services.obs.removeObserver(this, "profile-before-change");
 | |
|     Services.obs.removeObserver(this, "bhr-thread-hang");
 | |
|     Services.obs.removeObserver(this, "idle-daily");
 | |
|     this.submit();
 | |
|   },
 | |
| 
 | |
|   observe(aSubject, aTopic) {
 | |
|     switch (aTopic) {
 | |
|       case "profile-after-change":
 | |
|         this.resetPayload();
 | |
|         break;
 | |
|       case "bhr-thread-hang":
 | |
|         this.recordHang(aSubject.QueryInterface(Ci.nsIHangDetails));
 | |
|         break;
 | |
|       case "profile-before-change":
 | |
|         this.shutdown();
 | |
|         break;
 | |
|       case "idle-daily":
 | |
|         this.submit();
 | |
|         break;
 | |
|     }
 | |
|   },
 | |
| });
 | 
