| #!/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: |