use unix paths for dir tests and prefer PathFormat…
[LibreOffice.git] / bin / refcount_leak.py
blobde98d065a2aca851f486b6223ec86930e732703b
1 #!/usr/bin/python3
2 # -*- tab-width: 4; indent-tabs-mode: nil; py-indent-offset: 4 -*-
4 # This file is part of the LibreOffice project.
6 # This Source Code Form is subject to the terms of the Mozilla Public
7 # License, v. 2.0. If a copy of the MPL was not distributed with this
8 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
11 ### script to help debug leaks of reference counted objects
13 ## I. to use it, first override acquire() and release()
15 # Foo * g_pTrackedFoo = 0;
17 # Foo::Foo()
18 # static int nFoos = 0;
19 # if (++nFoos == 42) // track instance #42
20 # g_pTrackedFoo = this;
22 # void Foo::acquire()
23 # if (this == g_pTrackedFoo)
24 # ; // set gdb breakpoint here
25 # Foo_Base::acquire()
27 # void Foo::release()
28 # if (this == g_pTrackedFoo)
29 # ; // set gdb breakpoint here
30 # Foo_Base::release()
32 ## II. run test/soffice in gdb and set breakpoints in acquire/release
33 ## with a command to print the backtrace
35 # set logging enabled on
36 # break foo.cxx:123
37 # break foo.cxx:234
39 # command 1 2
40 # bt
41 # c
42 # end
43 # run
45 ## III. now feed logfile gdb.txt into this script
47 # bin/refcount_leak.py < gdb.txt
49 ###
51 from operator import itemgetter
52 import re
53 import sys
55 threshold = 2
57 class Trace:
58 clock = 0 # global counter
59 # frames: list of stack frames, beginning with outermost
60 def __init__(self, lines):
61 lines.reverse()
62 self.frames = lines
63 Trace.clock += 1
64 self.clock = Trace.clock
66 def addTrace(traces, lines):
67 if not(traces is None) and len(lines) > 0:
68 traces.append(Trace(lines))
70 def readGdbLog(infile):
71 traces_acquire = []
72 traces_release = []
73 current = None
74 lines = []
75 apattern = re.compile("^Breakpoint.*::acquire")
76 rpattern = re.compile("^Breakpoint.*::release")
77 for line in infile:
78 if apattern.match(line):
79 addTrace(current, lines)
80 lines = []
81 current = traces_acquire
82 if rpattern.match(line):
83 addTrace(current, lines)
84 lines = []
85 current = traces_release
86 if line.startswith("#"):
87 # strip #123 stack frame number, and newline
88 lines.append(line[line.index("0x"):-1])
89 addTrace(current, lines)
90 print("# parsed traces acquire: ", len(traces_acquire))
91 print("# parsed traces release: ", len(traces_release))
92 return (traces_acquire, traces_release)
94 def getFunction(frame):
95 start = frame.index(" in ") + len(" in ")
96 try:
97 end = frame.index(" at ", start)
98 except ValueError as e:
99 # argh... stack frames may be split across multiple lines if
100 # a parameter has a fancy pretty printer
101 return frame[start:]
102 return frame[start:end]
105 def matchStack(trace_acquire, trace_release):
106 if trace_release.clock < trace_acquire.clock:
107 return None # acquire must precede release
108 common = 0
109 refpattern = re.compile(r"::Reference<.*>::Reference\(")
110 for (frame1, frame2) in zip(trace_release.frames, trace_acquire.frames):
111 if frame1 == frame2:
112 common += 1
113 else:
114 if getFunction(frame1) == getFunction(frame2):
115 common += 1
116 acquireframes = len(trace_acquire.frames)
117 # there is sometimes a dozen frames of UNO type related junk
118 # on the stack where the acquire() happens, which breaks the
119 # matching; try to avoid that
120 for i in range(common, acquireframes):
121 if refpattern.search(trace_acquire.frames[i]):
122 acquireframes = i+1 # cut off junk above Reference ctor
123 break
124 score = max(len(trace_release.frames), acquireframes) - common
125 # smaller score is better
126 return (score, trace_release.clock - trace_acquire.clock)
128 # brute force greedy n^2 matching
129 def matchStacks(traces_acquire, traces_release):
130 matches = []
131 for release in traces_release:
132 for acquire in traces_acquire:
133 score = matchStack(acquire, release)
134 if score is not None:
135 matches.append((score, acquire, release))
136 matches.sort(key=itemgetter(0))
137 return matches
139 def bestMatches(traces_acquire, traces_release, matches):
140 traces_aunmatched = traces_acquire
141 traces_runmatched = traces_release
142 bestmatches = []
143 for (score,acquire,release) in matches:
144 if not(acquire in traces_aunmatched and release in traces_runmatched):
145 continue
146 traces_aunmatched.remove(acquire)
147 traces_runmatched.remove(release)
148 bestmatches.append((score,acquire,release))
149 print("# unmatched acquire: ", len(traces_aunmatched))
150 print("# unmatched release: ", len(traces_runmatched))
151 return (bestmatches,traces_aunmatched,traces_runmatched)
153 def printTrace(trace):
154 for frame in reversed(trace.frames):
155 print(" ", frame)
157 def printMatched(bestmatches):
158 for (score,acquire,release) in reversed(bestmatches):
159 print("\n*** Matched trace with score: ", score)
160 print(" acquire: ")
161 printTrace(acquire)
162 print(" release: ")
163 printTrace(release)
165 def printUnmatched(traces, prefix):
166 for trace in traces:
167 print("\n*** Unmatched trace (", prefix, "):")
168 printTrace(trace)
170 if __name__ == "__main__":
171 (traces_acquire, traces_release) = readGdbLog(sys.stdin)
172 matches = matchStacks(traces_acquire, traces_release)
173 (bestmatches,traces_au,traces_ru) = bestMatches(traces_acquire, traces_release, matches)
174 # print output, sorted with the most suspicious stuff first:
175 printUnmatched(traces_au, "acquire")
176 printUnmatched(traces_ru, "release")
177 printMatched(bestmatches)
179 # vim:set shiftwidth=4 softtabstop=4 expandtab: