# 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);
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) {
    waitpid ($pid, 0);
    if ($?>>8 != 0) { warn "census subproc died: $?"; }
    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); }

  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;
