Source code

Revision control

Copy as Markdown

Other Tools

# 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 https://mozilla.org/MPL/2.0/.
"""Hang-signature heuristics for BHR stack aggregation.
Ported from the frontend's getHangFrames in
aggregation job can trim stacks upstream instead of the frontend doing it on
every page render.
History:
- Originally introduced in python_mozetl issue #410 / PR (heuristics
migration) as a Python port of the JS algorithm, with byte-for-byte parity
verified against 2,080 real recorded samples.
- Moved here as part of the bhr_collection migration from python_mozetl into
mozilla-central. The algorithm is unchanged.
The three heuristics:
1. Nested event loop trim — stop walking the stack at the innermost
``nsThread::ProcessNextEvent`` frame. Frames outside that point are
ancestor event loops that don't help identify the hang.
2. Non-Mozilla code collapse — once we cross into Mozilla code walking from
leaf to root, drop all but the immediate entry-point non-Mozilla frame.
We care HOW Firefox code reached system libraries, not the system internals.
3. SpiderMonkey internals strip — between two JS frames, drop frames that
are recognizable as JS engine internals. The JS interpreter machinery
isn't useful signal for hang triage.
Plus an XPConnect-glue special case that strips XPC_WN_* / XPCWrappedNative
/ XPTC__InvokebyIndex chains when they sit between a JS frame and native code.
The function operates on a symbolicated stack — a list of
``(func_name, lib_name)`` tuples in outer-first (root -> leaf) order.
"""
import re
# Mozilla libraries we recognize as "Firefox code". The frontend's isMozLib
# operates on a normalized lib name (with any trailing ".pdb" stripped); we
# replicate that normalization in _is_moz_lib so Windows debug-name variants
# match the Linux/macOS spellings.
_MOZILLA_LIBS = frozenset(["xul", "XUL", "libxul.so", "mozglue", "libmozglue.so"])
_EVENT_LOOP_FUNC_PREFIX = "nsThread::ProcessNextEvent(bool"
# SpiderMonkey-internal frame name prefixes. Any frame whose name starts with
# one of these is treated as "engine machinery" between JS frames.
_JS_INTERNAL_PREFIXES = (
"js::",
"JS::",
"static bool InternalCall",
"static bool Interpret",
"static bool js::",
"bool js::",
"static bool SetExistingProperty",
"(unresolved)",
)
# Pattern identifying JS function names: contains ".js" or ".xul" (file
# basename style) or starts with "self-hosted:".
_JS_FUNC_NAME_RE = re.compile(r"\.js|\.xul|^self-hosted:")
def _is_moz_lib(lib_name):
if not lib_name:
return False
if lib_name.endswith(".pdb"):
lib_name = lib_name[:-4]
return lib_name in _MOZILLA_LIBS
def _is_js_func_name(func_name):
return bool(_JS_FUNC_NAME_RE.search(func_name))
def apply_hang_signature_heuristics(stack):
"""Trim a symbolicated stack using the frontend's hang-signature heuristics.
The input is a list of ``(func_name, lib_name)`` tuples in outer-first
(root -> leaf) order. Frames the frontend would mark as hidden are removed
entirely so the upstream output has the same shape the frontend would
render.
Mirrors getHangFrames in hang-stats/bhr.js. Walks the stack leaf-first
internally (the order the frontend uses) and reverses on return so callers
can keep working in outer-first order.
"""
frames = []
should_remove_prefix = True
for func_name, lib_name in reversed(stack):
if func_name.startswith(_EVENT_LOOP_FUNC_PREFIX):
break
if (
should_remove_prefix
and _is_moz_lib(lib_name)
and "::InterposedNt" not in func_name
):
should_remove_prefix = False
if len(frames) > 1:
for i in range(len(frames) - 1):
frames[i][2] = True
if not lib_name and _is_js_func_name(func_name) and frames:
i = len(frames) - 1
while i and frames[i][0].startswith(_JS_INTERNAL_PREFIXES):
i -= 1
anchor_func, anchor_lib, _ = frames[i]
anchor_is_js = (
not anchor_lib and _is_js_func_name(anchor_func)
) or anchor_func.startswith("static bool XPC_WN_")
if anchor_is_js:
for ii in range(i + 1, len(frames)):
frames[ii][2] = True
if (
len(frames) > 3
and frames[-3][0] in ("XPTC__InvokebyIndex", "NS_InvokeByIndex")
and frames[-2][0].startswith("XPCWrappedNative::CallMethod(")
and frames[-1][0].startswith("static bool XPC_WN_")
):
for ii in range(len(frames) - 3, len(frames)):
frames[ii][2] = True
frames.append([func_name, lib_name, False])
visible = [(f[0], f[1]) for f in frames if not f[2]]
visible.reverse()
return visible