blob: 25a62b48dd5c24bdd94d2260f1e5d4ae12a53f68 [file] [log] [blame]
# MemoryDump - save summaries of memory usage to files.
#
# Normally not "use"d by any code, purely for manual debugging.
# To use, pepper code with this:
#
# use Mail::SpamAssassin::Util::MemoryDump; Mail::SpamAssassin::Util::MemoryDump::MEMDEBUG();
#
# or:
#
# use Mail::SpamAssassin::Util::MemoryDump; Mail::SpamAssassin::Util::MemoryDump::MEMDEBUG_dump_obj();
#
# and run script with MEMDEBUG=1 set in the environment;
# "MEMDEBUG=1 spamassassin -Lt", for example.
#
# Each MEMDEBUG() statement will produce a file in a 'dumps' subdirectory.
#
# <@LICENSE>
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to you under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at:
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# </@LICENSE>
package Mail::SpamAssassin::Util::MemoryDump;
use strict;
use warnings;
# use bytes;
use re 'taint';
BEGIN {
use Exporter ();
our @ISA = qw(Exporter);
our @EXPORT_OK = qw(MEMDEBUG MEMDEBUG_dump_obj);
}
our $COUNTER = 0;
use Devel::Peek qw();
use Devel::Size qw(size total_size);
use Mail::SpamAssassin::Util qw(proc_status_ok exit_status_str);
eval q{ use Devel::Gladiator; };
###########################################################################
sub MEMDEBUG {
return unless $ENV{'MEMDEBUG'};
census_arena();
}
sub MEMDEBUG_dump_obj {
return unless $ENV{'MEMDEBUG'};
dump_obj(@_);
}
###########################################################################
sub census_arena {
# lots of good stuff nicked from bradfitz@SixApart's djabberd
warn "MEMDEBUG: census arena start\n";
my $name = new_dump_filename("census");
# do this in a subprocess, since it leaks refs to all objects!
my $pid = fork();
if ($pid) {
my $child_stat = waitpid($pid,0) > 0 ? $? : undef;
proc_status_ok($child_stat)
or warn "census subproc: ".exit_status_str($child_stat);
return;
}
# we are now in a subprocess
open (DUMP, ">$name") or warn "cannot write to $name";
my ($x, $y, $c, $subroutine, $d) = caller(2);
my ($e, $filename, $line, $f) = caller(1);
print DUMP "${subroutine}()\n";
print DUMP "$filename line: $line\n";
print DUMP "\nMEMDEBUG: census_arena:
(some values may be 0 due to bugs in Devel::Size etc.; this tends to be buggy)
";
my %objcount;
my %size;
eval {
my $all = Devel::Gladiator::walk_arena();
%objcount = ();
%size = ();
# be selective and don't use Devel::Size on GLOBs and some other ref
# types, it coredumps on several of them (perl 5.8.8, linux)
my $s;
foreach my $val (@$all) {
if (ref $val eq 'REF') {
$objcount{ref ${$val}}++;
$size{ref $val} += get_obj_size($val);
}
elsif (ref $val eq 'CODE') {
eval {
$objcount{Devel::Peek::CvGV($val)}++;
# $size{ref $val} += Devel::Size::size($val);
};
}
elsif (ref $val eq 'Regexp') {
$size{ref $val} += get_obj_size($val);
}
elsif (ref $val eq 'HASH') {
$size{ref $val} += get_obj_size($val);
}
elsif (ref $val eq 'ARRAY') {
$size{ref $val} += get_obj_size($val);
}
elsif (ref $val eq 'SCALAR') {
$size{ref $val} += get_obj_size($val);
}
$objcount{ref $val}++;
}
1;
} or do {
my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
warn "census: $eval_stat\n";
};
foreach my $id (sort { $objcount{$b} <=> $objcount{$a} } keys %objcount) {
my $c = $objcount{$id};
my $s = $size{$id} || 0;
next unless ($c > 10 || $s > 1024*256);
print DUMP "$c $s $id\n";
}
my $ps = `ps lxww`; $ps =~ /^(.*? $$ .*)$/m;
print DUMP "\n$1\n";
close DUMP or warn "close failed";
warn "MEMDEBUG: census arena end: wrote to $name\n";
exit; # fork over!
}
sub get_obj_size {
my $s = Devel::Size::size($_[0]);
# argh -- ignore buggy items
if ($s < 0 || $s > 10000000) { return 0; }
return $s;
}
###########################################################################
sub dump_obj {
my $obj = shift;
warn "MEMDEBUG_dump_obj start\n";
my $name = new_dump_filename("obj");
open (DUMP, ">$name") or warn "cannot write to $name";
my ($x, $y, $c, $subroutine, $d) = caller(2);
my ($e, $filename, $line, $f) = caller(1);
print DUMP "${subroutine}()\n";
print DUMP "$filename line: $line\n";
print DUMP "MEMDEBUG_dump_obj:\n";
eval {
use Data::Dumper;
$Data::Dumper::Purity = 0;
$Data::Dumper::Terse = 1;
my $dump = Dumper($obj);
$dump =~ s/ {8}/ /gs;
print DUMP $dump;
1;
} or do {
my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
warn "dump: $eval_stat\n";
};
close DUMP or warn "close failed";
warn "MEMDEBUG_dump_obj end: wrote to $name\n";
}
###########################################################################
sub new_dump_filename {
my $type = shift;
if (!-d "dumps") {
mkdir("dumps", 0777) or warn "dump: cannot create a directory: $!";
}
my ($e, $filename, $line, $f) = caller(2);
$filename =~ s/^.*[\/\\]//gs;
$filename =~ s/[^A-Za-z0-9\.]/_/gs;
$COUNTER++;
my $str = sprintf("dumps/%06d.%06d.%s_%d.%s", $$, $COUNTER,
$filename, $line, $type);
return $str;
}
1;