#!/usr/bin/perl # Copyright (C) 2007 Apple Inc. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of # its contributors may be used to endorse or promote products derived # from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # Script to run the Mac OS X leaks tool with more expressive '-exclude' lists. use strict; use warnings; use File::Basename; use Getopt::Long; sub runLeaks($); sub parseLeaksOutput(\@); sub removeMatchingRecords(\@$\@); sub reportError($); sub main() { # Read options. my $usage = "Usage: " . basename($0) . " [options] pid | executable name\n" . " --exclude-callstack regexp Exclude leaks whose call stacks match the regular expression 'regexp'.\n" . " --exclude-type regexp Exclude leaks whose data types match the regular expression 'regexp'.\n" . " --help Show this help message.\n"; my @callStacksToExclude = (); my @typesToExclude = (); my $help = 0; my $getOptionsResult = GetOptions( 'exclude-callstack:s' => \@callStacksToExclude, 'exclude-type:s' => \@typesToExclude, 'help' => \$help ); my $pidOrExecutableName = $ARGV[0]; if (!$getOptionsResult || $help) { print STDERR $usage; return 1; } if (!$pidOrExecutableName) { reportError("Missing argument: pid | executable."); print STDERR $usage; return 1; } # Run leaks tool. my $leaksOutput = runLeaks($pidOrExecutableName); if (!$leaksOutput) { return 1; } my $leakList = parseLeaksOutput(@$leaksOutput); if (!$leakList) { return 1; } # Filter output. my $leakCount = @$leakList; removeMatchingRecords(@$leakList, "callStack", @callStacksToExclude); removeMatchingRecords(@$leakList, "type", @typesToExclude); my $excludeCount = $leakCount - @$leakList; # Dump results. print $leaksOutput->[0]; print $leaksOutput->[1]; foreach my $leak (@$leakList) { print $leak->{"leaksOutput"}; } if ($excludeCount) { print "$excludeCount leaks excluded (not printed)\n"; } return 0; } exit(main()); # Returns the output of the leaks tool in list form. sub runLeaks($) { my ($pidOrExecutableName) = @_; my @leaksOutput = `leaks $pidOrExecutableName`; if (!@leaksOutput) { reportError("Error running leaks tool."); return; } return \@leaksOutput; } # Returns a list of hash references with the keys { address, size, type, callStack, leaksOutput } sub parseLeaksOutput(\@) { my ($leaksOutput) = @_; # Format: # Process 00000: 1234 nodes malloced for 1234 KB # Process 00000: XX leaks for XXX total leaked bytes. # Leak: 0x00000000 size=1234 [instance of 'blah'] # 0x00000000 0x00000000 0x00000000 0x00000000 a..d.e.e # ... # Call stack: leak_caller() | leak() | malloc # # We treat every line except for Process 00000: and Leak: as optional # Newer versions of the leaks output have a header section at the top, with the first line describing the version of the output format. # If we detect the new format is being used then we eat all of the header section so the output matches the format of older versions. # FIXME: In the future we may wish to propagate this section through to our output. if ($leaksOutput->[0] =~ /^leaks Report Version:/) { while ($leaksOutput->[0] !~ /^Process /) { shift @$leaksOutput; } } my ($leakCount) = ($leaksOutput->[1] =~ /[[:blank:]]+([0-9]+)[[:blank:]]+leaks?/); if (!defined($leakCount)) { reportError("Could not parse leak count reported by leaks tool."); return; } my @leakList = (); for my $line (@$leaksOutput) { next if $line =~ /^Process/; next if $line =~ /^node buffer added/; if ($line =~ /^Leak: /) { my ($address) = ($line =~ /Leak: ([[:xdigit:]x]+)/); if (!defined($address)) { reportError("Could not parse Leak address."); return; } my ($size) = ($line =~ /size=([[:digit:]]+)/); if (!defined($size)) { reportError("Could not parse Leak size."); return; } my ($type) = ($line =~ /'([^']+)'/); #' if (!defined($type)) { $type = ""; # The leaks tool sometimes omits the type. } my %leak = ( "address" => $address, "size" => $size, "type" => $type, "callStack" => "", # The leaks tool sometimes omits the call stack. "leaksOutput" => $line ); push(@leakList, \%leak); } else { $leakList[$#leakList]->{"leaksOutput"} .= $line; if ($line =~ /Call stack:/) { $leakList[$#leakList]->{"callStack"} = $line; } } } if (@leakList != $leakCount) { my $parsedLeakCount = @leakList; reportError("Parsed leak count($parsedLeakCount) does not match leak count reported by leaks tool($leakCount)."); return; } return \@leakList; } sub removeMatchingRecords(\@$\@) { my ($recordList, $key, $regexpList) = @_; RECORD: for (my $i = 0; $i < @$recordList;) { my $record = $recordList->[$i]; foreach my $regexp (@$regexpList) { if ($record->{$key} =~ $regexp) { splice(@$recordList, $i, 1); next RECORD; } } $i++; } } sub reportError($) { my ($errorMessage) = @_; print STDERR basename($0) . ": $errorMessage\n"; }