josefbacik/systing/systing-analyze
Claude Code and agent skill by josefbacik/systing
- community
Checking that the SKILL.md file is still in the repository.
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, sched_aggregate, network_*).
Install
npx vskill@latest install josefbacik/systing/systing-analyzeSKILL.md
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 | |
| How does the scheduler behave overall? Compare two schedulers/hosts? | sched_aggregate | one summary per capture: wakeup latency (ran on previous CPU / migrated), preempt wait, slice length, switch + migration rates, migrate-event counts, time-weighted runqueue length, per-CPU load vectors (incl. placement) + imbalance metrics, log2 histograms, tail threads; percentiles within ~6%; per-CPU runqueue exact on traces with sched_migrate (meta.placement_exact), approximate before | |
| 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|sched aggregate|network connections|network interfaces|network socket-pairs (and systing-analyze mcp to start this server).
A server started with systing-analyze mcp -d <trace.duckdb> --restrict-to-database serves that one database: omit path on every call (that database's own path is accepted too), and any other path is refused without being opened.
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, py_name) andprocess(upid → pid, name) for the Linux IDs.thread.nameis the kernel's name for the thread (comm);thread.py_name(from schema 24) is the name its Python process gave it (threading.Thread(name=...), e.g.Thread-1 (worker)), filled by thetask-stacksrecorder for Python 3.13+ processes with--task-stacks-frames python|alland NULL for every other thread.
Stack traces
Two representations exist:
Interned (the normal one) — stack_sample → stack → frame:
stack_sample(ts, utid, cpu, stack_id, stack_event_type, task_context_id, go_goid, go_labels_id).stack_event_type:0= uninterruptible sleep,1= CPU,2= interruptible sleep.task_context_id(from schema 25) is the id of what the sampled thread's program said it was working on, see below; NULL for none, and in every row of a trace recorded without--include-task-context.go_goidandgo_labels_id(from schema 30) are a Go program's goroutine and the id of its profiler label set, see below; NULL in every row of a trace recorded without--include-go-context.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.frame_file(frame_id, file)— the full source path of the frames that have one (from schema 23: Python frames, and the native and kernel frames oftask-stacksstacks where debug info has the directory, which is the build machine's path):frame.namehas only the file's name.LEFT JOIN frame_file ff ON ff.trace_id = f.trace_id AND ff.frame_id = f.id.- 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.
Python function calls — python_function_slice (the python-function-trace recorder, from schema 34):
python_function_slice(ts, dur, utid, depth, function, file, line, end_kind): one row per run of a Python frame, withend_kindone ofreturn,yield,unwound,open; a generator has a row per resumption.sysinfo.python_function_*says how many processes were traced, what was lost, and inpython_function_noteswhat was refused and why.durincludes the probes' own cost of a few microseconds a call, so rankings favour functions called often: compare counts as well as times.
Task-stacks snapshots — task_stack_event → stack (the task-stacks recorder, from schema 23):
task_stack_event(ts, dur, utid, start_iteration, end_iteration, utime_delta_ns, stime_delta_ns, runtime_delta_ns, state, stack_id, task_context_id, syscall_nr, syscall, in_iowait, holds_gil): a thread as a periodic snapshot found it, for as long as it stayed that way. Snapshots (iterations) are numbered from 1; a thread that had not run and was still in the same state is not re-recorded, its row is extended throughend_iteration, so a thread blocked for a minute is one row.stateis the one-letter task state (R,S,D, ...); anRrow that spans iterations is a thread that waited for a CPU through them, not one that ran, whatever itsruntime_delta_ns, which is the time before the row;utime_delta_ns/stime_delta_nsare CPU time since the thread's previous row, which advance by scheduler ticks and so read 0 for a short run;runtime_delta_nsis the time on a CPU since then to the nanosecond, the one to use for "did it run";stack_idis NULL when the thread had no frames.task_context_id(from schema 27,--include-task-contextbeside the recorder) is the id of what the thread said it was working on when its row was recorded, joined totask_contextonutidandidas forstack_sample; NULL for none. The thread's names are onthread:name(the kernel's) andpy_name(its Python process's). Before reading short user stacks as threads that were idle or unwalkable, readsysinfo.task_stacks_remote_reads(from schema 29):off:kernel-releasemeans the capture ran on an aarch64 kernel where the recorder does not read other tasks' memory, so every user stack is its first frame alone and there are no Python frames and notask_context_id, by design; no row says so by itself; with Python frames alone such a capture loads no iterator, so the table is empty and this value is what says why. NULL there means the recorder was not asked for or the capture predates the column (unknown, neveron).- A user stack can be short because the recorder stood back, whatever the thread itself was doing. It never waits at a process's memory-map lock: while that is held for writing or has a queue, a record is cut short at that point (user frames, Python frames and
task_context_id), for every thread of the process that has a record due; another thread at anmmap_lockwait in its kernel frames is the sign to look for. A thread that sleeps through it gets a new row once the lock is free, with the whole stack andruntime_delta_ns0, so a row like that follows the cut one. In Python-only mode the thread has no row in between, and the CPU time of that record is lost. It also follows a frame pointer only while that stays on the thread's stack, so frames beneath a switch of stacks are missing: a goroutine's beneath cgo orsystemstack, or those beneath a signal handler on an alternate stack. No row says either, so read the counts before trusting a short stack (from schema 33; NULL means unknown, never 0):sysinfo.task_stacks_cut_recordsandsysinfo.task_stacks_off_stack_records, each out ofsysinfo.task_stacks_full_records, which is one pertask_stack_eventrow (in Python-only mode a walk refused outright has no row and still counts as cut, so the cut count can pass it). - These are snapshots, not samples of CPU time: every targeted thread has a row whatever it was doing, so this is the table for "what was each thread blocked in" rather than for CPU flamegraphs.
- Idle or busy (from schema 31; all four NULL in an older trace, which means not recorded): most blocked threads are parked, not stuck, and these columns tell which without reading frames.
syscall/syscall_nris the system call the thread was in (futex,epoll_wait,nanosleep,acceptare where parked threads sit;read,pread64,fsync,connectare work), NULL when it was in none or was on a CPU as the snapshot reached it (mostRrows).in_iowaitis the kernel's mark of a thread blocked on disk I/O.holds_gilis whether the thread held its Python interpreter's GIL, NULL without Python frames.state = 'R'is py-spy's default "not idle" view andholds_gilits--gil; a row that is notRand hasholds_gilis a call that sleeps without releasing the GIL. A thread infutexmay be parked on a condition variable or blocked on a contended lock: the frames (stack.frame_names) say which.
This is the start of the file. Read the full SKILL.md on GitHub.
Security
- Security scanPassed, 90 of 100Pattern checks on the skill's files.
- Model reviewPassed
Quality evaluation
- Task evaluationNot runNo evaluation yet. Run one in Skill Studio.
Source
- SKILL.md in the repositoryCheckingskills/systing-analyze/SKILL.md
Popularity
- Repository stars
- 176
- Repository forks
- 18
- 7-day trend score
- 26
Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.
Works with
Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.
- Claude Code
- Cursor
- GitHub Copilot
- Windsurf
- Codex
- Gemini CLI
- Cline
- Amp
- Roo Code
- Goose
- Continue
- Kilo Code
- Junie
- Trae
- OpenHands
- Qwen Code
- AI Agent
- Tabnine Chat
- Sourcegraph Cody
- Amazon Q
- Firebase Genkit
- Sweep
- Devin
- Phind
- Mentat
- Claude Desktop
- Aider
- GPT Pilot
- Plandex
- bolt.new
- v0
- Replit Agent
- Anse
- TypingMind
- BetterChatGPT
- LibreChat
- Warp AI
- Pieces
- Fig AI
Embed badge
[](https://verified-skill.com/skills/josefbacik/systing/systing-analyze)