Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 20 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
</p>

<p align="center">
<img src="assets/hotspot.gif" alt="hotspot: picking a process from the live table, then a flame graph with a per-frame hover tooltip" width="820">
<img src="assets/hotspot.gif" alt="hotspot: a live flame graph of bash under a 499 Hz cgroup-scoped sampler, then the same profile as a flat self-time table with kernel frames tinted purple" width="820">
</p>

**`hotspot` is an eBPF sampling CPU profiler for Linux: pick a process out of a live table, click it, and watch a flat self-time profile build in real time with user and kernel frames side by side.**
Expand All @@ -27,7 +27,7 @@ The thing you would otherwise reach for is `perf record` followed by `perf repor
## Questions this tool answers

**One process is pinning a core and I need to know which function, right now.**
`yeet run gh:yeet-src/hotspot --tty`, click the process, and read the top of the table. Rows are self-time only, sorted hottest first, so the top row is where the CPU actually is rather than a caller that merely contains it. The first rows land in about a second at the default 499 Hz. See [What you're looking at](#what-youre-looking-at).
`yeet run gh:yeet-src/hotspot`, click the process, and read the top of the table. Rows are self-time only, sorted hottest first, so the top row is where the CPU actually is rather than a caller that merely contains it. The first rows land in about a second at the default 499 Hz. See [What you're looking at](#what-youre-looking-at).

**I'm SSHed into a box where I can't install `perf` and there's no matching `linux-tools` package. Can I still profile?**
Yes, and this is the case the shape is for. `perf` needs a `linux-tools` build matched to the running kernel, which is exactly what a minimal or slightly-behind image doesn't have. `hotspot` needs the yeet daemon and this script; the sampler is a CO-RE BPF program, so there's no per-kernel recompile and nothing to match. It draws in the terminal you're already in, so there's also no port to forward.
Expand Down Expand Up @@ -72,8 +72,8 @@ Not directly. `hotspot` is a mouse-driven TUI with no headless mode and no `--js
## Quick start

```sh
curl -fsSL https://yeet.cx | sh # install yeet, once
yeet run gh:yeet-src/hotspot --tty # clone, build and run in one step
curl -fsSL https://yeet.cx | sh # install yeet, once
yeet run gh:yeet-src/hotspot # clone, build and run in one step
```
[Manual install guide](https://yeet.cx/docs/install/manual-installation?utm_source=github&utm_medium=readme&utm_campaign=hotspot) | Linux only

Expand All @@ -86,13 +86,13 @@ Working on the script itself instead of just running it? Clone it and build in p

```sh
git clone https://github.com/yeet-src/hotspot && cd hotspot
make # compile bin/probe.bpf.o + bundle the JS (toolchain auto-fetched)
yeet run . --tty # the live process table; click a process to profile it
make # compile bin/probe.bpf.o + bundle the JS (toolchain auto-fetched)
yeet run . # the live process table; click a process to profile it
```

With no flags you land on the process table: every process with an executable, sorted by name. Kernel threads are filtered out because they have no `exe` and no user stacks. Click a row to select it, click the selected row (or `⏎`) to start profiling, and the pane becomes a live profile. No sampling happens until you open a process.

Script flags go **after `--`** so the runtime routes them to the script rather than consuming them itself, which is the most common first-run mistake. Note that `--tty` is a flag to `yeet run` itself and therefore goes *before* the `--`.
Script flags go **after `--`** so the runtime routes them to the script rather than consuming them itself, which is the most common first-run mistake. Flags belonging to `yeet run` itself (`--quiet`, `--watch`, and so on) go *before* the `--`.

| flag | default | meaning |
| --- | --- | --- |
Expand All @@ -103,9 +103,9 @@ Script flags go **after `--`** so the runtime routes them to the script rather t

```sh
R=gh:yeet-src/hotspot
yeet run $R --tty -- --freq 997 # sample harder
yeet run $R --tty -- --repo torvalds/linux --rev v6.12 # link kernel rows to a tag
yeet run $R --tty -- --repo me/svc --strip /build/src/ # bridge a container build path
yeet run $R -- --freq 997 # sample harder
yeet run $R -- --repo torvalds/linux --rev v6.12 # link kernel rows to a tag
yeet run $R -- --repo me/svc --strip /build/src/ # bridge a container build path
```

(From a clone, `.` replaces `gh:yeet-src/hotspot` in any of these.)
Expand All @@ -126,7 +126,7 @@ Read AGENTS.md first, then:
something to show on an idle box:
make demo
./demo/cafe &
4. Run: yeet run . --tty
4. Run: yeet run .
5. Navigate to the `cafe` process with the arrow keys and press Enter.
Confirm that within a few seconds you see rows with real function names
(toil, grind_beans, steam_milk, tamp_layer) and non-zero sample counts.
Expand Down Expand Up @@ -163,29 +163,15 @@ The mental model for what `hotspot` measures, and what that does and doesn't tel

Two screens. The process table:

```
hotspot · 214 processes click/⏎ profile · r refresh · q quit
1041 cafe /home/dev/hotspot/demo/cafe
892 containerd /usr/bin/containerd
1518 node /usr/lib/node_modules/.bin/node
734 postgres /usr/lib/postgresql/16/bin/postgres
1 systemd /usr/lib/systemd/systemd
```
<p align="center">
<img src="assets/hotspot-process.gif" alt="hotspot's process table: every process with an executable, sorted by name, with pid, command and path columns; arrowing down moves the highlighted row" width="820">
</p>

Then the flat profile, once you open one:

```
cafe (1041) · sampling 499 Hz (cgroup) ←/esc back · f flame · t stream · q quit
% samples ● function · 4,812 samples · 63 pcs symbolized
31.4% ██████████████ 1511 ● toil
18.2% ████████ 876 ● steam_milk
12.7% █████▌ 611 ● knead_dough
9.1% ████ 438 ● grind_beans
6.3% ██▊ 303 ● tamp_layer
4.8% ██▏ 231 ● __arch_clear_user kernel
2.1% ▉ 101 ● ??
● cafe ● libc.so.6 ● kernel
```
<p align="center">
<img src="assets/hotspot-flat-profile.gif" alt="hotspot's flat profile of postgres: self-time rows sorted hottest first with percentage, heat bar and sample count, kernel frames tinted purple, and an object legend along the bottom" width="820">
</p>

The **header** carries the target, the sampler's state (`arming sampler…`, `sampling 499 Hz (cgroup)`, or a probe error), and the keys for this view. The **status line** repeats the column layout and the running totals: cumulative samples and how many distinct PCs have been named so far. The **table** is one row per function, self-time only, hottest first. The **legend row** at the bottom keys the colored dots to the object each function lives in.

Expand Down Expand Up @@ -243,7 +229,7 @@ The **stream view** (`t`) is a live flame chart where x is time: one column per
When the target binary carries DWARF, each row knows the `file:line` of its **hottest** PC, meaning the hottest line inside the function rather than its declaration. Pass `--repo` and those become clickable links:

```sh
yeet run . --tty -- --repo me/service --rev deploy-2026-08-01
yeet run . -- --repo me/service --rev deploy-2026-08-01
```

Function names then paint as OSC 8 hyperlinks (ctrl or cmd-click in most terminals), and `o` copies the selected row's URL to your clipboard via OSC 52.
Expand Down Expand Up @@ -348,7 +334,7 @@ An idle box makes a boring profiler. `demo/` holds two synthetic CPU burners who
```sh
make demo
./demo/cafe & # or ./demo/patterns
yeet run . --tty # click `cafe` in the process list
yeet run . # click `cafe` in the process list
```

`cafe` simulates a café rotating through three shifts every four seconds, each burning CPU in its own call subtree, with a twelve-frame recursive tower in the espresso machine. It's the better single take: three distinct subtrees in flame mode and four-second bands sliding past in stream mode.
Expand Down Expand Up @@ -392,7 +378,7 @@ There's no capture script; [`demo/README.md`](demo/README.md) has the recording
That is a clone-authentication failure, not a script problem. `gh:` shorthand tries SSH first
(`git@github.com:yeet-src/hotspot`) so private repos work with your own keys, then retries over
HTTPS. If neither has a usable credential you get this. Check `ssh -T git@github.com` succeeds,
or clone it yourself and run `yeet run . --tty` from the directory.
or clone it yourself and run `yeet run .` from the directory.

**Why is the top row `??` with a spinner that never resolves?**
A spinner means queued, and a spinner that persists means the batch failed rather than that it's slow. The status line shows `sym: <error>` when symbolization errored; the most common cause is the process exiting while being profiled, since the resolver reads its live maps. Reopen the process.
Expand Down
Binary file added assets/hotspot-flat-profile.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/hotspot-process.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/hotspot.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 4 additions & 4 deletions demo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ idle box. Both are built for recording: their call graphs are designed so the
flame view has structure and the stream view has motion.

```sh
make demo # builds demo/cafe and demo/patterns
./demo/cafe # in one shell
yeet run . --tty # in another: click `cafe` in the process list
make demo # builds demo/cafe and demo/patterns
./demo/cafe # in one shell
yeet run . # in another: click `cafe` in the process list
```

Both are built `-O0 -g`: the stack walk needs frame pointers, and the whole
Expand Down Expand Up @@ -54,7 +54,7 @@ that produces a readable GIF:
make demo
./demo/cafe & # or ./demo/patterns
# resize the terminal to ~100x30, then record:
yeet run . --tty
yeet run .
```

A tour that fits in ~20 seconds, in the order that reads best:
Expand Down
Loading