The -c flag for strace doesn’t just count syscalls; it aggregates them by type and time, giving you a performance profile of your process.

Let’s see it in action. Imagine you have a simple Python script that writes to a file repeatedly:

# script.py
import time
import os

with open("output.txt", "w") as f:
    for i in range(1000):
        f.write(f"Line {i}\n")
        # Simulate some work
        time.sleep(0.001)

print("Done writing.")

Now, let’s strace it with the -c flag and observe the output:

strace -c python script.py

The output won’t be line-by-line syscalls. Instead, you’ll get a summary table:

% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 85.10    0.851000      851.00         1           write
 10.50    0.105000      105.00        10           read
  2.00    0.020000       20.00         1           openat
  1.50    0.015000       15.00         1           close
  0.90    0.009000        9.00         1           fstat
  0.00    0.000000        0.00         1           brk
  0.00    0.000000        0.00         3           futex
... (many more lines, often with 0% time) ...
------ ----------- ----------- --------- --------- ----------------
100.00    1.000000                  1000 total

This table tells us that 85.10% of the total execution time was spent in the write syscall, which was called only once. This is valuable information! It suggests that the dominant cost of this script is writing data to the file. The read syscall, while called 10 times, only consumed 10.50% of the time, indicating it’s much less of a bottleneck.

The mental model here is that strace -c acts as a profiler for system calls. Instead of showing you what your program is doing at the syscall level, it shows you where your program is spending its time between syscalls, by aggregating the time spent in each syscall and the overhead of making those calls. It categorizes and sums up the system calls made by a traced process.

The columns are:

  • % time: Percentage of total time spent in this syscall.
  • seconds: Total seconds spent in this syscall.
  • usecs/call: Average time in microseconds per call to this syscall.
  • calls: The number of times this syscall was invoked.
  • errors: The number of times this syscall returned an error.
  • syscall: The name of the system call.

The key insight is that a syscall with a high usecs/call or a high % time with a reasonable number of calls is a performance bottleneck. In our example, write is called only once but dominates the time, implying that the actual I/O operation is slow, or the amount of data written in that single call is large.

The openat, close, and fstat calls are also significant, but far less so than write. The read calls are also a notable portion, but again, less than the write. The brk and futex calls, which are common for memory management and synchronization, show 0% time, meaning they are not contributing to the observed slowness in this particular script.

One thing most people don’t realize is how strace -c attributes time. It includes the time spent in the kernel executing the syscall and the time spent on the user-space overhead of making the call (setting up arguments, context switching to kernel mode, and context switching back). When you see a high % time for a syscall, it’s a strong indicator that either the kernel work for that syscall is expensive, or you’re making too many of them, or the data being processed by the syscall is large.

If you wanted to optimize script.py, you’d focus on reducing the time spent in write. This might involve buffering writes in user space before flushing a larger chunk to disk, or investigating disk I/O performance itself.

The next step in performance analysis would be to use strace -T to see the time spent in each individual syscall, to pinpoint which specific calls are taking the longest.

Want structured learning?

Take the full Strace course →