+##############################################################################
+# Benchmarking
+##############################################################################
+
+# Perform a binary search for a number of hash iterations that makes password
+# hashing take the given target time on the current system.
+#
+# Assumptions:
+#
+# * The system load is low enough that this benchmark result is meaningful
+# and not heavily influenced by other programs running on the system. The
+# binary search may be unstable if the system load is too variable.
+#
+# * The static "password" string used for benchmarking will exhibit similar
+# performance to the statistically average password.
+#
+# Information about the iteration search process is printed to standard output
+# while the search runs.
+#
+# $target - The elapsed time, in real seconds, we're aiming for
+# $delta - The permissible delta around the target time
+#
+# Returns: The number of hash iterations with that performance characteristic
+# Throws: Text exception on failure to write to standard output
+sub find_iteration_count {
+ my ($target, $delta) = @_;
+ my $high = 0;
+ my $low = 0;
+
+ # A static password to use for benchmarking.
+ my $password = 'this is a benchmark';
+
+ # Start at the current configured iteration count. If this doesn't take
+ # long enough, it becomes the new low mark and we try double that
+ # iteration count. Otherwise, do binary search.
+ #
+ # We time twenty iterations each time, chosen because it avoids the
+ # warnings from Benchmark about too few iterations for a reliable count.
+ require Benchmark;
+ my $iterations = $HASH_ITERATIONS;
+ while (1) {
+ my $hash = sub { password_hash($password, $iterations) };
+ my $times = Benchmark::timethis(20, $hash, q{}, 'none');
+
+ # Extract the CPU time from the formatted time string. This will be
+ # the total time for all of the iterations, so divide by the iteration
+ # count to recover the time per iteration.
+ my $report = Benchmark::timestr($times);
+ my ($time) = ($report =~ m{ ([\d.]+) [ ] CPU }xms);
+ $time = $time / 20;
+
+ # Tell the user what we discovered.
+ say {*STDOUT} "Performing $iterations iterations takes $time seconds"
+ or die "$0: cannot write to standard output: $!\n";
+
+ # If this is what we're looking for, we're done.
+ if (abs($time - $target) < $delta) {
+ last;
+ }
+
+ # Determine the new iteration target.
+ if ($time > $target) {
+ $high = $iterations;
+ } else {
+ $low = $iterations;
+ }
+ if ($time < $target && $high == 0) {
+ $iterations = $iterations * 2;
+ } else {
+ $iterations = int(($high + $low) / 2);
+ }
+ }
+
+ # Report the result and return it.
+ say {*STDOUT} "Use $iterations iterations"
+ or die "$0: cannot write to standard output: $!\n";
+ return $iterations;
+}
+