Skill v1.0.1
Automated scan100/100+2 new
version: "1.0.1" name: systing-analyze description: Analyze a systing trace database (.duckdb). Use when the user asks about a systing trace — flamegraphs, scheduling latency, CPU hotspots, network behavior, memory/allocation activity, off-CPU time, TPU op/metric data, or any question about what's in a trace.duckdb file. Orchestrates the systing-analyze MCP tools (trace_info, query, flamegraph, sched_stats, cpu_stats, network_*).
Analyzing systing traces
Systing stores traces in DuckDB. The systing-analyze MCP server exposes structured tools to query them. This skill tells you which tool to reach for and how the data is laid out.
Recommended workflow
- `trace_info` — Always start here. Pass the
pathto the.duckdbfile. Returns trace IDs, time range, per-trace system/platform info (kernel, arch, hypervisor, cpufreq driver, sampling event/period), non-empty tables with row counts, and the top 25 processes by thread count. This also caches the DB so later calls can omitpath. - `list_tables` / `describe_table` — Discover schema for ad-hoc queries.
- High-level tools for common questions — see below.
- `query` — For anything the high-level tools don't cover, write SQL. Results cap at 10k rows (
truncated: truewhen hit); useLIMIT/OFFSETfor more. The DB is opened read-only.
Tool cheatsheet
| Question | Tool | Notes | |
|---|---|---|---|
| What's in this trace? | trace_info | First call; pass path | |
| Where is CPU time going? | flamegraph | stack_type="cpu" (default); optionally pid/tid | |
| Why is the process blocked / off-CPU? | flamegraph | stack_type="uninterruptible-sleep" (D) or "interruptible-sleep" (S), or "all-sleep" | |
| Is the scheduler oversubscribed? Latency? | sched_stats | no filter = whole-trace ranking; pid = per-thread breakdown; tid = single thread with end-state distribution | |
| Which CPUs are busy / idle? | cpu_stats | per-CPU utilization, idle%, IRQ/softIRQ time, runqueue depth p50/p90/p99 | |
| What's the network doing? | network_connections | per-connection bytes, retransmit % | |
| Interface-level network? | network_interfaces | per-interface, per-protocol breakdown | |
| Both sides of a connection (multi-node)? | network_socket_pairs | matched socket pairs, within or across traces | |
| Memory / allocations? | query | no dedicated tool — see memory schema below | |
| What ran on the TPU? Duty cycle? HBM? | query | no dedicated tool — see TPU schema below | |
| Anything else | query | raw SQL; see schema below |
There is also a CLI with the same analyses: systing-analyze query|stacktrace flamegraph|sched stats|sched cpu-stats|network connections|network interfaces|network socket-pairs (and systing-analyze mcp to start this server).
Key schema for query
Timestamps
All ts columns are nanoseconds from an arbitrary epoch. Convert durations: dur / 1e6 → ms, dur / 1e9 → sec.
Thread / process identity
utid/upidare internal IDs (dense, unique within DB).- Join to
thread(utid → tid, name, upid) andprocess(upid → pid, name) for the Linux IDs.
Stack traces
Two representations exist:
Interned (the normal one) — stack_sample → stack → frame:
stack_sample(ts, utid, cpu, stack_id, stack_event_type).stack_event_type:0= uninterruptible sleep,1= CPU,2= interruptible sleep.stack(id, frame_ids BIGINT[], depth, leaf_name). `frame_ids` is root-to-leaf (outermost caller first, innermost executing frame last);leaf_nameis the last frame's name.frame(id, name)— interned strings, dense per-trace ids.- Join on both
trace_idand the id:JOIN stack s ON s.trace_id = ss.trace_id AND s.id = ss.stack_id. - The
stack_framesview reconstructs aframe_names VARCHAR[]column (root-to-leaf) for ad-hoc queries — convenient but slower than joiningframedirectly.
-- Top 10 hottest leaf functions (on-CPU samples) — leaf_name avoids any joinSELECT leaf_name, count(*) AS samplesFROM stack_sample ssJOIN stack s ON s.trace_id = ss.trace_id AND s.id = ss.stack_idWHERE ss.stack_event_type = 1 -- 1=cpu, 0=uninterruptible, 2=interruptibleGROUP BY 1 ORDER BY 2 DESC LIMIT 10;-- Samples containing a given frame anywhere in the stackSELECT count(*)FROM stack_sample ssJOIN stack_frames sf ON sf.trace_id = ss.trace_id AND sf.id = ss.stack_idWHERE list_contains(sf.frame_names, 'do_futex_wait');
Normalized (Perfetto-style) — perf_sample → stack_profile_callsite (parent-child tree) → stack_profile_frame → stack_profile_symbol / stack_profile_mapping. Use when you need mapping/build-id info. Walk the parent_id chain to reconstruct stacks.
Sample weighting
sysinfo.sample_event / sysinfo.sample_period say what one CPU sample represents: sample_period cycles for cpu-cycles (so sample density tracks cycles consumed, not wall time — use sched_slice for time, and cpu_info min/max/base frequencies in kHz to convert), or sample_period nanoseconds for cpu-clock. NULL/absent in traces from systing < 1.9 (adaptive frequency mode, nominally 1000 Hz).
Scheduling
sched_slice(ts, dur, cpu, utid, end_state, priority): one row per scheduled slice.
end_stateis a bitmask of the task state at slice end, not an enum, and there is no `end_state_str` column:NULL→ preempted (still runnable)& 1→ interruptible sleep (S)& 2→ uninterruptible sleep (D)& 4stopped,& 8traced,& 16exit/dead,& 32exit/zombie- Test bits (
end_state & 2 != 0), don't compare for equality — compound states occur.
Related: thread_state(ts, dur, utid, state, cpu), wakeup_new, process_exit, irq_slice(irq, name, ret), softirq_slice(vec).
-- Longest uninterruptible-sleep episodesSELECT t.name, ss.dur/1e6 AS ms, ss.tsFROM sched_slice ss JOIN thread t ON t.trace_id = ss.trace_id AND t.utid = ss.utidWHERE ss.end_state & 2 != 0ORDER BY ss.dur DESC LIMIT 20;
Network
network_syscall— sendmsg/recvmsg:ts,dur,utid,event_type(sendmsg/recvmsg),socket_id,bytes, send-buffer fill (sndbuf_used/limit/fill_pct), recv-side (recv_seq_start/end,rcv_nxt,bytes_available).network_packet— packet-level:seq,length,tcp_flags,is_retransmit,retransmit_count,rto_ms,srtt_ms, windows (snd_wnd,rcv_wnd, zero-window probes), qdisc (qlen,qdisc_backlog,qdisc_latency_us), drops (drop_reason,drop_reason_str), TCP state changes (old_state_str,new_state_str).network_socket— socket metadata:socket_id,netns_inum,protocol,address_family, src/dest IP:port, first/last seen ts.network_poll— poll/epoll/select events per socket.network_interface— local interface metadata (namespace, name, IPs,netns_inum).network_dns— IP→hostname, only when the trace was captured with--resolve-addresses.- Join syscall/packet → socket on
socket_id(plustrace_id).
-- Retransmits by connectionSELECT s.src_ip, s.src_port, s.dest_ip, s.dest_port,count(*) FILTER (WHERE p.is_retransmit) AS retransmits,count(*) AS total_packetsFROM network_packet pJOIN network_socket s ON s.trace_id = p.trace_id AND s.socket_id = p.socket_idGROUP BY 1,2,3,4 HAVING retransmits > 0ORDER BY retransmits DESC;
Memory
From the memory / memory-alloc recorders. stack_id columns join to stack.id like sample stacks.
memory_rss(ts, utid, member, size, external)— RSS tracking.member:0=file,1=anon,2=swap,3=shmem,-1=hiwater_rss,-2=total_vm,-3=maj_flt,-4=thrashing_count,-5=thrashing_delay_ns (negatives are synthetic periodic samples). Units vary by member:sizeis bytes for0..3/-1/-2, a cumulative fault count for-3, a stall count for-4, and nanoseconds for-5— never aggregate across members.externalis true when the update came from an external reclaimer (kswapd, direct reclaim by another process) rather than the process itself;-4/-5rows appear only on delayacct-enabled hosts (zero-valued rows there mean "no thrash").memory_map(ts, utid, event_type, addr, size, prot, flags, stack_id)— mmap/munmap/brk.memory_fault(ts, utid, addr, error_code, stack_id)— user page faults (sampled 1-in-N, default 97).memory_alloc(ts, utid, op, addr, size, old_addr, stack_id)— malloc/calloc/realloc/free uprobes.
-- Anon RSS over time for the biggest processSELECT p.name, r.ts, r.sizeFROM memory_rss rJOIN thread t ON t.trace_id = r.trace_id AND t.utid = r.utidJOIN process p ON p.trace_id = t.trace_id AND p.upid = t.upidWHERE r.member = 1 ORDER BY r.ts;-- Allocation hotspots by stack leafSELECT s.leaf_name, count(*) AS allocs, sum(a.size) AS bytesFROM memory_alloc aJOIN stack s ON s.trace_id = a.trace_id AND s.id = a.stack_idWHERE a.op = 'malloc'GROUP BY 1 ORDER BY bytes DESC LIMIT 20;
Counters and slices
counter(ts, track_id, value) + counter_track(id, name, unit) for perf counters and CPU frequency; slice/track/args and instant/instant_args for custom trace events and markers.
TPU
No dedicated MCP tool — use query. Three tables:
- `tpu_device` — one row per TPU core.
id(join key),device_ordinal,chip_id,core_id,hostname,device_type,topology_{x,y,z},clock_rate_ghz,hbm_size_bytes,hbm_bandwidth_gbps. - `tpu_op` — XLA op execution slices (from
--tpu-profile).ts,dur(ns),tpu_device_id(FK →tpu_device.id),op_name,category,stream,group_id,flops,bytes_accessed,bytes_hbm,bytes_cmem,bytes_vmem. - `tpu_metric` — polled runtime counters (from
--tpu-metrics).ts,device_id(ordinal),metric_name,value. Default metrics:tpu.runtime.tensorcore.dutycycle.percent,tpu.runtime.hbm.memory.usage.bytes.
-- Top 20 TPU ops by total device timeSELECT op_name, category,count(*) AS calls,sum(dur)/1e6 AS total_ms,avg(dur)/1e3 AS avg_us,sum(flops) AS total_flops,sum(bytes_hbm) AS total_hbm_bytesFROM tpu_opGROUP BY 1,2 ORDER BY total_ms DESC LIMIT 20;-- TPU utilization (duty cycle) over time, per deviceSELECT device_id, ts, value AS dutycycle_pctFROM tpu_metricWHERE metric_name = 'tpu.runtime.tensorcore.dutycycle.percent'ORDER BY device_id, ts;-- Correlate TPU gaps with host-side blocking: intervals with no TPU op-- running for >1ms; check sched_slice for what the host thread was doingWITH gaps AS (SELECT ts + dur AS gap_start,lead(ts) OVER (PARTITION BY tpu_device_id ORDER BY ts) AS gap_endFROM tpu_op)SELECT gap_start, (gap_end - gap_start)/1e6 AS gap_msFROM gapsWHERE gap_end - gap_start > 1000000 -- >1msORDER BY gap_ms DESC LIMIT 20;
Multi-trace databases
Every table has a trace_id column. Always include it in joins, and filter on it when the DB contains multiple captures (_traces lists them, with the systing version that produced each).
Using flamegraph
Returns stacks (array of {frames, count}), metadata (total_samples, unique_stacks, time_range, stack_type), and folded — folded-stack text (semicolon-separated frames root→leaf, space, count) for inferno / brendangregg's FlameGraph. Parameters:
stack_type—"cpu"(default),"interruptible-sleep","uninterruptible-sleep","all-sleep","all"pid/tid— restrict to a process or threadstart_time/end_time— seconds offset from trace startmin_count— minimum sample count per stack (default 1)top_n— keep the top N stacks (default 500)trace_id— for multi-trace DBs
Common investigation patterns
"Why is my process slow?"
sched_statswithpid— is it on-CPU (CPU-bound), or mostly sleeping (blocked)? Noted_sleep_secondsis approximate: it folds in runqueue latency after the sleep.- If CPU-bound →
flamegraph stack_type="cpu" pid=<pid>for hotspots. - If blocked →
flamegraph stack_type="uninterruptible-sleep" pid=<pid>(I/O, locks) and"interruptible-sleep"(waits, timers).
"Is the machine overloaded?"
cpu_stats— CPUs with near-zero idle % and high runqueue depth percentiles. (Runqueue estimates model sleep→wake→run cycles only, exclude preempted threads, and are event-weighted, not time-weighted.)sched_stats(no filter) — preemption rates and CPU migrations.
"Network slow / dropping?"
network_connections— any connection with a high retransmit rate?queryonnetwork_packet—drop_reason_str IS NOT NULLfor kernel drop reasons;qdisc_latency_usfor queueing.queryonnetwork_syscall— sort bydurfor stalled recv/send, and checksndbuf_fill_pct/bytes_available.
"Memory growing?"
queryonmemory_rss— anon (member=1) trend per process.queryonmemory_map— large mmaps grouped bystack_id→stack.leaf_name.queryonmemory_alloc— allocation hotspots. Only trust alloc/free pairing if the trace used--memory-alloc-sample-rate 1.
"TPU underutilized?"
queryontpu_metric—tensorcore.dutycycle.percent; sustained low values mean the device is starved.queryontpu_op— large gaps between consecutive ops on the same device (see gap query above).- Cross-reference gap timestamps with
sched_slice/flamegraphon the host to find what the feeding process was doing (sleeping? blocked on recv? GIL?).
Arguments
If the user passed a path as an argument to this skill ($ARGUMENTS), use it as the path parameter in your first trace_info call.