diff --git a/CMakeLists.txt b/CMakeLists.txt index e36d596..d68f238 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.20) -project(xprobe VERSION 0.6.0 LANGUAGES C) +project(xprobe VERSION 0.6.1 LANGUAGES C) include(CTest) diff --git a/Cargo.lock b/Cargo.lock index e90bab7..09041fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -732,7 +732,7 @@ dependencies = [ [[package]] name = "xprobe-cli" -version = "0.6.0" +version = "0.6.1" dependencies = [ "clap", "serde", @@ -746,7 +746,7 @@ dependencies = [ [[package]] name = "xprobe-collector" -version = "0.6.0" +version = "0.6.1" dependencies = [ "libbpf-rs", "perf-event-open", @@ -756,7 +756,7 @@ dependencies = [ [[package]] name = "xprobe-core" -version = "0.6.0" +version = "0.6.1" dependencies = [ "cpp_demangle", "nix", @@ -768,7 +768,7 @@ dependencies = [ [[package]] name = "xprobe-correlator" -version = "0.6.0" +version = "0.6.1" dependencies = [ "regex", "serde_json", @@ -777,7 +777,7 @@ dependencies = [ [[package]] name = "xprobe-exporter" -version = "0.6.0" +version = "0.6.1" dependencies = [ "serde_json", "xprobe-protocol", @@ -785,7 +785,7 @@ dependencies = [ [[package]] name = "xprobe-protocol" -version = "0.6.0" +version = "0.6.1" dependencies = [ "schemars", "serde", diff --git a/Cargo.toml b/Cargo.toml index 1357027..f9bdb3b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ members = [ resolver = "2" [workspace.package] -version = "0.6.0" +version = "0.6.1" edition = "2024" license = "Apache-2.0" repository = "https://github.com/itdevwu/xprobe" diff --git a/README.md b/README.md index 16f7a79..d9ebd8d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@
AI-native heterogeneous profiling tool for CPU and GPU.
+Find out why your CPU, Python, or CUDA process is slow — in two commands.
+A bounded Linux profiler for AI coding agents and the humans working with them:
+ eBPF + perf + NVIDIA CUPTI, one CLI, strict JSON, no daemon.
+
+
Real numbers from the v0.6.0 release gate on an RTX 3060: a PyTorch CUDA workload and a request → launch → kernel path.
+ +## What it does + +Point xprobe at a running process. It watches one bounded window and tells you +where the time went — on-CPU hotspots, threads waiting for a CPU or blocked, +container CPU throttling, hot syscalls, GPU kernels, and GPU clock limits — +ranked by impact, each with a ready next measurement. The second command then +measures that exact latency: a function, a syscall, a CUDA API call, a kernel, +a copy, an NVTX range, or a whole request → launch → kernel path from a single +capture. + +- **Built for AI agents.** Ships as an [Agent Skill](skills/xprobe-measure-latency/SKILL.md) + for Claude Code, Codex, and Cursor. Compact JSON with a `good`/`caution` + quality verdict, so an agent can act on results without reading 100 KB of + events. +- **CPU, Python, and GPU in one tool.** Sampled native and CPython stacks, + eBPF uprobes, syscalls, tracepoints, CPython GC, and CUDA 12/13 activity via + CUPTI, correlated with explicit confidence. +- **Bounded by design.** Every capture is limited by samples or duration, + timeout, and capacity; there is no daemon; CUDA injection is disclosed + before it happens; the raw events are always kept for offline re-analysis. +- **Honest about what it knows.** Sampled shares are estimates, temporal + matches are labeled heuristic, and anything a capture cannot answer is listed + as not covered. ## Install @@ -23,7 +51,7 @@ only installation action required from the user: ```bash npx skills@1 add \ - https://github.com/itdevwu/xprobe/tree/v0.6.0/skills/xprobe-measure-latency \ + https://github.com/itdevwu/xprobe/tree/v0.6.1/skills/xprobe-measure-latency \ --global ``` diff --git a/docs/agent-integration.md b/docs/agent-integration.md index 36cfa2e..feea27a 100644 --- a/docs/agent-integration.md +++ b/docs/agent-integration.md @@ -17,7 +17,7 @@ repairs the matching xprobe CLI itself: ```bash npx skills@1 add \ - https://github.com/itdevwu/xprobe/tree/v0.6.0/skills/xprobe-measure-latency \ + https://github.com/itdevwu/xprobe/tree/v0.6.1/skills/xprobe-measure-latency \ --global ``` diff --git a/docs/assets/social-preview.png b/docs/assets/social-preview.png new file mode 100644 index 0000000..b0092f5 Binary files /dev/null and b/docs/assets/social-preview.png differ diff --git a/docs/assets/social-preview.svg b/docs/assets/social-preview.svg new file mode 100644 index 0000000..e76538c --- /dev/null +++ b/docs/assets/social-preview.svg @@ -0,0 +1 @@ + diff --git a/docs/assets/xprobe-flow-dark.svg b/docs/assets/xprobe-flow-dark.svg new file mode 100644 index 0000000..f7b5d22 --- /dev/null +++ b/docs/assets/xprobe-flow-dark.svg @@ -0,0 +1,34 @@ + diff --git a/docs/assets/xprobe-flow-light.svg b/docs/assets/xprobe-flow-light.svg new file mode 100644 index 0000000..ea0aabc --- /dev/null +++ b/docs/assets/xprobe-flow-light.svg @@ -0,0 +1,34 @@ + diff --git a/docs/installation.md b/docs/installation.md index a98ea9c..f1ea2a1 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -13,7 +13,7 @@ user needs to run: ```bash npx skills@1 add \ - https://github.com/itdevwu/xprobe/tree/v0.6.0/skills/xprobe-measure-latency \ + https://github.com/itdevwu/xprobe/tree/v0.6.1/skills/xprobe-measure-latency \ --global ``` @@ -29,7 +29,7 @@ The versioned bootstrap installs to `~/.local` without root access: ```bash curl --proto '=https' --tlsv1.2 -fsSL \ - https://raw.githubusercontent.com/itdevwu/xprobe/v0.6.0/install.sh | sh + https://raw.githubusercontent.com/itdevwu/xprobe/v0.6.1/install.sh | sh ``` The bootstrap downloads the release archive and its SHA256 file, verifies the @@ -48,7 +48,7 @@ prefix, download the script and pass `--prefix`: ```bash curl --proto '=https' --tlsv1.2 -fsSLO \ - https://raw.githubusercontent.com/itdevwu/xprobe/v0.6.0/install.sh + https://raw.githubusercontent.com/itdevwu/xprobe/v0.6.1/install.sh sh install.sh --prefix /opt/xprobe ``` @@ -60,7 +60,7 @@ script with `sudo`. The installer never elevates privileges itself. For a fully explicit archive workflow: ```bash -version=0.6.0 +version=0.6.1 base=https://github.com/itdevwu/xprobe/releases/download/v$version archive=xprobe-$version-linux-x86_64.tar.gz @@ -106,7 +106,7 @@ missing or damaged, manually install the complete version-matched directory: ```bash npx skills@1 add \ - https://github.com/itdevwu/xprobe/tree/v0.6.0/skills/xprobe-measure-latency \ + https://github.com/itdevwu/xprobe/tree/v0.6.1/skills/xprobe-measure-latency \ --global ``` @@ -115,7 +115,7 @@ installation names the target explicitly: ```bash npx --yes skills@1 add \ - https://github.com/itdevwu/xprobe/tree/v0.6.0/skills/xprobe-measure-latency \ + https://github.com/itdevwu/xprobe/tree/v0.6.1/skills/xprobe-measure-latency \ --agent codex --global --copy --yes ``` diff --git a/install.sh b/install.sh index d9e8ecb..46eb4ec 100755 --- a/install.sh +++ b/install.sh @@ -2,7 +2,7 @@ set -eu repository=${XPROBE_REPOSITORY:-itdevwu/xprobe} -version=${XPROBE_VERSION:-0.6.0} +version=${XPROBE_VERSION:-0.6.1} if [ -n "${XPROBE_PREFIX:-}" ]; then prefix=$XPROBE_PREFIX elif [ -n "${HOME:-}" ]; then @@ -19,7 +19,7 @@ Install a released xprobe binary and its CUDA Agents. Usage: install.sh [--version VERSION] [--prefix DIR] [--uninstall] Options: - --version VERSION Release to install (default: 0.6.0) + --version VERSION Release to install (default: 0.6.1) --prefix DIR Installation prefix (default: $HOME/.local) --uninstall Remove xprobe from the selected prefix -h, --help Show this help diff --git a/scripts/render-readme-art.py b/scripts/render-readme-art.py new file mode 100755 index 0000000..a93b7a7 --- /dev/null +++ b/scripts/render-readme-art.py @@ -0,0 +1,256 @@ +#!/usr/bin/env python3 +"""Render the README hero animation as light and dark SVGs. + +The numbers come from the v0.6.0 live release gate on an RTX 3060: the PyTorch +CUDA triage fixture and the multisource request/launch/kernel graph. Rerun +after changing the story: + + scripts/render-readme-art.py docs/assets +""" + +import pathlib +import sys +from html import escape + +WIDTH, HEIGHT = 960, 540 +CYCLE_S = 14.0 +FONT = "ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace" +SANS = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif" + +THEMES = { + "light": { + "bg": "#ffffff", + "panel": "#f6f8fa", + "border": "#d0d7de", + "text": "#1f2328", + "muted": "#59636e", + "prompt": "#0969da", + "cpu": "#8250df", + "gpu": "#1a7f37", + "env": "#9a6700", + "track": "#eaeef2", + }, + "dark": { + "bg": "#0d1117", + "panel": "#161b22", + "border": "#30363d", + "text": "#e6edf3", + "muted": "#8d96a0", + "prompt": "#4493f8", + "cpu": "#ab7df8", + "gpu": "#3fb950", + "env": "#d29922", + "track": "#21262d", + }, +} + +# (start, end) as fractions of the cycle for each stage. +STAGES = { + "cmd1": (0.02, 0.10), + "find1": (0.12, 0.16), + "find2": (0.16, 0.20), + "find3": (0.20, 0.24), + "next": (0.27, 0.31), + "cmd2": (0.34, 0.42), + "lanes": (0.44, 0.48), + "edge1": (0.50, 0.55), + "edge2": (0.56, 0.61), + "edge3": (0.62, 0.67), + "tagline": (0.72, 0.77), +} +HOLD_UNTIL = 0.93 + + +def keyframes(name: str, start: float, end: float, kind: str) -> str: + """Hidden until start, shown from end to the loop's end. + + The whole scene fades out and back in around the loop boundary, so + individual elements never need their own exit frames. + """ + s, e = (round(v * 100, 2) for v in (start, end)) + if kind == "type": + hidden, shown = "transform: scaleX(1)", "transform: scaleX(0)" + elif kind == "grow": + hidden, shown = "transform: scaleX(0)", "transform: scaleX(1)" + else: + hidden = "opacity: 0; transform: translateY(6px)" + shown = "opacity: 1; transform: translateY(0)" + return f"@keyframes {name} {{ 0%, {s}% {{ {hidden} }} {e}%, 100% {{ {shown} }} }}" + + +def scene_keyframes() -> str: + h = round(HOLD_UNTIL * 100, 2) + return ( + f"@keyframes scene {{ 0% {{ opacity: 0 }} 1.5%, {h}% {{ opacity: 1 }} " + f"99%, 100% {{ opacity: 0 }} }}" + ) + + +def style(theme: dict) -> str: + rules = [ + f".mono {{ font-family: {FONT}; }}", + f".sans {{ font-family: {SANS}; }}", + ".anim { transform-box: fill-box; }", + scene_keyframes(), + f".scene {{ animation: scene {CYCLE_S}s linear infinite; }}", + ] + for name, (start, end) in STAGES.items(): + kind = "type" if name.startswith("cmd") else "fade" + rules.append(keyframes(f"k-{name}", start, end, kind)) + rules.append( + f".{name} {{ animation: k-{name} {CYCLE_S}s ease-out infinite; " + f"{'transform-origin: right center;' if kind == 'type' else ''} }}" + ) + for name in ("find1", "find2", "find3"): + start, end = STAGES[name] + rules.append(keyframes(f"g-{name}", end, end + 0.06, "grow")) + rules.append( + f".bar-{name} {{ animation: g-{name} {CYCLE_S}s ease-out infinite; " + "transform-origin: left center; }" + ) + rules.append( + "@media (prefers-reduced-motion: reduce) { * { animation: none !important; } " + ".cover { display: none; } }" + ) + return "\n".join(rules) + + +def text(x, y, content, size, fill, cls="mono", weight="400", anchor="start") -> str: + return ( + f'