# B::TerseSize.pm
# Copyright (c) 1999 Doug MacEachern. All rights reserved.
# This module is free software; you can redistribute and/or modify
# it under the same terms as Perl itself.
# portions of this module are based on B::Terse, by Malcolm Beattie
package B::TerseSize;
use strict;
use constant IS_MODPERL => $ENV{MOD_PERL};
use constant MP2 => $ENV{MOD_PERL_API_VERSION} == 2 ? 1 : 0;
use B ();
use B::Asmdata qw(@specialsv_name);
use B::Size ();
{
no strict;
$VERSION = '0.09';
}
my $opcount;
my $opsize;
my $copsize;
my $curcop;
sub UNIVERSAL::op_size {
$opcount++;
my $size = shift->size;
$opsize += $size;
$copsize += $size;
}
my $mouse_attr =
qq( onclick='javascript: return false') .
qq( onmouseout='window.status=""; return true');
sub op_html_name {
my($op, $sname) = @_;
$sname =~ s/(\s+)$//;
my $pad = $1 || "";
my $desc = sprintf
qq(onmouseover='window.status="%s"; return true'),
B::OP::op_desc($op->type) || "unknown";
my $href = $curcop ? $curcop->line : "";
return qq(<a $desc $mouse_attr href="#$href">$sname</a>$pad);
}
sub peekop {
my $op = shift;
my $size = $op->size;
$opcount++;
$opsize += $size;
$copsize += $size;
my $name;
my $sname = sprintf "%-13s", $op->name;
if (IS_MODPERL) {
$name = op_html_name($op, $sname);
}
else {
$name = $sname;
}
my $addr = sprintf "0x%lx", $$op;
$addr = qq(<a name="$addr">$addr</a>) if IS_MODPERL;
return sprintf qq(%-6s $name $addr {%d bytes}),
B::class($op), $size;
}
my $hr = "=" x 60;
my %filelex = ();
sub package_size {
my($package) = @_;
#local *UNIVERSAL::op_size = \&universal_op_size;
my %retval = ();
my $total_opsize = 0;
my $total_opcount = 0;
my $stash;
{
no strict;
$stash = \%{"$package\::"};
}
for (keys %$stash) {
my $name = $package . "::$_";
my $has_code = 0;
{
no strict;
$has_code = *{$name}{CODE}; #defined() expects CvROOT || CvXSUB
}
unless ($has_code) { #CV_walk will measure
$total_opsize +=
B::Sizeof::GV + B::Sizeof::XPVGV + B::Sizeof::GP;
}
#measure global variables
for my $type (qw(ARRAY HASH SCALAR)) {
no strict;
next if $name =~ /::$/; #stash
next unless /^[\w_]/;
next if /^_</;
my $ref = *{$name}{$type};
next unless $ref;
my $obj = B::svref_2object($ref);
next if ref($obj) eq 'B::NULL';
my $tsize = $obj->size;
$total_opsize += $tsize;
$retval{"*${_}{$type}"} = {'size' => $tsize};
}
next unless defined $has_code;
CV_walk('slow', $name, 'op_size');
for (keys %{ $filelex{$package} }) {
my $fsize = $filelex{$package}->{$_};
$total_opsize += $opsize;
$retval{"my ${_} = ...;"} =
{'size' => $fsize};
}
%filelex = ();
$total_opsize += $opsize;
$total_opcount += $opcount;
$retval{$_} = {'count' => $opcount, 'size' => $opsize};
}
return (\%retval, $total_opcount, $total_opsize);
}
my $b_objsym = \&B::objsym;
sub objsym {
my $obj = shift;
my $value = $b_objsym->($obj);
return unless $value;
sprintf qq(<a href="#0x%lx">$value</a>), $$obj;
}
sub CV_walk {
my($order, $objname, $meth) = @_;
$meth ||= 'terse_size';
my $cvref = \&{$objname};
my $cv = B::svref_2object($cvref);
my($package, $func) = ($objname =~ /(.*)::([^:]+)$/);
$opsize = B::Sizeof::GV + B::Sizeof::XPVGV + B::Sizeof::GP;
$opcount = 0;
$curcop = "";
my $gv = $cv->GV;
$opsize += length $gv->NAME;
if (my $stash = $cv->is_alias($package)) {
return;
}
$opsize += B::Sizeof::XPVCV;
$opsize += B::Sizeof::SV;
if ($cv->FLAGS & B::SVf_POK) {
$opsize += B::Sizeof::XPV + length $cv->PV;
}
else {
$opsize += B::Sizeof::XPVIV; #IVX == -1 for no prototype
}
init_curpad_names($cvref);
no strict;
local *B::objesym = \&objsym if IS_MODPERL;
if ($order eq 'exec') {
B::walkoptree_exec($cv->START, $meth);
} else {
B::walkoptree_slow($cv->ROOT, $meth);
}
curcop_info() if $curcop;
my($padsize, $padsummary) = PADLIST_size($cv);
$opsize += $padsize;
$padsummary;
}
sub terse_size {
my($order, $objname) = @_;
my $padsummary = CV_walk($order, $objname);
print "\n$hr\nTotals: $opsize bytes | $opcount OPs\n$hr\n";
if ($padsummary) {
print "\nPADLIST summary:\n";
print @$padsummary;
}
}
my @curpad_names = ();
sub init_curpad_names {
my $cv = B::svref_2object(shift);
my $padlist = $cv->PADLIST;
return if ref($padlist) eq 'B::SPECIAL';
@curpad_names = ($padlist->ARRAY)[0]->ARRAY;
}
sub compile {
my $order = shift;
my @options = @_;
B::clearsym() if defined &B::clearsym;
if (@options) {
return sub {
my $objname;
foreach $objname (@options) {
$objname = "main::$objname" unless $objname =~ /::/;
terse_size($order, $objname);
}
}
} else {
if ($order eq "exec") {
return sub { B::walkoptree_exec(B::main_start, "terse_size");
curcop_info() if $curcop}
} else {
return sub { B::walkoptree_slow(B::main_root, "terse_size");
curcop_info() if $curcop}
}
}
}
sub indent {
my $level = shift;
return " " x $level;
}
#thanks B::Deparse
sub padname {
my $obj = shift;
return '?' unless ref $obj;
my $str = $obj->PV;
my $ix = index($str, "\0");
$str = substr($str, 0, $ix) if $ix != -1;
return $str;
}
sub B::OP::terse_size {
my ($op, $level) = @_;
my $t = $op->targ;
my $targ = "";
if ($t > 0) {
my $name = B::OP::op_name($op->targ);
my $desc = B::OP::op_desc($op->targ);
if ($op->type == 0) { #OP_NULL
$targ = $name eq $desc ? " [$name]" :
sprintf " [%s - %s]", $name, $desc;
}
else {
$targ = sprintf " [targ %d - %s]", $t,
padname($curpad_names[$t]);
}
}
print indent($level), peekop($op), $targ, "\n";
}
sub B::SVOP::terse_size {
my ($op, $level) = @_;
print indent($level), peekop($op), " ";
$op->sv->terse_size(0);
}
sub B::GVOP::terse_size {
my ($op, $level) = @_;
print indent($level), peekop($op), " ";
$op->gv->terse_size(0);
}
sub B::PMOP::terse_size {
my ($op, $level) = @_;
my $precomp = $op->precomp;
print indent($level), peekop($op),
(defined($precomp) ? " /$precomp/\n" : " (regexp not compiled)\n");
}
sub B::PVOP::terse_size {
my ($op, $level) = @_;
print indent($level), peekop($op), " ", B::cstring($op->pv), "\n";
}
my $hr2 = "-" x 60;
*cop_file = B::COP->can('file') || sub {
shift->filegv->SV->PV;
};
sub curcop_info {
my $line = $curcop->line;
my $linestr = "line $line";
if ($line > 0 && IS_MODPERL) {
my $anchor = "";
if ($line > 10) {
$anchor = "#" . ($line - 10);
}
my $window = sprintf "offset=%d&len=%d", $line - 100, $line + 100;
my $args = sprintf "noh_fileline=1&filename=%s&line=%d&$window",
cop_file($curcop), $line;
my $uri = MP2 ? Apache2::RequestUtil->request()->location() : Apache->request->location;
$linestr = qq(<a name="$line" target=top href="$uri?$args$anchor">$linestr</a>);
}
print "\n[$linestr size: $copsize bytes]\n";
}
sub B::COP::terse_size {
my ($op, $level) = @_;
my $label = $op->label || "";
if ($label) {
$label = " label ".B::cstring($label);
}
curcop_info() if $curcop;
$copsize = 0;
$curcop = $op;
print "\n$hr2\n", indent($level), peekop($op), "$label\n";
}
sub B::PV::terse_size {
my ($sv, $level) = @_;
print indent($level);
my $pv = B::cstring($sv->PV);
B::Size::escape_html(\$pv) if IS_MODPERL;
printf "%s %s\n", B::class($sv), $pv;
}
sub B::AV::terse_size {
my ($sv, $level) = @_;
print indent($level);
printf "%s FILL %d\n", B::class($sv), $sv->FILL;
}
sub B::GV::terse_size {
my ($gv, $level) = @_;
my $stash = $gv->STASH->NAME;
if ($stash eq "main") {
$stash = "";
} else {
$stash = $stash . "::";
}
print indent($level);
printf "%s *%s%s\n", B::class($gv), $stash, $gv->NAME;
}
sub B::IV::terse_size {
my ($sv, $level) = @_;
print indent($level);
printf "%s %d\n", B::class($sv), $sv->IV;
}
sub B::NV::terse_size {
my ($sv, $level) = @_;
print indent($level);
printf "%s %s\n", B::class($sv), $sv->NV;
}
sub B::RV::terse_size {
my ($sv, $level) = @_;
print indent($level);
printf "%s \n", B::class($sv);
}
sub B::NULL::terse_size {
my ($sv, $level) = @_;
print indent($level);
printf "%s \n", B::class($sv);
}
sub B::SPECIAL::terse_size {
my ($sv, $level) = @_;
print indent($level);
printf "%s #%d %s\n", B::class($sv), $$sv, $specialsv_name[$$sv];
}
my $padname_max = 0;
sub PADLIST_size {
my $cv = shift;
my $obj = UNIVERSAL::isa($cv, "B::CV") ? $cv : B::svref_2object($cv);
my $size = (B::Sizeof::AV + B::Sizeof::XPVAV) * 3; #padlist, names, values
if ($obj->PADLIST->isa('B::SPECIAL')) {
return B::Sizeof::AV; #XXX???
}
my($padnames, $padvals) = $obj->PADLIST->ARRAY;
my @names = $padnames->ARRAY;
$padname_max = 0;
my @names_pv = map {
my $pv = padname($_);
$padname_max = length($pv) > $padname_max ?
length($pv) : $padname_max;
$pv;
} @names;
my @vals = $padvals->ARRAY;
my $fill = $padnames->FILL;
my $fill_len = length $fill;
my @retval = ();
my $wantarray = wantarray;
for (my $i = 0; $i <= $fill; $i++) {
my $entsize = $names[$i]->size;
my $is_fake = $names[$i]->FLAGS & B::SVf_FAKE;
if ($is_fake) {
$entsize += B::Sizeof::SV; # just a reference to outside scope
if (B::class($obj->OUTSIDE->GV) eq 'SPECIAL') {
$filelex{ $obj->GV->STASH->NAME }->{ $names_pv[$i] } =
$vals[$i]->size;
}
else {
#XXX nested/anonsubs
}
}
else {
$entsize += $vals[$i]->size;
}
$size += $entsize;
next unless $wantarray;
my $class = B::class($vals[$i]);
my $byteinfo = sprintf "[%-4s %3d bytes]",
$class, $entsize;
no warnings;
push @retval, sprintf "%${fill_len}d: %${padname_max}s %s %s\n",
$i,
$names_pv[$i],
$byteinfo,
$is_fake ? '__SvFAKE__' : $vals[$i]->sizeval;
}
return $wantarray ? ($size, \@retval) : $size;
}
#hmm, I wonder if B::Deparse could be used instead
sub Apache::Status::noh_fileline {
my $r = shift;
my %args = $r->args;
my $offset = $args{offset} || 0;
my $len = $args{len} || 0;
local *FH;
my $filename = $args{filename};
$r->send_http_header('text/html');
unless (Apache::Status::status_config($r, "StatusTerseSize")) {
print "sorry, StatusTerseSize not enabled\n";
return 0;
}
unless (exists $main::{"_<$filename"}) {
#useithreads doesnt gv_fetchfile()
my $in_inc = 0;
for (keys %INC) {
if ($INC{$_} eq $filename) {
$in_inc = 1;
$main::{"_<$filename"} = $_;
last;
}
}
unless ($in_inc) {
print "sorry, `$filename' is not a file used by Perl\n";
return 0;
}
}
my $i = 0;
$r->print('<pre>');
if ($offset > 0) {
printf "%4d..%d [...]\n", 1, $offset-1;
}
open FH, $filename or die $!;
while (<FH>) {
$i++;
next if $len > 0 and $i > $len;
next if $offset > 0 and $i < $offset;
chomp;
s/^\t/ /; #indent proper
my $lineno = sprintf "%4d", $i;
B::Size::escape_html(\$_);
my $line = ($i == $args{line}) ?
\qq(<font color="#FF0000">$_</font>) : \$_;
print qq($lineno: <a name=$i>$$line</a>\n);
}
if ($len > 0 and $i > $len) {
printf "%4d..%d [...]\n", $len+1, $i;
}
close FH;
0;
}
sub Apache2::Status::noh_fileline {
my $r = shift;
my $args = $r->args;
require CGI;
my $CGI = CGI->new($args);
my %params = map { $_ => $CGI->param($_) } $CGI->param();
my $offset = $params{offset} || 0;
my $len = $params{len} || 0;
local *FH;
my $filename = $params{filename};
$r->content_type('text/html');
unless (Apache2::Status::status_config($r, "StatusTerseSize")) {
print "sorry, StatusTerseSize not enabled\n";
return 0;
}
unless (exists $main::{"_<$filename"}) {
#useithreads doesnt gv_fetchfile()
my $in_inc = 0;
for (keys %INC) {
if ($INC{$_} eq $filename) {
$in_inc = 1;
$main::{"_<$filename"} = $_;
last;
}
}
unless ($in_inc) {
print "sorry, '$filename' is not a file used by Perl\n";
return 0;
}
}
my $i = 0;
$r->print('<pre>');
if ($offset > 0) {
printf "%4d..%d [...]\n", 1, $offset-1;
}
open FH, $filename or die $!;
while (<FH>) {
$i++;
next if $len > 0 and $i > $len;
next if $offset > 0 and $i < $offset;
chomp;
s/^\t/ /; #indent proper
my $lineno = sprintf "%4d", $i;
B::Size::escape_html(\$_);
my $line = ($i == $params{line}) ?
\qq(<font color="#FF0000">$_</font>) : \$_;
print qq($lineno: <a name=$i>$$line</a>\n);
}
if ($len > 0 and $i > $len) {
printf "%4d..%d [...]\n", $len+1, $i;
}
close FH;
0;
}
sub max {
my($cur, $maybe) = @_;
$maybe > $cur ? $maybe : $cur;
}
my %summary_cache = ();
sub apache_package_size {
my $package = shift;
my($subs, $opcount, $opsize);
my $keys = 0;
my $cache = {};
{
no strict 'refs';
$keys = keys %{"$package\::"};
}
if ($cache = $summary_cache{$package}) {
if ($cache->{'keys'} == $keys) {
return @{ $cache->{'data'} } if $cache->{'data'};
}
}
$cache->{'keys'} = $keys;
$summary_cache{$package} = $cache;
@{ $cache->{'data'} } = B::TerseSize::package_size($package);
}
sub status_memory_usage {
my($r, $q) = @_;
if (MP2) {
unless (Apache2::Status::status_config($r, "StatusTerseSize")) {
return ["StatusTerseSize is not enabled"];
}
}
else {
unless (Apache::Status::status_config($r, "StatusTerseSize")) {
return ["StatusTerseSize is not enabled"];
}
}
unless ($r->dir_config("StatusTerseSizeMainSummary")) {
return ["StatusTerseSizeMainSummary is not enabled"];
}
my $script = MP2 ? $r->uri : $q->script_name;
my $stab = Devel::Symdump->rnew('main');
my %total;
my @retval = ('<pre>');
my($clen, $slen, $nlen);
for my $package ('main', $stab->packages) {
my($subs, $opcount, $opsize) = apache_package_size($package);
$total{$package} = {'count' => $opcount, 'size' => $opsize};
$nlen = max($nlen, length $package);
$slen = max($slen, length $opsize);
$clen = max($clen, length $opcount);
}
for (sort { $total{$b}->{size} <=> $total{$a}->{size} } keys %total) {
my $link = qq(<a href="$script/$_?noh_b_package_size">);
push @retval,
sprintf "$link%-${nlen}s</a> %${slen}d bytes | %${clen}d OPs\n",
$_, $total{$_}->{size}, $total{$_}->{count};
}
\@retval;
}
if (MP2) {
Apache2::Status->menu_item(
'status_memory_usage' => "Memory Usage",
\&status_memory_usage,
);
}
elsif (IS_MODPERL and Apache->module("Apache::Status")) {
Apache::Status->menu_item(
'status_memory_usage' => "Memory Usage",
\&status_memory_usage,
);
}
1;
__END__
=head1 NAME
B::TerseSize - Printing info about ops and their (estimated) size
=head1 SYNOPSIS
perl -MO=TerseSize[,OPTIONS] foo.pl
=head1 DESCRIPTION
The I<B::Size> and I<B::TerseSize> modules attempt to measure the size
of Perl op codes. The output of B<B::TerseSize> is similar to that of
I<B::Terse>, but includes the size of each OP in the tree and the
PADLIST (subroutine lexical variables). The module can be run just as
other compiler backends or used via I<Apache::Status> (version 2.02
and higher).
If the I<Apache::Status> I<StatusTerseSize> option is enabled, there
will be a main menu item added, "Memory Usage". Clicking on this link
will cause I<B::TerseSize> to produce a summary of package memory
usage. This summary can take quite a while to produce, as each
package subroutine syntax tree will be walked, adding up the
information. This information will be cached, so running httpd in
I<-X> (non-forking mode) is a good choice.
When browsing the Apache::Status "Symbol Table Dump", a "Memory
Usage" link will be at the bottom of each page. These summaries
also include measurements of package global variables.
The Apache::Status symbol table browser will also provide an option to
dump a subroutine tree along with the other subroutine options.
=head1 CAVEATS
The memory measurements are only an estimate. But, chances are, if a
measurement is not accurate, it is smaller than the actual size.
The "execution order" option under Apache::Status can only be run once
unless you are using Perl 5.6.0+ or apply the
I<patches/b_clearsym_60.pat> to older Perls.
=head1 SEE ALSO
B(3), B::Size(3), B::LexInfo(3), Apache::Status(3)
=head1 AUTHOR
Currently Maintained by Philip M. Gollucci
Previously Developed by Doug MacEachern
based in part on B::Terse by Malcolm Beattie
=cut
syntax highlighted by Code2HTML, v. 0.9.1