Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(4)

Side by Side Diff: tools_webrtc/valgrind/memcheck_analyze.py

Issue 2945753002: Roll chromium_revision b032878ebd..e438353b8b (480186:480311) (Closed)
Patch Set: Updated .gni Created 3 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « tools_webrtc/valgrind/locate_valgrind.sh ('k') | tools_webrtc/valgrind/valgrind.gni » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 #!/usr/bin/env python
2 # Copyright (c) 2017 The WebRTC project authors. All Rights Reserved.
3 #
4 # Use of this source code is governed by a BSD-style license
5 # that can be found in the LICENSE file in the root of the source
6 # tree. An additional intellectual property rights grant can be found
7 # in the file PATENTS. All contributing project authors may
8 # be found in the AUTHORS file in the root of the source tree.
9
10 # memcheck_analyze.py
11
12 ''' Given a valgrind XML file, parses errors and uniques them.'''
13
14 import gdb_helper
15
16 from collections import defaultdict
17 import hashlib
18 import logging
19 import optparse
20 import os
21 import re
22 import subprocess
23 import sys
24 import time
25 from xml.dom.minidom import parse
26 from xml.parsers.expat import ExpatError
27
28 import common
29
30 # Global symbol table (yuck)
31 TheAddressTable = None
32
33 # These are regexps that define functions (using C++ mangled names)
34 # we don't want to see in stack traces while pretty printing
35 # or generating suppressions.
36 # Just stop printing the stack/suppression frames when the current one
37 # matches any of these.
38 _BORING_CALLERS = common.BoringCallers(mangled=True, use_re_wildcards=True)
39
40 def getTextOf(top_node, name):
41 ''' Returns all text in all DOM nodes with a certain |name| that are children
42 of |top_node|.
43 '''
44
45 text = ""
46 for nodes_named in top_node.getElementsByTagName(name):
47 text += "".join([node.data for node in nodes_named.childNodes
48 if node.nodeType == node.TEXT_NODE])
49 return text
50
51 def getCDATAOf(top_node, name):
52 ''' Returns all CDATA in all DOM nodes with a certain |name| that are children
53 of |top_node|.
54 '''
55
56 text = ""
57 for nodes_named in top_node.getElementsByTagName(name):
58 text += "".join([node.data for node in nodes_named.childNodes
59 if node.nodeType == node.CDATA_SECTION_NODE])
60 if (text == ""):
61 return None
62 return text
63
64 def shortenFilePath(source_dir, directory):
65 '''Returns a string with the string prefix |source_dir| removed from
66 |directory|.'''
67 prefixes_to_cut = ["build/src/", "valgrind/coregrind/", "out/Release/../../"]
68
69 if source_dir:
70 prefixes_to_cut.append(source_dir)
71
72 for p in prefixes_to_cut:
73 index = directory.rfind(p)
74 if index != -1:
75 directory = directory[index + len(p):]
76
77 return directory
78
79 # Constants that give real names to the abbreviations in valgrind XML output.
80 INSTRUCTION_POINTER = "ip"
81 OBJECT_FILE = "obj"
82 FUNCTION_NAME = "fn"
83 SRC_FILE_DIR = "dir"
84 SRC_FILE_NAME = "file"
85 SRC_LINE = "line"
86
87 def gatherFrames(node, source_dir):
88 frames = []
89 for frame in node.getElementsByTagName("frame"):
90 frame_dict = {
91 INSTRUCTION_POINTER : getTextOf(frame, INSTRUCTION_POINTER),
92 OBJECT_FILE : getTextOf(frame, OBJECT_FILE),
93 FUNCTION_NAME : getTextOf(frame, FUNCTION_NAME),
94 SRC_FILE_DIR : shortenFilePath(
95 source_dir, getTextOf(frame, SRC_FILE_DIR)),
96 SRC_FILE_NAME : getTextOf(frame, SRC_FILE_NAME),
97 SRC_LINE : getTextOf(frame, SRC_LINE)
98 }
99
100 # Ignore this frame and all the following if it's a "boring" function.
101 enough_frames = False
102 for regexp in _BORING_CALLERS:
103 if re.match("^%s$" % regexp, frame_dict[FUNCTION_NAME]):
104 enough_frames = True
105 break
106 if enough_frames:
107 break
108
109 frames += [frame_dict]
110
111 global TheAddressTable
112 if TheAddressTable != None and frame_dict[SRC_LINE] == "":
113 # Try using gdb
114 TheAddressTable.Add(frame_dict[OBJECT_FILE],
115 frame_dict[INSTRUCTION_POINTER])
116 return frames
117
118 class ValgrindError:
119 ''' Takes a <DOM Element: error> node and reads all the data from it. A
120 ValgrindError is immutable and is hashed on its pretty printed output.
121 '''
122
123 def __init__(self, source_dir, error_node, commandline, testcase):
124 ''' Copies all the relevant information out of the DOM and into object
125 properties.
126
127 Args:
128 error_node: The <error></error> DOM node we're extracting from.
129 source_dir: Prefix that should be stripped from the <dir> node.
130 commandline: The command that was run under valgrind
131 testcase: The test case name, if known.
132 '''
133
134 # Valgrind errors contain one <what><stack> pair, plus an optional
135 # <auxwhat><stack> pair, plus an optional <origin><what><stack></origin>,
136 # plus (since 3.5.0) a <suppression></suppression> pair.
137 # (Origin is nicely enclosed; too bad the other two aren't.)
138 # The most common way to see all three in one report is
139 # a syscall with a parameter that points to uninitialized memory, e.g.
140 # Format:
141 # <error>
142 # <unique>0x6d</unique>
143 # <tid>1</tid>
144 # <kind>SyscallParam</kind>
145 # <what>Syscall param write(buf) points to uninitialised byte(s)</what>
146 # <stack>
147 # <frame>
148 # ...
149 # </frame>
150 # </stack>
151 # <auxwhat>Address 0x5c9af4f is 7 bytes inside a block of ...</auxwhat>
152 # <stack>
153 # <frame>
154 # ...
155 # </frame>
156 # </stack>
157 # <origin>
158 # <what>Uninitialised value was created by a heap allocation</what>
159 # <stack>
160 # <frame>
161 # ...
162 # </frame>
163 # </stack>
164 # </origin>
165 # <suppression>
166 # <sname>insert_a_suppression_name_here</sname>
167 # <skind>Memcheck:Param</skind>
168 # <skaux>write(buf)</skaux>
169 # <sframe> <fun>__write_nocancel</fun> </sframe>
170 # ...
171 # <sframe> <fun>main</fun> </sframe>
172 # <rawtext>
173 # <![CDATA[
174 # {
175 # <insert_a_suppression_name_here>
176 # Memcheck:Param
177 # write(buf)
178 # fun:__write_nocancel
179 # ...
180 # fun:main
181 # }
182 # ]]>
183 # </rawtext>
184 # </suppression>
185 # </error>
186 #
187 # Each frame looks like this:
188 # <frame>
189 # <ip>0x83751BC</ip>
190 # <obj>/data/dkegel/chrome-build/src/out/Release/base_unittests</obj>
191 # <fn>_ZN7testing8internal12TestInfoImpl7RunTestEPNS_8TestInfoE</fn>
192 # <dir>/data/dkegel/chrome-build/src/testing/gtest/src</dir>
193 # <file>gtest-internal-inl.h</file>
194 # <line>655</line>
195 # </frame>
196 # although the dir, file, and line elements are missing if there is
197 # no debug info.
198
199 self._kind = getTextOf(error_node, "kind")
200 self._backtraces = []
201 self._suppression = None
202 self._commandline = commandline
203 self._testcase = testcase
204 self._additional = []
205
206 # Iterate through the nodes, parsing <what|auxwhat><stack> pairs.
207 description = None
208 for node in error_node.childNodes:
209 if node.localName == "what" or node.localName == "auxwhat":
210 description = "".join([n.data for n in node.childNodes
211 if n.nodeType == n.TEXT_NODE])
212 elif node.localName == "xwhat":
213 description = getTextOf(node, "text")
214 elif node.localName == "stack":
215 assert description
216 self._backtraces.append([description, gatherFrames(node, source_dir)])
217 description = None
218 elif node.localName == "origin":
219 description = getTextOf(node, "what")
220 stack = node.getElementsByTagName("stack")[0]
221 frames = gatherFrames(stack, source_dir)
222 self._backtraces.append([description, frames])
223 description = None
224 stack = None
225 frames = None
226 elif description and node.localName != None:
227 # The lastest description has no stack, e.g. "Address 0x28 is unknown"
228 self._additional.append(description)
229 description = None
230
231 if node.localName == "suppression":
232 self._suppression = getCDATAOf(node, "rawtext");
233
234 def __str__(self):
235 ''' Pretty print the type and backtrace(s) of this specific error,
236 including suppression (which is just a mangled backtrace).'''
237 output = ""
238 output += "\n" # Make sure the ### is at the beginning of line.
239 output += "### BEGIN MEMORY TOOL REPORT (error hash=#%016X#)\n" % \
240 self.ErrorHash()
241 if (self._commandline):
242 output += self._commandline + "\n"
243
244 output += self._kind + "\n"
245 for backtrace in self._backtraces:
246 output += backtrace[0] + "\n"
247 filter = subprocess.Popen("c++filt -n", stdin=subprocess.PIPE,
248 stdout=subprocess.PIPE,
249 stderr=subprocess.STDOUT,
250 shell=True,
251 close_fds=True)
252 buf = ""
253 for frame in backtrace[1]:
254 buf += (frame[FUNCTION_NAME] or frame[INSTRUCTION_POINTER]) + "\n"
255 (stdoutbuf, stderrbuf) = filter.communicate(buf.encode('latin-1'))
256 demangled_names = stdoutbuf.split("\n")
257
258 i = 0
259 for frame in backtrace[1]:
260 output += (" " + demangled_names[i])
261 i = i + 1
262
263 global TheAddressTable
264 if TheAddressTable != None and frame[SRC_FILE_DIR] == "":
265 # Try using gdb
266 foo = TheAddressTable.GetFileLine(frame[OBJECT_FILE],
267 frame[INSTRUCTION_POINTER])
268 if foo[0] != None:
269 output += (" (" + foo[0] + ":" + foo[1] + ")")
270 elif frame[SRC_FILE_DIR] != "":
271 output += (" (" + frame[SRC_FILE_DIR] + "/" + frame[SRC_FILE_NAME] +
272 ":" + frame[SRC_LINE] + ")")
273 else:
274 output += " (" + frame[OBJECT_FILE] + ")"
275 output += "\n"
276
277 for additional in self._additional:
278 output += additional + "\n"
279
280 assert self._suppression != None, "Your Valgrind doesn't generate " \
281 "suppressions - is it too old?"
282
283 if self._testcase:
284 output += "The report came from the `%s` test.\n" % self._testcase
285 output += "Suppression (error hash=#%016X#):\n" % self.ErrorHash()
286 output += (" For more info on using suppressions see "
287 "http://dev.chromium.org/developers/tree-sheriffs/sheriff-details -chromium/memory-sheriff#TOC-Suppressing-memory-reports")
288
289 # Widen suppression slightly to make portable between mac and linux
290 # TODO(timurrrr): Oops, these transformations should happen
291 # BEFORE calculating the hash!
292 supp = self._suppression;
293 supp = supp.replace("fun:_Znwj", "fun:_Znw*")
294 supp = supp.replace("fun:_Znwm", "fun:_Znw*")
295 supp = supp.replace("fun:_Znaj", "fun:_Zna*")
296 supp = supp.replace("fun:_Znam", "fun:_Zna*")
297
298 # Make suppressions even less platform-dependent.
299 for sz in [1, 2, 4, 8]:
300 supp = supp.replace("Memcheck:Addr%d" % sz, "Memcheck:Unaddressable")
301 supp = supp.replace("Memcheck:Value%d" % sz, "Memcheck:Uninitialized")
302 supp = supp.replace("Memcheck:Cond", "Memcheck:Uninitialized")
303
304 # Split into lines so we can enforce length limits
305 supplines = supp.split("\n")
306 supp = None # to avoid re-use
307
308 # Truncate at line 26 (VG_MAX_SUPP_CALLERS plus 2 for name and type)
309 # or at the first 'boring' caller.
310 # (https://bugs.kde.org/show_bug.cgi?id=199468 proposes raising
311 # VG_MAX_SUPP_CALLERS, but we're probably fine with it as is.)
312 newlen = min(26, len(supplines));
313
314 # Drop boring frames and all the following.
315 enough_frames = False
316 for frameno in range(newlen):
317 for boring_caller in _BORING_CALLERS:
318 if re.match("^ +fun:%s$" % boring_caller, supplines[frameno]):
319 newlen = frameno
320 enough_frames = True
321 break
322 if enough_frames:
323 break
324 if (len(supplines) > newlen):
325 supplines = supplines[0:newlen]
326 supplines.append("}")
327
328 for frame in range(len(supplines)):
329 # Replace the always-changing anonymous namespace prefix with "*".
330 m = re.match("( +fun:)_ZN.*_GLOBAL__N_.*\.cc_" +
331 "[0-9a-fA-F]{8}_[0-9a-fA-F]{8}(.*)",
332 supplines[frame])
333 if m:
334 supplines[frame] = "*".join(m.groups())
335
336 output += "\n".join(supplines) + "\n"
337 output += "### END MEMORY TOOL REPORT (error hash=#%016X#)\n" % \
338 self.ErrorHash()
339
340 return output
341
342 def UniqueString(self):
343 ''' String to use for object identity. Don't print this, use str(obj)
344 instead.'''
345 rep = self._kind + " "
346 for backtrace in self._backtraces:
347 for frame in backtrace[1]:
348 rep += frame[FUNCTION_NAME]
349
350 if frame[SRC_FILE_DIR] != "":
351 rep += frame[SRC_FILE_DIR] + "/" + frame[SRC_FILE_NAME]
352 else:
353 rep += frame[OBJECT_FILE]
354
355 return rep
356
357 # This is a device-independent hash identifying the suppression.
358 # By printing out this hash we can find duplicate reports between tests and
359 # different shards running on multiple buildbots
360 def ErrorHash(self):
361 return int(hashlib.md5(self.UniqueString()).hexdigest()[:16], 16)
362
363 def __hash__(self):
364 return hash(self.UniqueString())
365 def __eq__(self, rhs):
366 return self.UniqueString() == rhs
367
368 def log_is_finished(f, force_finish):
369 f.seek(0)
370 prev_line = ""
371 while True:
372 line = f.readline()
373 if line == "":
374 if not force_finish:
375 return False
376 # Okay, the log is not finished but we can make it up to be parseable:
377 if prev_line.strip() in ["</error>", "</errorcounts>", "</status>"]:
378 f.write("</valgrindoutput>\n")
379 return True
380 return False
381 if '</valgrindoutput>' in line:
382 # Valgrind often has garbage after </valgrindoutput> upon crash.
383 f.truncate()
384 return True
385 prev_line = line
386
387 class MemcheckAnalyzer:
388 ''' Given a set of Valgrind XML files, parse all the errors out of them,
389 unique them and output the results.'''
390
391 SANITY_TEST_SUPPRESSIONS = {
392 "Memcheck sanity test 01 (memory leak).": 1,
393 "Memcheck sanity test 02 (malloc/read left).": 1,
394 "Memcheck sanity test 03 (malloc/read right).": 1,
395 "Memcheck sanity test 04 (malloc/write left).": 1,
396 "Memcheck sanity test 05 (malloc/write right).": 1,
397 "Memcheck sanity test 06 (new/read left).": 1,
398 "Memcheck sanity test 07 (new/read right).": 1,
399 "Memcheck sanity test 08 (new/write left).": 1,
400 "Memcheck sanity test 09 (new/write right).": 1,
401 "Memcheck sanity test 10 (write after free).": 1,
402 "Memcheck sanity test 11 (write after delete).": 1,
403 "Memcheck sanity test 12 (array deleted without []).": 1,
404 "Memcheck sanity test 13 (single element deleted with []).": 1,
405 "Memcheck sanity test 14 (malloc/read uninit).": 1,
406 "Memcheck sanity test 15 (new/read uninit).": 1,
407 }
408
409 # Max time to wait for memcheck logs to complete.
410 LOG_COMPLETION_TIMEOUT = 180.0
411
412 def __init__(self, source_dir, show_all_leaks=False, use_gdb=False):
413 '''Create a parser for Memcheck logs.
414
415 Args:
416 source_dir: Path to top of source tree for this build
417 show_all_leaks: Whether to show even less important leaks
418 use_gdb: Whether to use gdb to resolve source filenames and line numbers
419 in the report stacktraces
420 '''
421 self._source_dir = source_dir
422 self._show_all_leaks = show_all_leaks
423 self._use_gdb = use_gdb
424
425 # Contains the set of unique errors
426 self._errors = set()
427
428 # Contains the time when the we started analyzing the first log file.
429 # This variable is used to skip incomplete logs after some timeout.
430 self._analyze_start_time = None
431
432
433 def Report(self, files, testcase, check_sanity=False):
434 '''Reads in a set of files and prints Memcheck report.
435
436 Args:
437 files: A list of filenames.
438 check_sanity: if true, search for SANITY_TEST_SUPPRESSIONS
439 '''
440 # Beyond the detailed errors parsed by ValgrindError above,
441 # the xml file contain records describing suppressions that were used:
442 # <suppcounts>
443 # <pair>
444 # <count>28</count>
445 # <name>pango_font_leak_todo</name>
446 # </pair>
447 # <pair>
448 # <count>378</count>
449 # <name>bug_13243</name>
450 # </pair>
451 # </suppcounts
452 # Collect these and print them at the end.
453 #
454 # With our patch for https://bugs.kde.org/show_bug.cgi?id=205000 in,
455 # the file also includes records of the form
456 # <load_obj><obj>/usr/lib/libgcc_s.1.dylib</obj><ip>0x27000</ip></load_obj>
457 # giving the filename and load address of each binary that was mapped
458 # into the process.
459
460 global TheAddressTable
461 if self._use_gdb:
462 TheAddressTable = gdb_helper.AddressTable()
463 else:
464 TheAddressTable = None
465 cur_report_errors = set()
466 suppcounts = defaultdict(int)
467 badfiles = set()
468
469 if self._analyze_start_time == None:
470 self._analyze_start_time = time.time()
471 start_time = self._analyze_start_time
472
473 parse_failed = False
474 for file in files:
475 # Wait up to three minutes for valgrind to finish writing all files,
476 # but after that, just skip incomplete files and warn.
477 f = open(file, "r+")
478 pid = re.match(".*\.([0-9]+)$", file)
479 if pid:
480 pid = pid.groups()[0]
481 found = False
482 running = True
483 firstrun = True
484 skip = False
485 origsize = os.path.getsize(file)
486 while (running and not found and not skip and
487 (firstrun or
488 ((time.time() - start_time) < self.LOG_COMPLETION_TIMEOUT))):
489 firstrun = False
490 f.seek(0)
491 if pid:
492 # Make sure the process is still running so we don't wait for
493 # 3 minutes if it was killed. See http://crbug.com/17453
494 ps_out = subprocess.Popen("ps p %s" % pid, shell=True,
495 stdout=subprocess.PIPE).stdout
496 if len(ps_out.readlines()) < 2:
497 running = False
498 else:
499 skip = True
500 running = False
501 found = log_is_finished(f, False)
502 if not running and not found:
503 logging.warn("Valgrind process PID = %s is not running but its "
504 "XML log has not been finished correctly.\n"
505 "Make it up by adding some closing tags manually." % pid)
506 found = log_is_finished(f, not running)
507 if running and not found:
508 time.sleep(1)
509 f.close()
510 if not found:
511 badfiles.add(file)
512 else:
513 newsize = os.path.getsize(file)
514 if origsize > newsize+1:
515 logging.warn(str(origsize - newsize) +
516 " bytes of junk were after </valgrindoutput> in %s!" %
517 file)
518 try:
519 parsed_file = parse(file);
520 except ExpatError, e:
521 parse_failed = True
522 logging.warn("could not parse %s: %s" % (file, e))
523 lineno = e.lineno - 1
524 context_lines = 5
525 context_start = max(0, lineno - context_lines)
526 context_end = lineno + context_lines + 1
527 context_file = open(file, "r")
528 for i in range(0, context_start):
529 context_file.readline()
530 for i in range(context_start, context_end):
531 context_data = context_file.readline().rstrip()
532 if i != lineno:
533 logging.warn(" %s" % context_data)
534 else:
535 logging.warn("> %s" % context_data)
536 context_file.close()
537 continue
538 if TheAddressTable != None:
539 load_objs = parsed_file.getElementsByTagName("load_obj")
540 for load_obj in load_objs:
541 obj = getTextOf(load_obj, "obj")
542 ip = getTextOf(load_obj, "ip")
543 TheAddressTable.AddBinaryAt(obj, ip)
544
545 commandline = None
546 preamble = parsed_file.getElementsByTagName("preamble")[0];
547 for node in preamble.getElementsByTagName("line"):
548 if node.localName == "line":
549 for x in node.childNodes:
550 if x.nodeType == node.TEXT_NODE and "Command" in x.data:
551 commandline = x.data
552 break
553
554 raw_errors = parsed_file.getElementsByTagName("error")
555 for raw_error in raw_errors:
556 # Ignore "possible" leaks for now by default.
557 if (self._show_all_leaks or
558 getTextOf(raw_error, "kind") != "Leak_PossiblyLost"):
559 error = ValgrindError(self._source_dir,
560 raw_error, commandline, testcase)
561 if error not in cur_report_errors:
562 # We haven't seen such errors doing this report yet...
563 if error in self._errors:
564 # ... but we saw it in earlier reports, e.g. previous UI test
565 cur_report_errors.add("This error was already printed in "
566 "some other test, see 'hash=#%016X#'" % \
567 error.ErrorHash())
568 else:
569 # ... and we haven't seen it in other tests as well
570 self._errors.add(error)
571 cur_report_errors.add(error)
572
573 suppcountlist = parsed_file.getElementsByTagName("suppcounts")
574 if len(suppcountlist) > 0:
575 suppcountlist = suppcountlist[0]
576 for node in suppcountlist.getElementsByTagName("pair"):
577 count = getTextOf(node, "count");
578 name = getTextOf(node, "name");
579 suppcounts[name] += int(count)
580
581 if len(badfiles) > 0:
582 logging.warn("valgrind didn't finish writing %d files?!" % len(badfiles))
583 for file in badfiles:
584 logging.warn("Last 20 lines of %s :" % file)
585 os.system("tail -n 20 '%s' 1>&2" % file)
586
587 if parse_failed:
588 logging.error("FAIL! Couldn't parse Valgrind output file")
589 return -2
590
591 common.PrintUsedSuppressionsList(suppcounts)
592
593 retcode = 0
594 if cur_report_errors:
595 logging.error("FAIL! There were %s errors: " % len(cur_report_errors))
596
597 if TheAddressTable != None:
598 TheAddressTable.ResolveAll()
599
600 for error in cur_report_errors:
601 logging.error(error)
602
603 retcode = -1
604
605 # Report tool's insanity even if there were errors.
606 if check_sanity:
607 remaining_sanity_supp = MemcheckAnalyzer.SANITY_TEST_SUPPRESSIONS
608 for (name, count) in suppcounts.iteritems():
609 # Workaround for http://crbug.com/334074
610 if (name in remaining_sanity_supp and
611 remaining_sanity_supp[name] <= count):
612 del remaining_sanity_supp[name]
613 if remaining_sanity_supp:
614 logging.error("FAIL! Sanity check failed!")
615 logging.info("The following test errors were not handled: ")
616 for (name, count) in remaining_sanity_supp.iteritems():
617 logging.info(" * %dx %s" % (count, name))
618 retcode = -3
619
620 if retcode != 0:
621 return retcode
622
623 logging.info("PASS! No errors found!")
624 return 0
625
626
627 def _main():
628 '''For testing only. The MemcheckAnalyzer class should be imported instead.'''
629 parser = optparse.OptionParser("usage: %prog [options] <files to analyze>")
630 parser.add_option("", "--source-dir",
631 help="path to top of source tree for this build"
632 "(used to normalize source paths in baseline)")
633
634 (options, args) = parser.parse_args()
635 if len(args) == 0:
636 parser.error("no filename specified")
637 filenames = args
638
639 analyzer = MemcheckAnalyzer(options.source_dir, use_gdb=True)
640 return analyzer.Report(filenames, None)
641
642
643 if __name__ == "__main__":
644 sys.exit(_main())
OLDNEW
« no previous file with comments | « tools_webrtc/valgrind/locate_valgrind.sh ('k') | tools_webrtc/valgrind/valgrind.gni » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698