From 1f758a3669a80fb34bd7dbeeed0c2ccc5952837a Mon Sep 17 00:00:00 2001 From: anti Date: Fri, 17 Apr 2026 22:04:29 -0400 Subject: [PATCH] chore(profile): tolerate null/empty frames in walk_self_time Some pyinstrument frame trees contain branches where an identifier is missing (typically at the very top or with certain async boundaries), which crashed the aggregator with a KeyError mid-run. Short-circuit on None frames and missing identifiers so a single ugly HTML no longer kills the summary of the other few hundred. --- scripts/profile/aggregate_requests.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/profile/aggregate_requests.py b/scripts/profile/aggregate_requests.py index b636fcb..c8e376c 100755 --- a/scripts/profile/aggregate_requests.py +++ b/scripts/profile/aggregate_requests.py @@ -57,7 +57,7 @@ def _is_synthetic(identifier: str) -> bool: return identifier in _SYNTHETIC or identifier.startswith(("[self]", "[await]")) -def walk_self_time(frame: dict, acc: dict[str, float], parent_ident: str | None = None) -> None: +def walk_self_time(frame: dict | None, acc: dict[str, float], parent_ident: str | None = None) -> None: """ Accumulate self-time by frame identifier. @@ -65,7 +65,11 @@ def walk_self_time(frame: dict, acc: dict[str, float], parent_ident: str | None execution time. Rolling them into their parent ("self-time of X" vs. a global `[self]` bucket) is what gives us actionable per-function hotspots. """ - ident = frame["identifier"] + if not frame: + return + ident = frame.get("identifier") + if not ident: + return total = frame.get("time", 0.0) children = frame.get("children") or [] child_total = sum(c.get("time", 0.0) for c in children)