Rewrite mkjigsnap in (almost) pure perl for better performance
authorSteve McIntyre <steve@einval.com>
Mon, 13 Jun 2011 14:05:20 +0000 (15:05 +0100)
committerSteve McIntyre <steve@einval.com>
Mon, 13 Jun 2011 14:05:20 +0000 (15:05 +0100)
Originally mkjigsnap was in shell, but performance sucked. Since the
merge of the perl generate_snapshot_tree() code, things have been
faster but the script was a mess of a combination of shell, sed, perl
etc.

Now moved over to a single perl script which (as well as being
cleaner) is measurably faster (~20-30% in my tests). Still calls out
to other programs for best performance (zcat rather than gzreadline
when reading jigdo files).

Now has an extra dependency on Compress::Zlib, aka libio-compress-perl
in Debian systems.

mkjigsnap

index 507697c..f8061c5 100755 (executable)
--- a/mkjigsnap
+++ b/mkjigsnap
@@ -1,4 +1,4 @@
-#!/bin/sh
+#!/usr/bin/perl -w
 #
 # mkjigsnap
 #
 #    in the "ignore" file ~/mkjigsnap-ignore.list.)
 #      
 
-JIGDOS=""
-MODE="multi"
-DRYRUN=0
-VERBOSE=0
-STARTDATE=`date -u`
+use strict;
+use Getopt::Long;
+use File::Basename;
+use File::Find;
+use File::Copy;
+use Compress::Zlib;
+Getopt::Long::Configure ('no_ignore_case');
+Getopt::Long::Configure ('no_auto_abbrev');
 
-# Parse command line
-while [ $# -gt 0 ]
-do
-    case "$1"x in
-        "-d"x)
-            shift
-            DIRNAME=$1
-            shift
-            ;;
-        "-f"x)
-            shift
-            FAILEDFILE=$1
-            shift
-            ;;
-        "-i"x)
-            shift
-            IGNOREFILE=$1
-            shift
-            ;;
-        "-J"x)
-            shift
-            JIGDOLIST=$1
-            shift
-            ;;
-        "-j"x)
-            shift
-            JIGDOS="$JIGDOS $1"
-            shift
-            ;;
-        "-k"x)
-            shift
-            KEYWORDS="$KEYWORDS $1"
-            shift
-            ;;
-        "-m"x)
-            shift
-            MIRROR=$1
-            shift
-            ;;
-        "-N"x)
-            shift
-            DRYRUN=1
-            ;;
-        "-n"x)
-            MODE="single"
-            shift
-            CDNAME=$1
-            shift
-            ;;
-        "-o"x)
-            shift
-            OUT=$1
-            shift
-            ;;
-        "-T"x)
-            shift
-            TF_TMPDIR="--directory $1"
-            SORT_TMPDIR="-T $1"
-            shift
-            ;;
-        "-t"x)
-            shift
-            TEMPLATE=$1
-            shift
-            ;;
-        "-v"x)
-            shift
-            VERBOSE=1
-            ;;
-        ""*)
-            echo "Input error! ($1)"
-            exit 1
-            ;;
-    esac
-done
+my $mode = "multi";
+my $dryrun = 0;
+my $verbose = 0;
+my $startdate = `date -u`;
+my ($jlistdonedate, $parsedonedate, $snapdonedate);
+my @jigdos;
+my @keywords;
+my @mirrors;
+my ($dirname, $failedfile, $ignorefile, $jigdolist, $mirror, $cdname,
+    $outdir, $tempdir, $template);
+my $result;
+my $num_jigdos = 0;
+my $num_unsorted = 0;
+my $num_unique = 0;
+my @failed_files;
+my $old_deleted = 0;
+my %ignored_fails;
+my %file_list;
+my %ref;
 
-export DRYRUN
-export VERBOSE
+$result = GetOptions("d=s" => \$dirname,
+                     "f=s" => \$failedfile,
+                     "i=s" => \$ignorefile,
+                     "J=s" => \$jigdolist,
+                     "j=s" => \@jigdos,
+                     "k=s" => \@keywords,
+                     "m=s" => \@mirrors,
+                     "N"   => \$dryrun,
+                     "n=s" => \$cdname,
+                     "o=s" => \$outdir,
+                     "T=s" => \$tempdir,
+                     "t=s" => \$template,
+                     "v"   => \$verbose);
 
-generate_snapshot_tree () {
-    NUM=$1
-    MIRROR=$2
-    DIRNAME=$3
-    FAILEDFILE=$4
-    IGNOREFILE=$5
-    TMPFILE=$6
+# Sanity-check arguments
+if (!defined ($dirname)) {
+    die "You must specify the snapshot directory name!\n";
+}
+if (!@keywords) {
+    die "You must specify the keywords to match!\n";
+}
+if (!@mirrors) {
+    die "You must specify the location(s) of the mirror(s)!\n";
+}
+if (@jigdos) {
+    $num_jigdos += scalar(@jigdos);
+}
+if (defined($jigdolist)) {
+    $num_jigdos += `wc -w < $jigdolist`;
+}
+if ($num_jigdos == 0) {
+    die "No jigdo file(s) specified!\n";
+}
+if (defined($cdname)) {
+    $mode = "single";
+}
 
-    (echo $NUM; echo $MIRROR; echo $DIRNAME; echo $FAILEDFILE; echo $IGNOREFILE; cat $TMPFILE ) | perl -we '
-    use File::Basename;
-    use File::Find;
-    my $dryrun = $ENV{"DRYRUN"};
-    my $verbose = $ENV{"VERBOSE"};
-    my $num;
-    my $mirrorpath;
-    my $outdir;
-    my $dirname;
-    my $filename;
-    my $failedlog;
-    my $ignorefile;
-    my $done = 0;
-    my $failed = 0;
-    my $ignored = 0;
-    my @file_list;
-    my @failed_files;
-    my @ignored_fails;
-    my $old_deleted = 0;
-    my $link;
-    $| = 1;
+if ($mode eq "single") {
+    if (!defined($cdname)) {
+        die "You must specify the output name for the jigit conf!\n";
+    }
+    if (!defined($outdir)) {
+        die "You must specify where to set up the snapshot!\n";
+    }
+    if (!defined($template)) {
+        die "You must specify the template file!\n";
+    }
+    if ($num_jigdos != 1) {
+        die "More than one jigdo file specified ($num_jigdos) in single-jigdo mode!\n";
+    }
+    # In single-jigdo mode, the snapshot directory is relative to the
+    # output dir
+    $dirname="$outdir/$dirname";
+} else {
+    if (defined($cdname)) {
+        die "Output name is meaningless for multi-jigdo mode!\n";
+    }
+    if (defined($outdir)) {
+        die "Output dir is meaningless for multi-jigdo mode!\n";
+    }
+    if (defined($template)) {
+        die "Template file name is meaningless for multi-jigdo mode!\n";
+    }
+}
 
-    # Make a dir tree
-    sub mkdirs {
-        my $input = shift;
-        my $dir;
-        my @components;
-        my $need_slash = 0;
+# Make a dir tree
+sub mkdirs {
+    my $input = shift;
+    my $dir;
+    my @components;
+    my $need_slash = 0;
 
-        if (! -d $input) {
-            if ($verbose) {
-                print "mkdirs($input)\n";
-            }
-            if (!$dryrun) {
-                @components = split /\//,$input;
-                foreach $component (@components) {
-                    if ($need_slash) {
-                        $dir = join ("/", $dir, $component);
-                    } else {
-                        $dir = $component;
-                        $need_slash = 1;
-                    }
-                    mkdir $dir;
+    if (! -d $input) {
+        if ($verbose) {
+            print "mkdirs($input)\n";
+        }
+        if (!$dryrun) {
+            @components = split /\//,$input;
+            foreach my $component (@components) {
+                if ($need_slash) {
+                    $dir = join ("/", $dir, $component);
+                } else {
+                    $dir = $component;
+                    $need_slash = 1;
                 }
-            } else {
-                print "DRYRUN: not making directory tree $input\n";
+                mkdir $dir;
             }
+        } else {
+            print "DRYRUN: not making directory tree $input\n";
         }
-    }        
+    }
+}
 
-    sub delete_redundant {
-        my $ref;
+sub delete_redundant {
+    my $ref;
 
-        if (-f) {
-            $ref = $file_list{$File::Find::name};
-            if (!defined($ref)) {
-                if ($verbose) {
-                    print "delete_redundant($File::Find::name)\n";
-                }
-                if (!$dryrun) {
-                    unlink($File::Find::name);
-                } else  {
-                    print "DRYRUN: not deleting $File::Find::name\n";
-                }
-                $old_deleted++;
-                if ( !($old_deleted % 1000) ) {
-                    print "$old_deleted\n";
-                }
+    if (-f) {
+        $ref = $file_list{$File::Find::name};
+        if (!defined($ref)) {
+            if ($verbose) {
+                print "delete_redundant($File::Find::name)\n";
+            }
+            if (!$dryrun) {
+                unlink($File::Find::name);
+            } else  {
+                print "DRYRUN: not deleting $File::Find::name\n";
+            }
+            $old_deleted++;
+            if ( !($old_deleted % 1000) ) {
+                print "$old_deleted\n";
             }
         }
     }
+}
 
-    sub parse_ignore_file {
-        my $inputfile = shift;
-        my $num_ignored_loaded = 0;
-        open(INLIST, "$inputfile") or return;
-        while (defined (my $pkg = <INLIST>)) {
-            chomp $pkg;
-            $ignored_fails{$pkg}++;
-            $num_ignored_loaded++;
-        }
-        print "parse_ignore_file: loaded $num_ignored_loaded entries from file $inputfile\n";
+sub parse_ignore_file {
+    my $inputfile = shift;
+    my $num_ignored_loaded = 0;
+    open(INLIST, "$inputfile") or return;
+    while (defined (my $pkg = <INLIST>)) {
+        chomp $pkg;
+        $ignored_fails{$pkg}++;
+        $num_ignored_loaded++;
     }
+    print "parse_ignore_file: loaded $num_ignored_loaded entries from file $inputfile\n";
+}
 
-    while (<>) {
-        chomp;
+sub generate_snapshot_tree () {
+    my $done = 0;
+    my $failed = 0;
+    my $ignored = 0;
 
-        if (!defined($num)) {
-            $num = $_;
-            next;
-        }
-        if (!defined($mirrorpath)) {
-            $mirrorpath = $_;
-            next;
-        }
-        if (!defined($outdir)) {
-            $outdir = $_;
-            print "Linking $num files from $mirrorpath to $outdir\n";
-            next;
-        }
-        if (! -d $outdir) {
-            print "$outdir does not exist\n";
-            if (!$dryrun) {
-                mkdirs($outdir);
-            } else {
-                die "DRYRUN: not making it, so aborting\n";
-            }
-        }
-        if (!defined($failedlog)) {
-           $failedlog = $_;
-           next;
-        }
-        if (!defined($ignorefile)) {
-            $ignorefile = $_;
-                print "ignorefile is \"$ignorefile\"\n";
-            if (length($ignorefile) > 2) {
-                parse_ignore_file($ignorefile);
-            }
-            next;
-        }
+    $| = 1;
+
+    # Sorting is important here for performance, to help with
+    # directory lookups
+    foreach $_ (sort (keys %ref)) {
+        my $outfile = $dirname . "/" . $_;
 
-        $outfile = $outdir . "/" . $_;
         $file_list{$outfile}++;
+        if ($verbose) {
+            print "file_list hash updated for $outfile\n";
+        }
         if (! -e $outfile) {
-            $dirname = dirname($_);
-            $filename = basename($_);
-            mkdirs($outdir . "/" . $dirname);
-
+            my $dir = dirname($_);
+            my $filename = basename($_);
+            my $link;
             my $link_ok = 0;
-            foreach $mirror (split /:/,$mirrorpath) {
+            my $infile;
+
+            mkdirs($dirname . "/" . $dir);
+
+            foreach my $mirror (@mirrors) {
                 $infile = $mirror . "/" . $_;
                 if (-l $infile) {
                     $link = readlink($infile);
@@ -297,7 +256,7 @@ generate_snapshot_tree () {
                 if ($verbose) {
                     print "look for $_:\n";
                 }             
-                $outfile = $outdir . "/" . $_;
+                $outfile = $dirname . "/" . $_;
                 if (!$dryrun) {
                     if ($verbose) {
                         print "  try $infile\n";
@@ -330,7 +289,7 @@ generate_snapshot_tree () {
                 if ($ignored_fails{$_}) {
                     $ignored++;
                 } else {
-                    if (length($failedlog) <= 2) {
+                    if (!defined($failedfile)) {
                         # No logfile, print to stdout then
                         print "\nFailed to create link $outfile\n";
                     }
@@ -345,14 +304,14 @@ generate_snapshot_tree () {
         }
         $done++;
         if ( !($done % 10000) ) {
-            print "$done done, ignored $ignored, failed $failed out of $num\n";
+            print "$done done, ignored $ignored, failed $failed out of $num_unique\n";
         }
     }
-    print "  Finished: $done/$num, $failed failed, ignored $ignored\n\n";
+    print "  Finished: $done/$num_unique, $failed failed, ignored $ignored\n\n";
 
-    if ((length($failedlog) > 2) && ($failed > 0)) {
-        print "Writing list of failed files to $failedlog\n";
-        open(FAIL_LOG, "> $failedlog") or die "Failed to open $failedlog: $!\n";
+    if (defined($failedfile) && ($failed > 0)) {
+        print "Writing list of failed files to $failedfile\n";
+        open(FAIL_LOG, "> $failedfile") or die "Failed to open $failedfile: $!\n";
         foreach my $missing (@failed_files) {
             print FAIL_LOG "$missing\n";
         }
@@ -361,119 +320,104 @@ generate_snapshot_tree () {
 
     # Now walk the tree and delete files that we no longer need
     print "Scanning for now-redundant files\n";
-    find(\&delete_redundant, $outdir);
+    find(\&delete_redundant, $dirname);
     print "  Finished: $old_deleted old files removed\n";
-'
 }
 
-# Sanity-check arguments
-if [ "$DIRNAME"x = ""x ] ; then
-    echo "You must specify the snapshot directory name!"
-    exit 1
-fi    
-if [ "$KEYWORDS"x = ""x ] ; then
-    echo "You must specify the keywords to match!"
-    exit 1
-fi
-if [ "$MIRROR"x = ""x ] ; then
-    echo "You must specify the location of the mirror!"
-    exit 1
-fi
-NUM_JIGDOS=0
-if [ "$JIGDOS"x != ""x ] ; then
-    NUM_JIGDOS=$(($NUM_JIGDOS+`echo $JIGDOS | wc -w`))
-fi
-if [ "$JIGDOLIST"x != ""x ] ; then
-    NUM_JIGDOS=$(($NUM_JIGDOS+`cat $JIGDOLIST | wc -w`))
-fi
-if [ "$NUM_JIGDOS" -eq 0 ] ; then
-    echo "No jigdo file(s) specified!"
-    exit 1
-fi    
-if [ $MODE = "single" ] ; then
-    if [ "$CDNAME"x = ""x ] ; then
-        echo "You must specify the output name for the jigit conf!"
-        exit 1
-    fi
-    if [ "$OUT"x = ""x ] ; then
-        echo "You must specify where to set up the snapshot!"
-        exit 1
-    fi
-    if [ "$TEMPLATE"x = ""x ] ; then
-        echo "You must specify the template file!"
-        exit 1
-    fi
-    if [ "$NUM_JIGDOS" -ne 1 ] ; then
-        echo "More than one jigdo file specified ($NUM_JIGDOS) in single-jigdo mode!"
-        exit 1
-    fi    
-    # In single-jigdo mode, the snapshot directory is relative to the
-    # output dir
-    DIRNAME="$OUT"/"$DIRNAME"
-else
-    if [ "$CDNAME"x != ""x ] ; then
-        echo "Output name is meaningless for multi-jigdo mode!"
-        exit 1
-    fi
-    if [ "$OUT"x != ""x ] ; then
-        echo "Output dir is meaningless for multi-jigdo mode!"
-        exit 1
-    fi
-    if [ "$TEMPLATE"x != ""x ] ; then
-        echo "Template file name is meaningless for multi-jigdo mode!"
-        exit 1
-    fi
-fi
+# Parse jigdo_list file if we have one
+if (defined($jigdolist)) {
+    if ($verbose) {
+        print "Checking for jigdos in $jigdolist\n";
+    }
+    open (INLIST, "$jigdolist") or die "Can't open file $jigdolist: $!\n";
+    while ($_ = <INLIST>) {
+        chomp;
+        if (length($_) > 1) {
+            push (@jigdos, $_);
+        }
+    }
+    close INLIST;
+}
+$jlistdonedate = `date -u`;
 
-# Build the snapshot(s)
-for KEYWORD in $KEYWORDS; do
-    echo "Looking for keyword $KEYWORD in $NUM_JIGDOS jigdo file(s):"
-    TMPFILE=`tempfile $TF_TMPDIR`
-    if [ "$JIGDOLIST"x != ""x ] ; then
-        cat $JIGDOLIST | xargs zcat -f | sed -n "s/^.*${KEYWORD}://gp" >> $TMPFILE
-    fi
-    if [ "$JIGDOS"x != ""x ] ; then 
-        zcat -f $JIGDOS | sed -n "s/^.*${KEYWORD}://gp" >> $TMPFILE
-    fi
-    LISTDONEDATE=`date -u`
-    TMPFILE1=`tempfile $TF_TMPDIR`
-    TOTAL_FILES=`wc -l < $TMPFILE`
-    echo "  $LISTDONEDATE: Total references for \"$KEYWORD\": $TOTAL_FILES"
+if ($verbose) {
+    print "Working on $num_jigdos jigdo file(s)\n";
+}
+# Walk through the list of jigdos, parsing as we go
+my $num_parsed = 0;
+if ($verbose) {
+    print "Reading / parsing jigdo file(s)\n";
+}
 
-    # sort is prone to running out of space, so bail if it fails. We
-    # don't want to destroy an existing snapshot if we end up with an
-    # empty list of files!
-    sort -u $SORT_TMPDIR $TMPFILE > $TMPFILE1
-    SORTDONEDATE=`date -u`
-    mv -f $TMPFILE1 $TMPFILE
-    NUM_FILES=`wc -l < $TMPFILE`
-    echo "  $SORTDONEDATE: Unique references for \"$KEYWORD\": $NUM_FILES"
+open (INJIG, "zcat -f @jigdos |");
+while (<INJIG>) {
+    my $file;
+    chomp;
+    foreach my $keyword (@keywords) {
+        m/^......................=$keyword:(.*)$/ and $file = $1;
+    }
+    if (defined($file)) {
+        $num_unsorted++;
+        if (!exists $ref{$file}) {
+            $num_unique++;
+            $ref{$file} = 1;
+        }
+    }
+}
+close(INJIG);
+$parsedonedate = `date -u`;
+
+if ($num_unique < 5) {
+    die "Only $num_unique for the snapshot? Something is wrong; abort!\n"
+}
 
-    if [ $NUM_FILES -lt 5 ] ; then
-        echo "    Only $NUM_FILES for the snapshot? Something is wrong; abort!"
-        exit 1
-    fi
+# Now look at the snapshot dir
+if (! -d $dirname) {
+    print "$dirname does not exist\n";
+    if (!$dryrun) {
+        mkdirs($dirname);
+    } else {
+        die "DRYRUN: not making it, so aborting\n";
+    }
+}
+if (defined($ignorefile)) {
+    parse_ignore_file($ignorefile);
+}
 
-    echo "Creating snapshot tree in $DIRNAME:"
-    generate_snapshot_tree "$NUM_FILES" "$MIRROR" "$DIRNAME" "$FAILEDFILE" "$IGNOREFILE" "$TMPFILE"
-    SNAPDONEDATE=`date -u`
+print "Trying to snapshot-link $num_unique files into $dirname\n";
+generate_snapshot_tree();
+$snapdonedate = `date -u`;
 
-    echo "$STARTDATE: startup"
-    echo "$LISTDONEDATE: $TOTAL_FILES files found"
-    echo "$SORTDONEDATE: $NUM_FILES unique files after sorting"
-    echo "$SNAPDONEDATE: snapshot done"
-done
+chomp ($startdate, $jlistdonedate, $parsedonedate, $snapdonedate);
 
-if [ $MODE = "single" ] ; then
-    if [ "$DRYRUN" = "0" ] ; then
-        zcat -f $JIGDOS | sed "s:^Template=.*$:Template=$CDNAME.template:" | gzip -9 > $OUT/$CDNAME.jigdo
-        cp $TEMPLATE $OUT/$CDNAME.template
-        echo "JIGDO=$CDNAME.jigdo" > $OUT/$CDNAME.conf
-        echo "TEMPLATE=$CDNAME.template" >> $OUT/$CDNAME.conf
-        echo "SNAPSHOT=snapshot/$DIRNAME" >> $OUT/$CDNAME.conf
-        echo "Jigdo files, config and snapshot made in $OUT"
-    else
-        echo "DRYRUN: Not creating files in $OUT"
-    fi
-fi
+print "$startdate: startup\n";
+print "$jlistdonedate: found $num_jigdos jigdo files\n";
+print "$parsedonedate: found $num_unsorted files referenced in those jigdo files, $num_unique unique\n";
+print "$snapdonedate: snapshot done\n";
 
+if ($mode eq "single") {
+    if ($dryrun) {
+        print "DRYRUN: Not creating files in $outdir\n";
+    } else {
+        foreach my $jigdo (@jigdos) {
+            my ($gzin, $gzout, $line);
+            $gzin = gzopen($jigdo, "rb") or
+                die "Unable to open jigdo file $jigdo for reading: $!\n";
+            $gzout = gzopen("$outdir/$cdname.jigdo", "wb9") or
+                die "Unable to open new jigdo file $outdir/$cdname.jigdo for writing: $!\n";
+            while ($gzin->gzreadline($line) > 0) {
+                $line =~ s:^Template=.*$:Template=$cdname.template:;
+                $gzout->gzwrite($line);
+            }
+        }
+        copy("$template", "$outdir/$cdname.template") or
+            die "Failed to copy template file $template: $!\n";
+        open (CONF, "> $outdir/$cdname.conf") or
+            die "Failed to open conf file $outdir/$cdname.conf for writing: $!\n";
+        print CONF "JIGDO=$cdname.jigdo\n";
+        print CONF "TEMPLATE=$cdname.template\n";
+        print CONF "SNAPSHOT=snapshot/$dirname\n";
+        close(CONF);
+        print "Jigdo files, config and snapshot made in $outdir\n";
+    }
+}