Commit ad3d4e4656 for openssl.org

commit ad3d4e4656586471d70726c635b946bbe89944c0
Author: Bob Beck <beck@openssl.org>
Date:   Mon Aug 10 17:58:23 2026 -0600

    Stop reloading Pod::Html for every manual page

    Pod::Html is compiled before the first pod is read, so converting the
    manual to HTML started nine hundred perl processes to do about a
    second of work.

    Convert the whole set in one invocation that loads the module once and
    divides the pages between a few forked workers. Nothing depends on an
    individual page, since the install recipe takes them as arguments, so
    a stamp is enough to order the work, and the per-page rules stay for
    building one page by name. The single page option the VMS and Windows
    build files use is unchanged, and both paths convert through the same
    code.

    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:11 2026
    Merged-from: https://github.com/openssl/openssl/pull/32243

diff --git a/Configurations/unix-Makefile.tmpl b/Configurations/unix-Makefile.tmpl
index bb3ca6498b..6f0c8a602c 100644
--- a/Configurations/unix-Makefile.tmpl
+++ b/Configurations/unix-Makefile.tmpl
@@ -274,6 +274,17 @@ HTMLDOCS7={-
         join(" \\\n" . ' ' x 10,
              fill_lines(" ", $COLUMNS - 10,
                         @{$unified_info{htmldocs}->{man7}})) -}
+
+# The pods every HTML page is made from, which is what the whole set is
+# converted from in one go.  Each page's pod is the first element of its
+# generator, the same place generatesrc() below takes it from.
+HTMLPODS={-
+        join(" \\\n" . ' ' x 9,
+             fill_lines(" ", $COLUMNS - 9,
+                        map { $unified_info{generate}->{$_}->[0] }
+                        map { @{$unified_info{htmldocs}->{$_} // []} }
+                        qw(man1 man3 man5 man7))) -}
+
 MANDOCS1={-
         join(" \\\n" . ' ' x 9,
              fill_lines(" ", $COLUMNS - 9,
@@ -586,7 +597,18 @@ doc/man/.mandocs.stamp: {- $config{manpage_format} eq "roff"
      . "\n\t" . '@touch $@'
    : "\t" . '@touch $@' -}

-build_html_docs: $(HTMLDOCS1) $(HTMLDOCS3) $(HTMLDOCS5) $(HTMLDOCS7) ## Create HTML documentation
+build_html_docs: doc/html/.htmldocs.stamp ## Create HTML documentation
+
+# As with the manual pages, Pod::Html is compiled before the first pod is
+# read, so the whole set is converted in one invocation rather than in a
+# process per page.  Nothing depends on an individual page -- the install
+# recipe takes them as arguments -- so a stamp is enough to order the work,
+# and the per-page rules below stay available for one page by name.
+doc/html/.htmldocs.stamp: $(HTMLPODS)
+	@$(ECHO) "Converting the manual pages to HTML"
+	@$(PERL) $(SRCDIR)/util/mkpod2html.pl -o doc/html \
+		-r "$(SRCDIR)/doc" $(HTMLPODS)
+	@touch $@

 build_generated: $(GENERATED_MANDATORY)
 build_libs_nodep: $(LIBS) {- join(" ",map { platform->sharedlib_simple($_) // platform->sharedlib_import($_) // platform->sharedlib($_) // () } @{$unified_info{libraries}}) -}
@@ -670,6 +692,7 @@ clean: libclean ## Clean the workspace, keep the configuration
 	$(RM) $(HTMLDOCS3)
 	$(RM) $(HTMLDOCS5)
 	$(RM) $(HTMLDOCS7)
+	$(RM) doc/html/.htmldocs.stamp
 	$(RM) $(MANDOCS1)
 	$(RM) $(MANDOCS3)
 	$(RM) $(MANDOCS5)
diff --git a/util/mkpod2html.pl b/util/mkpod2html.pl
index ea1164d597..0401dc7ced 100755
--- a/util/mkpod2html.pl
+++ b/util/mkpod2html.pl
@@ -11,51 +11,164 @@ use warnings;

 use lib ".";
 use Getopt::Std;
+use File::Basename;
 use Pod::Html;
 use File::Spec::Functions qw(:DEFAULT rel2abs);

+# With -i, convert the one named pod, which is what the per-page rules and
+# the other build systems do.  Without it, convert every pod named on the
+# command line, deriving each output from the pod's own path.  Pod::Html is
+# expensive to compile and OpenSSL has over nine hundred pages, so loading
+# it once and dividing the pages between a few forked workers is far
+# cheaper than starting this program once per page.
+
 # Options.
-our($opt_i);    # -i INFILE
-our($opt_o);    # -o OUTFILE
-our($opt_t);    # -t TITLE
-our($opt_r);    # -r PODROOT
+our ($opt_i);    # -i INFILE, one page; without it, pods are read from @ARGV
+our ($opt_o);    # -o OUTFILE with -i, otherwise the directory to write into
+our ($opt_t);    # -t TITLE, only with -i
+our ($opt_r);    # -r PODROOT
+our ($opt_j);    # -j JOBS, only without -i

-getopts('i:o:t:r:');
-die "-i flag missing" unless $opt_i;
+getopts('i:o:t:r:j:');
 die "-o flag missing" unless $opt_o;
-die "-t flag missing" unless $opt_t;
 die "-r flag missing" unless $opt_r;

 # We originally used realpath() here, but the Windows implementation appears
 # to require that the directory or file exist to be able to process the input,
 # so we use rel2abs() instead, which only processes the string without
 # looking further.
-$opt_i = rel2abs($opt_i) or die "Can't convert to real path: $!";
-$opt_o = rel2abs($opt_o) or die "Can't convert to real path: $!";
-$opt_r = rel2abs($opt_r) or die "Can't convert to real path: $!";
-
-pod2html
-    "--infile=$opt_i",
-    "--outfile=$opt_o",
-    "--title=$opt_t",
-    "--podroot=$opt_r",
-    "--podpath=man1:man3:man5:man7",
-    "--htmldir=..";
-
-# Read in contents.
-open F, "<$opt_o"
-    or die "Can't read $opt_o, $!";
-my $contents = '';
+my $podroot = rel2abs($opt_r) or die "Can't convert to real path: $!";
+
+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 HTML page.
+sub format_page
+{
+    my ($pod, $out, $title) = @_;
+
+    $pod = rel2abs($pod) or die "Can't convert to real path: $!";
+    $out = rel2abs($out) or die "Can't convert to real path: $!";
+
+    pod2html "--infile=$pod",
+             "--outfile=$out",
+             "--title=$title",
+             "--podroot=$podroot",
+             "--podpath=man1:man3:man5:man7",
+             "--htmldir=..";
+
+    # Read in contents.
+    open my $fh, "<", $out
+        or die "Can't read $out, $!";
+    my $contents = do { local $/ = undef; <$fh> };
+    close $fh;
+    unlink $out;
+
+    $contents =~
+        s|href="http://man\.he\.net/(man\d/[^"]+)(?:\.html)?"|href="../$1.html"|g;
+    open $fh, ">", $out
+        or die "Can't write $out, $!";
+    print $fh $contents;
+    close $fh;
+}
+
+# One named page: the output file and title are given.
+if (defined $opt_i) {
+    die "-t flag missing" unless $opt_t;
+    format_page($opt_i, $opt_o, $opt_t);
+    exit 0;
+}
+
+# Otherwise every pod named on the command line, with $opt_o the directory
+# holding the man1..man7 subdirectories: doc/man3/BIO_s_mem.pod becomes
+# $opt_o/man3/BIO_s_mem.html, titled BIO_s_mem.
+sub page_of
+{
+    my $pod = shift;
+    my $name = basename($pod, ".pod");
+    my ($section) = basename(dirname($pod)) =~ m|^man(\d)$|;
+
+    die "Can't tell the section of $pod from its directory\n"
+        unless defined $section;
+
+    return ("$opt_o/man$section/$name.html", $name);
+}
+
+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.
+my $jobs = @pods > 1
+    ? (defined $opt_j && $opt_j > 0 ? int($opt_j) : cpu_count())
+    : 1;
+
+$jobs = scalar @pods if $jobs > @pods;
+
+sub format_pods
 {
-    local $/ = undef;
-    $contents = <F>;
-}
-close F;
-unlink $opt_o;
-
-$contents =~
-    s|href="http://man\.he\.net/(man\d/[^"]+)(?:\.html)?"|href="../$1.html"|g;
-open F, ">$opt_o"
-    or die "Can't write $opt_o, $!";
-print F $contents;
-close F;
+    foreach my $pod (@_) {
+        my ($out, $title) = page_of($pod);
+
+        # The page is current when it is newer than the pod it comes from.
+        next if -e $out && -M $out < -M $pod;
+
+        format_page($pod, $out, $title);
+    }
+}
+
+if ($jobs <= 1) {
+    format_pods(@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.
+        my @mine;
+
+        for (my $i = $worker; $i <= $#pods; $i += $jobs) {
+            push @mine, $pods[$i];
+        }
+        format_pods(@mine);
+        exit 0;
+    }
+    push @pids, $pid;
+}
+
+my $failed = 0;
+
+foreach my $pid (@pids) {
+    waitpid($pid, 0);
+    $failed = 1 if $?;
+}
+
+die "Failed to convert the manual pages to HTML\n" if $failed;