Demonstrations of stackcount, the Linux eBPF/bcc version.


This program traces functions and frequency counts them with their entire
stack trace, summarized in-kernel for efficiency. For example, counting
stack traces that led to the submit_bio() kernel function, which creates
block device I/O:

# ./stackcount submit_bio
Tracing 1 functions for "submit_bio"... Hit Ctrl-C to end.
^C
  submit_bio
  ext4_writepages
  do_writepages
  __filemap_fdatawrite_range
  filemap_flush
  ext4_alloc_da_blocks
  ext4_release_file
  __fput
  ____fput
  task_work_run
  exit_to_usermode_loop
  syscall_return_slowpath
  entry_SYSCALL_64_fastpath
  [unknown]
  [unknown]
    tar [15069]
    5

  submit_bio
  ext4_bio_write_page
  mpage_submit_page
  mpage_map_and_submit_buffers
  ext4_writepages
  do_writepages
  __filemap_fdatawrite_range
  filemap_flush
  ext4_alloc_da_blocks
  ext4_release_file
  __fput
  ____fput
  task_work_run
  exit_to_usermode_loop
  syscall_return_slowpath
  entry_SYSCALL_64_fastpath
  [unknown]
  [unknown]
    tar [15069]
    15

  submit_bio
  ext4_readpages
  __do_page_cache_readahead
  ondemand_readahead
  page_cache_async_readahead
  generic_file_read_iter
  __vfs_read
  vfs_read
  sys_read
  entry_SYSCALL_64_fastpath
  [unknown]
    tar [15069]
    113

Detaching...

The output shows unique stack traces, in order from leaf (on-CPU) to root,
followed by their occurrence count. The last stack trace in the above output
shows syscall handling, sys_read(), vfs_read(), and then "readahead" functions:
looks like an application issued file read has triggered read ahead. The
application can be seen after the stack trace, in this case, "tar [15069]"
for the "tar" command, PID 15069.

The order of printed stack traces is from least to most frequent. The most
frequent in this case, the ext4_rename() stack, was taken 113 times during
tracing.

The "[unknown]" frames are from user-level, since this simple workload is
the tar command, which apparently has been compiled without frame pointers.
It's a common compiler optimization, but it breaks frame pointer-based stack
walkers. Similar broken stacks will be seen by other profilers and debuggers
that use frame pointers. Hopefully your application preserves them so that
the user-level stack trace is visible. So how does one get frame pointers, if
your application doesn't have them to start with? For the current bcc (until
it supports other stack walkers), you need to be running a application binaries
that preserves frame pointers, eg, using gcc's -fno-omit-frame-pointer. That's
about all I'll say here: this is a big topic that is not bcc/BPF specific.

It can be useful to trace the path to submit_bio to explain unusual rates of
disk IOPS. These could have in-kernel origins (eg, background scrub).


Now adding the -d option to delimit kernel and user stacks:

# ./stackcount -d submit_bio
Tracing 1 functions for "submit_bio"... Hit Ctrl-C to end.
^C
  submit_bio
  submit_bh
  journal_submit_commit_record
  jbd2_journal_commit_transaction
  kjournald2
  kthread
  ret_from_fork
    --
    jbd2/xvda1-8 [405]
    1

  submit_bio
  submit_bh
  jbd2_journal_commit_transaction
  kjournald2
  kthread
  ret_from_fork
    --
    jbd2/xvda1-8 [405]
    2

  submit_bio
  ext4_writepages
  do_writepages
  __filemap_fdatawrite_range
  filemap_flush
  ext4_alloc_da_blocks
  ext4_release_file
  __fput
  ____fput
  task_work_run
  exit_to_usermode_loop
  syscall_return_slowpath
  entry_SYSCALL_64_fastpath
    --
  [unknown]
  [unknown]
    tar [15187]
    5

  submit_bio
  ext4_bio_write_page
  mpage_submit_page
  mpage_map_and_submit_buffers
  ext4_writepages
  do_writepages
  __filemap_fdatawrite_range
  filemap_flush
  ext4_alloc_da_blocks
  ext4_release_file
  __fput
  ____fput
  task_work_run
  exit_to_usermode_loop
  syscall_return_slowpath
  entry_SYSCALL_64_fastpath
    --
  [unknown]
  [unknown]
    tar [15187]
    15

  submit_bio
  ext4_readpages
  __do_page_cache_readahead
  ondemand_readahead
  page_cache_async_readahead
  generic_file_read_iter
  __vfs_read
  vfs_read
  sys_read
  entry_SYSCALL_64_fastpath
    --
  [unknown]
  [unknown]
  [unknown]
    tar [15187]
    171

Detaching...

A "--" is printed between the kernel and user stacks.


As a different example, here is the kernel function hrtimer_init_sleeper():

# ./stackcount.py -d hrtimer_init_sleeper
Tracing 1 functions for "hrtimer_init_sleeper"... Hit Ctrl-C to end.
^C
  hrtimer_init_sleeper
  do_futex
  SyS_futex
  entry_SYSCALL_64_fastpath
    --
  [unknown]
    containerd [16020]
    1

  hrtimer_init_sleeper
  do_futex
  SyS_futex
  entry_SYSCALL_64_fastpath
    --
  __pthread_cond_timedwait
  Monitor::IWait(Thread*, long)
  Monitor::wait(bool, long, bool)
  CompileQueue::get()
  CompileBroker::compiler_thread_loop()
  JavaThread::thread_main_inner()
  JavaThread::run()
  java_start(Thread*)
  start_thread
    java [4996]
    1

  hrtimer_init_sleeper
  do_futex
  SyS_futex
  entry_SYSCALL_64_fastpath
    --
  [unknown]
  [unknown]
    containerd [16020]
    1

  hrtimer_init_sleeper
  do_futex
  SyS_futex
  entry_SYSCALL_64_fastpath
    --
  __pthread_cond_timedwait
  VMThread::loop()
  VMThread::run()
  java_start(Thread*)
  start_thread
    java [4996]
    3

  hrtimer_init_sleeper
  do_futex
  SyS_futex
  entry_SYSCALL_64_fastpath
    --
  [unknown]
    dockerd [16008]
    4

  hrtimer_init_sleeper
  do_futex
  SyS_futex
  entry_SYSCALL_64_fastpath
    --
  [unknown]
  [unknown]
    dockerd [16008]
    4

  hrtimer_init_sleeper
  do_futex
  SyS_futex
  entry_SYSCALL_64_fastpath
    --
  __pthread_cond_timedwait
  Lio/netty/util/ThreadDeathWatcher$Watcher;::run
  Interpreter
  Interpreter
  call_stub
  JavaCalls::call_helper(JavaValue*, methodHandle*, JavaCallArguments*, Thread*)
  JavaCalls::call_virtual(JavaValue*, KlassHandle, Symbol*, Symbol*, JavaCallArguments*, Thread*)
  JavaCalls::call_virtual(JavaValue*, Handle, KlassHandle, Symbol*, Symbol*, Thread*)
  thread_entry(JavaThread*, Thread*)
  JavaThread::thread_main_inner()
  JavaThread::run()
  java_start(Thread*)
  start_thread
    java [4996]
    4

  hrtimer_init_sleeper
  do_futex
  SyS_futex
  entry_SYSCALL_64_fastpath
    --
  __pthread_cond_timedwait
  clock_gettime
  [unknown]
    java [4996]
    79

Detaching...

I was just trying to find a more interesting example. This output includes
some Java stacks where user-level has been walked correctly (even includes a
JIT symbol translation). dockerd and containerd don't have frame pointers
(grumble), but Java does (which is running with -XX:+PreserveFramePointer).


Here's another kernel function, ip_output():

# ./stackcount.py -d ip_output
Tracing 1 functions for "ip_output"... Hit Ctrl-C to end.
^C
  ip_output
  ip_queue_xmit
  tcp_transmit_skb
  tcp_write_xmit
  __tcp_push_pending_frames
  tcp_push
  tcp_sendmsg
  inet_sendmsg
  sock_sendmsg
  sock_write_iter
  __vfs_write
  vfs_write
  SyS_write
  entry_SYSCALL_64_fastpath
    --
  __write_nocancel
  [unknown]
    sshd [15015]
    5

  ip_output
  ip_queue_xmit
  tcp_transmit_skb
  tcp_write_xmit
  __tcp_push_pending_frames
  tcp_push
  tcp_sendmsg
  inet_sendmsg
  sock_sendmsg
  sock_write_iter
  __vfs_write
  vfs_write
  SyS_write
  entry_SYSCALL_64_fastpath
    --
  __write_nocancel
  [unknown]
  [unknown]
    sshd [8234]
    5

  ip_output
  ip_queue_xmit
  tcp_transmit_skb
  tcp_write_xmit
  __tcp_push_pending_frames
  tcp_push
  tcp_sendmsg
  inet_sendmsg
  sock_sendmsg
  sock_write_iter
  __vfs_write
  vfs_write
  SyS_write
  entry_SYSCALL_64_fastpath
    --
  __write_nocancel
    sshd [15015]
    7

Detaching...

This time just sshd is triggering ip_output() calls.


Watch what happens if I filter on kernel stacks only (-K) for ip_output():

# ./stackcount.py -K ip_output
Tracing 1 functions for "ip_output"... Hit Ctrl-C to end.
^C
  ip_output
  ip_queue_xmit
  tcp_transmit_skb
  tcp_write_xmit
  __tcp_push_pending_frames
  tcp_push
  tcp_sendmsg
  inet_sendmsg
  sock_sendmsg
  sock_write_iter
  __vfs_write
  vfs_write
  SyS_write
  entry_SYSCALL_64_fastpath
    13

Detaching...

They have grouped together as a single unique stack, since the kernel part
was the same.


Here is just the user stacks, fetched during the kernel function ip_output():

# ./stackcount.py -U ip_output
Tracing 1 functions for "ip_output"... Hit Ctrl-C to end.
^C
  [unknown]
    snmpd [1645]
    1

  __write_nocancel
  [unknown]
  [unknown]
    sshd [8234]
    3

  __write_nocancel
    sshd [15015]
    4

I should really run a custom sshd with frame pointers so we can see its
stack trace...


User-space functions can also be traced if a library name is provided. For
example, to quickly identify code locations that allocate heap memory for
PID 4902 (using -p), by tracing malloc from libc ("c:malloc"):

# ./stackcount -p 4902 c:malloc
Tracing 1 functions for "malloc"... Hit Ctrl-C to end.
^C
  malloc
  rbtree_new
  main
  [unknown]
    12

  malloc
  _rbtree_node_new_internal
  _rbtree_node_insert
  rbtree_insert
  main
  [unknown]
    1189

Detaching...

Kernel stacks are absent as this didn't enter kernel code.

Note that user-space uses of stackcount can be somewhat more limited because
a lot of user-space libraries and binaries are compiled without frame-pointers
as discussed earlier (the -fomit-frame-pointer compiler default) or are used
without debuginfo.


In addition to kernel and user-space functions, kernel tracepoints and USDT
tracepoints are also supported. 

For example, to determine where threads are being created in a particular 
process, use the pthread_create USDT tracepoint:

# ./stackcount -p $(pidof parprimes) u:pthread:pthread_create
Tracing 1 functions for "u:pthread:pthread_create"... Hit Ctrl-C to end.
^C

    parprimes [11923]
  pthread_create@@GLIBC_2.2.5
  main
  __libc_start_main
  [unknown]
    7

You can use "readelf -n file" to see if it has USDT tracepoints.


Similarly, to determine where context switching is happening in the kernel, 
use the sched:sched_switch kernel tracepoint:

# ./stackcount t:sched:sched_switch
  __schedule
  schedule
  worker_thread
  kthread
  ret_from_fork
    kworker/0:2 [25482]
    1

  __schedule
  schedule
  schedule_hrtimeout_range_clock
  schedule_hrtimeout_range
  ep_poll
  SyS_epoll_wait
  entry_SYSCALL_64_fastpath
  epoll_wait
  Lsun/nio/ch/SelectorImpl;::lockAndDoSelect
  Lsun/nio/ch/SelectorImpl;::select
  Lio/netty/channel/nio/NioEventLoop;::select
  Lio/netty/channel/nio/NioEventLoop;::run
  Interpreter
  Interpreter
  call_stub
  JavaCalls::call_helper(JavaValue*, methodHandle*, JavaCallArguments*, Thread*)
  JavaCalls::call_virtual(JavaValue*, KlassHandle, Symbol*, Symbol*, JavaCallArguments*, Thread*)
  JavaCalls::call_virtual(JavaValue*, Handle, KlassHandle, Symbol*, Symbol*, Thread*)
  thread_entry(JavaThread*, Thread*)
  JavaThread::thread_main_inner()
  JavaThread::run()
  java_start(Thread*)
  start_thread
    java [4996]
    1

... (omitted for brevity)

  __schedule
  schedule
  schedule_preempt_disabled
  cpu_startup_entry
  xen_play_dead
  arch_cpu_idle_dead
  cpu_startup_entry
  cpu_bringup_and_idle
    swapper/1 [0]
    289


A -i option can be used to set an output interval, and -T to include a
timestamp. For example:

# ./stackcount.py -Tdi 1 submit_bio
Tracing 1 functions for "submit_bio"... Hit Ctrl-C to end.

06:05:13

06:05:14
  submit_bio
  xfs_do_writepage
  write_cache_pages
  xfs_vm_writepages
  do_writepages
  __writeback_single_inode
  writeback_sb_inodes
  __writeback_inodes_wb
  wb_writeback
  wb_workfn
  process_one_work
  worker_thread
  kthread
  ret_from_fork
    --
    kworker/u16:1 [15686]
    1

  submit_bio
  process_one_work
  worker_thread
  kthread
  ret_from_fork
    --
    kworker/u16:0 [16007]
    1

  submit_bio
  xfs_buf_submit
  xlog_bdstrat
  xlog_sync
  xlog_state_release_iclog
  _xfs_log_force
  xfs_log_force
  xfs_fs_sync_fs
  sync_fs_one_sb
  iterate_supers
  sys_sync
  entry_SYSCALL_64_fastpath
    --
  [unknown]
    sync [16039]
    1

  submit_bio
  submit_bh
  journal_submit_commit_record
  jbd2_journal_commit_transaction
  kjournald2
  kthread
  ret_from_fork
    --
    jbd2/xvda1-8 [405]
    1

  submit_bio
  process_one_work
  worker_thread
  kthread
  ret_from_fork
    --
    kworker/0:2 [25482]
    2

  submit_bio
  ext4_writepages
  do_writepages
  __writeback_single_inode
  writeback_sb_inodes
  __writeback_inodes_wb
  wb_writeback
  wb_workfn
  process_one_work
  worker_thread
  kthread
  ret_from_fork
    --
    kworker/u16:0 [16007]
    4

  submit_bio
  xfs_vm_writepages
  do_writepages
  __writeback_single_inode
  writeback_sb_inodes
  __writeback_inodes_wb
  wb_writeback
  wb_workfn
  process_one_work
  worker_thread
  kthread
  ret_from_fork
    --
    kworker/u16:1 [15686]
    5

  submit_bio
  __block_write_full_page
  block_write_full_page
  blkdev_writepage
  __writepage
  write_cache_pages
  generic_writepages
  blkdev_writepages
  do_writepages
  __filemap_fdatawrite_range
  filemap_fdatawrite
  fdatawrite_one_bdev
  iterate_bdevs
  sys_sync
  entry_SYSCALL_64_fastpath
    --
  [unknown]
    sync [16039]
    7

  submit_bio
  submit_bh
  jbd2_journal_commit_transaction
  kjournald2
  kthread
  ret_from_fork
    --
    jbd2/xvda1-8 [405]
    8

  submit_bio
  ext4_bio_write_page
  mpage_submit_page
  mpage_map_and_submit_buffers
  ext4_writepages
  do_writepages
  __writeback_single_inode
  writeback_sb_inodes
  __writeback_inodes_wb
  wb_writeback
  wb_workfn
  process_one_work
  worker_thread
  kthread
  ret_from_fork
    --
    kworker/u16:0 [16007]
    8

  submit_bio
  __block_write_full_page
  block_write_full_page
  blkdev_writepage
  __writepage
  write_cache_pages
  generic_writepages
  blkdev_writepages
  do_writepages
  __writeback_single_inode
  writeback_sb_inodes
  __writeback_inodes_wb
  wb_writeback
  wb_workfn
  process_one_work
  worker_thread
  kthread
  ret_from_fork
    --
    kworker/u16:0 [16007]
    60


06:05:15

06:05:16

Detaching...

This only included output for the 06:05:14 interval. The other internals
did not span block device I/O.


The -s output prints the return instruction offset for each function (aka
symbol offset). Eg:

# ./stackcount.py -s tcp_sendmsg
Tracing 1 functions for "tcp_sendmsg"... Hit Ctrl-C to end.
^C
  tcp_sendmsg+0x1
  sock_sendmsg+0x38
  sock_write_iter+0x85
  __vfs_write+0xe3
  vfs_write+0xb8
  SyS_write+0x55
  entry_SYSCALL_64_fastpath+0x1e
  __write_nocancel+0x7
    sshd [15015]
    3

  tcp_sendmsg+0x1
  sock_sendmsg+0x38
  sock_write_iter+0x85
  __vfs_write+0xe3
  vfs_write+0xb8
  SyS_write+0x55
  entry_SYSCALL_64_fastpath+0x1e
  __write_nocancel+0x7
    sshd [8234]
    3

Detaching...

If it wasn't clear how one function called another, knowing the instruction
offset can help you locate the lines of code from a disassembly dump.


The -v output is verbose, and shows raw addresses:

./stackcount.py -v tcp_sendmsg
Tracing 1 functions for "tcp_sendmsg"... Hit Ctrl-C to end.
^C
  ffffffff817b05c1 tcp_sendmsg
  ffffffff8173ea48 sock_sendmsg
  ffffffff8173eae5 sock_write_iter
  ffffffff81232b33 __vfs_write
  ffffffff812331b8 vfs_write
  ffffffff81234625 SyS_write
  ffffffff818739bb entry_SYSCALL_64_fastpath
  7f120511e6e0     __write_nocancel
    sshd [8234]
    3

  ffffffff817b05c1 tcp_sendmsg
  ffffffff8173ea48 sock_sendmsg
  ffffffff8173eae5 sock_write_iter
  ffffffff81232b33 __vfs_write
  ffffffff812331b8 vfs_write
  ffffffff81234625 SyS_write
  ffffffff818739bb entry_SYSCALL_64_fastpath
  7f919c5a26e0     __write_nocancel
    sshd [15015]
    11

Detaching...


A wildcard can also be used. Eg, all functions beginning with "tcp_send",
kernel stacks only (-K) with offsets (-s):

# ./stackcount -Ks 'tcp_send*'
Tracing 14 functions for "tcp_send*"... Hit Ctrl-C to end.
^C
  tcp_send_delayed_ack0x1
  tcp_rcv_established0x3b1
  tcp_v4_do_rcv0x130
  tcp_v4_rcv0x8e0
  ip_local_deliver_finish0x9f
  ip_local_deliver0x51
  ip_rcv_finish0x8a
  ip_rcv0x29d
  __netif_receive_skb_core0x637
  __netif_receive_skb0x18
  netif_receive_skb_internal0x23
    1

  tcp_send_delayed_ack0x1
  tcp_rcv_established0x222
  tcp_v4_do_rcv0x130
  tcp_v4_rcv0x8e0
  ip_local_deliver_finish0x9f
  ip_local_deliver0x51
  ip_rcv_finish0x8a
  ip_rcv0x29d
  __netif_receive_skb_core0x637
  __netif_receive_skb0x18
  netif_receive_skb_internal0x23
    4

  tcp_send_mss0x1
  inet_sendmsg0x67
  sock_sendmsg0x38
  sock_write_iter0x78
  __vfs_write0xaa
  vfs_write0xa9
  sys_write0x46
  entry_SYSCALL_64_fastpath0x16
    7

  tcp_sendmsg0x1
  sock_sendmsg0x38
  sock_write_iter0x78
  __vfs_write0xaa
  vfs_write0xa9
  sys_write0x46
  entry_SYSCALL_64_fastpath0x16
    7

Detaching...

Use -r to allow regular expressions.


The -f option will emit folded output, which can be used as input to other
tools including flame graphs. For example, with delimiters as well:

# ./stackcount.py -df t:sched:sched_switch
^Csnmp-pass;[unknown];[unknown];[unknown];[unknown];[unknown];-;entry_SYSCALL_64_fastpath;SyS_select;core_sys_select;do_select;poll_schedule_timeout;schedule_hrtimeout_range;schedule_hrtimeout_range_clock;schedule;__schedule 1
kworker/7:0;-;ret_from_fork;kthread;worker_thread;schedule;__schedule 1
watchdog/0;-;ret_from_fork;kthread;smpboot_thread_fn;schedule;__schedule 1
snmp-pass;[unknown];[unknown];[unknown];[unknown];[unknown];-;entry_SYSCALL_64_fastpath;SyS_select;core_sys_select;do_select;poll_schedule_timeout;schedule_hrtimeout_range;schedule_hrtimeout_range_clock;schedule;__schedule 1
svscan;[unknown];-;entry_SYSCALL_64_fastpath;SyS_nanosleep;hrtimer_nanosleep;do_nanosleep;schedule;__schedule 1
python;[unknown];__select_nocancel;-;entry_SYSCALL_64_fastpath;SyS_select;core_sys_select;do_select;poll_schedule_timeout;schedule_hrtimeout_range;schedule_hrtimeout_range_clock;schedule;__schedule 1
kworker/2:0;-;ret_from_fork;kthread;worker_thread;schedule;__schedule 1
[...]

Flame graphs visualize stack traces. For information about them and links
to open source software, see http://www.brendangregg.com/flamegraphs.html .
This folded output can be piped directly into flamegraph.pl (the Perl version).


USAGE message:

# ./stackcount -h
usage: stackcount [-h] [-p PID] [-i INTERVAL] [-D DURATION] [-T] [-r] [-s]
                  [-P] [-K] [-U] [-v] [-d] [-f] [--debug]
                  pattern

Count events and their stack traces

positional arguments:
  pattern               search expression for events

optional arguments:
  -h, --help            show this help message and exit
  -p PID, --pid PID     trace this PID only
  -i INTERVAL, --interval INTERVAL
                        summary interval, seconds
  -D DURATION, --duration DURATION
                        total duration of trace, seconds
  -T, --timestamp       include timestamp on output
  -r, --regexp          use regular expressions. Default is "*" wildcards
                        only.
  -s, --offset          show address offsets
  -P, --perpid          display stacks separately for each process
  -K, --kernel-stacks-only
                        kernel stack only
  -U, --user-stacks-only
                        user stack only
  -v, --verbose         show raw addresses
  -d, --delimited       insert delimiter between kernel/user stacks
  -f, --folded          output folded format
  --debug               print BPF program before starting (for debugging
                        purposes)

examples:
    ./stackcount submit_bio         # count kernel stack traces for submit_bio
    ./stackcount -d ip_output       # include a user/kernel stack delimiter
    ./stackcount -s ip_output       # show symbol offsets
    ./stackcount -sv ip_output      # show offsets and raw addresses (verbose)
    ./stackcount 'tcp_send*'        # count stacks for funcs matching tcp_send*
    ./stackcount -r '^tcp_send.*'   # same as above, using regular expressions
    ./stackcount -Ti 5 ip_output    # output every 5 seconds, with timestamps
    ./stackcount -p 185 ip_output   # count ip_output stacks for PID 185 only
    ./stackcount -p 185 c:malloc    # count stacks for malloc in PID 185
    ./stackcount t:sched:sched_fork # count stacks for sched_fork tracepoint
    ./stackcount -p 185 u:node:*    # count stacks for all USDT probes in node
    ./stackcount -K t:sched:sched_switch   # kernel stacks only
    ./stackcount -U t:sched:sched_switch   # user stacks only
