#!/usr/bin/python3 # -*- tab-width: 4; indent-tabs-mode: nil; py-indent-offset: 4 -*- # # This file is part of the LibreOffice project. # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. # ### script to help debug leaks of reference counted objects ## I. to use it, first override acquire() and release() # Foo * g_pTrackedFoo = 0; # Foo::Foo() # static int nFoos = 0; # if (++nFoos == 42) // track instance #42 # g_pTrackedFoo = this; # void Foo::acquire() # if (this == g_pTrackedFoo) # ; // set gdb breakpoint here # Foo_Base::acquire() # void Foo::release() # if (this == g_pTrackedFoo) # ; // set gdb breakpoint here # Foo_Base::release() ## II. run test/soffice in gdb and set breakpoints in acquire/release ## with a command to print the backtrace # set logging enabled on # break foo.cxx:123 # break foo.cxx:234 # command 1 2 # bt # c # end # run ## III. now feed logfile gdb.txt into this script # bin/refcount_leak.py < gdb.txt ### from operator import itemgetter import re import sys threshold = 2 class Trace: clock = 0 # global counter # frames: list of stack frames, beginning with outermost def __init__(self, lines): lines.reverse() self.frames = lines Trace.clock += 1 self.clock = Trace.clock def addTrace(traces, lines): if traces is not None and len(lines) > 0: traces.append(Trace(lines)) def readGdbLog(infile): traces_acquire = [] traces_release = [] current = None lines = [] apattern = re.compile("^Breakpoint.*::acquire") rpattern = re.compile("^Breakpoint.*::release") for line in infile: if apattern.match(line): addTrace(current, lines) lines = [] current = traces_acquire if rpattern.match(line): addTrace(current, lines) lines = [] current = traces_release if line.startswith("#"): # strip #123 stack frame number, and newline lines.append(line[line.index("0x"):-1]) addTrace(current, lines) print("# parsed traces acquire: ", len(traces_acquire)) print("# parsed traces release: ", len(traces_release)) return (traces_acquire, traces_release) def getFunction(frame): start = frame.index(" in ") + len(" in ") try: end = frame.index(" at ", start) except ValueError: # argh... stack frames may be split across multiple lines if # a parameter has a fancy pretty printer return frame[start:] return frame[start:end] def matchStack(trace_acquire, trace_release): if trace_release.clock < trace_acquire.clock: return None # acquire must precede release common = 0 refpattern = re.compile(r"::Reference<.*>::Reference\(") for (frame1, frame2) in zip(trace_release.frames, trace_acquire.frames): if frame1 == frame2: common += 1 else: if getFunction(frame1) == getFunction(frame2): common += 1 acquireframes = len(trace_acquire.frames) # there is sometimes a dozen frames of UNO type related junk # on the stack where the acquire() happens, which breaks the # matching; try to avoid that for i in range(common, acquireframes): if refpattern.search(trace_acquire.frames[i]): acquireframes = i+1 # cut off junk above Reference ctor break score = max(len(trace_release.frames), acquireframes) - common # smaller score is better return (score, trace_release.clock - trace_acquire.clock) # brute force greedy n^2 matching def matchStacks(traces_acquire, traces_release): matches = [] for release in traces_release: for acquire in traces_acquire: score = matchStack(acquire, release) if score is not None: matches.append((score, acquire, release)) matches.sort(key=itemgetter(0)) return matches def bestMatches(traces_acquire, traces_release, matches): traces_aunmatched = traces_acquire traces_runmatched = traces_release bestmatches = [] for (score,acquire,release) in matches: if not(acquire in traces_aunmatched and release in traces_runmatched): continue traces_aunmatched.remove(acquire) traces_runmatched.remove(release) bestmatches.append((score,acquire,release)) print("# unmatched acquire: ", len(traces_aunmatched)) print("# unmatched release: ", len(traces_runmatched)) return (bestmatches,traces_aunmatched,traces_runmatched) def printTrace(trace): for frame in reversed(trace.frames): print(" ", frame) def printMatched(bestmatches): for (score,acquire,release) in reversed(bestmatches): print("\n*** Matched trace with score: ", score) print(" acquire: ") printTrace(acquire) print(" release: ") printTrace(release) def printUnmatched(traces, prefix): for trace in traces: print("\n*** Unmatched trace (", prefix, "):") printTrace(trace) if __name__ == "__main__": (traces_acquire, traces_release) = readGdbLog(sys.stdin) matches = matchStacks(traces_acquire, traces_release) (bestmatches,traces_au,traces_ru) = bestMatches(traces_acquire, traces_release, matches) # print output, sorted with the most suspicious stuff first: printUnmatched(traces_au, "acquire") printUnmatched(traces_ru, "release") printMatched(bestmatches) # vim:set shiftwidth=4 softtabstop=4 expandtab: