atop-flame is intentionally small — five Go files, no goroutines, no
network I/O. It reads stdin, builds an in-memory aggregate, and walks that
aggregate twice (once per renderer).
.
├── main.go flags, --help / --version, dispatch
├── parser.go line-level regex + per-label field extractor
├── aggregate.go ProcAgg, DeviceAgg, NetAgg, GpuAgg, system singletons
├── render_cli.go 24-bit ANSI bar sections + textual system summary
├── render_html.go go-echarts multi-chart page
├── go.mod / go.sum module path: github.com/rachlenko/atop-flame
└── vendor/ deps committed for hermetic builds
┌───────────────────┐
stdin ──────▶│ parser.go │
│ parseInput(r) │
│ per-label regex │
│ → *Aggregate │
└────────┬──────────┘
│
┌──────┴────────────────┐
▼ ▼
render_cli.go render_html.go
renderCLI(agg) renderHTML(agg, path)
ANSI to stdout go-echarts page
parser.go exposes a single entry point:
func parseInput(r io.Reader) *AggregateTwo regexes split lines into the common prefix
(LABEL host epoch date time interval …) plus a per-label tail. Each label
has a dedicated branch that pulls the relevant numeric fields out of the
tail. Unknown labels and short / malformed tails are silently skipped —
robustness over strictness, because atop's column layout has changed
across versions.
SEP separator lines bump Aggregate.Snapshots, which the renderers use
to label the system-summary block.
aggregate.go defines four maps keyed by name (with parallel *Order
slices for stable insertion-order iteration):
| Field | Holds | Keyed by |
|---|---|---|
procs |
*ProcAgg |
process name |
disks / lvms / mdds |
*DeviceAgg |
device name |
nets / ifbs |
*NetAgg |
interface name |
gpus |
*GpuAgg |
GPU id |
System singletons (CPU, CPL, MEM, SWP, PAG, PSI, NFS,
PerCore) are pointers — nil when the corresponding label was never
seen.
Merging rules per metric are documented in Metrics. The headline ones:
PRCticks → sum (per-thread atop output → one row per process)PRMRSS / VSize → max (threads share memory)PRD/PRN→ sumPRG→ max
Both renderers share the same shape:
for each section:
list := agg.topProcs(scoreFn, topN) // or topDevices / topNets
if len(list) == 0 { continue } // ← absent-section skip
drawSection(list)agg.HasAny() short-circuits the whole pipeline when stdin contained nothing
recognizable — the binary prints a stderr notice and exits zero.
CLI (render_cli.go):
- 24-bit ANSI escapes via
go-colorful. - Per-section bar widths normalized to 36 columns; the longest row in the section fills the full bar.
- The system summary at the end is plain text, not a bar.
HTML (render_html.go):
go-echarts v2horizontal stacked bars.- One
*charts.Barper section, combined into acomponents.NewPage(). - Page is rendered to disk; no JS bundler, no asset pipeline — output is a single ~100 KB HTML file.
For per-thread metrics, atop emits one line per thread under the same
process name. PRC ticks are independent per thread (each thread eats its
own CPU), so summing reflects the parent process's CPU usage. PRM,
however, reports virtual / resident size per thread, and threads in one
process share their address space — summing those would multiply a
single process's memory by its thread count. The merge rule per metric is
chosen so that the bar length matches the user's mental model of "how much
of this resource did this program use."
main.go exposes a small resolveVersion() helper that returns the most
specific version it can find:
- The ldflags-injected
appVersionvalue (CI / Makefile builds). - The module version stamped into the binary by
go install …@vX.Y.Z, read at runtime fromruntime/debug.ReadBuildInfo. - The literal string
devas a last resort.
This keeps atop-flame --version truthful in every install path without
needing per-path build glue.
- No
--output FILE. HTML mode always writes./atop-flame.htmlso there's one canonical artifact name to find and clean up. - No automatic browser launch. Side effects on the host are opt-in via
--browser <executable>. Scripts and CI runs are predictable by default. - No per-snapshot time series. Aggregating-then-charting matches the CI freeze-debugging use case (one snapshot, "who was eating resources") much better than line graphs. A time-series view is on the roadmap.
- No external state. No config file, no env vars, no cache. The binary's behavior is a function of its flags and stdin.