Skip to content

Commit dc728bb

Browse files
Nikhil Thomasmeta-codesync[bot]
authored andcommitted
Add Xenon gCPU % to SSA Inspector
Summary: Add exclusive (leaf) CPU % from the xenon Scuba dataset to the `top` and `inspect` commands, so users can see whether a hot translation is actually worth optimizing. - New file `xenon_fetcher.py`: queries xenon via Bamboo for the top 500 functions by sample count, computes each function's % of total CPU. - `top` command now shows a `gCPU=X.XX%` column (or `-` if the function isn't in xenon's top 500). - `inspect` command shows a `Xenon gCPU: X.XX% of total CPU` header line. - `--xenon-hours N` flag on both commands (default 4, set to 0 to skip). Reviewed By: ricklavoie Differential Revision: D94533406 fbshipit-source-id: caef01810fc0eb85532da750f277b0c3d0622e3d
1 parent 1885ca9 commit dc728bb

3 files changed

Lines changed: 157 additions & 10 deletions

File tree

hphp/tools/ssa-inspector/README.md

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,26 +17,31 @@ decisions, machine code ranges with disassembly, and profile execution counts.
1717
# 1. List available runs (find recent data)
1818
buck run fbcode//hphp/tools/ssa-inspector:main -- list-runs --days 7
1919

20-
# 2. Find the hottest translations in a run
20+
# 2. Find the hottest translations in a run (includes Xenon gCPU %)
2121
buck run fbcode//hphp/tools/ssa-inspector:main -- \
22-
top --date 2026-02-24 --run-uuid "2026-02-24.prod.web.cln.0-1" --top-n 10
22+
top --date 2026-02-25 --run-uuid "2026-02-25.prod.web.cln.0-1" --top-n 10
2323

24-
# 3. Inspect a specific translation in detail
24+
# 2b. Skip Xenon lookup (faster, no Scuba query)
2525
buck run fbcode//hphp/tools/ssa-inspector:main -- \
26-
inspect --trans-id 90516 \
27-
--date 2026-02-24 --run-uuid "2026-02-24.prod.web.cln.0-1" \
26+
top --date 2026-02-25 --run-uuid "2026-02-25.prod.web.cln.0-1" --top-n 10 \
27+
--xenon-hours 0
28+
29+
# 3. Inspect a specific translation in detail (shows gCPU % in header)
30+
buck run fbcode//hphp/tools/ssa-inspector:main -- \
31+
inspect --trans-id 58753 \
32+
--date 2026-02-25 --run-uuid "2026-02-25.prod.web.cln.0-1" \
2833
--format detail
2934

3035
# 4. Search by function name
3136
buck run fbcode//hphp/tools/ssa-inspector:main -- \
3237
inspect --function "contains_key" \
33-
--date 2026-02-24 --run-uuid "2026-02-24.prod.web.cln.0-1" \
38+
--date 2026-02-25 --run-uuid "2026-02-25.prod.web.cln.0-1" \
3439
--format summary
3540

3641
# 5. Include machine code disassembly
3742
buck run fbcode//hphp/tools/ssa-inspector:main -- \
38-
inspect --trans-id 90544 \
39-
--date 2026-02-24 --run-uuid "2026-02-24.prod.web.cln.0-1" \
43+
inspect --trans-id 58753 \
44+
--date 2026-02-25 --run-uuid "2026-02-25.prod.web.cln.0-1" \
4045
--format detail --disasm
4146

4247
# 6. Load from a local trace file (no Hive needed)
@@ -55,6 +60,19 @@ Run `top` to identify the hottest translations by execution count (profCount).
5560
Focus on `TransOptimize` translations — these are the fully optimized JIT output
5661
and represent the code that actually runs in production.
5762

63+
By default, the `top` and `inspect` commands also query the **Xenon** Scuba
64+
dataset (`xenon`) to show each function's **exclusive gCPU %** — what fraction
65+
of total CPU the function itself consumes (leaf cost, not including callees).
66+
This answers "is optimizing this function worth it?" since a high profCount
67+
doesn't necessarily mean high CPU cost.
68+
69+
- `gCPU=0.18%` means the function is 0.18% of total fleet CPU (last 4 hours).
70+
- `gCPU= -` means the function isn't in xenon's top 500 — it likely consumes
71+
negligible CPU at the leaf level (common for builtins like `ord`, `strlen`
72+
whose actual cost is attributed to native code).
73+
- Use `--xenon-hours N` to change the lookback window (default: 4 hours).
74+
- Use `--xenon-hours 0` to skip the Xenon query entirely.
75+
5876
### Step 2: Inspect in Summary Mode
5977

6078
Use `--format summary` to quickly scan a translation's structure. The summary
@@ -164,4 +182,5 @@ Key opcodes and their meanings:
164182
| `data_fetcher.py` | Presto/Hive queries + local file loading |
165183
| `ir_parser.py` | JSON blob → dataclass parsing |
166184
| `formatter.py` | Summary and detail output formatting |
185+
| `xenon_fetcher.py` | Xenon Scuba queries for gCPU % (exclusive/leaf cost) |
167186
| `main.py` | CLI entry point (argparse) |

hphp/tools/ssa-inspector/main.py

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@
1212
from __future__ import annotations
1313

1414
import argparse
15+
import json
1516
import sys
1617

1718
from hphp.tools.ssa_inspector import data_fetcher
1819
from hphp.tools.ssa_inspector.formatter import format_translation
1920
from hphp.tools.ssa_inspector.ir_parser import parse_json_blob
21+
from hphp.tools.ssa_inspector.xenon_fetcher import fetch_gcpu_for_functions
2022

2123

2224
def cmd_list_runs(args: argparse.Namespace) -> None:
@@ -42,14 +44,26 @@ def cmd_top(args: argparse.Namespace) -> None:
4244
print("No translations found.")
4345
return
4446

47+
# Fetch xenon gCPU data if requested
48+
gcpu_map: dict[str, float] = {}
49+
if args.xenon_hours > 0:
50+
func_names = list({r.get("func_name", "") for r in rows if r.get("func_name")})
51+
gcpu_map = fetch_gcpu_for_functions(func_names, args.xenon_hours)
52+
4553
print(f"Top {len(rows)} translations by profCount:\n")
4654
for i, r in enumerate(rows, 1):
4755
pc = r.get("prof_count", 0)
56+
func_name = r.get("func_name", "?")
57+
gcpu_str = ""
58+
if args.xenon_hours > 0:
59+
gcpu_pct = gcpu_map.get(func_name)
60+
gcpu_str = f" gCPU={gcpu_pct:>5.2f}%" if gcpu_pct else " gCPU= -"
4861
print(
4962
f" {i:3d}. trans_id={r['trans_id']:<8}"
5063
f" profCount={pc:>12,}"
64+
f"{gcpu_str}"
5165
f" kind={r.get('kind', '?'):<18}"
52-
f" {r.get('func_name', '?')}"
66+
f" {func_name}"
5367
)
5468

5569
if args.format != "list":
@@ -65,6 +79,20 @@ def cmd_top(args: argparse.Namespace) -> None:
6579
print()
6680

6781

82+
def _extract_func_names(rows: list[dict]) -> list[str]:
83+
"""Extract function names from translation data rows."""
84+
func_names: list[str] = []
85+
for r in rows:
86+
try:
87+
data = json.loads(r["data"])
88+
name = data.get("translation", {}).get("funcName", "")
89+
if name:
90+
func_names.append(name)
91+
except (json.JSONDecodeError, KeyError):
92+
pass
93+
return func_names
94+
95+
6896
def cmd_inspect(args: argparse.Namespace) -> None:
6997
if args.local_file:
7098
rows = data_fetcher.load_from_local_file(
@@ -101,7 +129,28 @@ def cmd_inspect(args: argparse.Namespace) -> None:
101129
print("No translations found.")
102130
return
103131

104-
print(f"Found {len(rows)} translation(s).\n")
132+
# Extract function names for xenon lookup
133+
func_names = _extract_func_names(rows)
134+
135+
# Fetch xenon gCPU data if requested
136+
gcpu_map: dict[str, float] = {}
137+
if args.xenon_hours > 0 and func_names:
138+
gcpu_map = fetch_gcpu_for_functions(list(set(func_names)), args.xenon_hours)
139+
140+
print(f"Found {len(rows)} translation(s).")
141+
142+
if args.xenon_hours > 0 and func_names:
143+
# Show gCPU for the primary function
144+
gcpu_pct = gcpu_map.get(func_names[0])
145+
if gcpu_pct is not None:
146+
print(
147+
f"Xenon gCPU: {gcpu_pct:.2f}% of total CPU "
148+
f"(last {args.xenon_hours}h, exclusive)"
149+
)
150+
else:
151+
print(f"Xenon gCPU: not in top 500 functions (last {args.xenon_hours}h)")
152+
153+
print()
105154
for r in rows:
106155
translation = parse_json_blob(r["data"])
107156
print(format_translation(translation, fmt=args.format, show_disasm=args.disasm))
@@ -142,6 +191,12 @@ def main() -> None:
142191
action="store_true",
143192
help="Include machine code disassembly",
144193
)
194+
p_top.add_argument(
195+
"--xenon-hours",
196+
type=int,
197+
default=4,
198+
help="Xenon lookback window in hours for gCPU %% (0 to skip, default: 4)",
199+
)
145200
p_top.set_defaults(func=cmd_top)
146201

147202
# inspect
@@ -169,6 +224,12 @@ def main() -> None:
169224
action="store_true",
170225
help="Include machine code disassembly",
171226
)
227+
p_inspect.add_argument(
228+
"--xenon-hours",
229+
type=int,
230+
default=4,
231+
help="Xenon lookback window in hours for gCPU %% (0 to skip, default: 4)",
232+
)
172233
p_inspect.set_defaults(func=cmd_inspect)
173234

174235
args = parser.parse_args()
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary.
2+
3+
"""
4+
Fetch Xenon gCPU data from the xenon Scuba dataset.
5+
6+
Provides exclusive (leaf) CPU % for PHP functions, matching the fn_name
7+
column format used by tcprint (ClassName::methodName).
8+
"""
9+
10+
from __future__ import annotations
11+
12+
from analytics.bamboo import Bamboo as bb
13+
14+
15+
def _escape_scuba_sql(s: str) -> str:
16+
"""Escape a string for use in a Scuba SQL string literal.
17+
18+
Backslashes in PHP namespaces (e.g. FlibSL\\C\\contains_key) need
19+
to be doubled in Scuba SQL.
20+
"""
21+
return s.replace("\\", "\\\\").replace("'", "\\'")
22+
23+
24+
def fetch_gcpu_for_functions(
25+
function_names: list[str],
26+
lookback_hours: int = 4,
27+
) -> dict[str, float]:
28+
"""Return {func_name: gcpu_pct} for each function.
29+
30+
Queries xenon for exclusive CPU % (leaf cost) using the fn_name column.
31+
Returns percentages as floats (e.g. 0.12 means 0.12% of total CPU).
32+
"""
33+
if not function_names:
34+
return {}
35+
36+
query = (
37+
f"SELECT `fn_name`, COUNT(1) AS `sample_count` "
38+
f"FROM `xenon` "
39+
f"WHERE `time` >= NOW() - {lookback_hours} * 3600 "
40+
f"AND `is_io` = '0' "
41+
f"GROUP BY `fn_name` "
42+
f"ORDER BY `sample_count` DESC "
43+
f"LIMIT 500"
44+
)
45+
46+
df = bb.query_scuba_nullable(sql=query)
47+
48+
if df.empty:
49+
return {}
50+
51+
# Build a map of fn_name -> sample_count from the results
52+
total_samples = int(df["sample_count"].sum())
53+
if total_samples == 0:
54+
return {}
55+
56+
name_to_count: dict[str, int] = dict(
57+
zip(df["fn_name"].astype(str), df["sample_count"].astype(int))
58+
)
59+
60+
# Compute gCPU % for requested functions
61+
result: dict[str, float] = {}
62+
for func in function_names:
63+
count = name_to_count.get(func, 0)
64+
if count > 0:
65+
result[func] = round(count / total_samples * 100, 4)
66+
67+
return result

0 commit comments

Comments
 (0)