forked from mirrors/gecko-dev
		
	 ed9ed87b3e
			
		
	
	
		ed9ed87b3e
		
	
	
	
	
		
			
			Pulls out the NMParser work (parsing nm results and turning them into an ArrayBuffer'd map of addresses to symbols) into a worker. For OSX we will still need to do some work to run c++filt in the background, but this gets us most of the way there. Without a Subprocess.jsm usable from a worker, we'll have to bounce data back to the main thread in order to bounce it to the c++filt worker. MozReview-Commit-ID: LZi7J1qGpmh --HG-- extra : rebase_source : 048329fb085542ecc4c8f8d872e6c4cf0b535376
		
			
				
	
	
		
			352 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			352 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 | |
| /* vim: set sts=2 sw=2 et tw=80: */
 | |
| "use strict";
 | |
| 
 | |
| // The ext-* files are imported into the same scopes.
 | |
| /* import-globals-from ../../../toolkit/components/extensions/ext-toolkit.js */
 | |
| 
 | |
| Cu.import("resource://gre/modules/Services.jsm");
 | |
| Cu.importGlobalProperties(["fetch", "TextEncoder", "TextDecoder"]);
 | |
| 
 | |
| XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
 | |
| XPCOMUtils.defineLazyModuleGetter(this, "ParseSymbols", "resource:///modules/ParseSymbols.jsm");
 | |
| XPCOMUtils.defineLazyModuleGetter(this, "Subprocess", "resource://gre/modules/Subprocess.jsm");
 | |
| 
 | |
| const PREF_ASYNC_STACK = "javascript.options.asyncstack";
 | |
| const PREF_SYMBOLS_URL = "extensions.geckoProfiler.symbols.url";
 | |
| const PREF_GET_SYMBOL_RULES = "extensions.geckoProfiler.getSymbolRules";
 | |
| 
 | |
| const ASYNC_STACKS_ENABLED = Services.prefs.getBoolPref(PREF_ASYNC_STACK, false);
 | |
| 
 | |
| var {
 | |
|   ExtensionError,
 | |
| } = ExtensionUtils;
 | |
| 
 | |
| const parseSym = data => {
 | |
|   const worker = new ChromeWorker("resource://app/modules/ParseBreakpadSymbols-worker.js");
 | |
|   const promise = new Promise((resolve, reject) => {
 | |
|     worker.onmessage = (e) => {
 | |
|       if (e.data.error) {
 | |
|         reject(e.data.error);
 | |
|       } else {
 | |
|         resolve(e.data.result);
 | |
|       }
 | |
|     };
 | |
|   });
 | |
|   worker.postMessage(data);
 | |
|   return promise;
 | |
| };
 | |
| 
 | |
| class NMParser {
 | |
|   constructor() {
 | |
|     this._worker = new ChromeWorker("resource://app/modules/ParseNMSymbols-worker.js");
 | |
|   }
 | |
| 
 | |
|   consume(buffer) {
 | |
|     this._worker.postMessage({buffer}, [buffer]);
 | |
|   }
 | |
| 
 | |
|   finish() {
 | |
|     const promise = new Promise((resolve, reject) => {
 | |
|       this._worker.onmessage = (e) => {
 | |
|         if (e.data.error) {
 | |
|           reject(e.data.error);
 | |
|         } else {
 | |
|           resolve(e.data.result);
 | |
|         }
 | |
|       };
 | |
|     });
 | |
|     this._worker.postMessage({
 | |
|       finish: true,
 | |
|       isDarwin: Services.appinfo.OS === "Darwin",
 | |
|     });
 | |
|     return promise;
 | |
|   }
 | |
| }
 | |
| 
 | |
| class CppFiltParser {
 | |
|   constructor() {
 | |
|     this._worker = new ChromeWorker("resource://app/modules/ParseCppFiltSymbols-worker.js");
 | |
|   }
 | |
| 
 | |
|   consume(buffer) {
 | |
|     this._worker.postMessage({buffer}, [buffer]);
 | |
|   }
 | |
| 
 | |
|   finish() {
 | |
|     const promise = new Promise((resolve, reject) => {
 | |
|       this._worker.onmessage = (e) => {
 | |
|         if (e.data.error) {
 | |
|           reject(e.data.error);
 | |
|         } else {
 | |
|           resolve(e.data.result);
 | |
|         }
 | |
|       };
 | |
|     });
 | |
|     this._worker.postMessage({
 | |
|       finish: true,
 | |
|     });
 | |
|     return promise;
 | |
|   }
 | |
| }
 | |
| 
 | |
| const readAllData = async function(pipe, processData) {
 | |
|   let data;
 | |
|   while ((data = await pipe.read()) && data.byteLength) {
 | |
|     processData(data);
 | |
|   }
 | |
| };
 | |
| 
 | |
| const spawnProcess = async function(name, cmdArgs, processData, stdin = null) {
 | |
|   const opts = {
 | |
|     command: await Subprocess.pathSearch(name),
 | |
|     arguments: cmdArgs,
 | |
|   };
 | |
|   const proc = await Subprocess.call(opts);
 | |
| 
 | |
|   if (stdin) {
 | |
|     const encoder = new TextEncoder("utf-8");
 | |
|     proc.stdin.write(encoder.encode(stdin));
 | |
|     proc.stdin.close();
 | |
|   }
 | |
| 
 | |
|   await readAllData(proc.stdout, processData);
 | |
| };
 | |
| 
 | |
| const getSymbolsFromNM = async function(path, arch) {
 | |
|   const parser = new NMParser();
 | |
| 
 | |
|   const args = [path];
 | |
|   if (Services.appinfo.OS === "Darwin") {
 | |
|     args.unshift("-arch", arch);
 | |
|   } else {
 | |
|     // Mac's `nm` doesn't support the demangle option, so we have to
 | |
|     // post-process the symbols with c++filt.
 | |
|     args.unshift("--demangle");
 | |
|   }
 | |
| 
 | |
|   await spawnProcess("nm", args, data => parser.consume(data));
 | |
|   await spawnProcess("nm", ["-D", ...args], data => parser.consume(data));
 | |
|   let result = await parser.finish();
 | |
|   if (Services.appinfo.OS !== "Darwin") {
 | |
|     return result;
 | |
|   }
 | |
| 
 | |
|   const [addresses, symbolsJoinedBuffer] = result;
 | |
|   const decoder = new TextDecoder();
 | |
|   const symbolsJoined = decoder.decode(symbolsJoinedBuffer);
 | |
|   const demangler = new CppFiltParser(addresses.length);
 | |
|   await spawnProcess("c++filt", [], data => demangler.consume(data), symbolsJoined);
 | |
|   const [newIndex, newBuffer] = await demangler.finish();
 | |
|   return [addresses, newIndex, newBuffer];
 | |
| };
 | |
| 
 | |
| const pathComponentsForSymbolFile = (debugName, breakpadId) => {
 | |
|   const symName = debugName.replace(/(\.pdb)?$/, ".sym");
 | |
|   return [debugName, breakpadId, symName];
 | |
| };
 | |
| 
 | |
| const urlForSymFile = (debugName, breakpadId) => {
 | |
|   const profilerSymbolsURL = Services.prefs.getCharPref(PREF_SYMBOLS_URL,
 | |
|                                                         "http://symbols.mozilla.org/");
 | |
|   return profilerSymbolsURL + pathComponentsForSymbolFile(debugName, breakpadId).join("/");
 | |
| };
 | |
| 
 | |
| const getContainingObjdirDist = path => {
 | |
|   let curPath = path;
 | |
|   let curPathBasename = OS.Path.basename(curPath);
 | |
|   while (curPathBasename) {
 | |
|     if (curPathBasename === "dist") {
 | |
|       return curPath;
 | |
|     }
 | |
|     const parentDirPath = OS.Path.dirname(curPath);
 | |
|     if (curPathBasename === "bin") {
 | |
|       return parentDirPath;
 | |
|     }
 | |
|     curPath = parentDirPath;
 | |
|     curPathBasename = OS.Path.basename(curPath);
 | |
|   }
 | |
|   return null;
 | |
| };
 | |
| 
 | |
| const filePathForSymFileInObjDir = (binaryPath, debugName, breakpadId) => {
 | |
|   // `mach buildsymbols` generates symbol files located
 | |
|   // at /path/to/objdir/dist/crashreporter-symbols/.
 | |
|   const objDirDist = getContainingObjdirDist(binaryPath);
 | |
|   if (!objDirDist) {
 | |
|     return null;
 | |
|   }
 | |
|   return OS.Path.join(objDirDist,
 | |
|                       "crashreporter-symbols",
 | |
|                       ...pathComponentsForSymbolFile(debugName, breakpadId));
 | |
| };
 | |
| 
 | |
| const symbolCache = new Map();
 | |
| 
 | |
| const primeSymbolStore = libs => {
 | |
|   for (const {debugName, breakpadId, path, arch} of libs) {
 | |
|     symbolCache.set(urlForSymFile(debugName, breakpadId), {path, arch});
 | |
|   }
 | |
| };
 | |
| 
 | |
| const isRunningObserver = {
 | |
|   _observers: new Set(),
 | |
| 
 | |
|   observe(subject, topic, data) {
 | |
|     switch (topic) {
 | |
|       case "profiler-started":
 | |
|       case "profiler-stopped":
 | |
|         // Call observer(false) or observer(true), but do it through a promise
 | |
|         // so that it's asynchronous.
 | |
|         // We don't want it to be synchronous because of the observer call in
 | |
|         // addObserver, which is asynchronous, and we want to get the ordering
 | |
|         // right.
 | |
|         const isRunningPromise = Promise.resolve(topic === "profiler-started");
 | |
|         for (let observer of this._observers) {
 | |
|           isRunningPromise.then(observer);
 | |
|         }
 | |
|         break;
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   _startListening() {
 | |
|     Services.obs.addObserver(this, "profiler-started");
 | |
|     Services.obs.addObserver(this, "profiler-stopped");
 | |
|   },
 | |
| 
 | |
|   _stopListening() {
 | |
|     Services.obs.removeObserver(this, "profiler-started");
 | |
|     Services.obs.removeObserver(this, "profiler-stopped");
 | |
|   },
 | |
| 
 | |
|   addObserver(observer) {
 | |
|     if (this._observers.size === 0) {
 | |
|       this._startListening();
 | |
|     }
 | |
| 
 | |
|     this._observers.add(observer);
 | |
|     observer(Services.profiler.IsActive());
 | |
|   },
 | |
| 
 | |
|   removeObserver(observer) {
 | |
|     if (this._observers.delete(observer) && this._observers.size === 0) {
 | |
|       this._stopListening();
 | |
|     }
 | |
|   },
 | |
| };
 | |
| 
 | |
| this.geckoProfiler = class extends ExtensionAPI {
 | |
|   getAPI(context) {
 | |
|     return {
 | |
|       geckoProfiler: {
 | |
|         async start(options) {
 | |
|           const {bufferSize, interval, features, threads} = options;
 | |
| 
 | |
|           Services.prefs.setBoolPref(PREF_ASYNC_STACK, false);
 | |
|           if (threads) {
 | |
|             Services.profiler.StartProfiler(bufferSize, interval, features, features.length, threads, threads.length);
 | |
|           } else {
 | |
|             Services.profiler.StartProfiler(bufferSize, interval, features, features.length);
 | |
|           }
 | |
|         },
 | |
| 
 | |
|         async stop() {
 | |
|           if (ASYNC_STACKS_ENABLED !== null) {
 | |
|             Services.prefs.setBoolPref(PREF_ASYNC_STACK, ASYNC_STACKS_ENABLED);
 | |
|           }
 | |
| 
 | |
|           Services.profiler.StopProfiler();
 | |
|         },
 | |
| 
 | |
|         async pause() {
 | |
|           Services.profiler.PauseSampling();
 | |
|         },
 | |
| 
 | |
|         async resume() {
 | |
|           Services.profiler.ResumeSampling();
 | |
|         },
 | |
| 
 | |
|         async getProfile() {
 | |
|           if (!Services.profiler.IsActive()) {
 | |
|             throw new ExtensionError("The profiler is stopped. " +
 | |
|               "You need to start the profiler before you can capture a profile.");
 | |
|           }
 | |
| 
 | |
|           return Services.profiler.getProfileDataAsync();
 | |
|         },
 | |
| 
 | |
|         async getProfileAsArrayBuffer() {
 | |
|           if (!Services.profiler.IsActive()) {
 | |
|             throw new ExtensionError("The profiler is stopped. " +
 | |
|               "You need to start the profiler before you can capture a profile.");
 | |
|           }
 | |
| 
 | |
|           return Services.profiler.getProfileDataAsArrayBuffer();
 | |
|         },
 | |
| 
 | |
|         async getSymbols(debugName, breakpadId) {
 | |
|           if (symbolCache.size === 0) {
 | |
|             primeSymbolStore(Services.profiler.sharedLibraries);
 | |
|           }
 | |
| 
 | |
|           const cachedLibInfo = symbolCache.get(urlForSymFile(debugName, breakpadId));
 | |
| 
 | |
|           const symbolRules = Services.prefs.getCharPref(PREF_GET_SYMBOL_RULES, "localBreakpad,remoteBreakpad");
 | |
|           const haveAbsolutePath = cachedLibInfo && OS.Path.split(cachedLibInfo.path).absolute;
 | |
| 
 | |
|           // We have multiple options for obtaining symbol information for the given
 | |
|           // binary.
 | |
|           //  "localBreakpad"  - Use existing symbol dumps stored in the object directory of a local
 | |
|           //      Firefox build, generated using `mach buildsymbols` [requires path]
 | |
|           //  "remoteBreakpad" - Use symbol dumps from the Mozilla symbol server [only requires
 | |
|           //      debugName + breakpadId]
 | |
|           //  "nm"             - Use the command line tool `nm` [linux/mac only, requires path]
 | |
|           for (const rule of symbolRules.split(",")) {
 | |
|             try {
 | |
|               switch (rule) {
 | |
|                 case "localBreakpad":
 | |
|                   if (haveAbsolutePath) {
 | |
|                     const {path} = cachedLibInfo;
 | |
|                     const filepath = filePathForSymFileInObjDir(path, debugName, breakpadId);
 | |
|                     if (filepath) {
 | |
|                       // NOTE: here and below, "return await" is used to ensure we catch any
 | |
|                       // errors in the promise. A simple return would give the error to the
 | |
|                       // caller.
 | |
|                       return await parseSym({filepath});
 | |
|                     }
 | |
|                   }
 | |
|                   break;
 | |
|                 case "remoteBreakpad":
 | |
|                   const url = urlForSymFile(debugName, breakpadId);
 | |
|                   return await parseSym({url});
 | |
|                 case "nm":
 | |
|                   if (haveAbsolutePath) {
 | |
|                     const {path, arch} = cachedLibInfo;
 | |
|                     return await getSymbolsFromNM(path, arch);
 | |
|                   }
 | |
|                   break;
 | |
|               }
 | |
|             } catch (e) {
 | |
|               // Each of our options can go wrong for a variety of reasons, so on failure
 | |
|               // we will try the next one.
 | |
|               // "localBreakpad" will fail if this is not a local build that's running from the object
 | |
|               // directory or if the user hasn't run `mach buildsymbols` on it.
 | |
|               // "remoteBreakpad" will fail if this is not an official mozilla build (e.g. Nightly) or a
 | |
|               // known system library.
 | |
|               // "nm" will fail if `nm` is not available.
 | |
|             }
 | |
|           }
 | |
| 
 | |
|           throw new Error(`Ran out of options to get symbols from library ${debugName} ${breakpadId}.`);
 | |
|         },
 | |
| 
 | |
|         onRunning: new EventManager(context, "geckoProfiler.onRunning", fire => {
 | |
|           isRunningObserver.addObserver(fire.async);
 | |
|           return () => {
 | |
|             isRunningObserver.removeObserver(fire.async);
 | |
|           };
 | |
|         }).api(),
 | |
|       },
 | |
|     };
 | |
|   }
 | |
| };
 |