commit 79e88998d6c2ddf916cfb7d749cfa21c5868cd05
parent c39fee754e453a6ebeac9c254473ce01ec0b8f7a
Author: Florian Quèze <florian@queze.net>
Date: Thu, 9 Oct 2025 21:56:22 +0000
Bug 1993214 - Show test_status and log message stacks on profiler markers, r=ahal,mstange.
Differential Revision: https://phabricator.services.mozilla.com/D267453
Diffstat:
1 file changed, 106 insertions(+), 25 deletions(-)
diff --git a/testing/mozbase/mozsystemmonitor/mozsystemmonitor/resourcemonitor.py b/testing/mozbase/mozsystemmonitor/mozsystemmonitor/resourcemonitor.py
@@ -677,10 +677,9 @@ class SystemResourceMonitor:
time_sec = data["time"] / 1000
timestamp = SystemResourceMonitor.instance.convert_to_monotonic_time(time_sec)
- action = data.get("action")
marker_data = {"type": "TestStatus"}
- if action == "process_output":
+ if data.get("action") == "process_output":
# Process output uses "output" as marker name
marker_name = "output"
message = data.get("data")
@@ -697,19 +696,20 @@ class SystemResourceMonitor:
elif status == "ERROR":
marker_data["color"] = "red"
- subtest = data.get("subtest")
- if subtest:
+ if subtest := data.get("subtest"):
marker_data["subtest"] = subtest
message = data.get("message")
- test_name = data.get("test")
- if test_name:
+ if test_name := data.get("test"):
marker_data["test"] = test_name
if message:
marker_data["message"] = message
+ if stack := data.get("stack"):
+ marker_data["stack"] = stack
+
SystemResourceMonitor.record_event(marker_name, timestamp, marker_data)
@staticmethod
@@ -1465,6 +1465,75 @@ class SystemResourceMonitor:
stringArray.append(string)
return len(stringArray) - 1
+ def parse_stack(stack_string):
+ """Parse a JavaScript stack trace into structured format.
+
+ Supports two formats:
+ 1. JavaScript Error.stack format: "func@file:line:col\nfunc@file:line:col\n..."
+ 2. Normalized nsIStackFrame format: "file:func:line\nfile:func:line\n..."
+ Returns an array of frame dicts.
+ """
+ if not stack_string:
+ return None
+
+ frames = []
+ for line in stack_string.strip().split("\n"):
+ if not line:
+ continue
+
+ file_name = None
+ func_part = None
+ line_num = None
+ col_num = None
+
+ # Parse "func@file:line:col" (JavaScript Error.stack format)
+ if "@" in line:
+ func_part, location = line.rsplit("@", 1)
+ func_part = func_part.strip()
+
+ # Parse "file:line:col"
+ parts = location.rsplit(":", 2)
+ if len(parts) == 3:
+ file_name, line_str, col_str = parts
+ try:
+ line_num = int(line_str)
+ col_num = int(col_str)
+ except ValueError:
+ pass
+ elif len(parts) == 2:
+ file_name, line_str = parts
+ try:
+ line_num = int(line_str)
+ except ValueError:
+ pass
+ else:
+ file_name = location
+ else:
+ # Parse "file:func:line" (normalized nsIStackFrame format)
+ parts = line.rsplit(":", 2)
+ if len(parts) == 3:
+ file_name, func_part, line_str = parts
+ try:
+ line_num = int(line_str)
+ except ValueError:
+ func_part = line.strip()
+ file_name = None
+ else:
+ func_part = line.strip()
+
+ frame_dict = {"is_js": True}
+ if func_part:
+ frame_dict["function"] = func_part
+ if file_name:
+ frame_dict["file"] = file_name
+ if line_num is not None:
+ frame_dict["line"] = line_num
+ if col_num is not None:
+ frame_dict["column"] = col_num
+ frames.append(frame_dict)
+
+ return frames
+
def get_stack_index(stack_frames):
"""Get a stack index from a structured stack (array of frame dicts).
@@ -1473,8 +1542,10 @@ class SystemResourceMonitor:
- module: module/library name (optional)
- file: source file path (optional)
- line: line number (optional)
+ - column: column number (optional)
- offset: hex offset for unsymbolicated frames (optional)
- inlined: boolean indicating if this is an inlined frame (optional)
+ - is_js: boolean indicating if this is a JavaScript frame (optional)
Returns the index of the innermost stack frame, or None if stack_frames is empty.
"""
@@ -1502,6 +1573,8 @@ class SystemResourceMonitor:
module_name = frame_data.get("module")
file_name = frame_data.get("file")
line_num = frame_data.get("line")
+ col_num = frame_data.get("column")
+ is_js = frame_data.get("is_js", False)
# Get offsets - different handling for native vs JIT frames
module_offset = frame_data.get("module_offset")
@@ -1512,24 +1585,26 @@ class SystemResourceMonitor:
if not func_name and (offset := module_offset or raw_offset):
func_name = hex(offset)
- # Get or create resource for the module
+ # Get or create resource for the module or file
resource_index = -1
- if module_name:
+ resource_name = module_name or (file_name if is_js else None)
+ if resource_name:
# Find existing resource
for i, name_idx in enumerate(resourceTable["name"]):
- if firstThread["stringArray"][name_idx] == module_name:
+ if firstThread["stringArray"][name_idx] == resource_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["name"].append(get_string_index(resource_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)
+ resource_type = 1 if module_name else (5 if is_js else 0)
+ resourceTable["type"].append(resource_type)
resourceTable["length"] += 1
# Create native symbol for unsymbolicated frames
@@ -1574,13 +1649,13 @@ class SystemResourceMonitor:
break
else:
func_index = funcTable["length"]
- funcTable["isJS"].append(False)
- funcTable["relevantForJS"].append(False)
+ funcTable["isJS"].append(is_js)
+ funcTable["relevantForJS"].append(is_js)
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["columnNumber"].append(col_num)
funcTable["length"] += 1
# Get or create frame index
@@ -1590,6 +1665,7 @@ class SystemResourceMonitor:
if (
func_idx == func_index
and frameTable["line"][i] == line_num
+ and frameTable["column"][i] == col_num
and frameTable["inlineDepth"][i] == inline_depth
and frameTable["nativeSymbol"][i] == native_symbol_index
and frameTable["address"][i] == frame_address
@@ -1607,7 +1683,7 @@ class SystemResourceMonitor:
frameTable["innerWindowID"].append(0)
frameTable["implementation"].append(None)
frameTable["line"].append(line_num)
- frameTable["column"].append(None)
+ frameTable["column"].append(col_num)
frameTable["length"] += 1
# Create stack entry
@@ -1642,19 +1718,24 @@ class SystemResourceMonitor:
markers["category"].append(category_index)
markers["name"].append(name_index)
- # Extract and process stack if present (structured format: array of frame dicts)
+ # Extract and process stack if present
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,
- }
+ del data["stack"]
+
+ # Convert string stack to structured format if needed
+ if isinstance(stack, str):
+ stack = parse_stack(stack)
+
+ stack_index = get_stack_index(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)