Files
natsdotnet/tools/dtp_parse.py
Joseph Doherty 5de4962bd3 Improve docs coverage and refresh profiling parser artifacts
Add domain-specific XML documentation across src server components to satisfy CommentChecker, and update dotTrace parsing outputs used for diagnostics.
2026-03-14 04:06:04 -04:00

122 lines
4.1 KiB
Python

#!/usr/bin/env python3
import argparse
import json
import os
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
HELPER_PROJECT = ROOT / "tools" / "DtpSnapshotExtractor" / "DtpSnapshotExtractor.csproj"
HELPER_DLL = ROOT / "tools" / "DtpSnapshotExtractor" / "bin" / "Debug" / "net10.0" / "DtpSnapshotExtractor.dll"
DEFAULT_DOTTRACE_DIR = Path.home() / "Applications" / "dotTrace.app" / "Contents" / "DotFiles"
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Parse a raw dotTrace .dtp snapshot family into JSON call-tree data."
)
parser.add_argument("snapshot", help="Path to the .dtp snapshot index file.")
parser.add_argument("--out", help="Write JSON to this file.")
parser.add_argument("--stdout", action="store_true", help="Write JSON to stdout.")
parser.add_argument("--top", type=int, default=200, help="Maximum hotspot and path entries to emit.")
parser.add_argument("--filter", dest="name_filter", help="Case-insensitive substring filter for node names.")
parser.add_argument(
"--flat",
"--paths",
dest="flat_paths",
action="store_true",
help="Include the top heaviest call paths as flat strings.",
)
idle_group = parser.add_mutually_exclusive_group()
idle_group.add_argument(
"--exclude-idle",
dest="exclude_idle",
action="store_true",
default=True,
help="Exclude idle and wait methods from hotspot and path rankings.",
)
idle_group.add_argument(
"--include-idle",
dest="exclude_idle",
action="store_false",
help="Keep idle and wait methods in hotspot and path rankings.",
)
return parser.parse_args()
def find_dottrace_dir() -> Path:
value = os.environ.get("DOTTRACE_APP_DIR")
if value:
candidate = Path(value).expanduser()
if candidate.is_dir():
return candidate
if DEFAULT_DOTTRACE_DIR.is_dir():
return DEFAULT_DOTTRACE_DIR
raise FileNotFoundError(
f"dotTrace assemblies not found. Set DOTTRACE_APP_DIR or install dotTrace under {DEFAULT_DOTTRACE_DIR}."
)
def build_helper(dottrace_dir: Path) -> None:
command = [
"dotnet",
"build",
str(HELPER_PROJECT),
"-nologo",
"-clp:ErrorsOnly",
f"-p:DotTraceAppDir={dottrace_dir}",
]
result = subprocess.run(command, cwd=ROOT, capture_output=True, text=True, check=False)
if result.returncode != 0:
raise RuntimeError(result.stderr.strip() or result.stdout.strip() or "dotnet build failed")
def run_helper(snapshot: Path, dottrace_dir: Path, args: argparse.Namespace) -> dict:
command = ["dotnet", str(HELPER_DLL), str(snapshot), "--top", str(args.top)]
if args.name_filter:
command.extend(["--filter", args.name_filter])
if args.flat_paths:
command.append("--flat")
if not args.exclude_idle:
command.append("--include-idle")
env = os.environ.copy()
env["DOTTRACE_APP_DIR"] = str(dottrace_dir)
result = subprocess.run(command, cwd=ROOT, capture_output=True, text=True, check=False, env=env)
if result.returncode != 0:
raise RuntimeError(result.stderr.strip() or result.stdout.strip() or "extractor failed")
return json.loads(result.stdout)
def main() -> int:
args = parse_args()
snapshot = Path(args.snapshot).expanduser().resolve()
if not snapshot.is_file():
print(f"Snapshot not found: {snapshot}", file=sys.stderr)
return 2
if not args.stdout and not args.out:
args.stdout = True
try:
dottrace_dir = find_dottrace_dir()
build_helper(dottrace_dir)
payload = run_helper(snapshot, dottrace_dir, args)
except Exception as exc: # noqa: BLE001
print(str(exc), file=sys.stderr)
return 1
text = json.dumps(payload, indent=2)
if args.out:
out_path = Path(args.out).expanduser().resolve()
out_path.write_text(text + "\n", encoding="utf-8")
if args.stdout:
sys.stdout.write(text + "\n")
return 0
if __name__ == "__main__":
raise SystemExit(main())