1#!/usr/bin/env python 2# @lint-avoid-python-3-compatibility-imports 3# 4# funclatency Time functions and print latency as a histogram. 5# For Linux, uses BCC, eBPF. 6# 7# USAGE: funclatency [-h] [-p PID] [-i INTERVAL] [-T] [-u] [-m] [-F] [-r] [-v] 8# pattern 9# 10# Run "funclatency -h" for full usage. 11# 12# The pattern is a string with optional '*' wildcards, similar to file 13# globbing. If you'd prefer to use regular expressions, use the -r option. 14# 15# Currently nested or recursive functions are not supported properly, and 16# timestamps will be overwritten, creating dubious output. Try to match single 17# functions, or groups of functions that run at the same stack layer, and 18# don't ultimately call each other. 19# 20# Copyright (c) 2015 Brendan Gregg. 21# Licensed under the Apache License, Version 2.0 (the "License") 22# 23# 20-Sep-2015 Brendan Gregg Created this. 24# 06-Oct-2016 Sasha Goldshtein Added user function support. 25 26from __future__ import print_function 27from bcc import BPF 28from time import sleep, strftime 29import argparse 30import signal 31 32# arguments 33examples = """examples: 34 ./funclatency do_sys_open # time the do_sys_open() kernel function 35 ./funclatency c:read # time the read() C library function 36 ./funclatency -u vfs_read # time vfs_read(), in microseconds 37 ./funclatency -m do_nanosleep # time do_nanosleep(), in milliseconds 38 ./funclatency -i 2 -d 10 c:open # output every 2 seconds, for duration 10s 39 ./funclatency -mTi 5 vfs_read # output every 5 seconds, with timestamps 40 ./funclatency -p 181 vfs_read # time process 181 only 41 ./funclatency 'vfs_fstat*' # time both vfs_fstat() and vfs_fstatat() 42 ./funclatency 'c:*printf' # time the *printf family of functions 43 ./funclatency -F 'vfs_r*' # show one histogram per matched function 44""" 45parser = argparse.ArgumentParser( 46 description="Time functions and print latency as a histogram", 47 formatter_class=argparse.RawDescriptionHelpFormatter, 48 epilog=examples) 49parser.add_argument("-p", "--pid", type=int, 50 help="trace this PID only") 51parser.add_argument("-i", "--interval", type=int, 52 help="summary interval, in seconds") 53parser.add_argument("-d", "--duration", type=int, 54 help="total duration of trace, in seconds") 55parser.add_argument("-T", "--timestamp", action="store_true", 56 help="include timestamp on output") 57parser.add_argument("-u", "--microseconds", action="store_true", 58 help="microsecond histogram") 59parser.add_argument("-m", "--milliseconds", action="store_true", 60 help="millisecond histogram") 61parser.add_argument("-F", "--function", action="store_true", 62 help="show a separate histogram per function") 63parser.add_argument("-r", "--regexp", action="store_true", 64 help="use regular expressions. Default is \"*\" wildcards only.") 65parser.add_argument("-v", "--verbose", action="store_true", 66 help="print the BPF program (for debugging purposes)") 67parser.add_argument("pattern", 68 help="search expression for functions") 69parser.add_argument("--ebpf", action="store_true", 70 help=argparse.SUPPRESS) 71args = parser.parse_args() 72if args.duration and not args.interval: 73 args.interval = args.duration 74if not args.interval: 75 args.interval = 99999999 76 77def bail(error): 78 print("Error: " + error) 79 exit(1) 80 81parts = args.pattern.split(':') 82if len(parts) == 1: 83 library = None 84 pattern = args.pattern 85elif len(parts) == 2: 86 library = parts[0] 87 libpath = BPF.find_library(library) or BPF.find_exe(library) 88 if not libpath: 89 bail("can't resolve library %s" % library) 90 library = libpath 91 pattern = parts[1] 92else: 93 bail("unrecognized pattern format '%s'" % pattern) 94 95if not args.regexp: 96 pattern = pattern.replace('*', '.*') 97 pattern = '^' + pattern + '$' 98 99# define BPF program 100bpf_text = """ 101#include <uapi/linux/ptrace.h> 102 103typedef struct ip_pid { 104 u64 ip; 105 u64 pid; 106} ip_pid_t; 107 108typedef struct hist_key { 109 ip_pid_t key; 110 u64 slot; 111} hist_key_t; 112 113BPF_HASH(start, u32); 114STORAGE 115 116int trace_func_entry(struct pt_regs *ctx) 117{ 118 u64 pid_tgid = bpf_get_current_pid_tgid(); 119 u32 pid = pid_tgid; 120 u32 tgid = pid_tgid >> 32; 121 u64 ts = bpf_ktime_get_ns(); 122 123 FILTER 124 ENTRYSTORE 125 start.update(&pid, &ts); 126 127 return 0; 128} 129 130int trace_func_return(struct pt_regs *ctx) 131{ 132 u64 *tsp, delta; 133 u64 pid_tgid = bpf_get_current_pid_tgid(); 134 u32 pid = pid_tgid; 135 u32 tgid = pid_tgid >> 32; 136 137 // calculate delta time 138 tsp = start.lookup(&pid); 139 if (tsp == 0) { 140 return 0; // missed start 141 } 142 delta = bpf_ktime_get_ns() - *tsp; 143 start.delete(&pid); 144 FACTOR 145 146 // store as histogram 147 STORE 148 149 return 0; 150} 151""" 152 153# do we need to store the IP and pid for each invocation? 154need_key = args.function or (library and not args.pid) 155 156# code substitutions 157if args.pid: 158 bpf_text = bpf_text.replace('FILTER', 159 'if (tgid != %d) { return 0; }' % args.pid) 160else: 161 bpf_text = bpf_text.replace('FILTER', '') 162if args.milliseconds: 163 bpf_text = bpf_text.replace('FACTOR', 'delta /= 1000000;') 164 label = "msecs" 165elif args.microseconds: 166 bpf_text = bpf_text.replace('FACTOR', 'delta /= 1000;') 167 label = "usecs" 168else: 169 bpf_text = bpf_text.replace('FACTOR', '') 170 label = "nsecs" 171if need_key: 172 bpf_text = bpf_text.replace('STORAGE', 'BPF_HASH(ipaddr, u32);\n' + 173 'BPF_HISTOGRAM(dist, hist_key_t);') 174 # stash the IP on entry, as on return it's kretprobe_trampoline: 175 bpf_text = bpf_text.replace('ENTRYSTORE', 176 'u64 ip = PT_REGS_IP(ctx); ipaddr.update(&pid, &ip);') 177 pid = '-1' if not library else 'tgid' 178 bpf_text = bpf_text.replace('STORE', 179 """ 180 u64 ip, *ipp = ipaddr.lookup(&pid); 181 if (ipp) { 182 ip = *ipp; 183 hist_key_t key; 184 key.key.ip = ip; 185 key.key.pid = %s; 186 key.slot = bpf_log2l(delta); 187 dist.increment(key); 188 ipaddr.delete(&pid); 189 } 190 """ % pid) 191else: 192 bpf_text = bpf_text.replace('STORAGE', 'BPF_HISTOGRAM(dist);') 193 bpf_text = bpf_text.replace('ENTRYSTORE', '') 194 bpf_text = bpf_text.replace('STORE', 195 'dist.increment(bpf_log2l(delta));') 196if args.verbose or args.ebpf: 197 print(bpf_text) 198 if args.ebpf: 199 exit() 200 201# signal handler 202def signal_ignore(signal, frame): 203 print() 204 205# load BPF program 206b = BPF(text=bpf_text) 207 208# attach probes 209if not library: 210 b.attach_kprobe(event_re=pattern, fn_name="trace_func_entry") 211 b.attach_kretprobe(event_re=pattern, fn_name="trace_func_return") 212 matched = b.num_open_kprobes() 213else: 214 b.attach_uprobe(name=library, sym_re=pattern, fn_name="trace_func_entry", 215 pid=args.pid or -1) 216 b.attach_uretprobe(name=library, sym_re=pattern, 217 fn_name="trace_func_return", pid=args.pid or -1) 218 matched = b.num_open_uprobes() 219 220if matched == 0: 221 print("0 functions matched by \"%s\". Exiting." % args.pattern) 222 exit() 223 224# header 225print("Tracing %d functions for \"%s\"... Hit Ctrl-C to end." % 226 (matched / 2, args.pattern)) 227 228# output 229def print_section(key): 230 if not library: 231 return BPF.sym(key[0], -1) 232 else: 233 return "%s [%d]" % (BPF.sym(key[0], key[1]), key[1]) 234 235exiting = 0 if args.interval else 1 236seconds = 0 237dist = b.get_table("dist") 238while (1): 239 try: 240 sleep(args.interval) 241 seconds += args.interval 242 except KeyboardInterrupt: 243 exiting = 1 244 # as cleanup can take many seconds, trap Ctrl-C: 245 signal.signal(signal.SIGINT, signal_ignore) 246 if args.duration and seconds >= args.duration: 247 exiting = 1 248 249 print() 250 if args.timestamp: 251 print("%-8s\n" % strftime("%H:%M:%S"), end="") 252 253 if need_key: 254 dist.print_log2_hist(label, "Function", section_print_fn=print_section, 255 bucket_fn=lambda k: (k.ip, k.pid)) 256 else: 257 dist.print_log2_hist(label) 258 dist.clear() 259 260 if exiting: 261 print("Detaching...") 262 exit() 263