Commit 49be7ee783 for openssl.org

commit 49be7ee7839885469abe51428088aab900e76d32
Author: Bob Beck <beck@openssl.org>
Date:   Mon Aug 10 17:47:33 2026 -0600

    Stop reloading Pod::Man for every manual page

    pod2man is a perl program that compiles Pod::Man before it reads a
    single pod, and there are over nine hundred pages, so formatting the
    manual spent most of its effort compiling the same module again and
    again -- 24 seconds of user time and 6 of system time to produce
    about two and a half seconds of formatting.

    Format the manual in one invocation that loads the module once and
    divides the pages between a few forked workers. No target depends on
    an individual page, since the install recipes take them as arguments,
    so a stamp is enough to order the work. The per-page rules stay,
    calling the same script with a single pod, so one page can still be
    built by name, and that path is quicker than pod2man was too.

    Reviewed-by: Neil Horman <nhorman@openssl.org>
    Reviewed-by: Andrew Dinh <andrewd@openssl.org>
    Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
    Merge-date: Tue Sep  1 14:03:10 2026
    Merged-from: https://github.com/openssl/openssl/pull/32243

diff --git a/Configurations/unix-Makefile.tmpl b/Configurations/unix-Makefile.tmpl
index 102ead2513..bb3ca6498b 100644
--- a/Configurations/unix-Makefile.tmpl
+++ b/Configurations/unix-Makefile.tmpl
@@ -291,6 +291,16 @@ MANDOCS7={-
              fill_lines(" ", $COLUMNS - 9,
                         @{$unified_info{mandocs}->{man7}})) -}

+# The pods every manual page is made from, which is what the whole manual
+# is formatted from in one go.  Each page's pod is the first element of its
+# generator, the same place generatesrc() below takes it from.
+MANPODS={-
+        join(" \\\n" . ' ' x 8,
+             fill_lines(" ", $COLUMNS - 8,
+                        map { $unified_info{generate}->{$_}->[0] }
+                        map { @{$unified_info{mandocs}->{$_} // []} }
+                        qw(man1 man3 man5 man7))) -}
+
 APPS_OPENSSL="{- use File::Spec::Functions;
                  catfile("apps","openssl") -}"

@@ -557,7 +567,25 @@ cov-report: $(SHLIBS)
 ##@ Documentation
 build_generated_pods: $(GENERATED_PODS)
 build_docs: build_man_docs build_html_docs ## Create documentation
-build_man_docs: $(MANDOCS1) $(MANDOCS3) $(MANDOCS5) $(MANDOCS7) ## Create manpages
+build_man_docs: doc/man/.mandocs.stamp ## Create manpages
+
+# The whole manual is formatted in one run.  pod2man is a perl program that
+# compiles Pod::Man before it reads a single pod, so a process per page
+# spends most of its time loading the same module over nine hundred times.
+# No target depends on an individual page -- the install recipes take them
+# as arguments -- so a stamp is enough to order the work, and the per-page
+# rules below stay available for building one page by name.
+doc/man/.mandocs.stamp: {- $config{manpage_format} eq "roff"
+                           ? '$(MANPODS)'
+                           : '$(MANDOCS1) $(MANDOCS3) $(MANDOCS5) $(MANDOCS7)' -}
+{- $config{manpage_format} eq "roff"
+   ? "\t" . '@$(ECHO) "Formatting the manual pages"'
+     . "\n\t" . '@$(PERL) $(SRCDIR)/util/mkpod2man.pl -o doc/man \\'
+     . "\n\t\t" . '-m "$(MANSUFFIX)" -d "$(RELEASE_DATE)" -r "$(VERSION)" \\'
+     . "\n\t\t" . '$(MANPODS)'
+     . "\n\t" . '@touch $@'
+   : "\t" . '@touch $@' -}
+
 build_html_docs: $(HTMLDOCS1) $(HTMLDOCS3) $(HTMLDOCS5) $(HTMLDOCS7) ## Create HTML documentation

 build_generated: $(GENERATED_MANDATORY)
@@ -646,6 +674,7 @@ clean: libclean ## Clean the workspace, keep the configuration
 	$(RM) $(MANDOCS3)
 	$(RM) $(MANDOCS5)
 	$(RM) $(MANDOCS7)
+	$(RM) doc/man/.mandocs.stamp
 	$(RM) $(PROGRAMS) $(TESTPROGS) $(MODULES) $(FIPSMODULE) $(SCRIPTS)
 	$(RM) $(GENERATED_MANDATORY) $(GENERATED)
 	$(RM) core
@@ -1577,11 +1606,14 @@ $args{src}: $pod
 		 $pod >\$\@
 EOF
           } elsif ($config{manpage_format} eq "roff") {
+              # One page at a time, through the same script that formats
+              # the whole manual, so there is a single implementation.  It
+              # does not fork for a single pod, and still beats pod2man,
+              # which has more of its own start-up to do.
               return <<"EOF";
 $args{src}: $pod
-	pod2man --name=$name --section=$section\$(MANSUFFIX) --center=OpenSSL \\
-		--date=\$(RELEASE_DATE) --release=\$(VERSION) \\
-		$pod >\$\@
+	\$(PERL) \$(SRCDIR)/util/mkpod2man.pl -o doc/man -m "\$(MANSUFFIX)" \\
+		-d "\$(RELEASE_DATE)" -r "\$(VERSION)" $pod
 EOF
           } else {
               die "Unhandled manpage format: $config{manpage_format}";
diff --git a/util/mkpod2man.pl b/util/mkpod2man.pl
new file mode 100644
index 0000000000..08903e4755
--- /dev/null
+++ b/util/mkpod2man.pl
@@ -0,0 +1,131 @@
+#! /usr/bin/env perl
+# Copyright 2026 The OpenSSL Project Authors. All Rights Reserved.
+#
+# Licensed under the Apache License 2.0 (the "License").  You may not use
+# this file except in compliance with the License.  You can obtain a copy
+# in the file LICENSE in the source distribution or at
+# https://www.openssl.org/source/license.html
+
+use strict;
+use warnings;
+
+use Getopt::Std;
+use File::Basename;
+use Pod::Man;
+
+# Format man pages without loading Pod::Man once per page.  pod2man is a
+# perl program that compiles Pod::Man before it reads a single pod, and
+# OpenSSL has over nine hundred pages, so running it once per page spends
+# most of that time compiling the same module again and again.  Here the
+# module is loaded once and the pages are then divided between a few
+# forked workers, which inherit it already compiled.
+
+# Options.  The whole manual is formatted in one run, so the section of
+# each page is taken from the directory its pod lives in rather than
+# given on the command line: doc/man3/BIO_s_mem.pod is section 3 and
+# becomes OUTDIR/man3/BIO_s_mem.3.
+our ($opt_o);    # -o OUTDIR, the directory holding the man1..man7 dirs
+our ($opt_m);    # -m MANSUFFIX, appended to the section inside the page
+our ($opt_d);    # -d DATE
+our ($opt_r);    # -r RELEASE
+our ($opt_j);    # -j JOBS
+
+getopts('o:m:d:r:j:');
+die "-o flag missing" unless defined $opt_o;
+$opt_m = '' unless defined $opt_m;
+$opt_d = '' unless defined $opt_d;
+$opt_r = '' unless defined $opt_r;
+
+sub cpu_count
+{
+    my $cpus = $ENV{"NUMBER_OF_PROCESSORS"};    # Windows sets this.
+
+    if (!defined($cpus) && $^O =~ /linux/) {
+        my $tmp = qx(nproc 2>/dev/null);
+
+        $cpus = $tmp if $? == 0 && $tmp > 0;
+    }
+    if (!defined($cpus) && -r "/proc/cpuinfo") {
+        my $tmp = qx(grep -c ^processor /proc/cpuinfo 2>/dev/null);
+
+        $cpus = $tmp if $? == 0 && $tmp > 0;
+    }
+    if (!defined($cpus)) {
+        my $tmp = qx(sysctl -n hw.ncpu 2>/dev/null);    # BSDs, macOS
+
+        $cpus = $tmp if $? == 0 && $tmp > 0;
+    }
+
+    return defined($cpus) && $cpus > 0 ? int($cpus) : 1;
+}
+
+# Turn one pod into one man page.
+sub format_page
+{
+    my $pod = shift;
+    my $name = basename($pod, ".pod");
+    my $dir = basename(dirname($pod));
+    my ($section) = $dir =~ m|^man(\d)$|;
+
+    die "Can't tell the section of $pod from its directory\n"
+        unless defined $section;
+
+    my $out = "$opt_o/man$section/$name.$section";
+
+    # The page is current when it is newer than the pod it comes from.
+    return if -e $out && -M $out < -M $pod;
+
+    Pod::Man->new(name => uc $name,
+                  section => "$section$opt_m",
+                  center => "OpenSSL",
+                  date => $opt_d,
+                  release => $opt_r)
+        ->parse_from_file($pod, $out);
+}
+
+my @pods = @ARGV;
+
+exit 0 unless @pods;
+
+# Only ask how many processors there are when the answer can matter;
+# the count is found by running a command, and the per-page rules call
+# this script with a single pod.
+my $jobs = @pods > 1
+    ? (defined $opt_j && $opt_j > 0 ? int($opt_j) : cpu_count())
+    : 1;
+
+$jobs = scalar @pods if $jobs > @pods;
+
+# One page, or no reason to fork: do the work here.  This is also the path
+# taken where fork() is emulated and would cost more than it saves.
+if ($jobs <= 1) {
+    format_page($_) foreach @pods;
+    exit 0;
+}
+
+my @pids;
+
+foreach my $worker (0 .. $jobs - 1) {
+    my $pid = fork();
+
+    die "Can't fork, $!\n" unless defined $pid;
+    if (!$pid) {
+        # Deal every $jobs'th page to this worker.  The pages differ a lot
+        # in size, and dealing them out interleaves the large ones instead
+        # of handing one worker a contiguous run of them.
+        for (my $i = $worker; $i <= $#pods; $i += $jobs) {
+            format_page($pods[$i]);
+        }
+        exit 0;
+    }
+    push @pids, $pid;
+}
+
+my $failed = 0;
+
+foreach my $pid (@pids) {
+    waitpid($pid, 0);
+    $failed = 1 if $?;
+}
+
+die "Failed to format the manual pages\n" if $failed;