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;