From 73d5a2647e19b451ad25f8eaa3da5c04b30b821d Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <postgres@jeltef.nl>
Date: Wed, 4 Mar 2026 09:24:24 +0100
Subject: [PATCH v2 7/7] pgindent: Allow parallel pgindent runs

Running pgindent on the whole source tree can take a while, especially
when also having it run perltidy on perl files. This adds support for
pgindent to indent files in parallel. This speeds up a full pgindent run
from more than a minute on my machine to ~7 seconds.
---
 src/tools/pgindent/pgindent | 112 ++++++++++++++++++++++++++++++------
 1 file changed, 93 insertions(+), 19 deletions(-)

diff --git a/src/tools/pgindent/pgindent b/src/tools/pgindent/pgindent
index d906c00059f..d286bba3750 100755
--- a/src/tools/pgindent/pgindent
+++ b/src/tools/pgindent/pgindent
@@ -17,6 +17,8 @@ use File::Spec;
 use File::Temp;
 use IO::Handle;
 use Getopt::Long;
+use Fcntl qw(:flock);
+use POSIX qw(:sys_wait_h);
 
 # Ensure SIGINT triggers a clean exit so File::Temp can remove temp files.
 $SIG{INT} = sub { exit 130; };
@@ -30,10 +32,12 @@ my $indent_opts =
 
 my $devnull = File::Spec->devnull;
 
-my ($typedefs_file, $typedef_str, @excludes, $indent, $diff,
-	$check, $help, @commits, $perltidy_arg, $perl_only,);
+my ($typedefs_file, $typedef_str, @excludes, $indent,
+	$diff, $check, $help, @commits,
+	$perltidy_arg, $perl_only, $jobs,);
 
 $help = 0;
+$jobs = 0;
 
 # Save @ARGV before parsing so we can distinguish --perltidy=PATH (where
 # the value should be used as the perltidy path) from --perltidy PATH
@@ -50,7 +54,8 @@ my %options = (
 	"perltidy:s" => \$perltidy_arg,
 	"perl-only" => \$perl_only,
 	"diff" => \$diff,
-	"check" => \$check,);
+	"check" => \$check,
+	"jobs|j=i" => \$jobs,);
 GetOptions(%options) || usage("bad command line argument");
 
 if (defined($perltidy_arg) && $perltidy_arg ne '')
@@ -62,6 +67,27 @@ if (defined($perltidy_arg) && $perltidy_arg ne '')
 	}
 }
 
+sub get_num_cpus
+{
+	# Try nproc (Linux, some BSDs), then sysctl (macOS, FreeBSD).
+	for my $cmd ('nproc', 'sysctl -n hw.ncpu')
+	{
+		my $n = `$cmd 2>$devnull`;
+		chomp $n;
+		return $n + 0 if ($? == 0 && $n =~ /^\d+$/ && $n > 0);
+	}
+	return 1;
+}
+
+if ($jobs == 0)
+{
+	$jobs = get_num_cpus();
+}
+elsif ($jobs < 0)
+{
+	usage("--jobs must be a non-negative number");
+}
+
 usage() if $help;
 
 usage("Cannot use --commit with command line file list")
@@ -465,6 +491,7 @@ Options:
 	--perl-only             format only Perl files, skip C formatting
 	--diff                  show the changes that would be made
 	--check                 exit with status 2 if any changes would be made
+	--jobs=N, -j N          number of parallel workers (0 = num CPUs, default 0)
 The --excludes and --commit options can be given more than once.
 EOF
 	if ($help)
@@ -592,20 +619,18 @@ warn "No files to process" unless @files;
 # remove excluded files from the file list
 process_exclude();
 
-my %processed;
-my $status = 0;
+# Used by forked children to serialize diff output to STDOUT via flock().
+my $stdout_lock_fh = new File::Temp(TEMPLATE => "pglockXXXXX");
 
-foreach my $source_filename (@files)
+sub process_file
 {
-	# skip duplicates
-	next if $processed{$source_filename};
-	$processed{$source_filename} = 1;
+	my $source_filename = shift;
 
 	# don't try to format a file that doesn't exist
 	unless (-f $source_filename)
 	{
 		warn "Could not find $source_filename";
-		next;
+		return 0;
 	}
 
 	my $source = read_source($source_filename);
@@ -620,11 +645,7 @@ foreach my $source_filename (@files)
 		($formatted, $failure) = format_perl($source, $source_filename);
 	}
 
-	if ($failure)
-	{
-		$status = 3;
-		next;
-	}
+	return 3 if $failure;
 
 	if ($formatted ne $source)
 	{
@@ -636,14 +657,67 @@ foreach my $source_filename (@files)
 		{
 			if ($diff)
 			{
-				print diff($formatted, $source_filename);
+				my $output = diff($formatted, $source_filename);
+				flock($stdout_lock_fh, LOCK_EX);
+				print $output;
+				STDOUT->flush();
+				flock($stdout_lock_fh, LOCK_UN);
 			}
 
-			if ($check)
+			return 2 if $check;
+		}
+	}
+
+	return 0;
+}
+
+# deduplicate file list
+my %seen;
+@files = grep { !$seen{$_}++ } @files;
+
+my $status = 0;
+
+if ($jobs <= 1)
+{
+	foreach my $source_filename (@files)
+	{
+		my $file_status = process_file($source_filename);
+		$status = $file_status if $file_status > $status;
+		last if $check && $status >= 2 && !$diff;
+	}
+}
+else
+{
+	my %children;    # pid => 1
+
+	my $file_idx = 0;
+	while ($file_idx < scalar(@files) || %children)
+	{
+		# Fork new children up to $jobs limit
+		while ($file_idx < scalar(@files) && scalar(keys %children) < $jobs)
+		{
+			my $source_filename = $files[ $file_idx++ ];
+
+			my $pid = fork();
+			die "fork failed: $!\n" unless defined $pid;
+
+			if ($pid == 0)
 			{
-				$status ||= 2;
-				last unless $diff;
+				# child
+				my $child_status = process_file($source_filename);
+				exit $child_status;
 			}
+
+			$children{$pid} = 1;
+		}
+
+		# Wait for at least one child to finish
+		my $pid = waitpid(-1, 0);
+		if ($pid > 0 && exists $children{$pid})
+		{
+			delete $children{$pid};
+			my $child_status = $? >> 8;
+			$status = $child_status if $child_status > $status;
 		}
 	}
 }
-- 
2.53.0

