Files
Ilker ManapandClaude Opus 5.5 c866b3a8ee
CI / test (push) Failing after 6m42s
How much of each GPU works, beside whether anything does
The busy figure is nvidia-smi's utilization.gpu: the share of time any
kernel ran, which a kernel on one SM fills. On cuda01 decode read 92% busy
while the SMs had work 69% of the time and issued on 18% of their cycles.

Busy is now said to be that, and beside it: each card's draw against its
power limit (efficiency table, fleet and node charts, the console's GPU
work chart), and where the profiling counters can be read, SMs active, SM
occupancy, tensor cores and DRAM (llmgw_gpu_*_active_percent), from NVML's
GPM on Hopper and later or a dcgm-exporter on the node
(discovery.dcgm_exporter). The GPU map colours by work by default and keeps
busy as a mode. A MIG instance is never published with its card's profile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 20:11:26 +01:00

576 lines
21 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package cli
import (
"context"
"fmt"
"math"
"net/http"
"sort"
"strconv"
"strings"
"time"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"gitea.mynodes.xyz/ilker/llmgw/internal/control/adminapi"
"gitea.mynodes.xyz/ilker/llmgw/internal/control/inventory"
"gitea.mynodes.xyz/ilker/llmgw/internal/promq"
)
// The monitor: the cluster drawn the way btop draws a machine. History comes
// from the Prometheus that scrapes the control plane (monitoring.prometheus in
// server.yaml), read through the admin API; without one, from what this
// console has seen since it opened.
var monRanges = []struct {
label string
window time.Duration
refresh time.Duration
}{
{"5m", 5 * time.Minute, 5 * time.Second},
{"1h", time.Hour, 15 * time.Second},
{"24h", 24 * time.Hour, time.Minute},
{"7d", 7 * 24 * time.Hour, 5 * time.Minute},
}
type monitorState struct {
rng int
res adminapi.MetricsResult
err error
at time.Time
key string
// local is what this console sampled itself, every refresh, by series:
// the charts when there is no Prometheus.
local map[string][]float64
localAt []time.Time
}
type monitorMsg struct {
key string
res adminapi.MetricsResult
err error
}
const localKeep = 1200 // an hour of three-second samples
// sample keeps what the cluster looks like now, for the charts without a
// Prometheus.
func (s *monitorState) sample(d consoleData) {
if s.local == nil {
s.local = map[string][]float64{}
}
add := func(k string, v float64) {
s.local[k] = append(s.local[k], v)
if len(s.local[k]) > localKeep {
s.local[k] = s.local[k][len(s.local[k])-localKeep:]
}
}
var cpu, used, total float64
n := 0
for _, nd := range d.nodes {
if nd.Load == nil {
continue
}
l := nd.Load
cpu += l.CPUPercent
n++
used, total = used+float64(l.MemUsed), total+float64(l.MemTotal)
add("nodecpu/"+nd.ID, l.CPUPercent)
add("nodemem/"+nd.ID, l.MemPercent)
for _, g := range l.GPUs {
card := nd.ID + ":" + strconv.Itoa(g.Index)
add("gpu/"+card, g.UtilPercent)
if w := gpuWork(g); w >= 0 {
add("gpuwork/"+card, w)
}
if g.MemoryTotalMB > 0 {
add("vram/"+card, 100*g.MemoryUsedMB/g.MemoryTotalMB)
}
add("gputemp/"+card, g.TempC)
add("gpupow/"+card, g.PowerW)
}
}
if n > 0 {
add("cpu/", cpu/float64(n))
}
if total > 0 {
add("mem/", 100*used/total)
}
}
// localSeries is the series sampled under an id, as charts take them.
func (s monitorState) localSeries(id string) []promq.Series {
var out []promq.Series
for k, vals := range s.local {
pre, name, ok := strings.Cut(k, "/")
if !ok || pre != id {
continue
}
ser := promq.Series{Name: name}
for _, v := range vals {
ser.Points = append(ser.Points, promq.Point{Value: v})
}
out = append(out, ser)
}
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
return out
}
// netDevs and diskDevs leave out what is not the machine's own traffic:
// loopback, container bridges, device-mapper over the disks it is made of.
const (
netDevs = `job="nodes",device!~"lo|veth.*|docker.*|br-.*|cni.*|flannel.*|cali.*|virbr.*|tap.*"`
diskDevs = `job="nodes",device!~"loop.*|ram.*|dm-.*|sr.*"`
)
// bps is bytes a second as a person reads it.
func bps(v float64) string { return bytesShort(int64(v)) + "/s" }
// promDur is a duration as PromQL writes one.
func promDur(d time.Duration) string {
if d%time.Minute == 0 {
return strconv.Itoa(int(d/time.Minute)) + "m"
}
return strconv.Itoa(int(d/time.Second)) + "s"
}
// monitorQueries are what the monitor draws, for a window drawn at points
// points wide.
func monitorQueries(window time.Duration, points int) (time.Duration, []adminapi.MetricsQuery) {
step := max(5*time.Second, (window / time.Duration(max(points, 1))).Round(time.Second))
// A rate over at least four scrapes, or it is noise.
rw := promDur(max(time.Minute, 4*step).Round(time.Second))
card := func(expr string) string { return `label_join(` + expr + `, "card", ":", "node", "gpu")` }
return step, []adminapi.MetricsQuery{
{ID: "cpu", Expr: `avg(llmgw_node_cpu_utilization_percent)`},
{ID: "mem", Expr: `100 * sum(llmgw_node_memory_used_bytes) / sum(llmgw_node_memory_bytes)`},
{ID: "gpu", Expr: card(`llmgw_gpu_utilization_percent`), By: "card"},
{ID: "gpuwork", Expr: card(gpuWorkExpr("")), By: "card"},
{ID: "vram", Expr: card(`100 * llmgw_gpu_memory_used_bytes / llmgw_gpu_memory_total_bytes`), By: "card"},
{ID: "gputemp", Expr: card(`llmgw_gpu_temperature_celsius`), By: "card"},
{ID: "gpupow", Expr: card(`llmgw_gpu_power_watts`), By: "card"},
// Totals fall back to zero: a counter not yet incremented since the
// control plane started is no series at all, and a quiet hour should
// read as none, not as missing.
{ID: "rps", Expr: `sum(rate(llmgw_requests_total[` + rw + `])) or vector(0)`},
{ID: "rpsby", Expr: `sum by (model) (rate(llmgw_requests_total[` + rw + `]))`, By: "model"},
{ID: "errs", Expr: `sum(rate(llmgw_requests_total{status=~"5..|429"}[` + rw + `])) or vector(0)`},
{ID: "tokin", Expr: `sum(rate(llmgw_tokens_total{direction="prompt"}[` + rw + `])) or vector(0)`},
{ID: "tokout", Expr: `sum(rate(llmgw_tokens_total{direction="completion"}[` + rw + `])) or vector(0)`},
// The machines' own, from their node exporters.
{ID: "netin", Expr: `sum(rate(node_network_receive_bytes_total{` + netDevs + `}[` + rw + `]))`},
{ID: "netout", Expr: `sum(rate(node_network_transmit_bytes_total{` + netDevs + `}[` + rw + `]))`},
{ID: "diskr", Expr: `sum(rate(node_disk_read_bytes_total{` + diskDevs + `}[` + rw + `]))`},
{ID: "diskw", Expr: `sum(rate(node_disk_written_bytes_total{` + diskDevs + `}[` + rw + `]))`},
{ID: "nodenet", Expr: `sum by (node) (rate(node_network_receive_bytes_total{` + netDevs + `}[` + rw + `]) + rate(node_network_transmit_bytes_total{` + netDevs + `}[` + rw + `]))`, By: "node"},
{ID: "ttft50", Expr: `histogram_quantile(0.5, sum by (le) (rate(llmgw_ttft_seconds_bucket[` + rw + `])))`},
{ID: "ttft99", Expr: `histogram_quantile(0.99, sum by (le) (rate(llmgw_ttft_seconds_bucket[` + rw + `])))`},
{ID: "inflight", Expr: `sum(llmgw_endpoint_inflight)`},
{ID: "queue", Expr: `sum(llmgw_admission_queue_depth)`},
{ID: "nodecpu", Expr: `llmgw_node_cpu_utilization_percent`, By: "node"},
{ID: "nodemem", Expr: `100 * llmgw_node_memory_used_bytes / llmgw_node_memory_bytes`, By: "node"},
}
}
func (m consoleModel) monitorWidth() int { return max(40, m.width-navWidth-8) }
// monitorKey says what is drawn: the range and the width it is drawn at.
func (m consoleModel) monitorKey() string {
return fmt.Sprintf("%d/%d", m.mon.rng, m.monitorWidth())
}
// monitorDue is whether the charts should be read again.
func (m consoleModel) monitorDue() bool {
return m.mon.key != m.monitorKey() || time.Since(m.mon.at) >= monRanges[m.mon.rng].refresh
}
func (m *consoleModel) fetchMonitor() tea.Cmd {
r := monRanges[m.mon.rng]
step, qs := monitorQueries(r.window, (m.monitorWidth()/2-2)*2)
key, c := m.monitorKey(), m.client
m.mon.at, m.mon.key = time.Now(), key
return func() tea.Msg {
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
var res adminapi.MetricsResult
err := c.do(ctx, http.MethodPost, "/api/v1/metrics/range",
adminapi.MetricsRequest{Window: r.window, Step: step, Queries: qs}, &res)
return monitorMsg{key: key, res: res, err: err}
}
}
// series is what is drawn under an id: from Prometheus, or sampled here.
func (m consoleModel) series(id string) []chartSeries {
src := m.mon.res.Series[id]
if !m.mon.res.Configured {
src = m.mon.localSeries(id)
}
return toChart(src, m.mon.res.Configured, m.mon.at, monRanges[m.mon.rng].window)
}
// toChart turns what Prometheus gave into chart series placed by time over
// the window that ended when it was read; sampled here, they are spread
// over what was seen.
func toChart(src []promq.Series, timed bool, readAt time.Time, window time.Duration) []chartSeries {
out := make([]chartSeries, 0, len(src))
for _, s := range src {
cs := chartSeries{name: s.Name}
stamped := timed
for _, p := range s.Points {
cs.values = append(cs.values, p.Value)
cs.at = append(cs.at, p.At)
stamped = stamped && !p.At.IsZero()
}
if !stamped {
cs.at = nil
}
if stamped && !readAt.IsZero() {
cs.from, cs.to = readAt.Add(-window), readAt
}
out = append(out, cs)
}
return out
}
func lastOf(s []chartSeries) float64 {
t := 0.0
for _, x := range s {
for i := len(x.values) - 1; i >= 0; i-- {
if !math.IsNaN(x.values[i]) {
t += x.values[i]
break
}
}
}
return t
}
func rate(v float64) string {
switch {
case v >= 1000:
return fmt.Sprintf("%.1fk/s", v/1000)
case v >= 10:
return fmt.Sprintf("%.0f/s", v)
}
return fmt.Sprintf("%.2f/s", v)
}
// side puts two blocks next to each other, each w wide.
func side(w int, a, b string) string {
st := lipgloss.NewStyle().Width(w)
return lipgloss.JoinHorizontal(lipgloss.Top, st.Render(a), " ", st.Render(b))
}
func (m consoleModel) monitorPanel(pw int) (string, string, string) {
keys := "→ or Enter open · ↑↓ choose · N test the webhooks · Q leave"
if m.inPanel {
keys = "1–4 or R range · PgUp/PgDn scroll · ← back · Q leave"
}
r := monRanges[m.mon.rng]
var head strings.Builder
head.WriteString("\n ")
for i, x := range monRanges {
if i == m.mon.rng {
head.WriteString(fSelected.Render("["+x.label+"]") + " ")
} else {
head.WriteString(sDim.Render(" "+x.label+" ") + " ")
}
}
src := "from Prometheus"
switch {
case m.mon.err != nil:
src = sBad.Render("the control plane: " + m.mon.err.Error())
case !m.mon.res.Configured:
src = sWarn.Render("from what this console has seen since it opened") +
sDim.Render(" — set monitoring.prometheus in server.yaml for history")
case len(m.mon.res.Errors) > 0:
for _, e := range m.mon.res.Errors {
src = sWarn.Render("Prometheus: " + truncate(e, 80))
break
}
}
head.WriteString(" " + src + "\n\n")
w := (pw - 6) / 2
ch := 5
span := r.label
if !m.mon.res.Configured {
span = "since opened"
}
pct := func(id string) string { return fmt.Sprintf("%.0f%%", lastOf(m.series(id))) }
box := func(title, now string, s []chartSeries, top float64) string {
if len(s) == 0 {
empty := "no samples yet"
if m.mon.res.Configured {
empty = "no requests in the last " + span
}
return mdBold.Render(title) + "\n\n " + sDim.Render(empty)
}
return chartBox(title, now, s, w, ch, top, span)
}
netBox := func(title string, s []chartSeries) string {
if len(s) == 0 {
return mdBold.Render(title) + "\n\n " + sDim.Render("no node exporter answering: llmgw node add installs one")
}
return box(title, bps(lastOf(s)), s, 0)
}
var b strings.Builder
b.WriteString(indent(side(w, box("CPU", pct("cpu"), m.series("cpu"), 100),
box("Memory", pct("mem"), m.series("mem"), 100)), " ") + "\n\n")
if g := m.series("gpu"); len(g) > 0 || len(m.data.sum.GPUs) > 0 {
b.WriteString(indent(side(w, box("GPU busy", "", g, 100), box("GPU work", "", m.series("gpuwork"), 100)), " ") + "\n\n")
b.WriteString(indent(side(w, box("GPU memory", "", m.series("vram"), 100), " "+sDim.Render(gpuWorkNote)), " ") + "\n\n")
}
if m.mon.res.Configured {
rps := m.series("rps")
tok := append(named(m.series("tokin"), "prompt"), named(m.series("tokout"), "completion")...)
b.WriteString(indent(side(w, box("Requests", rate(lastOf(rps)), rps, 0),
box("Tokens", rate(lastOf(tok)), tok, 0)), " ") + "\n\n")
ttft := append(named(m.series("ttft50"), "p50"), named(m.series("ttft99"), "p99")...)
load := append(named(m.series("inflight"), "in flight"), named(m.series("queue"), "queued")...)
load = append(load, named(m.series("errs"), "errors/s")...)
if len(ttft) == 0 {
ttft = nil
}
b.WriteString(indent(side(w, box("Time to first token (s)", "", ttft, 0),
box("In flight · queued · errors", "", load, 0)), " ") + "\n\n")
net := append(named(m.series("netin"), "in"), named(m.series("netout"), "out")...)
disk := append(named(m.series("diskr"), "read"), named(m.series("diskw"), "written")...)
b.WriteString(indent(side(w, netBox("Network", net), netBox("Disk", disk)), " ") + "\n\n")
} else {
b.WriteString(" " + sDim.Render("Requests, tokens, latency, network and disk are drawn from Prometheus.") + "\n\n")
}
// Each node: its CPU and memory now, and how they went.
cpuBy, memBy, netBy := byName(m.series("nodecpu")), byName(m.series("nodemem")), byName(m.series("nodenet"))
sw := max(10, (pw-50)/2)
b.WriteString(" " + mdBold.Render("Nodes") + sDim.Render(fmt.Sprintf("%*s", 25+sw, "CPU")) +
sDim.Render(fmt.Sprintf("%*s", 18+sw, "memory")) + "\n")
for _, nd := range m.data.nodes {
c, mm := 0.0, 0.0
if nd.Load != nil {
c, mm = nd.Load.CPUPercent, nd.Load.MemPercent
}
netCol := ""
if v, ok := netBy[nd.ID]; ok {
netCol = fmt.Sprintf(" net %s %9s", sparkSeries(v, 12, 0), bps(last(v.values)))
}
fmt.Fprintf(&b, " %-16s %s %4.0f%% %s %s %4.0f%% %s%s\n", truncate(nd.ID, 16),
meter(c/100, 8), c, sparkSeries(cpuBy[nd.ID], sw, 100), meter(mm/100, 8), mm, sparkSeries(memBy[nd.ID], sw, 100), netCol)
}
// Each card: busy, memory, heat and power.
if gpus := m.series("gpu"); len(gpus) > 0 {
vram, temp, pow := byName(m.series("vram")), byName(m.series("gputemp")), byName(m.series("gpupow"))
work := byName(m.series("gpuwork"))
b.WriteString("\n " + mdBold.Render("GPUs") + "\n")
for _, g := range gpus {
fmt.Fprintf(&b, " %-16s busy %s %4.0f%% work %s %4.0f%% memory %s %4.0f%% %3.0f°C %s %4.0f W\n", truncate(g.name, 16),
sparkSeries(g, sw/3, 100), last(g.values), sparkSeries(work[g.name], sw/3, 100), last(work[g.name].values),
sparkSeries(vram[g.name], sw/3, 100), last(vram[g.name].values),
last(temp[g.name].values), sparkSeries(temp[g.name], 8, 100), last(pow[g.name].values))
}
}
// Each model's requests.
if rps := m.series("rpsby"); len(rps) > 0 {
b.WriteString("\n " + mdBold.Render("Models") + sDim.Render(" requests a second") + "\n")
for _, s := range rps {
fmt.Fprintf(&b, " %-30s %s %s\n", truncate(s.name, 30), sparkSeries(s, max(20, pw-50), 0), rate(last(s.values)))
}
}
return "Monitor", head.String() + b.String(), keys
}
// GPU busy is the driver's figure: the share of time any kernel ran, which
// a kernel on one SM of a hundred fills. GPU work is how much of the chip:
// the SMs' share of time with work where the card's profiling counters are
// read (GPM, DCGM), and its draw against its power limit elsewhere.
const gpuWorkNote = "busy: some kernel ran. work: how much of the chip —\nSMs active where the counters are read, power/limit elsewhere."
// gpuWorkExpr is GPU work in PromQL, for the series matching sel.
func gpuWorkExpr(sel string) string {
return `llmgw_gpu_sm_active_percent` + sel + ` or (100 * llmgw_gpu_power_watts` + sel +
` / llmgw_gpu_power_limit_watts` + sel + `)`
}
// gpuWork is GPU work from a sample, or -1 without the figures for it.
func gpuWork(g inventory.GPUSensor) float64 {
if p := g.Profile; p != nil && p.SMActive >= 0 {
return p.SMActive
}
if l := g.PowerLoad(); l >= 0 {
return 100 * l
}
return -1
}
func named(s []chartSeries, name string) []chartSeries {
for i := range s {
s[i].name = name
}
return s
}
func byName(s []chartSeries) map[string]chartSeries {
out := map[string]chartSeries{}
for _, x := range s {
out[x.name] = x
}
return out
}
func last(v []float64) float64 {
for i := len(v) - 1; i >= 0; i-- {
if !math.IsNaN(v[i]) {
return v[i]
}
}
return 0
}
// Charts on a node's and a model's own page: the same reading, kept apart so
// it does not disturb the monitor's.
func detailQueries(kind, id string, window time.Duration, points int) (time.Duration, []adminapi.MetricsQuery) {
step := max(5*time.Second, (window / time.Duration(max(points, 1))).Round(time.Second))
rw := promDur(max(time.Minute, 4*step).Round(time.Second))
q := strconv.Quote(id)
card := func(expr string) string { return `label_join(` + expr + `, "card", ":", "node", "gpu")` }
if kind == "node" {
return step, []adminapi.MetricsQuery{
{ID: "cpu", Expr: `llmgw_node_cpu_utilization_percent{node=` + q + `}`},
{ID: "mem", Expr: `100 * llmgw_node_memory_used_bytes{node=` + q + `} / llmgw_node_memory_bytes{node=` + q + `}`},
{ID: "gpu", Expr: card(`llmgw_gpu_utilization_percent{node=` + q + `}`), By: "card"},
{ID: "gpuwork", Expr: card(gpuWorkExpr(`{node=` + q + `}`)), By: "card"},
{ID: "vram", Expr: card(`100 * llmgw_gpu_memory_used_bytes{node=` + q + `} / llmgw_gpu_memory_total_bytes{node=` + q + `}`), By: "card"},
{ID: "netin", Expr: `sum(rate(node_network_receive_bytes_total{` + netDevs + `,node=` + q + `}[` + rw + `]))`},
{ID: "netout", Expr: `sum(rate(node_network_transmit_bytes_total{` + netDevs + `,node=` + q + `}[` + rw + `]))`},
{ID: "diskr", Expr: `sum(rate(node_disk_read_bytes_total{` + diskDevs + `,node=` + q + `}[` + rw + `]))`},
{ID: "diskw", Expr: `sum(rate(node_disk_written_bytes_total{` + diskDevs + `,node=` + q + `}[` + rw + `]))`},
}
}
return step, []adminapi.MetricsQuery{
{ID: "rps", Expr: `sum(rate(llmgw_requests_total{model=` + q + `}[` + rw + `]))`},
{ID: "errs", Expr: `sum(rate(llmgw_requests_total{model=` + q + `,status=~"5..|429"}[` + rw + `]))`},
{ID: "tok", Expr: `sum by (direction) (rate(llmgw_tokens_total{model=` + q + `,direction!="cached"}[` + rw + `]))`, By: "direction"},
{ID: "ttft50", Expr: `histogram_quantile(0.5, sum by (le) (rate(llmgw_ttft_seconds_bucket{model=` + q + `}[` + rw + `])))`},
{ID: "ttft99", Expr: `histogram_quantile(0.99, sum by (le) (rate(llmgw_ttft_seconds_bucket{model=` + q + `}[` + rw + `])))`},
}
}
// detailTarget is the node or model whose page is open, if any.
func (m consoleModel) detailTarget() (kind, id string) {
if !m.detail {
return "", ""
}
switch {
case m.menu == cNodes && m.row < len(m.data.nodes):
return "node", m.data.nodes[m.row].ID
case m.menu == cModels && m.row < len(m.data.models):
return "model", m.data.models[m.row].Name
}
return "", ""
}
func (m consoleModel) detailKey() string {
kind, id := m.detailTarget()
return fmt.Sprintf("%s/%s/%d/%d", kind, id, m.mon.rng, m.monitorWidth())
}
func (m consoleModel) detailDue() bool {
if k, _ := m.detailTarget(); k == "" {
return false
}
return m.det.key != m.detailKey() || time.Since(m.det.at) >= monRanges[m.mon.rng].refresh
}
type detailMsg monitorMsg
func (m *consoleModel) fetchDetail() tea.Cmd {
kind, id := m.detailTarget()
if kind == "" {
return nil
}
r := monRanges[m.mon.rng]
step, qs := detailQueries(kind, id, r.window, (m.monitorWidth()/2-2)*2)
key, c := m.detailKey(), m.client
m.det.at = time.Now()
return func() tea.Msg {
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
var res adminapi.MetricsResult
err := c.do(ctx, http.MethodPost, "/api/v1/metrics/range",
adminapi.MetricsRequest{Window: r.window, Step: step, Queries: qs}, &res)
return detailMsg{key: key, res: res, err: err}
}
}
// detailCharts are the charts at the foot of a node's or model's page.
func (m consoleModel) detailCharts(pw int) string {
kind, id := m.detailTarget()
if kind == "" {
return ""
}
r := monRanges[m.mon.rng]
head := "\n " + mdBold.Render("Over the last "+r.label) + sDim.Render(" (1–4 or R another range)") + "\n\n"
if m.det.err != nil {
return head + " " + sBad.Render(m.det.err.Error())
}
get := func(qid string) []chartSeries {
return toChart(m.det.res.Series[qid], true, m.det.at, r.window)
}
if !m.det.res.Configured {
// Without Prometheus, a node's own samples from this console.
if kind != "node" {
return head + " " + sDim.Render("A model's traffic is drawn from Prometheus: set monitoring.prometheus in server.yaml.")
}
get = func(qid string) []chartSeries {
var out []chartSeries
for _, s := range m.mon.localSeries("node" + qid) {
if s.Name == id {
cs := chartSeries{name: s.Name}
for _, p := range s.Points {
cs.values = append(cs.values, p.Value)
}
out = append(out, cs)
}
}
return out
}
}
w := (pw - 6) / 2
box := func(title, now string, s []chartSeries, top float64) string {
if len(s) == 0 {
return mdBold.Render(title) + "\n\n " + sDim.Render("no samples")
}
return chartBox(title, now, s, w, 5, top, r.label)
}
var b strings.Builder
b.WriteString(head)
if kind == "node" {
cpu, mem := get("cpu"), get("mem")
b.WriteString(indent(side(w, box("CPU", fmt.Sprintf("%.0f%%", lastOf(cpu)), cpu, 100),
box("Memory", fmt.Sprintf("%.0f%%", lastOf(mem)), mem, 100)), " "))
if g := get("gpu"); len(g) > 0 {
b.WriteString("\n\n" + indent(side(w, box("GPU busy", "", g, 100), box("GPU work", "", get("gpuwork"), 100)), " "))
b.WriteString("\n\n" + indent(side(w, box("GPU memory", "", get("vram"), 100), " "+sDim.Render(gpuWorkNote)), " "))
}
if m.det.res.Configured {
net := append(named(get("netin"), "in"), named(get("netout"), "out")...)
disk := append(named(get("diskr"), "read"), named(get("diskw"), "written")...)
if len(net)+len(disk) > 0 {
b.WriteString("\n\n" + indent(side(w, box("Network", bps(lastOf(net)), net, 0), box("Disk", bps(lastOf(disk)), disk, 0)), " "))
} else {
b.WriteString("\n\n " + sDim.Render("Network and disk come from the node's exporter, which is not answering: apt-get install prometheus-node-exporter"))
}
}
return b.String()
}
rps, tok := get("rps"), get("tok")
b.WriteString(indent(side(w, box("Requests", rate(lastOf(rps)), rps, 0), box("Tokens", rate(lastOf(tok)), tok, 0)), " "))
ttft := append(named(get("ttft50"), "p50"), named(get("ttft99"), "p99")...)
b.WriteString("\n\n" + indent(side(w, box("Time to first token (s)", "", ttft, 0),
box("Errors", rate(lastOf(get("errs"))), get("errs"), 0)), " "))
return b.String()
}