microscope syscalls shows the system calls a program makes. Its output and
most of its flags match strace, so a strace command line usually works with
microscope in place of strace. Use it to see which files a program opens,
why a call fails, or where a program spends its time waiting on the kernel.
Put the program and its arguments after --:
microscope syscalls -- ./server --port 8080Each call prints as it returns:
openat(AT_FDCWD, "/etc/hostname", O_RDONLY) = 3
read(3, "localhost\n", 262144) = 10
close(3) = 0
+++ exited with 0 +++
Children and threads are traced too. Pass --no-follow to trace only the
program itself. When the trace ends, microscope syscalls exits with the
program's exit status.
To trace a process that is already running, give its ID:
microscope syscalls -p 1842Stop tracing with Ctrl-C. The process keeps running. microscope never traces the whole machine, so a target is always required.
-e trace= picks calls by name or by class, as strace does:
microscope syscalls -e trace=openat,close -- ./server
microscope syscalls -e trace=%file,%net -- ./serverOther filters narrow the output further:
| Option | Shows |
|---|---|
-z |
Only calls that succeeded |
-Z |
Only calls that failed |
-P PATH |
Only calls that name PATH, by argument or descriptor |
--trace-fds 3,4 |
Only calls on these descriptors |
--slower-than 10ms |
Only calls that took at least this long |
--sample 1/N |
One call in N; the table still counts every call |
A few options change how calls print: -y shows what each descriptor refers
to, -T shows how long each call took, -t adds the time of day, and
-s SIZE sets how much of each string is shown. --format ndjson writes one
JSON object per line for other programs to read.
-T and -r, which shows the time since the previous line, print
microseconds. As with strace, their long forms take a precision of s,
ms, us, or ns:
microscope syscalls --syscall-times=ns -e trace=close -- ./serverclose(3) = 0 <0.000003887>
The short forms take no value, as in strace: write --syscall-times=ns,
not -Tns or -T=ns. When both forms are given, the last one wins.
-t prints the time of day, -tt adds microseconds, and -ttt prints
seconds since the epoch. --absolute-timestamps (or --timestamps) sets
the format and precision separately, as strace does: a format of time,
unix or none, and a precision of s, ms, us or ns, in either
order. --absolute-timestamps=unix,ns prints 1793023895.396960108. A
value that names only one of the two keeps the other from the flag before.
-t and --absolute-timestamps can't be given together. With -r too, the
time since the previous line follows in (+ ).
A call that is still blocked after a second is reported while it waits, with
how long it has been blocked so far. Change the threshold with
--in-flight, or turn it off with --in-flight off.
Run microscope syscalls --help for every option.
-c prints a table of calls, errors, and time instead of the calls
themselves:
microscope syscalls -c -- ls /% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
...
100.00 0.003840 37 103 36 total
-C prints both. -S sorts the table and -U chooses its columns, which
include p50, p90, and p99 latencies.
The table's times are in microseconds, as strace's are, so a call that
takes less than one counts as 0. --syscall-times=ns gives them in
nanoseconds: seconds gets nine decimals, and the other time columns
become nsecs/call, min nsecs, max nsecs, and p50 nsecs to
p99 nsecs:
microscope syscalls -c --syscall-times=ns -e trace=getppid -U name,calls,min-time,p50 -- ./serversyscall calls min nsecs p50 nsecs
---------------- --------- ----------- -----------
getppid 20 60 202
---------------- --------- ----------- -----------
total 20 60 202
strace ignores -T with -c. With -c, microscope takes only us or
ns, since milliseconds or seconds would turn even more calls into 0.
With -C, s and ms set the precision of the lines, and the table stays
in microseconds.
-k prints the stack of each call. -kk adds source lines:
microscope syscalls -kk -e trace=openat -- ./serverThe BPF engine, described below, finds stacks by walking frame pointers. A
program built without them gives short stacks; -k=dwarf copies part of the
stack and unwinds it with the program's debug information instead. A stack
that ends early says why. A broken frame-pointer chain names the module
where it stopped. If more than a quarter of the shown stacks stop because
code does not keep frame pointers, one hint at the end names the module
responsible for the most breaks and suggests -k=dwarf.
-k=dwarf still has a bounded stack copy. If it runs out of copied bytes,
the stack ends with stack copy exhausted, not a frame-pointer warning.
A Python program's stacks show each Python frame's function, file, and line,
with either engine; see Python. In ndjson, such
a frame has function, file, and line, where a native frame has
symbol.
A Node program's stacks show its JavaScript frames the same way, among the
native frames of V8 that run them. The ptrace engine reads the stopped
thread as the debugger does, so every frame has its line, in every tier. The
BPF engine names every frame's function. A frame V8's interpreter runs has
its line. A frame that Sparkplug, Maglev, or Turbofan compiled has no line,
because the engine does not prove which compiled code the frame ran. A
frame whose function V8's garbage collector moved before microscope read it
is <unknown>, never another function. Calls made before microscope has
read V8's layout, just after the program starts, end with interpreted frames unknown. A call made from native code that keeps something other
than a frame pointer in its frame-pointer register, as an addon built
without frame pointers may, hides the JavaScript frames under it from the
BPF engine, and its stack ends with interpreted frames ended early. See
Node.js.
When the -o file name ends in .usprof, microscope saves the whole trace:
microscope syscalls -o server.usprof -- ./serverAny other -o name gets the text output, as strace writes it.
microscope report server.usprof prints the -c table. When the calls have
stacks, it then shows where they were made, the sites that took the most time
first:
By call site:
% time seconds calls errors p99 usecs site
------ ----------- --------- --------- ----------- ----------------
66.70 0.001027 1 775 ld-linux-x86-64.so.2+0x1fd40
5.83 0.000089 11 13 mmap
4.99 0.000076 9 7 12 __GI___open64_nocancel
2.66 0.000041 1 36 files 路 syscalls.c:19
...
------ ----------- --------- --------- ----------- ----------------
100.00 0.001540 63 16 775 total
and 10 more sites, counted in the total; --limit shows more
A call's site is the line of the program's own code that made the call,
shown as its function, file, and line. microscope looks past the C library and
Rust's standard library, whose wrappers every call of a kind goes through. A
program built without debug information has no lines, so its sites are its
functions instead. --limit sets how many sites are shown, 20 unless told.
microscope web server.usprof opens the trace in the browser, where you can
filter calls, group them by file, process, or call site, see latency over
time, and open any call to read its arguments and stack.
microscope convert writes a saved trace's calls for other tools. In pprof
and folded stacks each call is a sample of its stack, with the call's name as
the innermost frame; pprof gives each sample the number of calls and the
nanoseconds they took, and folded stacks give only the number of calls. A
Chrome trace shows each call that returned as a span on its thread, with its
result and its site:
microscope convert server.usprof server.pb.gz
microscope convert server.usprof server.folded
microscope convert server.usprof server.jsonmicroscope has two ways to see system calls.
The default BPF engine runs small programs in the kernel. The traced
program never stops, and each call costs it about a tenth of a microsecond.
It needs the CAP_BPF and CAP_PERFMON capabilities, Linux 5.15 or newer, and
the kernel's BTF type information.
The ptrace engine stops the program at each traced call, as strace does.
It needs no special privilege, but each call costs several microseconds.
Choose it with --engine ptrace:
microscope syscalls --engine ptrace -- ./servermicroscope never switches engines on its own. If the BPF engine cannot run, microscope says why and stops.
The ptrace engine filters calls in the kernel when it starts the program, so
calls you did not ask for cost nothing. A running process cannot be given that
filter. Attaching the ptrace engine therefore stops the process at every
system call, and microscope asks you to accept that with --stop-every-call.
The BPF engine needs CAP_BPF and CAP_PERFMON. Without them it stops and
explains what is missing.
From a systemd unit, grant them as ambient capabilities:
[Service]
AmbientCapabilities=CAP_BPF CAP_PERFMONOn NixOS, microscope's flake has a module that installs a wrapper with these
capabilities. Import nixosModules.microscope-bpf, rebuild, and run microscope
through it:
/run/wrappers/bin/microscope-bpf microscope syscalls -- ./serverThe wrapper is open to members of the wheel group.
microscope cannot use a BPF token, which some container managers offer instead of capabilities. When it finds one, the error says so.
Trace every process in a cgroup, including ones that start later:
microscope syscalls --cgroup /sys/fs/cgroup/system.slice/server.serviceOr name a container by its ID. Docker, Podman, containerd, and CRI-O containers are found by their cgroup names, and the first 12 characters of the ID are enough:
microscope syscalls --container 3f9c2a1b8e07Process IDs are shown as the processes inside see them, when they all share one PID namespace. Otherwise microscope shows the host's IDs and says why. The trace ends when you stop it or the cgroup is removed.
A cgroup trace needs the BPF engine. It cannot show stacks.
--budget PERCENT holds microscope to a share of one CPU:
microscope syscalls --budget 1 --container 3f9c2a1b8e07Each second it is over budget, microscope gives up one more kind of detail: first stacks, then the end of long strings, then it shows only one call in 2, 4, and up to 64, and finally it only counts calls. Paths are never cut. Every call is still counted, and microscope prints each step as it takes it. A saved trace keeps the steps, and the browser shows them under the trace.
--continuous DIR traces until stopped and writes a file of statistics every
minute:
microscope syscalls --continuous traces --keep 24h --container 3f9c2a1b8e07--segment sets how long each file covers, and --keep how long files are
kept. microscope report traces and microscope web traces read the files as
one trace.
The files hold counts and latencies, not individual calls. To keep the calls
around a problem, microscope holds the latest --window of calls (10 seconds by
default) and saves them to traces/windows when:
- a call takes at least
--trigger-slower-than; - a call fails with an error named by
--trigger-errno, such asEIO,ENOSPC; - microscope receives
SIGUSR2.
At most one window is saved per window length, so a burst of failures does not save the same calls over and over.
microscope record --syscalls traces calls alongside a CPU profile, on the same
clock:
microscope record --syscalls %file,%net -o server.usprof -- ./serverIt takes the same sets as -e trace=, or all. In the browser, the calls
appear on the timeline beside the samples. It uses the BPF engine and needs
its privileges, and it cannot be used with --pid. See
Profiling.
The debugger can stop a program at a system call. See Stop at system calls.
Some calls, such as clock_gettime and gettimeofday, usually run in the
vDSO without entering the kernel. Neither engine sees them.
Under heavy load, the BPF engine can fall behind and drop calls. It says how many:
--- lost 4502 events ---
The kernel hands calls to microscope through a ring of 16 MiB, or 64 MiB with
-k=dwarf, whose stack copies make each call about 20 times as large. Each
call's stack is unwound on a thread of its own and named as it is shown, which
takes a few microseconds, so a program making more than about 250,000 calls a
second with -k, or 200,000 with -k=dwarf, can outrun it on a fast machine
with a CPU to spare, and fewer on a busy one. --ring 256M gives a burst more room; it does not make
the reader faster. A call whose stack is lost is still shown, its stack
reading [stack ends: no stack], and the stack counts among the lost.
For a short list of calls, the BPF engine attaches to the kernel functions
behind them, which costs other processes nothing. exit and exit_group
attach only at entry; the thread's exit completes their = ? record.
With function attachments, a call in progress is lost if another tracer
attaches to or detaches from the same
function. microscope counts that call as lost rather than show it without its
result.