root/mirror/tools/valgrind/valpyk.py

Revision 9304, 13.2 kB (checked in by haypo, 2 years ago)

Display the number of skipped errors/memory leaks

  • Property svn:eol-style set to native
  • Property svn:executable set to *
Line 
1#!/usr/bin/env python
2# -*- coding: ASCII -*-
3"""
4Copyright(C) 2006 INL
5Written by Victor Stinner <victor.stinner@inl.fr>
6
7This program is free software; you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, version 2 of the License.
10
11This program is distributed in the hope that it will be useful,
12but WITHOUT ANY WARRANTY; without even the implied warranty of
13MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14GNU General Public License for more details.
15
16You should have received a copy of the GNU General Public License
17along with this program; if not, write to the Free Software
18Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
19---
20Script to parse memory leaks in Valgrind log.
21
22Run it without argument for more information.
23
24Warnings and errors are written in stderr.
25"""
26
27import re
28import sys
29
30class TextParser:
31    """
32    Very basic plain text parser useful to read one line after the other.
33
34    It calls a different function for each line, and each function returns
35    next function to be called for next line.
36
37    Interresting methods and attributes:
38    - line_number is the current line number of input file (starting at 1)
39    - reset(): function called when parser is created
40    - stop(): function called when the parser is done
41    - parserError(): raise an exception with reason and line number
42    """
43    def __init__(self, input, first_parser):
44        """
45        Parse input file object, first_parser is the first function
46        used to parse the file content.
47        """
48        self.input = input
49        self.line_number = 0
50        self.first_parser = first_parser
51        self.reset()
52        self.runParser()
53
54    def parserError(self, message):
55        raise Exception("Error at line %s: %s" % \
56            (self.line_number, message))
57
58    def reset(self):
59        pass
60
61    def stop(self):
62        pass
63
64    def runParser(self):
65        parser = self.first_parser
66        while True:
67            line = self.input.readline()
68            if len(line) == 0:
69                break
70            line = line.rstrip()
71            self.line_number += 1
72            new_parser = parser(line)
73            if new_parser:
74                parser = new_parser
75        self.stop()
76
77class Function:
78    """
79    A function with attributes: name, file, line number, address.
80    File and line address are optional.
81
82    You can compare functions using hash(func) and convert to
83    string using str(func)
84    """
85    def __init__(self, name, addr, file=None, line=None):
86        if name and name != "???":
87            self.name = "%s()" % name
88        else:
89            self.name = "-unknow-"
90        self.file = file
91        self.line = line
92        self.addr = addr
93
94    def __hash__(self):
95        if self.line:
96            line = self.line//10
97        else:
98            line = None
99        return hash((self.name, self.file, line))
100
101    def __str__(self):
102        text = [self.name]
103        if self.file:
104            if self.line is not None:
105                text.append(" at %s:%s" % (self.file, self.line))
106            else:
107                text.append(" at %s" % self.file)
108        return "".join(text)
109
110class UninitialisedValueError:
111    """
112    "Use of uninitialised value of size (...)" error.
113
114    Attributes: backtrace (list of functions) and bytes (size of uninitialized
115    value).
116
117    Methods:
118    - hash(err): use it to compare errors and find duplicates
119    - str(err): Create one line of text to describe the error
120    """
121    def __init__(self, bytes):
122        self.backtrace = []
123        self.bytes = bytes
124
125    def __hash__(self):
126        data = [hash(func) for func in self.backtrace] + [hash(self.bytes)]
127        return hash(tuple(data))
128
129    def __nonzero__(self):
130        return self.bytes != None
131
132    def __str__(self):
133        return "Uninitialised value error: %s bytes" % self.bytes
134
135class InvalidReadError(UninitialisedValueError):
136    """
137    "Invalid read of size (...)" error.
138    """
139    def __str__(self):
140        return "Invalid read: %s bytes" % self.bytes
141
142class ProgramError(UninitialisedValueError):
143    """
144    "Process terminating with (...)" error.
145    """
146    def __init__(self, exit_code):
147        UninitialisedValueError.__init__(self, 0)
148        self.exit_code = exit_code
149        self.reason = None
150
151    def __str__(self):
152        return "Program terminating: %s (%s)" % (self.exit_code, self.reason)
153
154class MemoryLeak(UninitialisedValueError):
155    """
156    Memory leak error, message like: "10 bytes in (...) loss record 2 of 9"
157    """
158    def __str__(self):
159        return "Memory leak: %s bytes" % self.bytes
160
161class ValgrindParser(TextParser):
162    """
163    Valgrind log parser: convert plain text log to Python objects.
164
165    Errors are filtered using methods:
166    - filterLeak(): only for memory leaks
167    - filterError(): for all other errors
168
169    Note: filterError() calls filterLeak()
170    """
171    regex_pid = "==[0-9]+=="
172    regex_empty = re.compile(r"^%s$" % regex_pid)
173    regex_indirect = " \([0-9,]+ direct, [0-9,]+ indirect\)"
174
175    regex_terminating = re.compile("%s Process terminating with (.*)$" % regex_pid)
176    regex_program_reason = re.compile("%s  (.*)$" % regex_pid)
177    regex_uninit = re.compile(r"^%s Use of uninitialised value of size ([0-9,]+)$" % regex_pid)
178    regex_invalid_read = re.compile(r"^%s Invalid read of size ([0-9,]+)$" % regex_pid)
179
180    regex_leak_header = re.compile(r"^%s ([0-9,]+)(?:%s)? bytes in [0-9,]+ blocks are .* in loss record [0-9]+ of [0-9]+$" % (regex_pid, regex_indirect))
181    regex_backtrace_name = re.compile(r"^%s    (?:at|by) (0x[0-9A-F]+): (.+) \(([^:]+):([0-9]+)\)$" % regex_pid)
182    regex_backtrace_name_in = re.compile(r"^%s    (?:at|by) (0x[0-9A-F]+): ([^ ]+) \(in ([^)]+)\)$" % regex_pid)
183    regex_backtrace_within = re.compile(r"^%s    (?:at|by) (0x[0-9A-F]+): \((?:with)?in (.*)\)$" % regex_pid)
184    regex_backtrace_unknow = re.compile(r"^%s    (?:at|by) (0x[0-9A-F]+): (\?\?\?)$" % regex_pid)
185
186    def __init__(self, input):
187        """
188        Constructor: argument input is a file object
189        """
190        self.errors = []
191        self.leaks = []
192        self.skipped_errors = 0
193        self.skipped_leaks = 0
194        TextParser.__init__(self, input, self.searchLeakHeader)
195
196    def searchLeakHeader(self, line):
197        """
198        Search first line of memory leak or any other type of error
199        """
200        match = self.regex_leak_header.match(line)
201        if match:
202            size = match.group(1).replace(",", "")
203            self.error = MemoryLeak(int(size))
204            return self.parseBacktrace
205
206        match = self.regex_uninit.match(line)
207        if match:
208            size = match.group(1).replace(",", "")
209            self.error = UninitialisedValueError(int(size))
210            return self.parseBacktrace
211
212        match = self.regex_invalid_read.match(line)
213        if match:
214            size = match.group(1).replace(",", "")
215            self.error = InvalidReadError(int(size))
216            return self.parseBacktrace
217
218        match = self.regex_terminating.match(line)
219        if match:
220            self.error = ProgramError(match.group(1))
221            return self.parseProgramError
222
223    def parseProgramError(self, line):
224        """
225        Parse second line of a program error
226        """
227        match = self.regex_program_reason.match(line)
228        if not match:
229            self.parserError("Unable to get program exit reason")
230        self.error.reason = match.group(1)
231        return self.parseBacktrace
232
233    def parseBacktrace(self, line):
234        """
235        Parse a backtrace (list of functions)
236        """
237        # ==14694==    at 0x401C7AA: calloc (vg_replace_malloc.c:279)
238        match = self.regex_backtrace_name.match(line)
239        if match:
240            addr, name, filename, linenb = match.groups()
241            func = Function(name, addr, filename, int(linenb))
242            self.error.backtrace.append(func)
243            return
244
245        # ==14694==    at 0x401C7AA: calloc (in /lib/...)
246        match = self.regex_backtrace_name_in.match(line)
247        if match:
248            addr, name, filename = match.groups()
249            func = Function(name, addr, filename)
250            self.error.backtrace.append(func)
251            return
252
253        # ==14694==    by 0x4187E56: (within /lib/tls...)
254        match = self.regex_backtrace_within.match(line)
255        if match:
256            addr, filename = match.groups()
257            func = Function(None, addr, filename)
258            self.error.backtrace.append(func)
259            return
260
261        # ==14694==    by 0x402646A: ???
262        match = self.regex_backtrace_unknow.match(line)
263        if match:
264            addr, name = match.groups()
265            func = Function(name, addr)
266            self.error.backtrace.append(func)
267            return
268
269        if not self.regex_empty.match(line):
270            print >>sys.stderr, 'Unknow: "%s"' % line
271        self.addError()
272        return self.searchLeakHeader
273
274    def stop(self):
275        self.addError()
276
277    def reset(self):
278        self.error = None
279
280    def filterError(self, error):
281        result = self.filterLeak(error)
282        if result is not True:
283            return result
284        names = [func.name for func in self.error.backtrace]
285        for name in names:
286            if name.startswith("gcry_"):
287                return name
288        return True
289
290    def filterLeak(self, leak):
291        names = [func.name for func in self.error.backtrace]
292        if "PyObject_Realloc()" in names:
293            return "PyObject_Realloc()"
294        if "PyObject_Free()" in names:
295            return "PyObject_Free()"
296        if "dlsym()" in names:
297            return "dlsym()"
298        if "dlopen()" in names:
299            return "dlopen()"
300        if "_dl_open()" in names:
301            return "_dl_open()"
302        if "gcry_check_version" in names:
303            return "gcry_check_version()"
304        for name in names:
305            if name.startswith("pthread_create"):
306                return name
307            if name.startswith("g_thread_init"):
308                return name
309            if "sasl_" in name:
310                return name
311            if name.endswith("dlopen_mode()"):
312                return "dlopen_mode()"
313            if "sasldb_" in name:
314                return name
315            if name.startswith("gnutls_"):
316                return name
317            if name.startswith("gcry_"):
318                return name
319            if name.startswith("g_iconv"):
320                return name
321        for func in self.error.backtrace:
322            if func.file and "libdl" in func.file:
323                return func.file
324        return True
325
326    def addError(self):
327        if self.error:
328            if self.error.__class__ == MemoryLeak:
329                result = self.filterLeak(self.error)
330                if result is True:
331                    self.leaks.append(self.error)
332                else:
333                    print >>sys.stderr, "Skip memory leak %s at line %s" % (result, self.line_number)
334                    self.skipped_leaks += 1
335            else:
336                result = self.filterError(self.error)
337                if result is True:
338                    self.errors.append(self.error)
339                else:
340                    print >>sys.stderr, "Skip error %s at line %s" % (result, self.line_number)
341                    self.skipped_errors += 1
342        self.reset()
343
344def usage():
345    print """usage: %s logfilename
346
347Valgrind memory leak parser. To get good logs, run valgrind with options:
348   --leak-check=full: see all informations about memory leaks
349   --show-reachable=yes: also display reachable memory leaks
350   --run-libc-freeres=yes: avoid some libc memory leaks
351   --verbose: gives more informations
352
353Other useful options:
354    --log-file-exactly=yourname.log
355
356If you use glib, also set environment variable G_SLICE to find memory leaks:
357export G_SLICE=always-malloc""" % sys.argv[0]
358
359def displayErrors(errors, max_error=None, reverse=True):
360    """
361    Function to display a list of errors.
362    """
363    if max_error and max_error < len(errors):
364        print >>sys.stderr, "Only display top %s memory errors" % max_error
365        errors = errors[-max_error:]
366    else:
367        errors = errors
368    if reverse:
369        errors = errors[::-1]
370    displayed = set()
371    for error in errors:
372        key = hash(error)
373        if key in displayed:
374            print >>sys.stderr, "Skip duplicate"
375            continue
376        displayed.add(key)
377        # Display memory error
378        print error
379
380        # Display backtrace
381        #backtrace = [ func for func in error.backtrace if func.name != "-unknow-" ]
382        backtrace = [ func for func in error.backtrace ]
383        for func in backtrace:
384            print "   > %s" % func
385
386    # Display memory errors count
387    print "Total: %s (%s)" % (len(displayed), len(errors))
388    print
389
390def main():
391    # Read log filename
392    if len(sys.argv) != 2:
393        usage()
394        sys.exit(1)
395    filename = sys.argv[1]
396
397    # Parse input log
398    parser = ValgrindParser( open(filename, "r") )
399
400    # Display all errors
401    displayErrors(parser.errors)
402
403    # Display memory leaks in reverse order (bigest to smallest leak)
404    # Only display top 10 leaks
405    displayErrors(parser.leaks, 10, True)
406
407    if parser.skipped_errors:
408        print "Skipped errors: %s" % parser.skipped_errors
409
410    if parser.skipped_leaks:
411        print "Skipped memory leaks: %s" % parser.skipped_leaks
412
413if __name__ == "__main__":
414    main()
Note: See TracBrowser for help on using the browser.