diff options
author | Konsta Holtta <kholtta@nvidia.com> | 2013-08-29 21:37:38 +0300 |
---|---|---|
committer | Dan Willemsen <dwillemsen@nvidia.com> | 2013-09-26 19:46:06 -0700 |
commit | 982f617199323795a88f8939b4c3161dbb619177 (patch) | |
tree | 89922a3f48c9f0cae54d589ef8311929284b59a4 /scripts | |
parent | 0449413e6937c04fb1794bdde1b269ce2759313c (diff) |
scripts: tool for visualizing DMA mapping API ftrace dump
Add a tool for parsing and plotting data from dmadebug:* ftrace events.
Bug 1173494
Change-Id: Id7e175fda4c1ef42ef6a0403f1bece36b273cd99
Signed-off-by: Konsta Holtta <kholtta@nvidia.com>
Signed-off-by: Hiroshi Doyu <hdoyu@nvidia.com>
Reviewed-on: http://git-master/r/268051
Reviewed-by: Krishna Reddy <vdumpa@nvidia.com>
Diffstat (limited to 'scripts')
-rw-r--r-- | scripts/tracing/dma-api/.gitignore | 1 | ||||
-rw-r--r-- | scripts/tracing/dma-api/README | 36 | ||||
-rw-r--r-- | scripts/tracing/dma-api/plotting.py | 100 | ||||
-rw-r--r-- | scripts/tracing/dma-api/smmu.py | 202 | ||||
-rw-r--r-- | scripts/tracing/dma-api/trace.py | 349 |
5 files changed, 688 insertions, 0 deletions
diff --git a/scripts/tracing/dma-api/.gitignore b/scripts/tracing/dma-api/.gitignore new file mode 100644 index 000000000000..0d20b6487c61 --- /dev/null +++ b/scripts/tracing/dma-api/.gitignore @@ -0,0 +1 @@ +*.pyc diff --git a/scripts/tracing/dma-api/README b/scripts/tracing/dma-api/README new file mode 100644 index 000000000000..caeb88f23ea2 --- /dev/null +++ b/scripts/tracing/dma-api/README @@ -0,0 +1,36 @@ + + DMA API trace BASIC USAGE + =========================== + + +Enable CONFIG_DMA_API_DEBUG +---------------------------- +$TOP/kernel/scripts/config \ + --file $OUT/obj/KERNEL/.config \ + -e DMA_API_DEBUG +make -C$TOP/kernel ARCH=arm O=$OUT/obj/KERNEL oldconfig +grep DMA_API_DEBUG $OUT/obj/KERNEL/.config + + + +Via debugfs +----------- +adb shell cat /d/dma-api/dump_{mappings,allocs,allocs_detail} + + +Via Ftrace +---------- +# All ftraces from boot, add "trace_event=dmadebug:*" to kernel command line +sed -i '/strlcpy(cmd_line, boot_command_line, /i \ + \tstrcat(boot_command_line, " trace_event=dmadebug:*");' \ + $TOP/kernel/arch/arm/kernel/setup.c + +Or + +adb shell "echo 'dmadebug:*' >> /d/tracing/set_event" + +adb shell <your_test_here> +adb shell "echo '\!dmadebug:*' >> /d/tracing/set_event" +adb pull /d/tracing/trace + +python $TOP/kernel/scripts/tracing/dma-api/trace.py trace diff --git a/scripts/tracing/dma-api/plotting.py b/scripts/tracing/dma-api/plotting.py new file mode 100644 index 000000000000..f3d69b6a36a4 --- /dev/null +++ b/scripts/tracing/dma-api/plotting.py @@ -0,0 +1,100 @@ +"""Ugly graph drawing tools""" +import matplotlib.pyplot as plt +import matplotlib.cm as cmap +#import numpy as np +from matplotlib import cbook + +# http://stackoverflow.com/questions/4652439/is-there-a-matplotlib-equivalent-of-matlabs-datacursormode +class DataCursor(object): + """A simple data cursor widget that displays the x,y location of a + matplotlib artist when it is selected.""" + def __init__(self, artists, tolerance=5, offsets=(-20, 20), + template='x: %0.2f\ny: %0.2f', display_all=False): + """Create the data cursor and connect it to the relevant figure. + "artists" is the matplotlib artist or sequence of artists that will be + selected. + "tolerance" is the radius (in points) that the mouse click must be + within to select the artist. + "offsets" is a tuple of (x,y) offsets in points from the selected + point to the displayed annotation box + "template" is the format string to be used. Note: For compatibility + with older versions of python, this uses the old-style (%) + formatting specification. + "display_all" controls whether more than one annotation box will + be shown if there are multiple axes. Only one will be shown + per-axis, regardless. + """ + self.template = template + self.offsets = offsets + self.display_all = display_all + if not cbook.iterable(artists): + artists = [artists] + self.artists = artists + self.axes = tuple(set(art.axes for art in self.artists)) + self.figures = tuple(set(ax.figure for ax in self.axes)) + + self.annotations = {} + for ax in self.axes: + self.annotations[ax] = self.annotate(ax) + + for artist in self.artists: + artist.set_picker(tolerance) + for fig in self.figures: + fig.canvas.mpl_connect('pick_event', self) + + def annotate(self, ax): + """Draws and hides the annotation box for the given axis "ax".""" + annotation = ax.annotate(self.template, xy=(0, 0), ha='right', + xytext=self.offsets, textcoords='offset points', va='bottom', + bbox=dict(boxstyle='round,pad=0.5', fc='yellow', alpha=0.5), + arrowprops=dict(arrowstyle='->', connectionstyle='arc3,rad=0') + ) + annotation.set_visible(False) + return annotation + + def __call__(self, event): + """Intended to be called through "mpl_connect".""" + # Rather than trying to interpolate, just display the clicked coords + # This will only be called if it's within "tolerance", anyway. + x, y = event.mouseevent.xdata, event.mouseevent.ydata + try: + annotation = self.annotations[event.artist.axes] + except KeyError: + return + if x is not None: + if not self.display_all: + # Hide any other annotation boxes... + for ann in self.annotations.values(): + ann.set_visible(False) + # Update the annotation in the current axis.. + annotation.xy = x, y + annotation.set_text(self.template % (x, y)) + annotation.set_visible(True) + event.canvas.draw() + +def plotseries(*serieslabels): + """Plot lists of series in separate axes, tie time axis together""" + global fig + fig, axes = plt.subplots(nrows=len(serieslabels), sharex=True) + + for subplot, ax in zip(serieslabels, axes): + for ser, lab in zip(*subplot): # subplot = ([x], [y]) + ax.step(ser[0], ser[1], label=lab, where="post") + ax.grid(True) + ax.legend() + + (DataCursor(ax.lines)) + plt.grid(True) + plt.show() + + + +def disp_pic(bitmap): + """Display the allocation bitmap. TODO.""" + fig=plt.figure() + a=fig.add_subplot(1,1,1) + fig.clf() + implt=plt.imshow(bitmap, extent=(0, len(bitmap[0]), 0, len(bitmap)), + interpolation="nearest", cmap=cmap.gist_heat) + fig.canvas.draw() + plt.show() diff --git a/scripts/tracing/dma-api/smmu.py b/scripts/tracing/dma-api/smmu.py new file mode 100644 index 000000000000..0fbadff7119f --- /dev/null +++ b/scripts/tracing/dma-api/smmu.py @@ -0,0 +1,202 @@ +"""Low-level memory management tracking""" + +VERBOSITY = 0 # TODO: use logging + +class Bitmap(object): + """Just a raw bitmap for reserving the pages""" + def __init__(self, size, verbosity): + self._size = size + self._bits = 0 + self._verbosity = verbosity + self._bits_allocd = 0 + + def _mask(self, offset, length): + """length amount of 1's, shifted left by offset""" + assert offset >= 0, "offset < 0" + assert offset < self._size, "offset 0x%d >= size %s" % (offset, self._size) + bitstring = (1 << length) - 1 + return bitstring << offset + + def _indices(self, offset, length): + """Bit numbers starting from offset""" + return range(offset, offset + length) + + def alloc(self, offset, length): + """Reserve the bits, warn if verbose and some set already""" + mask = self._mask(offset, length) + if (self._bits & mask) != 0 and self._verbosity >= 1: + print "warning: duplicate allocation" + self._bits |= mask + self._bits_allocd += length + return self._indices(offset, length) + + def free(self, offset, length): + """Free the bits, warn if verbose and some not set yet""" + mask = self._mask(offset, length) + if (self._bits & mask) != mask and self._verbosity >= 1: + print "warning: freeing freed memory, mapbits %x" % (self._bits & mask) + self._bits &= ~mask + self._bits_allocd -= length + return self._indices(offset, length) + + def contains(self, offset, length): + """Are some bits in the given range set?""" + mask = self._mask(offset, length) + return self._bits & mask + + def __repr__(self): + return "<bitmap, %d/%d allocd>" % (self._bits_allocd, self._size) + +class Memory(object): + """Store and handle raw bitmaps, check mapping collisions between devices""" + PAGESHIFT = 12 + PAGESIZE = 1 << PAGESHIFT + PAGEMASK = PAGESIZE - 1 + + def __init__(self, addr, size, asid): + """addr: lowest possible address, size: bytes, asid: arbitrary id""" + assert (addr & self.PAGEMASK) == 0, addr + assert (size & self.PAGEMASK) == 0, size + + self._addr = addr + self._size = size + self._end = addr + size + self._asid = asid + self._bitmap = Bitmap(size >> self.PAGESHIFT, VERBOSITY) + self._devmaps = {} + + if VERBOSITY >= 1: + print "memory at %08x-%08x" % (addr, addr + size) + + def to_bit(self, addr): + """Address to bitmap position""" + return addr >> self.PAGESHIFT + + def alloc(self, dev, addr, size): + """Allocate (map) for the given device, verify things""" + if addr >= self._end: + if VERBOSITY >= 1: + print "warning: %s mapping beyond bitmap: %08x" % (dev, addr) + return [] + + if (addr & self.PAGEMASK) != 0: + if VERBOSITY >= 1: + print "warning: alloc not aligned at 0x%x, size %d (new addr 0x%x, size %d)" % ( + addr, size, addr & ~self.PAGEMASK, size + (addr & self.PAGEMASK)) + addr &= ~self.PAGEMASK + size += addr & self.PAGEMASK + + if size < self.PAGESIZE: + size = self.PAGESIZE + + for user, bmp in self._devmaps.iteritems(): + if bmp.contains(self.to_bit(addr - self._addr), self.to_bit(size)): + if VERBOSITY >= 1: + print "warning: %s mapping [0x%x,0x%x) already used by %s" % ( + dev, addr, addr + size, user) + + devmap = self._devmaps.setdefault(dev, Bitmap(self._bitmap._size, 0)) + + self._alloc(devmap, addr, size) + bits = self._alloc(self._bitmap, addr, size) + return bits + + def _alloc(self, bitmap, addr, size): + """Allocate from an internal bitmap""" + return bitmap.alloc(self.to_bit(addr - self._addr), self.to_bit(size)) + + def free(self, dev, addr, size): + """Free (unmap) for the given device, verify things""" + if (addr & self.PAGEMASK) != 0: + if VERBOSITY >= 1: + print "warning: free not aligned at 0x%x, size %d (new addr 0x%x, size %d)" % ( + addr, size, addr & ~self.PAGEMASK, size + (addr & self.PAGEMASK)) + addr &= ~self.PAGEMASK + size += addr & self.PAGEMASK + + if size < self.PAGESIZE: + size = self.PAGESIZE + + devmap = self._devmaps.setdefault(dev, Bitmap(self._bitmap._size, 0)) + + owners = [] + for user, bmp in self._devmaps.iteritems(): + if bmp.contains(self.to_bit(addr - self._addr), self.to_bit(size)): + owners.append((user, bmp)) + + if len(owners) == 0: + if VERBOSITY >= 1: + print "warning: %s freeing 0x%x that nobody owns" % (dev, addr) + elif len(owners) == 1: + if owners[0][0] != dev and VERBOSITY >= 2: + print "note: %s freeing 0x%x allocd by %s" % ( + dev, addr, owners[0][0]) + devmap = owners[0][1] + + self._free(devmap, addr, size) + bits = self._free(self._bitmap, addr, size) + return bits + + def _free(self, bitmap, addr, size): + """Free from an internal bitmap""" + return bitmap.free(self.to_bit(addr - self._addr), self.to_bit(size)) + + +class Device(object): + """Keep track of allocations per device/process + + This needs more tricky work for tracking inter-process maps/unmaps :( + """ + + def __init__(self, name, mem): + self._name = name + self._mem = mem + self._max_alloc = 0 + self._cur_alloc = 0 + self._alloc_history = [] + self._addresses = [] + + def alloc(self, addr, size): + pages = self._mem.alloc(self, addr, size) + if pages is not False: + self._cur_alloc += size + self._max_alloc = max(self._max_alloc, self._cur_alloc) + self._alloc_history.append(self._cur_alloc) + if addr in self._addresses: + if VERBOSITY >= 1: + print "warning: %s allocing dupe address %x %s" % (self._name, addr, len([x for x in self._addresses if x == addr])) + self._addresses.append(addr) + return pages + + def free(self, addr, size): + pages = self._mem.free(self, addr, size) + self._cur_alloc -= size + if addr in self._addresses: + self._addresses.remove(addr) + else: + if VERBOSITY >= 1: + print "warning: %s freeing unallocated %x" % (self._name, addr) + return pages + + def history_at(self, i): + return self._alloc_history[i] + + @property + def name(self): + return self._name + + def __str__(self): + return self.name + + def __repr__(self): + return "<dev: %s>" % self.name + + @property + def max_allocated(self): + return self._max_alloc + + +class AsidSpace(object): + # TODO: don't pre-grep by archdata but put devices' mem maps here. + pass + diff --git a/scripts/tracing/dma-api/trace.py b/scripts/tracing/dma-api/trace.py new file mode 100644 index 000000000000..c4507b6c4d02 --- /dev/null +++ b/scripts/tracing/dma-api/trace.py @@ -0,0 +1,349 @@ +"""Main program and stuff""" + +#from pprint import pprint +from sys import stdin +import os.path +import re +from argparse import ArgumentParser +import cPickle as pickle +from collections import namedtuple +from plotting import plotseries, disp_pic +import smmu + +class TracelineParser(object): + """Parse the needed information out of an ftrace line""" + # <...>-6 [000] d..2 5.287079: dmadebug_iommu_map_page: device=sdhci-tegra.3, addr=0x01048000, size=4096 page=c13e7214 archdata=ed504640 + def __init__(self): + self.pattern = re.compile("device=(?P<dev>.*), addr=(?P<addr>.*), size=(?P<size>.*) page=(?P<page>.*) archdata=(?P<archdata>.*)") + def parse(self, args): + args = self.pattern.match(args) + return (args.group("dev"), int(args.group("addr"), 16), + int(args.group("size")), int(args.group("page"), 16), + int(args.group("archdata"), 16)) + +def biggest_indices(items, n): + """Return list of indices of n biggest elements in items""" + with_indices = [(x, i) for i, x in enumerate(items)] + ordered = sorted(with_indices) + return [i for x, i in ordered[-n:]] + +def by_indices(xs, ids): + """Get elements from the list xs by their indices""" + return [xs[i] for i in ids] + +"""Event represents one input line""" +Event = namedtuple("Event", ["time", "dev", "data", "delta"]) + +class Trace(object): + def __init__(self, args): + smmu.VERBOSITY = args.verbosity + self._args = args + self.devlist = [] + self.events = [] + self.metrics = { + "max_peak": self._usage_peak, + "activity_rate": self._usage_activity, + "average_mem": self._usage_avg + } + self.traceliner = TracelineParser() + + @staticmethod + def get_metrics(): + """What filter metrics to get max users""" + return ["max_peak", "activity_rate", "average_mem"] + + def show(self): + """Shuffle events around, build plots, and show them""" + if self._args.max_plots: + evs = self.merge_events() + else: + evs = self.events + series, devlist = self.unload(evs) + if not self._args.no_plots: + self.plot(series, devlist) + + def _get_usage(self, evs): + """Return a metric of how active the events in evs are""" + return self.metrics[self._args.max_metric](evs) + + def _usage_peak(self, evs): + """Return the biggest peak""" + return max(e.data for e in evs) + + def _usage_activity(self, evs): + """Return the activity count: simply the length of the event list""" + return len(evs) + + def _usage_avg(self, evs): + """Return the average over all points""" + # FIXME: the data points are not uniform in time, so this might be + # somewhat off. + return float(sum(e.data for e in evs)) / len(e) + + def merge_events(self): + """Find out biggest users, keep them and flatten others to a single user""" + sizes = [] + dev_evs = [] + for i, dev in enumerate(self.devlist): + dev_evs.append([e for e in self.events if e.dev == dev]) + sizes.append(self._get_usage(dev_evs[i])) + + # indices of the devices + biggestix = biggest_indices(sizes, self._args.max_plots) + print biggestix + is_big = {} + for i, dev in enumerate(self.devlist): + is_big[dev] = i in biggestix + + evs = [] + for e in self.events: + if not is_big[e.dev]: + e = Event(e.time, "others", e.data, e.delta) + evs.append(e) + + self.devlist.append("others") + return evs + + def unload(self, events): + """Prepare the event list for plotting + + series ends up as [([time0], [data0]), ([time1], [data1]), ...] + """ + # ([x], [y]) for matplotlib + series = [([], []) for x in self.devlist] + devidx = dict([(d, i) for i, d in enumerate(self.devlist)]) + + for event in events: + devid = devidx[event.dev] + series[devid][0].append(event.time) + series[devid][1].append(event.data) # self.dev_data(event.dev)) + + series_out = [] + devlist_out = [] + + for ser, dev in zip(series, self.devlist): + if len(ser[0]) > 0: + series_out.append(ser) + devlist_out.append(dev) + + return series_out, devlist_out + + def plot(self, series, devlist): + """Display the plots""" + #series, devlist = flatten_axes(self.series, self.devlist, + # self._args.max_plots) + devinfo = (series, map(str, devlist)) + allocfreeinfo = (self.allocsfrees, ["allocd", "freed", "current"]) + plotseries(devinfo, allocfreeinfo) + #plotseries(devinfo) + + def dev_data(self, dev): + """what data to plot against time""" + return dev._cur_alloc + + def _cache_hash(self, filename): + """The trace files are probably not of the same size""" + return str(os.path.getsize(filename)) + + def load_cache(self): + """Get the trace data from a database file, if one exists""" + has = self._cache_hash(self._args.filename) + try: + cache = open("trace." + has) + except IOError: + pass + else: + self._load_cache(pickle.load(cache)) + return True + return False + + def save_cache(self): + """Store the raw trace data to a database""" + data = self._save_cache() + fh = open("trace." + self._cache_hash(self._args.filename), "w") + pickle.dump(data, fh) + + def _save_cache(self): + """Return the internal data that is needed to be pickled""" + return self.events, self.devlist, self.allocsfrees + + def _load_cache(self, data): + """Get the data from an unpickled object""" + self.events, self.devlist, self.allocsfrees = data + + def load_events(self): + """Get the internal data from a trace file or cache""" + if self._args.filename: + if self._args.cache and self.load_cache(): + return + fh = open(self._args.filename) + else: + fh = stdin + + self.parse(fh) + + if self._args.cache and self._args.filename: + self.save_cache() + + def parse(self, fh): + """Parse the trace file in fh, store data to self""" + mems = {} + dev_by_name = {} + devlist = [] + buf_owners = {} + events = [] + allocsfrees = [([], []), ([], []), ([], [])] # allocs, frees, current + allocs = 0 + frees = 0 + curbufs = 0 + + mem_bytes = 1024 * 1024 * 1024 + npages = mem_bytes / 4096 + ncols = 512 + le_pic = [0] * npages + lastupd = 0 + + for lineidx, line in enumerate(fh): + # no comments + if line.startswith("#"): + continue + + taskpid, cpu, flags, timestamp, func, args = line.strip().split(None, 5) + func = func[:-len(":")] + # unneeded events may be there too + if not func.startswith("dmadebug"): + continue + + if self._args.verbosity >= 3: + print line.rstrip() + + timestamp = float(timestamp[:-1]) + if timestamp < self._args.start: + continue + if timestamp >= self._args.end: + break + + devname, addr, size, page, archdata = self.traceliner.parse(args) + if self._args.processes: + devname = taskpid.split("-")[0] + mapping = archdata + + try: + memmap = mems[mapping] + except KeyError: + memmap = mem(mapping) + mems[mapping] = memmap + + try: + dev = dev_by_name[devname] + except KeyError: + dev = smmu.Device(devname, memmap) + dev_by_name[devname] = dev + devlist.append(dev) + + allocfuncs = ["dmadebug_map_page", "dmadebug_map_sg", "dmadebug_alloc_coherent"] + freefuncs = ["dmadebug_unmap_page", "dmadebug_unmap_sg", "dmadebug_free_coherent"] + ignfuncs = [] + + if timestamp-lastupd > 0.1: + # just some debug prints for now + lastupd = timestamp + print lineidx,timestamp + le_pic2 = [le_pic[i:i+ncols] for i in range(0, npages, ncols)] + #disp_pic(le_pic2) + + # animating the bitmap would be cool + #for row in le_pic: + # for i, a in enumerate(row): + # pass + #row[i] = 0.09 * a + + if func in allocfuncs: + pages = dev_by_name[devname].alloc(addr, size) + for p in pages: + le_pic[p] = 1 + buf_owners[addr] = dev_by_name[devname] + allocs += 1 + curbufs += 1 + allocsfrees[0][0].append(timestamp) + allocsfrees[0][1].append(allocs) + elif func in freefuncs: + if addr not in buf_owners: + if self._args.verbosity >= 1: + print "warning: %s unmapping unmapped %s" % (dev, addr) + buf_owners[addr] = dev + # fixme: move this to bitmap handling + # get to know the owners of bits + # allocs/frees calls should be traced separately from maps? + # map_pages is traced per page :( + if buf_owners[addr] != dev and self._args.verbosity >= 2: + print "note: %s unmapping [%d,%d) mapped by %s" % ( + dev, addr, addr+size, buf_owners[addr]) + pages = buf_owners[addr].free(addr, size) + for p in pages: + le_pic[p] = 0 + frees -= 1 + curbufs -= 1 + allocsfrees[1][0].append(timestamp) + allocsfrees[1][1].append(frees) + elif func not in ignfuncs: + raise ValueError("unhandled %s" % func) + + allocsfrees[2][0].append(timestamp) + allocsfrees[2][1].append(curbufs) + + events.append(Event(timestamp, dev, self.dev_data(dev), size)) + + self.events = events + self.devlist = devlist + self.allocsfrees = allocsfrees + + le_pic2 = [le_pic[i:i+ncols] for i in range(0, npages, ncols)] + # FIXME: not quite ready yet + disp_pic(le_pic2) + + return + +def mem(asid): + """Create a new memory object for the given asid space""" + SZ_2G = 2 * 1024 * 1024 * 1024 + SZ_1M = 1 * 1024 * 1024 + # arch/arm/mach-tegra/include/mach/iomap.h TEGRA_SMMU_(BASE|SIZE) + base = 0x80000000 + size = SZ_2G - SZ_1M + return smmu.Memory(base, size, asid) + +def get_args(): + """Eat command line arguments, return argparse namespace for settings""" + parser = ArgumentParser() + parser.add_argument("filename", nargs="?", + help="trace file dump, stdin if not given") + parser.add_argument("-s", "--start", type=float, default=0, + help="start timestamp") + parser.add_argument("-e", "--end", type=float, default=1e9, + help="end timestamp") + parser.add_argument("-v", "--verbosity", action="count", default=0, + help="amount of extra information: once for warns (dup addrs), " + "twice for notices (different client in map/unmap), " + "three for echoing all back") + parser.add_argument("-p", "--processes", action="store_true", + help="use processes as memory clients instead of devices") + parser.add_argument("-n", "--no-plots", action="store_true", + help="Don't draw the plots, only read the trace") + parser.add_argument("-c", "--cache", action="store_true", + help="Pickle the data and make a cache file for fast reloading") + parser.add_argument("-m", "--max-plots", type=int, + help="Maximum number of clients to show; show biggest and sum others") + parser.add_argument("-M", "--max-metric", choices=Trace.get_metrics(), + default=Trace.get_metrics()[0], + help="Metric to use when choosing clients in --max-plots") + return parser.parse_args() + +def main(): + args = get_args() + trace = Trace(args) + trace.load_events() + trace.show() + +if __name__ == "__main__": + main() |