1256 lines
40 KiB
Python
Executable File
1256 lines
40 KiB
Python
Executable File
#!/usr/bin/env python
|
|
|
|
"""
|
|
Copyright (C) 2012 Ezequiel Garcia <elezegarcia@gmail.com>
|
|
Licensed under the terms of the GNU GPL License version 2
|
|
|
|
trace_analize.py
|
|
----------------
|
|
|
|
0. Introduction
|
|
---------------
|
|
|
|
This script allows to perform some analysis on kernel dynamic memory
|
|
allocations by post-processing ftrace kmem event.
|
|
In addition, it can also report on static footprint on a built kernel tree.
|
|
|
|
trace_analyze.py typically needs access to:
|
|
1) a built kernel tree and, 2) an ftrace kmem log.
|
|
|
|
Since reading the kmem event log is a costly operation,
|
|
you can also generate a 'db' file to speed-up subsequent runs of the script.
|
|
|
|
This script and work related has been done thanks to the CEWG project
|
|
"Kernel dynamic memory allocation tracking and reduction"
|
|
You can find lot more information about this script and on kernel dynamic
|
|
memory tracking here:
|
|
|
|
http://elinux.org/Kernel_dynamic_memory_analysis
|
|
|
|
Disclaimer:
|
|
trace_analyze.py is not stable, so expect some roughness.
|
|
Testing and feedback is more than welcome.
|
|
In fact, even some flames are welcome.
|
|
|
|
1. Using trace_analyze.py for static analysis
|
|
---------------------------------------------
|
|
|
|
Usage is fairly simple
|
|
|
|
$ ./trace_analyze.py -k /usr/src/linux -r foo.png
|
|
$ ./trace_analyze.py --kernel /usr/src/linux --rings-file foo.png
|
|
|
|
This should produce a ringchart png file in the current directory.
|
|
Of course, you can use absolute and relative paths in the path parameter
|
|
|
|
$ ./trace_analyze.py -k ../../torvalds -r foo.png
|
|
|
|
If you're interested in a specific subsystem you can use a parameter to specify
|
|
the directory tree branch to take as root
|
|
|
|
$ ./trace_analyze -k linux --start-branch fs/ext2 -r ext2.png
|
|
$ ./trace_analyze -k linux -b drivers -r drivers.png
|
|
$ ./trace_analyze -k linux -b mm -r mm.png
|
|
|
|
Each of this commands will produce a ringchart png file in the
|
|
curent directory, named as specified.
|
|
|
|
What's under the hood?
|
|
The script will perform a directory walk, internally creating a tree matching
|
|
the provided kernel tree. On each object file found (like fs/inode.o) it will
|
|
perform a 'readelf --syms' to get a list of symbols contained in it. Nothing fancy.
|
|
|
|
2. Using trace_analyze.py for dynamic analysis
|
|
----------------------------------------------
|
|
|
|
2.1. Producing a kmem trace log file
|
|
|
|
In case you don't know or don't remember how to use ftrace to
|
|
produce kmem events, here's a little remainder.
|
|
For more information, please refer to the canonical
|
|
trace documentation at the linux tree:
|
|
|
|
- Documentation/trace/ftrace.txt
|
|
- Documentation/trace/tracepoint-analysis.txt
|
|
- and everything else inside Documentation/trace/
|
|
|
|
The purpose of trace_analyze script is to perform dynamic memory analysis.
|
|
For this to work you need feed it with a kmem trace log file
|
|
(of course, you also need to give hime a built kernel tree).
|
|
|
|
Such log must be produced on the running target kernel,
|
|
but you can post-process it off-box.
|
|
For instance, you boot your kernel with kmem parameters
|
|
to enable ftrace kmem events:
|
|
(it's recommended to enable all events, despite not running a NUMA machine).
|
|
|
|
kmem="kmem:kmalloc,kmem:kmalloc_node,kmem:kfree, \
|
|
kmem:kmem_cache_alloc,kmem:kmem_cache_alloc_node,kmem:kmem_cache_free"
|
|
|
|
This parameter will have linux to start tracing as soon as possible.
|
|
Of course some early traces will be lost, see below.
|
|
|
|
(on your target kernel)
|
|
|
|
# To stop tracing
|
|
$ echo "0" > /sys/kernel/debug/tracing/tracing_on
|
|
# Dump
|
|
$ cat /sys/kernel/debug/tracing/trace > kmem.log
|
|
|
|
Now you need to get this file so you can post-process
|
|
it using trace_analyze.py.
|
|
In my case, I use qemu with a file backing serial device,
|
|
so I simply do:
|
|
|
|
(on your target kernel)
|
|
|
|
$ cat /sys/kernel/debug/tracing/trace > /dev/ttyS0
|
|
|
|
And I get the log on qemu's backing file.
|
|
|
|
Now you have everything you need to start the analysis.
|
|
|
|
2.2. Slab accounting file output
|
|
|
|
To obtain a memory accounting file you need to use
|
|
--acount-file (-c) parameter, like this:
|
|
|
|
$ ./trace_analyze.py -k linux -f kmem.log --account-file account.txt
|
|
$ ./trace_analyze.py -k linux -f kmem.log -c account.txt
|
|
|
|
This will produce an account file like this:
|
|
|
|
current bytes allocated: 669696
|
|
current bytes requested: 618823
|
|
current wasted bytes: 50873
|
|
number of allocs: 7649
|
|
number of frees: 2563
|
|
number of callers: 115
|
|
|
|
total waste net alloc/free caller
|
|
---------------------------------------------
|
|
299200 0 298928 1100/1 alloc_inode+0x4fL
|
|
189824 0 140544 1483/385 __d_alloc+0x22L
|
|
51904 0 47552 811/68 sysfs_new_dirent+0x4eL
|
|
16384 8088 16384 1/0 __seq_open_private+0x24L
|
|
15936 1328 15936 83/0 device_create_vargs+0x42L
|
|
14720 10898 14016 460/22 sysfs_new_dirent+0x29L
|
|
|
|
2.3. Controlling account output
|
|
|
|
You can ask the script to read only kmalloc events
|
|
(notice the option name is *--malloc*):
|
|
|
|
$ ./trace_analyze.py -k linux -f kmem.log -c account.txt --malloc
|
|
|
|
Or you can ask the script to read only kmem_cache events:
|
|
|
|
$ ./trace_analyze.py -k linux -f kmem.log -c account.txt --cache
|
|
|
|
If you want to order the account file you can use --order-by (-o):
|
|
|
|
$ ./trace_analyze.py -k linux -f kmem.log -c account.txt --order-by=waste
|
|
$ ./trace_analyze.py -k linux -f kmem.log -c account.txt --malloc -o waste
|
|
The possible options for order-by parameter are:
|
|
|
|
* total_dynamic: Added allocations size
|
|
* current_dynamic: Currently allocated size
|
|
* alloc_count: Number of allocations
|
|
* free_count: Number of frees
|
|
* waste: Currently wasted size
|
|
|
|
You can pick a directory to get an account file showing
|
|
only the allocations from that directory.
|
|
This is done with the --start-branch (-b) option,
|
|
just like we've done for the static analysis:
|
|
|
|
$ ./trace_analyze.py -k linux -f kmem.log -c account.txt -b drivers/base/
|
|
|
|
All of these options can be combined.
|
|
For instance, if you want to get kmalloc events only,
|
|
coming from fs/ directory and ordered by current dynamic footprint:
|
|
|
|
$ ./trace_analyze.py -k linux -f kmem.log -b fs -c account.txt -o current_dynamic --malloc
|
|
|
|
2.4. Producing a pretty ringchart for dynamic allocations
|
|
|
|
As already explained in the static analysis section, it's possible to produce
|
|
a ringchart to get **the big picture** of dynamic allocations.
|
|
You will need to have *matplotlib* installed, which should be as easy as:
|
|
|
|
$ {your_pkg_manager} install matplotlib
|
|
|
|
The script usage is very simple,
|
|
just pass the parameter --rings-file (-r) along with a filename
|
|
|
|
$ ./trace_analyze.py -k linux -f kmem.log --rings-file=dynamic.png
|
|
|
|
This command will produce a png file named as specified.
|
|
The plot will show current dynamic allocations by default.
|
|
You can control the used attrbute used for the ringchart
|
|
plot using --rings-attr (-a) parameter.
|
|
|
|
The available options are:
|
|
|
|
- current: static + current dynamic size
|
|
- static: static size
|
|
- waste: wasted size
|
|
- current_dynamic: current dynamic size
|
|
- total_dyamic: added dynamic size
|
|
|
|
For instance, you may want a ringchart for wasted bytes
|
|
|
|
$ ./trace_analyze.py -k linux -f kmem.log -r -a waste
|
|
|
|
You can use --start-branch (-b) parameter to plot allocations made from just one directory.
|
|
For instance, if you want to get wasted bytes for ext4 filesystem:
|
|
|
|
$ ./trace_analyze.py -k ../torvalds -f kmem.log \
|
|
-r ext4_waste.png -a waste -b fs/ext4
|
|
|
|
Or, if you want to see static footprint of arch-dependent mm code:
|
|
|
|
$ ./trace_analyze.py -k ../torvalds -f kmem.log \
|
|
-r x86_static.png -a static -b arch/x86/mm
|
|
|
|
Also, you can filter kmalloc or kmem_cache traces
|
|
using either --malloc, or --cache:
|
|
|
|
$ ./trace_analyze.py -k linux/ -f boot_kmem.log -r kmallocs.png --malloc
|
|
|
|
2.5. Pitfall: wrongly reported allocation (and how to fix it)
|
|
|
|
There are a number of functions (kstrdup, kmemdup, krealloc, etc) that do
|
|
some kind of allocation on behalf of its caller.
|
|
|
|
Of course, we don't want to get trace reports from these functions,
|
|
but rather from its caller. To acomplish this, we must use a variant
|
|
of kmalloc, called kmalloc_track_caller, which does exactly that.
|
|
|
|
Let's see an example. As of today kvasprintf() implementation looks
|
|
like this
|
|
|
|
(see lib/kasprintf.c:14)
|
|
char *kvasprintf(gfp_t gfp, const char *fmt, va_list ap)
|
|
{
|
|
/* code removed */
|
|
p = kmalloc(len+1, gfp);
|
|
|
|
And trace_analyze produces the account file
|
|
|
|
total waste net alloc/free caller
|
|
---------------------------------------------
|
|
2161 1184 2161 148/0 kvasprintf
|
|
|
|
The source of this 148 allocations may be a single caller,
|
|
or it may be multiple callers. We just can't know.
|
|
However, if we replace kmalloc with kmalloc_track_caller,
|
|
we're going to find that out.
|
|
|
|
char *kvasprintf(gfp_t gfp, const char *fmt, va_list ap)
|
|
{
|
|
/* code removed */
|
|
p = kmalloc_track_caller(len+1, gfp);
|
|
|
|
After running the re-built kernel, and comparing both current
|
|
and previous account files, we find this is the real caller:
|
|
|
|
total waste net alloc/free caller
|
|
---------------------------------------------
|
|
2161 1184 2161 148/0 kobject_set_name_vargs
|
|
|
|
So, we've accurately tracked this allocation down to the kobject code.
|
|
|
|
3. Using a DB file to speed-up multiple runs
|
|
--------------------------------------------
|
|
|
|
You may find yourself analyzing a large kmem log file.
|
|
Probably, you want to run the script
|
|
several times to get different kinds of results.
|
|
|
|
The script is not very clever and will re-read the
|
|
long kmem file on each run.
|
|
To alleviate this problem you can have trace_analyze.py
|
|
create a so-called DB file,and use this file instead
|
|
of the kmem log file on subsequent runs.
|
|
|
|
This is done using the --save-db and --db-file parameters.
|
|
Like this:
|
|
|
|
$ ./trace_analyze.py -k ../torvalds/ -f kmem.log --save-db db
|
|
|
|
Notice you should create the DB file without any filters,
|
|
like --malloc or --start-branch, in order to save the full kmem event log.
|
|
|
|
Once you have the **db** file created, you would use it on each run
|
|
|
|
$ ./trace_analyze.py -k ../torvalds/ --db-file db \
|
|
-r rings.png -c account.txt
|
|
|
|
Hopefully, this would prevent you from cursing trace_analyze for being so slow.
|
|
|
|
"""
|
|
|
|
import sys
|
|
import string
|
|
import re
|
|
import subprocess
|
|
import math
|
|
import pickle
|
|
import os
|
|
from optparse import OptionParser
|
|
|
|
# Skip this directories when walking kernel build
|
|
BLACKLIST = ("scripts", "tools")
|
|
|
|
class Ptr:
|
|
def __init__(self, fun, ptr, alloc, req):
|
|
self.fun = fun
|
|
self.ptr = ptr
|
|
self.alloc = alloc
|
|
self.req = req
|
|
|
|
|
|
class Callsite:
|
|
def __init__(self):
|
|
self.__alloc = 0
|
|
self.__req = 0
|
|
self.__alloc_count = 0
|
|
self.__free_count = 0
|
|
self.ptrs = []
|
|
|
|
def total_dynamic(self):
|
|
return self.__alloc
|
|
|
|
def alloc_count(self):
|
|
return self.__alloc_count
|
|
|
|
def free_count(self):
|
|
return self.__free_count
|
|
|
|
def current_dynamic(self):
|
|
alloc = 0
|
|
for ptr in self.ptrs:
|
|
alloc += ptr.alloc
|
|
return alloc
|
|
|
|
def current_req(self):
|
|
req = 0
|
|
for ptr in self.ptrs:
|
|
req += ptr.req
|
|
return req
|
|
|
|
def waste(self):
|
|
return self.current_dynamic() - self.current_req()
|
|
|
|
def do_alloc(self, alloc, req, ptr):
|
|
self.__alloc += alloc
|
|
self.__req += req
|
|
self.__alloc_count += 1
|
|
self.ptrs.append(ptr)
|
|
|
|
def do_free(self, ptr):
|
|
self.__free_count += 1
|
|
self.ptrs.remove(ptr)
|
|
|
|
|
|
# Based on addr2sym.py
|
|
class SymbolMap:
|
|
def __init__(self, filemap):
|
|
self.fmap = {}
|
|
self.flist = []
|
|
self.cache = {}
|
|
|
|
try:
|
|
f = open(filemap)
|
|
except:
|
|
print("[ERROR] Cannot read symbol map file {}".format(filemap))
|
|
sys.exit(1)
|
|
|
|
for line in f.readlines():
|
|
(addr_str, symtype, name) = string.split(line, None, 3)
|
|
self.fmap[addr_str] = name
|
|
addr = eval("0x" + addr_str + "L")
|
|
self.flist.append((addr, name))
|
|
|
|
f.close()
|
|
|
|
def lookup(self, addr_str):
|
|
|
|
# return a tuple (string, offset) for a given address
|
|
if addr_str in self.fmap:
|
|
return (self.fmap[addr_str],0)
|
|
|
|
# convert address from string to number
|
|
addr = eval("0x" + addr_str + "L")
|
|
if addr in self.cache:
|
|
return self.cache[addr]
|
|
|
|
# if address is outside range of addresses in the
|
|
# map file, just return the address without converting it
|
|
if addr < self.flist[0][0] or addr > self.flist[-1][0]:
|
|
return (addr_str,0)
|
|
|
|
# no exact match found, now do binary search for closest function
|
|
# do a binary search in funclist for the function
|
|
# use a collapsing range to find the closest addr
|
|
lower = 0
|
|
upper = len(self.flist)-1
|
|
while (lower != upper-1):
|
|
guess_index = lower + (upper-lower)/2
|
|
guess_addr = self.flist[guess_index][0]
|
|
if addr < guess_addr:
|
|
upper = guess_index
|
|
if addr >= guess_addr:
|
|
lower = guess_index
|
|
|
|
offset = addr-self.flist[lower][0]
|
|
name = self.flist[lower][1]
|
|
if name.startswith("."):
|
|
name = name[1:]
|
|
self.cache[addr] = (name, offset)
|
|
return (name, offset)
|
|
|
|
|
|
class EventDB:
|
|
def __init__(self):
|
|
self.f = {}
|
|
self.p = {}
|
|
self.num_allocs = 0
|
|
self.total_dynamic = 0
|
|
self.total_req = 0
|
|
self.num_frees = 0
|
|
self.num_lost_frees = 0
|
|
|
|
def slurp(self, path, buildpath, do_malloc, do_cache):
|
|
print("Reading symbol map at {}".format(buildpath))
|
|
sym = SymbolMap(buildpath + "/System.map")
|
|
|
|
try:
|
|
logfile = open(path)
|
|
except:
|
|
print("[ERROR] Cannot read log file {}".format(path))
|
|
sys.exit(1)
|
|
|
|
kmalloc_re = r".*kmalloc.*call_site=([a-f0-9]+).*ptr=([a-f0-9]+).*bytes_req=([0-9]+)\s*bytes_alloc=([0-9]+)"
|
|
kfree_re = r".*kfree.*call_site=[a-f0-9+]+.*ptr=([a-f0-9]+)"
|
|
cache_alloc_re = r".*cache_alloc.*call_site=([a-f0-9]+).*ptr=([a-f0-9]+).*bytes_req=([0-9]+)\s*bytes_alloc=([0-9]+)"
|
|
cache_free_re = r".*cache_free.*call_site=[a-f0-9+]+.*ptr=([a-f0-9]+)"
|
|
both_alloc_re = r".*k.*alloc.*call_site=([a-f0-9]+).*ptr=([a-f0-9]+).*bytes_req=([0-9]+)\s*bytes_alloc=([0-9]+)"
|
|
both_free_re = r".*k.*free.*call_site=[a-f0-9+]+.*ptr=([a-f0-9]+)"
|
|
|
|
if do_malloc is True and do_cache is None:
|
|
print("Slurping event log, kmalloc events only")
|
|
alloc_re = kmalloc_re
|
|
free_re = kfree_re
|
|
elif do_malloc is None and do_cache is True:
|
|
print("Slurping event log, kmem_cache events only")
|
|
alloc_re = cache_alloc_re
|
|
free_re = cache_free_re
|
|
else:
|
|
print("Slurping event log")
|
|
alloc_re = both_alloc_re
|
|
free_re = both_free_re
|
|
|
|
for line in logfile:
|
|
m = re.match(alloc_re, line)
|
|
if m:
|
|
(fun, offset) = sym.lookup(m.group(1))
|
|
self.add_malloc("{}+0x{:x}".format(fun, offset),
|
|
m.group(2),
|
|
int(m.group(3)),
|
|
int(m.group(4)), line)
|
|
|
|
m = re.match(free_re, line)
|
|
if m:
|
|
self.add_free(m.group(1))
|
|
|
|
def get_bytes(self):
|
|
alloc = 0
|
|
req = 0
|
|
for fun, callsite in self.f.items():
|
|
alloc += callsite.current_dynamic()
|
|
req += callsite.current_req()
|
|
return (alloc, req)
|
|
|
|
def add_malloc(self, fun, ptr, req, alloc, line):
|
|
self.num_allocs += 1
|
|
self.total_dynamic += alloc
|
|
self.total_req += req
|
|
|
|
ptr_obj = Ptr(fun, ptr, alloc, req)
|
|
|
|
if ptr in self.p:
|
|
print("[WARNING] Duplicate pointer! {}".format(line))
|
|
|
|
self.p[ptr] = ptr_obj
|
|
|
|
if not fun in self.f:
|
|
self.f[fun] = Callsite()
|
|
|
|
self.f[fun].do_alloc(alloc, req, ptr_obj)
|
|
|
|
def add_free(self, ptr):
|
|
self.num_frees += 1
|
|
|
|
if not ptr in self.p:
|
|
self.num_lost_frees += 1
|
|
return
|
|
|
|
ptr_obj = self.p[ptr]
|
|
|
|
self.f[ptr_obj.fun].do_free(ptr_obj)
|
|
|
|
# Remove it from pointers dictionary
|
|
del self.p[ptr]
|
|
|
|
def print_callers(self, filepath, filter_tree=None):
|
|
|
|
if filter_tree is None:
|
|
filter_symbol = lambda f: True
|
|
get_symbol_dir = lambda f: ""
|
|
else:
|
|
filter_symbol = filter_tree.symbol_is_here
|
|
get_symbol_dir = filter_tree.get_symbol_dir
|
|
|
|
syms = [(f,c) for f,c in self.f.items() if filter_symbol(f)]
|
|
|
|
f = open(filepath, 'w')
|
|
|
|
for name, c in syms:
|
|
|
|
symdir = get_symbol_dir(name)
|
|
f.write("{:<60} {:<8} {:<8} {:<8}\n".format(name,
|
|
c.current_dynamic(),
|
|
c.waste(),
|
|
symdir))
|
|
|
|
f.close()
|
|
|
|
def print_account(self, filepath, order_by, filter_tree=None):
|
|
|
|
current_dynamic = 0
|
|
current_req = 0
|
|
alloc_count = 0
|
|
free_count = 0
|
|
|
|
if filter_tree is None:
|
|
filter_symbol = lambda f: True
|
|
else:
|
|
filter_symbol = filter_tree.symbol_is_here
|
|
|
|
syms = [(f,c) for f,c in self.f.items() if filter_symbol(f)]
|
|
|
|
f = open(filepath, 'w')
|
|
|
|
for fun, callsite in syms:
|
|
current_dynamic += callsite.current_dynamic()
|
|
current_req += callsite.current_req()
|
|
alloc_count += callsite.alloc_count()
|
|
free_count += callsite.free_count()
|
|
|
|
f.write("current bytes allocated: {:>10}\n".format(current_dynamic))
|
|
f.write("current bytes requested: {:>10}\n".format(current_req))
|
|
f.write("current wasted bytes: {:>10}\n".format((current_dynamic -
|
|
current_req)))
|
|
f.write("number of allocs: {:>10}\n".format(alloc_count))
|
|
f.write("number of frees: {:>10}\n".format(free_count))
|
|
f.write("number of callers: {:>10}\n".format(len(syms)))
|
|
f.write("\n")
|
|
f.write(" total waste net alloc/free caller\n")
|
|
f.write("---------------------------------------------\n")
|
|
|
|
for fun, callsite in sorted(syms,
|
|
key=lambda item: getattr(item[1],
|
|
order_by)(),
|
|
reverse=True):
|
|
|
|
f.write("%8d %8d %8d %5d/%-5d %s\n" % (callsite.total_dynamic(),
|
|
callsite.waste(),
|
|
callsite.current_dynamic(),
|
|
callsite.alloc_count(),
|
|
callsite.free_count(),
|
|
fun))
|
|
|
|
f.close()
|
|
|
|
|
|
class MemTreeNodeSize:
|
|
def __init__(self, node):
|
|
self.__static = 0
|
|
self.__total_dynamic = 0
|
|
self.__current_dynamic = 0
|
|
self.__waste = 0
|
|
|
|
# First for my symbols
|
|
for sym, size in node.data.items():
|
|
self.__static += size
|
|
for sym, size in node.text.items():
|
|
self.__static += size
|
|
for sym, call in node.funcs.items():
|
|
self.__total_dynamic += call.total_dynamic()
|
|
self.__current_dynamic += call.current_dynamic()
|
|
self.__waste += call.current_dynamic() - call.current_req()
|
|
|
|
# Now, for my children's symbols.
|
|
# Or, instead, we could first add all my children's
|
|
# symbols here and then get the node size.
|
|
for name, child in node.childs.items():
|
|
self.__total_dynamic += child.size().total_dynamic()
|
|
self.__current_dynamic += child.size().current_dynamic()
|
|
self.__static += child.size().static()
|
|
self.__waste += child.size().waste()
|
|
|
|
def current(self):
|
|
return self.__static + self.__current_dynamic
|
|
|
|
def waste(self):
|
|
return self.__waste
|
|
|
|
def static(self):
|
|
return self.__static
|
|
|
|
def current_dynamic(self):
|
|
return self.__current_dynamic
|
|
|
|
def total_dynamic(self):
|
|
return self.__total_dynamic
|
|
|
|
|
|
class MemTreeNode:
|
|
def __init__(self, name="", parent=None, db=None):
|
|
self.name = name
|
|
self.parent = parent
|
|
self.childs = {}
|
|
self.funcs = {}
|
|
self.data = {}
|
|
self.text = {}
|
|
self.node_size = None
|
|
self.fill = getattr(self, "fill_per_file")
|
|
|
|
# If db is None, use parent db
|
|
if db is None:
|
|
if parent is not None:
|
|
self.db = parent.db
|
|
else:
|
|
self.db = db
|
|
|
|
def get_symbol_dir(self, symbol):
|
|
if symbol in self.funcs:
|
|
return self.full_name()
|
|
else:
|
|
for name, child in self.childs.items():
|
|
symdir = child.get_symbol_dir(symbol)
|
|
if symdir is not None:
|
|
return symdir
|
|
return None
|
|
|
|
def symbol_is_here(self, symbol):
|
|
if symbol in self.funcs:
|
|
return True
|
|
else:
|
|
for name, child in self.childs.items():
|
|
if child.symbol_is_here(symbol):
|
|
return True
|
|
return False
|
|
|
|
def full_name(self):
|
|
l = [self.name,]
|
|
parent = self.parent
|
|
while parent:
|
|
if parent.name != "":
|
|
l.append(parent.name)
|
|
parent = parent.parent
|
|
|
|
return "/".join(reversed(l))
|
|
|
|
def size(self):
|
|
if self.node_size is None:
|
|
self.node_size = MemTreeNodeSize(self)
|
|
return self.node_size
|
|
|
|
def __collapse(self):
|
|
# Collapse one-child empty nodes
|
|
for name, child in self.childs.items():
|
|
if len(child.childs) > 2:
|
|
child.__collapse()
|
|
|
|
if len(child.childs) == 1 and not child.funcs and not child.data:
|
|
# Remove from child
|
|
(k, v) = child.childs.items()[0]
|
|
del child.childs[k]
|
|
|
|
# Add here
|
|
self.childs[k] = v
|
|
v.parent = self
|
|
|
|
def __strip(self):
|
|
# Remove empty nodes
|
|
for name, child in self.childs.items():
|
|
if child.childs:
|
|
child.__strip()
|
|
if not child.funcs and not child.data and not child.childs:
|
|
del self.childs[name]
|
|
|
|
def __get_root(self):
|
|
if len(self.childs) == 1:
|
|
child = self.childs.itervalues().next()
|
|
# This is a pedantic test, the first node with
|
|
# multiple childs is the root we're searching
|
|
if not child.name.endswith(".o"):
|
|
return child.__get_root()
|
|
|
|
return self
|
|
|
|
# Obtain a clean tree.
|
|
# We do it this way because collapse() and strip() must be called
|
|
# in an ordered fashion.
|
|
def get_clean(self):
|
|
self.__collapse()
|
|
self.__strip()
|
|
return self.__get_root()
|
|
|
|
def find_first_branch(self, which):
|
|
if self.name == which:
|
|
return self
|
|
|
|
for name, node in self.childs.items():
|
|
if which == name:
|
|
return node
|
|
|
|
for name, node in self.childs.items():
|
|
return node.find_first_branch(which)
|
|
|
|
print("[WARNING] Can't find first branch '{}'".format(which))
|
|
return None
|
|
|
|
# This are for debug purposes, move along
|
|
def treelike(self, level=0, attr="current_dynamic"):
|
|
str = ""
|
|
str += "{}\n".format(self.name)
|
|
for name, node in self.childs.items():
|
|
child_str = node.treelike(level+1, attr)
|
|
if child_str:
|
|
str += "{}{}".format(" "*(level+1), child_str)
|
|
return str
|
|
|
|
def treelike2(self, level=0, attr="current_dynamic"):
|
|
str = ""
|
|
|
|
attr_val = getattr(self.size(), attr)()
|
|
|
|
if self.name and attr_val != 0:
|
|
str += "{} - {}={}\n".format(self.name, attr, attr_val)
|
|
|
|
for name, node in self.childs.items():
|
|
child_str = node.treelike2(level+1, attr)
|
|
if child_str:
|
|
str += "{}{}".format(" "*(level+1), child_str)
|
|
return str
|
|
|
|
def fill_per_file(self, path):
|
|
|
|
filepath = "{}{}/{}".format(MemTreeNode.abs_slash, self.full_name(), path)
|
|
|
|
if path not in self.childs:
|
|
self.childs[path] = MemTreeNode(path, self)
|
|
|
|
child = self.childs[path]
|
|
|
|
output = []
|
|
try:
|
|
p1 = subprocess.Popen(["readelf", "--wide", "-s", filepath], stdout=subprocess.PIPE)
|
|
output = p1.communicate()[0].split("\n")
|
|
except:
|
|
pass
|
|
|
|
for line in output:
|
|
if line == '':
|
|
continue
|
|
|
|
m = re.match(r".*\s([0-9]+)\sFUNC.*\s+([a-zA-Z0-9_\.]+)\b", line)
|
|
if m:
|
|
if m.group(2) in child.text:
|
|
print("Duplicate text entry! {}".format(m.group(2)))
|
|
child.text[m.group(2)] = int(m.group(1))
|
|
|
|
# Search every callsite in db matching this name
|
|
for name, callsite in child.db.f.iteritems():
|
|
if name.startswith(m.group(2)):
|
|
child.funcs[name] = callsite
|
|
|
|
m = re.match(r".*\s([0-9]+)\sOBJECT.*\s+([a-zA-Z0-9_\.]+)\b", line)
|
|
if m:
|
|
if m.group(2) in child.data:
|
|
print("[WARNING] Duplicate data entry! {}".format(m.group(2)))
|
|
child.data[m.group(2)] = int(m.group(1))
|
|
|
|
# This is deprecated, fill_per_file should be used instead.
|
|
# I keep it here just to have the code handy.
|
|
def fill_per_dir(self, path):
|
|
|
|
if self.funcs or self.data:
|
|
print("[WARNING] Oooops, already filled")
|
|
|
|
filepath = "." + self.full_name() + "/built-in.o"
|
|
|
|
output = []
|
|
try:
|
|
p1 = subprocess.Popen(["readelf", "--wide", "-s", filepath], stdout=subprocess.PIPE)
|
|
output = p1.communicate()[0].split("\n")
|
|
except:
|
|
pass
|
|
|
|
for line in output:
|
|
if line == '':
|
|
continue
|
|
m = re.match(r".*FUNC.*\b([a-zA-Z0-9_]+)\b", line)
|
|
if m:
|
|
if m.group(1) in self.funcs:
|
|
print("[WARNING] Duplicate entry! {}".format(m.group(1)))
|
|
|
|
if m.group(1) in self.db.f:
|
|
self.funcs[m.group(1)] = self.db.f[m.group(1)]
|
|
|
|
m = re.match(r".*([0-9]+)\sOBJECT.*\b([a-zA-Z0-9_]+)\b", line)
|
|
if m:
|
|
self.data[m.group(2)] = int(m.group(1))
|
|
|
|
# path is should be an object file, like fs/ext2/inode.o
|
|
def add_child(self, path):
|
|
# adding a child invalidates node_size object
|
|
self.node_size = None
|
|
|
|
parts = path.split('/', 1)
|
|
if len(parts) == 1:
|
|
self.fill(path)
|
|
pass
|
|
else:
|
|
node, others = parts
|
|
if node not in self.childs:
|
|
self.childs[node] = MemTreeNode(node, self)
|
|
self.childs[node].add_child(others)
|
|
|
|
def add_path(self, path):
|
|
for root, dirs, files in os.walk(path):
|
|
|
|
blacklisted = False
|
|
for bdir in BLACKLIST:
|
|
if root.startswith("{}/{}".format(path, bdir)):
|
|
blacklisted = True
|
|
|
|
if blacklisted:
|
|
continue
|
|
|
|
for filepath in [os.path.join(root,f) for f in files]:
|
|
if filepath.endswith("built-in.o"):
|
|
continue
|
|
if filepath.endswith("vmlinux.o"):
|
|
continue
|
|
if filepath.endswith(".o"):
|
|
# We need to check if this object file,
|
|
# has a corresponding source file
|
|
filesrc = "{}.c".format(os.path.splitext(filepath)[0])
|
|
if os.path.exists(filesrc):
|
|
self.add_child(filepath)
|
|
|
|
|
|
|
|
##########################################################################
|
|
##
|
|
## Main
|
|
##
|
|
##########################################################################
|
|
|
|
def main():
|
|
|
|
parser = OptionParser()
|
|
parser.add_option("-k", "--kernel",
|
|
dest="buildpath",
|
|
default=".",
|
|
help="path to built kernel tree (default is current dir)")
|
|
|
|
parser.add_option("-f", "--file",
|
|
dest="file",
|
|
default="",
|
|
help="trace log file to analyze")
|
|
|
|
parser.add_option("--db-file",
|
|
dest="db_file",
|
|
default="",
|
|
help="use db_file as DB instead of creating one")
|
|
|
|
parser.add_option("--save-db",
|
|
dest="save_db_file",
|
|
default="",
|
|
help="save a db_file to use as DB")
|
|
|
|
parser.add_option("-b", "--start-branch",
|
|
dest="start_branch",
|
|
default="",
|
|
help="first directory name to use as ringchart root")
|
|
|
|
parser.add_option("-r", "--rings-file",
|
|
dest="rings_file",
|
|
default="",
|
|
help="plot ringchart information")
|
|
|
|
parser.add_option("-i", "--rings-show",
|
|
dest="rings_show",
|
|
action="store_true",
|
|
help="show interactive ringchart")
|
|
|
|
parser.add_option("-a", "--rings-attr",
|
|
dest="rings_attr",
|
|
default="current_dynamic",
|
|
help="attribute to visualize [static, current, \
|
|
current_dynamic, total_dynamic, waste]")
|
|
|
|
parser.add_option("--malloc",
|
|
dest="do_malloc",
|
|
action="store_true",
|
|
help="trace kmalloc/kfree only")
|
|
|
|
parser.add_option("--cache",
|
|
dest="do_cache",
|
|
action="store_true",
|
|
help="trace kmem_cache_alloc/kmem_cache_free only")
|
|
|
|
parser.add_option("-c", "--account-file",
|
|
dest="account_file",
|
|
default="",
|
|
help="show output matching slab_account output")
|
|
|
|
parser.add_option("-l", "--callers-file",
|
|
dest="callers_file",
|
|
default="",
|
|
help="show callers file suitable for ringchart generation")
|
|
|
|
parser.add_option("-o", "--order-by",
|
|
dest="order_by",
|
|
default="current_dynamic",
|
|
help="attribute to order account \
|
|
[current_dynamic, total_dynamic, alloc_count, free_count, waste]")
|
|
|
|
|
|
(opts, args) = parser.parse_args()
|
|
|
|
# Kernel build path is a mandatory parameter.
|
|
# We need to look at compiled objects and also for System.map.
|
|
if len(opts.db_file) == 0 and len(opts.buildpath) == 0:
|
|
print("Please set a kernel build path or a DB file!")
|
|
parser.print_help()
|
|
return
|
|
|
|
# Check valid options
|
|
if len(opts.order_by) > 0:
|
|
if opts.order_by not in dir(Callsite):
|
|
print("Hey! {} is not a valid --order-by option".format(opts.order_by))
|
|
parser.print_help()
|
|
return
|
|
|
|
if len(opts.rings_attr) > 0:
|
|
if opts.rings_attr not in dir(MemTreeNodeSize):
|
|
print("Hey! {} is not a valid --rings-attr option".format(opts.rings_attr))
|
|
parser.print_help()
|
|
return
|
|
|
|
# Clean user provided kernel path from dirty slashes
|
|
buildpath = opts.buildpath.rstrip("/")
|
|
|
|
# If we don't have a trace log file,
|
|
# and we don't have a DB file
|
|
# then we'll fallback to static report mode.
|
|
if len(opts.db_file) == 0 and len(opts.file) == 0:
|
|
print("No trace log file or DB file specified: will report on static size only")
|
|
opts.rings_attr = "static"
|
|
opts.do_malloc = False
|
|
opts.do_cache = False
|
|
opts.account_file = ""
|
|
opts.just_static = True
|
|
# Set some default
|
|
if len(opts.rings_file) == 0:
|
|
opts.rings_file = "rings_static.png"
|
|
else:
|
|
opts.just_static = False
|
|
|
|
if opts.rings_show is None:
|
|
opts.rings_show = False
|
|
|
|
rootDB = EventDB()
|
|
# Get root database, if need to
|
|
if not opts.just_static:
|
|
if len(opts.db_file) != 0:
|
|
print("Using db file '{}'".format(opts.db_file))
|
|
f = open(opts.db_file)
|
|
buildpath = pickle.load(f)
|
|
rootDB = pickle.load(f)
|
|
f.close()
|
|
else:
|
|
rootDB.slurp(opts.file, buildpath, opts.do_malloc, opts.do_cache)
|
|
|
|
if len (opts.save_db_file) != 0:
|
|
print("Saving db file at '{}'".format(opts.save_db_file))
|
|
f = open(opts.save_db_file, 'w')
|
|
pickle.dump(buildpath,f)
|
|
pickle.dump(rootDB, f)
|
|
f.close()
|
|
|
|
if len(opts.callers_file) == 0 and \
|
|
len(opts.account_file) == 0 and \
|
|
len(opts.rings_file) == 0 and \
|
|
opts.rings_show is False:
|
|
sys.exit(0)
|
|
|
|
root_path = "{}/{}".format(buildpath, opts.start_branch).rstrip("/")
|
|
|
|
print("Creating tree from compiled symbols at '{}'".format(root_path))
|
|
|
|
# We need to specify if user provided buildpath is absolute
|
|
MemTreeNode.abs_slash = buildpath.startswith("/") and "/" or ""
|
|
|
|
tree = MemTreeNode(db = rootDB)
|
|
tree.add_path(root_path)
|
|
|
|
print("Cleaning tree")
|
|
tree = tree.get_clean()
|
|
|
|
# DEBUG--ONLY. Should we add an option for this?
|
|
#print(tree.treelike2(attr = opts.rings_attr))
|
|
if len(opts.callers_file) != 0:
|
|
print("Creating callers file at '{}'".format(opts.callers_file))
|
|
rootDB.print_callers(opts.callers_file,
|
|
tree)
|
|
|
|
|
|
if len(opts.account_file) != 0:
|
|
print("Creating account file at '{}'".format(opts.account_file))
|
|
rootDB.print_account(opts.account_file,
|
|
opts.order_by,
|
|
tree)
|
|
|
|
if len(opts.rings_file) != 0:
|
|
if tree is None:
|
|
print("Sorry, there is nothing to plot for branch '{}'".format(opts.start_branch))
|
|
else:
|
|
print("Creating ringchart for attribute '{}'".format(opts.rings_attr))
|
|
visualize_mem_tree(tree, opts.rings_attr, opts.rings_file, opts.rings_show)
|
|
|
|
|
|
##########################################################################
|
|
##
|
|
## Visualization stuff
|
|
##
|
|
##########################################################################
|
|
|
|
|
|
CENTER_X = 1.0
|
|
CENTER_Y = 1.0
|
|
WIDTH = 0.2
|
|
tango_colors = ['#ef2929',
|
|
'#ad7fa8',
|
|
'#729fcf',
|
|
'#8ae234',
|
|
'#e9b96e',
|
|
'#fcaf3e',]
|
|
|
|
|
|
def human_bytes(bytes, precision=1):
|
|
"""Return a humanized string representation of a number of bytes.
|
|
|
|
Assumes `from __future__ import division`.
|
|
|
|
>>> humanize_bytes(1)
|
|
'1 byte'
|
|
>>> humanize_bytes(1024)
|
|
'1.0 kB'
|
|
>>> humanize_bytes(1024*123)
|
|
'123.0 kB'
|
|
>>> humanize_bytes(1024*12342)
|
|
'12.1 MB'
|
|
>>> humanize_bytes(1024*12342,2)
|
|
'12.05 MB'
|
|
>>> humanize_bytes(1024*1234,2)
|
|
'1.21 MB'
|
|
>>> humanize_bytes(1024*1234*1111,2)
|
|
'1.31 GB'
|
|
>>> humanize_bytes(1024*1234*1111,1)
|
|
'1.3 GB'
|
|
"""
|
|
abbrevs = (
|
|
(1<<50L, 'PB'),
|
|
(1<<40L, 'TB'),
|
|
(1<<30L, 'GB'),
|
|
(1<<20L, 'MB'),
|
|
(1<<10L, 'kB'),
|
|
(1, 'bytes')
|
|
)
|
|
if bytes == 1:
|
|
return '1 byte'
|
|
for factor, suffix in abbrevs:
|
|
if bytes >= factor:
|
|
break
|
|
return '{0:.{1}f} {2}'.format(float(bytes)/factor, precision, suffix)
|
|
|
|
|
|
class Section:
|
|
def __init__(self, node, size, total_size, total_angle, start_angle):
|
|
self.node = node
|
|
self.size = size
|
|
self.start_angle = start_angle
|
|
self.angle = size * total_angle / total_size
|
|
|
|
|
|
def ring_color(start_angle, level):
|
|
from matplotlib.colors import colorConverter
|
|
|
|
# f: [1 - 0.26]
|
|
# rel: [0 - 198]
|
|
# icolor: [0 - 5]
|
|
|
|
if level == 1:
|
|
return colorConverter.to_rgb('#808080')
|
|
|
|
f = 1 - (((level-1) * 0.3) / 8)
|
|
rel = start_angle / 180. * 99
|
|
icolor = int(rel / (100./3))
|
|
next_icolor = (icolor + 1) % 6
|
|
|
|
# Interpolate (?)
|
|
color = colorConverter.to_rgb(tango_colors[icolor])
|
|
next_color = colorConverter.to_rgb(tango_colors[next_icolor])
|
|
p = (rel - icolor * 100./3) / (100./3)
|
|
|
|
color = [f * (c - p * (c - n)) for c, n in zip(color, next_color)]
|
|
|
|
return color
|
|
|
|
|
|
def create_child_rings(tree, level=2, level_angle=360, start_angle=0, rings=[],
|
|
radius=WIDTH, center=(CENTER_X, CENTER_Y), size_attr="static"):
|
|
|
|
from matplotlib.patches import Wedge
|
|
|
|
child_size = 0
|
|
max_size = getattr(tree.size(), size_attr)()
|
|
|
|
if len(tree.childs) == 0:
|
|
return rings
|
|
|
|
if max_size == 0:
|
|
for name, node in tree.childs.items():
|
|
max_size += getattr(node.size(), size_attr)()
|
|
if max_size == 0:
|
|
return rings
|
|
|
|
s_angle = start_angle
|
|
sections = {}
|
|
|
|
# Create child wedges
|
|
for name, node in tree.childs.items():
|
|
|
|
size = getattr(node.size(), size_attr)()
|
|
s = Section(node, size, max_size, level_angle, s_angle)
|
|
sections[name] = s
|
|
|
|
create_child_rings(node, level+1, s.angle, s_angle, rings, radius, center, size_attr)
|
|
s_angle += s.angle
|
|
child_size += size
|
|
|
|
# Just a check
|
|
if child_size > max_size:
|
|
print("[{}] Ooops, child size is greater than max size".format(name))
|
|
|
|
for name, section in sections.items():
|
|
|
|
# Create tuple: (wedge, name)
|
|
name = "{} {}".format(name, human_bytes(section.size))
|
|
tup = ( Wedge(center,
|
|
level * radius,
|
|
section.start_angle,
|
|
section.start_angle + section.angle,
|
|
width=radius,
|
|
facecolor=ring_color(section.start_angle, level)),
|
|
name)
|
|
|
|
rings.append(tup)
|
|
|
|
return rings
|
|
|
|
|
|
def visualize_mem_tree(tree, size_attr, filename, show):
|
|
import pylab
|
|
|
|
RING_MIN_WIDTH = 1
|
|
TEXT_MIN_WIDTH = 5
|
|
|
|
rings = create_child_rings(tree, size_attr=size_attr)
|
|
|
|
fig = pylab.figure()
|
|
ax = fig.add_subplot(111)
|
|
annotations = []
|
|
labels = []
|
|
|
|
name = tree.name
|
|
if name == '.':
|
|
name = "/"
|
|
|
|
text = "{} {}".format(name,
|
|
human_bytes(getattr(tree.size(), size_attr)()))
|
|
ann = ax.annotate(text,
|
|
size=12,
|
|
bbox=dict(boxstyle="round", fc="w", ec="0.5", alpha=0.8),
|
|
xy=(CENTER_X, CENTER_Y), xycoords='data',
|
|
xytext=(CENTER_X, CENTER_Y), textcoords='data')
|
|
annotations.append(ann)
|
|
|
|
for p in rings:
|
|
wedge = p[0]
|
|
|
|
# Skip if too small
|
|
if (wedge.theta2 - wedge.theta1) < RING_MIN_WIDTH:
|
|
continue
|
|
|
|
# Add wedge
|
|
ax.add_patch(wedge)
|
|
|
|
# Skip text if too small
|
|
if (wedge.theta2 - wedge.theta1) < TEXT_MIN_WIDTH:
|
|
continue
|
|
|
|
theta = math.radians((wedge.theta1 + wedge.theta2) / 2.)
|
|
x0 = wedge.center[0] + (wedge.r - wedge.width / 2.) * math.cos(theta)
|
|
y0 = wedge.center[1] + (wedge.r - wedge.width / 2.) * math.sin(theta)
|
|
x = wedge.center[0] + (0.1 + wedge.r * 1.5 - wedge.width / 2.) * math.cos(theta)
|
|
y = wedge.center[1] + (0.1 + wedge.r * 1.5 - wedge.width / 2.) * math.sin(theta)
|
|
|
|
ax.plot(x0, y0, ".", color="black")
|
|
|
|
text = p[1]
|
|
ann = ax.annotate(text,
|
|
size=12,
|
|
bbox=dict(boxstyle="round", fc="w", ec="0.5", alpha=0.8),
|
|
xy=(x0, y0), xycoords='data',
|
|
xytext=(x, y), textcoords='data',
|
|
arrowprops=dict(arrowstyle="-", connectionstyle="angle3, angleA=0, angleB=90"),)
|
|
annotations.append(ann)
|
|
|
|
(alloc, req) = tree.db.get_bytes()
|
|
|
|
pylab.axis('off')
|
|
|
|
if len(filename) != 0:
|
|
print("Plotting to file '{}'".format(filename))
|
|
pylab.savefig("{}".format(filename),
|
|
bbox_extra_artists=annotations,
|
|
bbox_inches='tight', dpi=300)
|
|
if show:
|
|
print("Plotting interactive")
|
|
pylab.show()
|
|
|
|
|
|
##########################################################################
|
|
|
|
if __name__ == "__main__":
|
|
main()
|