tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit c39fee754e453a6ebeac9c254473ce01ec0b8f7a
parent 82ed6edd84ed7174720e69464c6e3898cb9e43ac
Author: Florian Quèze <florian@queze.net>
Date:   Thu,  9 Oct 2025 21:56:22 +0000

Bug 1993214 - Show native stacks in resource usage profiles for crash markers, r=ahal,mstange.

Differential Revision: https://phabricator.services.mozilla.com/D267944

Diffstat:
Mtesting/mozbase/mozcrash/mozcrash/mozcrash.py | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtesting/mozbase/mozlog/mozlog/structuredlog.py | 1+
Mtesting/mozbase/mozsystemmonitor/mozsystemmonitor/resourcemonitor.py | 178+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 245 insertions(+), 0 deletions(-)

diff --git a/testing/mozbase/mozcrash/mozcrash/mozcrash.py b/testing/mozbase/mozcrash/mozcrash/mozcrash.py @@ -44,6 +44,7 @@ StackInfo = namedtuple( "pid", "reason", "java_stack", + "crashing_thread_stack", ], ) @@ -378,6 +379,7 @@ class CrashInfo: annotations = None pid = None process_type = "unknown" + crashing_thread_stack = None if ( self.stackwalk_binary and os.path.exists(self.stackwalk_binary) @@ -430,6 +432,7 @@ class CrashInfo: processed_crash = self._process_json_output(json_output) signature = processed_crash.get("signature") pid = processed_crash.get("pid") + crashing_thread_stack = processed_crash.get("crashing_thread_stack") elif not self.stackwalk_binary: errors.append( @@ -474,11 +477,13 @@ class CrashInfo: pid, reason, java_stack, + crashing_thread_stack, ) def _process_json_output(self, json_path): signature = None pid = None + crashing_thread_stack = None try: json_file = open(json_path) @@ -487,6 +492,7 @@ class CrashInfo: signature = self._generate_signature(crash_json) pid = crash_json.get("pid") + crashing_thread_stack = self._extract_crashing_thread_stack(crash_json) except Exception as e: traceback.print_exc() @@ -495,6 +501,7 @@ class CrashInfo: return { "pid": pid, "signature": signature, + "crashing_thread_stack": crashing_thread_stack, } def _generate_signature(self, crash_json): @@ -537,6 +544,65 @@ class CrashInfo: return signature + def _extract_crashing_thread_stack(self, crash_json): + """Extract the crashing thread stack as a structured array of frames. + + Returns a list of frame dictionaries with keys: + - function: function name (optional) + - module: module/library name (optional) + - file: source file path (optional) + - line: line number (optional) + - offset: hex offset for unsymbolicated frames (optional) + - inlined: boolean indicating if this is an inlined frame + """ + if not ( + (crashing_thread := crash_json.get("crashing_thread")) + and (frames := crashing_thread.get("frames")) + ): + return None + + structured_stack = [] + + def process_frame(frame, parent_frame=None): + """Process a single frame (inline or main) and append to structured_stack. + + Args: + frame: Frame data dictionary + parent_frame: Parent frame dict (for inline frames to inherit module) + """ + result = {} + + if func := frame.get("function"): + result["function"] = func + if file_str := frame.get("file"): + result["file"] = file_str + if line := frame.get("line"): + result["line"] = line + + # Inline frames inherit module from parent, main frames have their own + if parent_frame: + result["inlined"] = True + if module := parent_frame.get("module"): + result["module"] = module + elif module := frame.get("module"): + result["module"] = module + if module_offset := frame.get("module_offset"): + result["module_offset"] = int(module_offset, 16) + if function_offset := frame.get("function_offset"): + result["function_offset"] = int(function_offset, 16) + elif offset := frame.get("offset"): + # JIT code or unknown frame - use raw address + result["offset"] = int(offset, 16) + + structured_stack.append(result) + + for frame in frames: + for inline in frame.get("inlines") or []: + process_frame(inline, parent_frame=frame) + process_frame(frame) + + return structured_stack if structured_stack else None + def _parse_extra_file(self, path): with open(path) as file: try: diff --git a/testing/mozbase/mozlog/mozlog/structuredlog.py b/testing/mozbase/mozlog/mozlog/structuredlog.py @@ -584,6 +584,7 @@ class StructuredLogger: Unicode("java_stack", default=None, optional=True), Unicode("process_type", default=None, optional=True), List(Unicode, "stackwalk_errors", default=None), + List(Any, "crashing_thread_stack", default=None, optional=True), Unicode("subsuite", default=None, optional=True), Boolean("quiet", default=False, optional=True), ) diff --git a/testing/mozbase/mozsystemmonitor/mozsystemmonitor/resourcemonitor.py b/testing/mozbase/mozsystemmonitor/mozsystemmonitor/resourcemonitor.py @@ -722,6 +722,7 @@ class SystemResourceMonitor: - "reason": crash reason (optional) - "test": test name (optional) - "minidump_path": path to minidump file (optional) + - "stack": structured stack (array of frame dicts) (optional) - "time": timestamp in milliseconds """ if not SystemResourceMonitor.instance: @@ -749,6 +750,10 @@ class SystemResourceMonitor: minidump_name = os.path.splitext(os.path.basename(minidump_path))[0] marker_data["minidump"] = minidump_name + # Add stack if available (structured format: array of frame dicts) + if stack := data.get("crashing_thread_stack"): + marker_data["stack"] = stack + SystemResourceMonitor.record_event("CRASH", timestamp, marker_data) @contextmanager @@ -1392,6 +1397,7 @@ class SystemResourceMonitor: "endTime": [], "phase": [], "category": [], + "stack": [], "length": 0, }, "stackTable": { @@ -1459,6 +1465,162 @@ class SystemResourceMonitor: stringArray.append(string) return len(stringArray) - 1 + def get_stack_index(stack_frames): + """Get a stack index from a structured stack (array of frame dicts). + + Each frame dict contains: + - function: function name (optional) + - module: module/library name (optional) + - file: source file path (optional) + - line: line number (optional) + - offset: hex offset for unsymbolicated frames (optional) + - inlined: boolean indicating if this is an inlined frame (optional) + + Returns the index of the innermost stack frame, or None if stack_frames is empty. + """ + if not stack_frames: + return None + + stackTable = firstThread["stackTable"] + frameTable = firstThread["frameTable"] + funcTable = firstThread["funcTable"] + resourceTable = firstThread["resourceTable"] + nativeSymbols = firstThread["nativeSymbols"] + + # Build stack from outermost to innermost + stack_index = None + inline_depth = 0 + + for frame_data in reversed(stack_frames): + # Handle inline depth tracking + if frame_data.get("inlined"): + inline_depth += 1 + else: + inline_depth = 0 + + # Get frame components + module_name = frame_data.get("module") + file_name = frame_data.get("file") + line_num = frame_data.get("line") + + # Get offsets - different handling for native vs JIT frames + module_offset = frame_data.get("module_offset") + function_offset = frame_data.get("function_offset") + raw_offset = frame_data.get("offset") # For JIT frames without module + + func_name = frame_data.get("function") + if not func_name and (offset := module_offset or raw_offset): + func_name = hex(offset) + + # Get or create resource for the module + resource_index = -1 + if module_name: + # Find existing resource + for i, name_idx in enumerate(resourceTable["name"]): + if firstThread["stringArray"][name_idx] == module_name: + resource_index = i + break + else: + # Create new resource if not found + resource_index = resourceTable["length"] + resourceTable["lib"].append(None) + resourceTable["name"].append(get_string_index(module_name)) + resourceTable["host"].append(None) + # Possible resourceTypes: + # 0 = unknown, 1 = library, 2 = addon, 3 = webhost, 4 = otherhost, 5 = url + # https://github.com/firefox-devtools/profiler/blob/32cb6672c7ed47311e9d84963023d51f5147042b/src/profile-logic/data-structures.ts#L322 + resourceTable["type"].append(1) + resourceTable["length"] += 1 + + # Create native symbol for unsymbolicated frames + # nativeSymbols.address = module_offset - function_offset + native_symbol_index = None + if ( + module_offset is not None + and function_offset is not None + and module_name + ): + symbol_address = module_offset - function_offset + + # Check if this native symbol already exists + for i in range(nativeSymbols["length"]): + if ( + nativeSymbols["libIndex"][i] == resource_index + and nativeSymbols["address"][i] == symbol_address + ): + native_symbol_index = i + break + else: + # Create new native symbol if not found + native_symbol_index = nativeSymbols["length"] + nativeSymbols["libIndex"].append(resource_index) + nativeSymbols["address"].append(symbol_address) + nativeSymbols["name"].append(get_string_index(func_name)) + nativeSymbols["functionSize"].append(None) + nativeSymbols["length"] += 1 + + # Get or create func index + func_name_index = get_string_index(func_name) + file_name_index = get_string_index(file_name) if file_name else None + + for i, name_idx in enumerate(funcTable["name"]): + if ( + name_idx == func_name_index + and funcTable["resource"][i] == resource_index + and funcTable["fileName"][i] == file_name_index + and funcTable["lineNumber"][i] == line_num + ): + func_index = i + break + else: + func_index = funcTable["length"] + funcTable["isJS"].append(False) + funcTable["relevantForJS"].append(False) + funcTable["name"].append(func_name_index) + funcTable["resource"].append(resource_index) + funcTable["fileName"].append(file_name_index) + funcTable["lineNumber"].append(line_num) + funcTable["columnNumber"].append(None) + funcTable["length"] += 1 + + # Get or create frame index + # frameTable.address = module_offset for native frames, or offset for JIT frames + frame_address = module_offset or raw_offset or -1 + for i, func_idx in enumerate(frameTable["func"]): + if ( + func_idx == func_index + and frameTable["line"][i] == line_num + and frameTable["inlineDepth"][i] == inline_depth + and frameTable["nativeSymbol"][i] == native_symbol_index + and frameTable["address"][i] == frame_address + ): + frame_index = i + break + else: + frame_index = frameTable["length"] + frameTable["address"].append(frame_address) + frameTable["inlineDepth"].append(inline_depth) + frameTable["category"].append(OTHER_CATEGORY) + frameTable["subcategory"].append(0) + frameTable["func"].append(func_index) + frameTable["nativeSymbol"].append(native_symbol_index) + frameTable["innerWindowID"].append(0) + frameTable["implementation"].append(None) + frameTable["line"].append(line_num) + frameTable["column"].append(None) + frameTable["length"] += 1 + + # Create stack entry + new_stack_index = stackTable["length"] + stackTable["frame"].append(frame_index) + stackTable["prefix"].append(stack_index) + stackTable["category"].append(0) + stackTable["subcategory"].append(0) + stackTable["length"] += 1 + stack_index = new_stack_index + + return stack_index + def add_marker( name_index, start, end, data, category_index=OTHER_CATEGORY, precision=None ): @@ -1479,7 +1641,23 @@ class SystemResourceMonitor: markers["phase"].append(1) markers["category"].append(category_index) markers["name"].append(name_index) + + # Extract and process stack if present (structured format: array of frame dicts) + stack_index = None + if isinstance(data, dict) and "stack" in data: + stack = data["stack"] + if isinstance(stack, list): + stack_index = get_stack_index(stack) + del data["stack"] + # Add cause object to marker data for processed profile format + if stack_index is not None: + data["cause"] = { + "time": markers["startTime"][-1], + "stack": stack_index, + } + markers["data"].append(data) + markers["stack"].append(stack_index) markers["length"] = markers["length"] + 1 def format_percent(value):