DateTime-Lite

 view release on metacpan or  search on metacpan

scripts/benchmark.pl  view on Meta::CPAN

#!/usr/bin/env perl
##----------------------------------------------------------------------------
## DateTime::Lite - scripts/benchmark.pl
##
## Compares DateTime and DateTime::Lite on four axes:
##   - Module count loaded into %INC
##   - Declared runtime prerequisites
##   - CPU / wall-clock time for common operations
##   - Resident memory (RSS) after loading
##
## Architecture: the parent process forks before loading any module.
## Each child loads only its own module set, measures what it needs,
## serialises results to a temp file, and exits.  The parent aggregates
## and prints.  This guarantees a clean %INC for every measurement,
## avoiding false results caused by shared module state.
##
## Usage:
##   perl -Iblib/lib -Iblib/arch scripts/benchmark.pl [--iterations N]
##   perl -Iblib/lib -Iblib/arch scripts/benchmark.pl --csv
##
## DateTime must be installed on the system.
## DateTime::Lite is loaded from blib/ (run `make` first).
##----------------------------------------------------------------------------
use v5.10.1;
use strict;
use warnings;

# These modules are loaded BEFORE any fork so they are available in every
# child via copy-on-write.  They must not pull in DateTime or DateTime::Lite.
use POSIX         qw( WIFEXITED WEXITSTATUS );
use File::Temp    qw( tempfile );
use Getopt::Long  qw( GetOptions );
use Scalar::Util  qw( looks_like_number );

my $ITERATIONS = 10_000;
my $CSV        = 0;
GetOptions(
    'iterations=i' => \$ITERATIONS,
    'csv'          => \$CSV,
) or die "Usage: $0 [--iterations N] [--csv]\n";

# Snapshot @INC before any fork so children inherit it correctly.
my @INC_SNAP = @INC;

# ---------------------------------------------------------------------------
# IPC helpers
# ---------------------------------------------------------------------------

# Run a closure in a child process.
# The closure receives a CODE ref it must call with a hashref of results.
# The parent waits and returns that hashref.
sub in_fork
{
    my $code = shift;

    my( $fh, $fname ) = tempfile( UNLINK => 1 );
    close $fh;

    my $pid = fork();
    die "fork() failed: $!" unless defined $pid;

    if( $pid == 0 )
    {
        # Child: restore @INC so blib paths are available
        @INC = @INC_SNAP;

        # Run measurement, get results hashref
        my $results = {};
        eval { $code->( $results ) };
        if( $@ )
        {
            warn "Child error: $@";
            exit 1;
        }

        # Serialise to temp file and exit cleanly
        require JSON;
        open my $out, '>', $fname or die "Cannot write $fname: $!";
        print $out JSON->new->encode( $results );
        close $out;
        exit 0;
    }

    # Parent: wait for child
    waitpid( $pid, 0 );
    my $status = $?;
    die "Child exited with error status $status\n"
        unless WIFEXITED($status) && WEXITSTATUS($status) == 0;

    require JSON;
    open my $in, '<', $fname or die "Cannot read $fname: $!";
    return JSON->new->decode( do { local $/; <$in> } );
}

# ---------------------------------------------------------------------------
# Measurement helpers (run inside children)
# ---------------------------------------------------------------------------

sub _rss_kb
{

scripts/benchmark.pl  view on Meta::CPAN

        printf "  %-46s  %9s  %9s  %s\n",
            $label, $dt_val, $dtl_val, $unit;
    }
}

sub section { print "\n--- $_[0] ---\n" unless $CSV }
sub header
{
    return if $CSV;
    printf "  %-46s  %9s  %9s  %s\n",
        'Operation', 'DateTime', 'DTL', 'unit';
    printf "  %s\n", '-' x 78;
}

# ---------------------------------------------------------------------------
# Output
# ---------------------------------------------------------------------------

unless( $CSV )
{
    print "=" x 80, "\n";
    print "DateTime vs DateTime::Lite  -  benchmark\n";
    printf "  DateTime version:       %s\n", $dt->{version}  // '?';
    printf "  DateTime::Lite version: %s\n", $dtl->{version} // '?';
    printf "  Perl:                   %s (%s)\n", $], $^O;
    printf "  Iterations:             %d\n", $ITERATIONS;
    printf "  Platform:               %s\n", (POSIX::uname())[4] // '?';
    print "=" x 80, "\n";
}

my $N = $ITERATIONS;

section( 'Module count  (entries in %INC after loading, measured in clean fork)' );
header();
row( 'use Module',         $dt->{mods_full},    $dtl->{mods_full},   'modules' );
row( 'use TimeZone alone', $dt_tz->{mods_tz},   $dtl_tz->{mods_tz},  'modules' );
row( 'Runtime prereqs (META.json)',
     $dt_prereqs,          $dtl_prereqs,         'packages' );

section( 'Load time  (cold require, measured in clean fork)' );
header();
row( 'require Module',
     sprintf( '%.3f', $dt_load->{load_time} ),
     sprintf( '%.3f', $dtl_load->{load_time} ), 'seconds' );

section( 'Memory  (RSS kB, measured in clean fork after loading)' );
header();
row( 'RSS: use Module (before any zone use)',
     $dt->{rss_full},   $dtl->{rss_full},   'kB' );
row( 'RSS: after first named zone construction',
     $dt->{rss_after_tz},  $dtl->{rss_after_tz},  'kB' );
row( 'RSS: use TimeZone alone',
     $dt_tz->{rss_tz},  $dtl_tz->{rss_tz},  'kB' );
row( 'Modules after first named zone construction',
     $dt->{mods_after_tz}, $dtl->{mods_after_tz}, 'modules' );
print "  Note: DT::TimeZone loads one .pm per zone on first use;\n"
    . "        DTL::TimeZone uses SQLite so module count stays flat.\n"
    . "        DTL RSS includes DBD::SQLite (~14 MB compiled native code).\n"
    unless $CSV;

section( "CPU / wall-clock  ($ITERATIONS iterations, µs per call, measured in clean fork)" );
header();

row( 'new( UTC )',
     fmt( $dt->{new_utc_wall} / $N * 1e6 ),
     fmt( $dtl->{new_utc_wall}/ $N * 1e6 ), 'µs/call' );
row( 'new( named zone, string )',
     fmt( $dt->{new_ny_wall}  / $N * 1e6 ),
     fmt( $dtl->{new_ny_wall} / $N * 1e6 ), 'µs/call' );
row( 'new( named zone, mem cache enabled )',
     fmt( $dt->{new_ny_wall}                     / $N * 1e6 ),
     fmt( ( $dtl->{new_ny_cache_wall} // 0 )     / $N * 1e6 ), 'µs/call' );
row( 'now( UTC )',
     fmt( $dt->{now_wall}  / $N * 1e6 ),
     fmt( $dtl->{now_wall} / $N * 1e6 ), 'µs/call' );
row( 'year + month + day + epoch',
     sprintf( '%.3f', $dt->{acc_wall}  / $N * 1e6 ),
     sprintf( '%.3f', $dtl->{acc_wall} / $N * 1e6 ), 'µs/call' );
row( 'clone + add( days + hours )',
     fmt( $dt->{add_wall}  / $N * 1e6 ),
     fmt( $dtl->{add_wall} / $N * 1e6 ), 'µs/call' );
row( 'strftime',
     fmt( $dt->{strf_wall}  / $N * 1e6 ),
     fmt( $dtl->{strf_wall} / $N * 1e6 ), 'µs/call' );
row( 'TimeZone->new (warm, no mem cache)',
     fmt( $dt->{tz_warm_wall}   / $N * 1e6 ),
     fmt( $dtl->{tz_warm_wall}  / $N * 1e6 ), 'µs/call' );
row( 'TimeZone->new (DTL mem cache enabled)',
     fmt( $dt->{tz_warm_wall}    / $N * 1e6 ),
     fmt( $dtl->{tz_cache_wall}  / $N * 1e6 ), 'µs/call' );

if( $CSV )
{
    print "operation,DateTime,DateTime::Lite,unit\n";
    printf "%s,%s,%s,%s\n", @$_ for @csv_rows;
}
else
{
    print "\n", "=" x 80, "\n";
}

__END__

=head1 NAME

benchmark.pl - Compare DateTime and DateTime::Lite performance

=head1 SYNOPSIS

    cd DateTime-Lite-vX.X.X
    perl Makefile.PL && make
    perl -Iblib/lib -Iblib/arch scripts/benchmark.pl

    perl scripts/benchmark.pl --iterations 20000
    perl scripts/benchmark.pl --csv > results.csv

=head1 DESCRIPTION

Forks the process before loading any module so that every measurement
runs in a clean C<%INC>.  Results are serialised to temp files and
aggregated by the parent.

=head1 OPTIONS

=over 4

=item C<--iterations N>

Timing loop iterations (default: 10_000).

=item C<--csv>

Machine-readable CSV output.

=back

=cut



( run in 0.664 second using v1.01-cache-2.11-cpan-71847e10f99 )