Commit: Enhance --stats to report internal linker resource usage

Message ID 87ecya3n18.fsf@redhat.com
State New
Headers
Series Commit: Enhance --stats to report internal linker resource usage |

Checks

Context Check Description
linaro-tcwg-bot/tcwg_binutils_build--master-arm fail Patch failed to apply
linaro-tcwg-bot/tcwg_binutils_build--master-aarch64 fail Patch failed to apply

Commit Message

Nick Clifton April 2, 2025, 9:55 a.m. UTC
  Hi Guys,

  I am applying my patch to extend the linker's --stats option so that
  it also reports the resource usage of various steps in the linking
  process.  This change is only triggered if the --stats option is given
  an optional filename argument.  The output looks something like this:

Stats: linker version: (GNU Binutils) 2.44.50.20250401
Stats: linker started: Wed Apr  2 09:36:41 2025
Stats: args: ld -z norelro -z nomemory-seal -z no-separate-code -o a.out [...]

Stats: phase               cpu time    memory      user time    system time    
Stats: name                (microsec)   (KiB)      (seconds)      (seconds)    
Stats: ALL                   390082    217740              0              0    
Stats: ctf processing            12         0              0              0    
Stats: string merge            1324         0              0              0    
Stats: parsing                  349       288              0              0    
Stats: plugins                    1         0              0              0    
Stats: processing files      259616    214524              0              0    
Stats: write                 116493         0              0              0    

  The code is designed to be extensible so that new phases and resources
  can easily be added.

  The patch also tweaks one of the sec64k tests so that it records the
  stats to a file (tmpdir/64ksec.stats).  The file is opened in append
  mode so that subsequent runs of the test will add to the file, thus
  allowing the user to see changes in the performance of the linker over
  time.

  If there are any comments or questions, please let me know.

Cheers
  Nick
  

Patch

diff --git a/ld/NEWS b/ld/NEWS
index 494bb83e49b..7b5e2e47c07 100644
--- a/ld/NEWS
+++ b/ld/NEWS
@@ -1,5 +1,14 @@ 
 -*- text -*-
 
+* The linker's --stats option can take an optional argument which if used is
+  interpreted as a filename into which resource usage information should be
+  stored.  As an alternative mechanism the LD_STATS environment variable can
+  also be used to achieve the same results.  Resource usage information for
+  various phases of the linking operation is now included in the report.
+  If a map file is being produced then the information is also included there.
+  The --no-stats option can be used to disable stat reporting, should it have
+  been enabled.
+
 * Remove the linker -taso option for Alpha target, as Linux/Alpha kernel
   support for 32-bit pointers has been removed.
 
diff --git a/ld/config.in b/ld/config.in
index 2d7b6406d2b..e10c9e73cc6 100644
--- a/ld/config.in
+++ b/ld/config.in
@@ -122,6 +122,9 @@ 
 /* Define to 1 if you have the `getpagesize' function. */
 #undef HAVE_GETPAGESIZE
 
+/* Define to 1 if you have the `getrusage' function. */
+#undef HAVE_GETRUSAGE
+
 /* Define if the GNU gettext() function is already present or preinstalled. */
 #undef HAVE_GETTEXT
 
diff --git a/ld/configure b/ld/configure
index b7af25d1e5f..3f745ac883e 100755
--- a/ld/configure
+++ b/ld/configure
@@ -18753,7 +18753,7 @@  fi
 
 done
 
-for ac_func in close glob lseek mkstemp open realpath waitpid
+for ac_func in close getrusage glob lseek mkstemp open realpath waitpid
 do :
   as_ac_var=`$as_echo "ac_cv_func_$ac_func" | $as_tr_sh`
 ac_fn_c_check_func "$LINENO" "$ac_func" "$as_ac_var"
diff --git a/ld/configure.ac b/ld/configure.ac
index 228f2ee4089..1ee0c0c77f9 100644
--- a/ld/configure.ac
+++ b/ld/configure.ac
@@ -414,7 +414,7 @@  AC_SUBST(NATIVE_LIB_DIRS)
 AC_CHECK_HEADERS(fcntl.h elf-hints.h limits.h inttypes.h stdint.h \
 		 sys/file.h sys/mman.h sys/param.h sys/stat.h sys/time.h \
 		 sys/types.h unistd.h)
-AC_CHECK_FUNCS(close glob lseek mkstemp open realpath waitpid)
+AC_CHECK_FUNCS(close getrusage glob lseek mkstemp open realpath waitpid)
 
 BFD_BINARY_FOPEN
 
diff --git a/ld/emultempl/ppc64elf.em b/ld/emultempl/ppc64elf.em
index f7a8f1eb259..857cf54ad06 100644
--- a/ld/emultempl/ppc64elf.em
+++ b/ld/emultempl/ppc64elf.em
@@ -606,14 +606,15 @@  gld${EMULATION_NAME}_finish (void)
     einfo (_("%X%P: can not build stubs: %E\n"));
 
   fflush (stdout);
+  FILE * out = config.stats_file ? config.stats_file : stderr;
   for (line = msg; line != NULL; line = endline)
     {
       endline = strchr (line, '\n');
       if (endline != NULL)
 	*endline++ = '\0';
-      fprintf (stderr, "%s: %s\n", program_name, line);
+      fprintf (out, "%s: %s\n", program_name, line);
     }
-  fflush (stderr);
+  fflush (out);
   free (msg);
 
   ldelf_finish ();
diff --git a/ld/ld.h b/ld/ld.h
index 254f0a097bb..c8688153bd4 100644
--- a/ld/ld.h
+++ b/ld/ld.h
@@ -295,6 +295,10 @@  typedef struct
   char *map_filename;
   FILE *map_file;
 
+  char *stats_filename;
+  /* If non-NULL then resource use information should be written to this file.  */
+  FILE *stats_file;
+
   char *dependency_file;
 
   unsigned int split_by_reloc;
@@ -330,6 +334,39 @@  typedef struct
   enum compressed_debug_section_type compress_debug;
 } ld_config_type;
 
+/* An enumeration of the linker phases for which resource usage information
+   is recorded.  PHASE_ALL is special as it covers the entire link process.
+
+   Instructions for adding a new phase:
+     1. Add an entry to this enumeration.
+     2. Add an entry for the phase to the phase_data[] structure in ldmain.c.
+     3. Add calls to ld_start_phase(PHASE_xxx) and ld_stop_phase(PHASE_xxx)
+        at the appropriate place(s) in the code.  It does not matter if the
+	new phase overlaps with or is contained by any other phase.
+
+    Instructions for adding a new resource:
+      1. If necessary add a new field to the phase_data structure defined in
+         ldmain.c.
+      2. Add code to initialise the field in ld_main.c:ld_start_phase().
+      3. Add code to finalise the field in ld_main.c:ld_stop_phase().
+      4. Add code to report the field in ld_main.c:report_phases().  */
+typedef enum
+{
+  PHASE_ALL = 0,
+  PHASE_CTF,
+  PHASE_MERGE,
+  PHASE_PARSE,
+  PHASE_PLUGINS,
+  PHASE_PROCESS,
+  PHASE_WRITE,
+
+  NUM_PHASES /* This must be the last entry.  */
+}
+ld_phase;
+
+extern void ld_start_phase (ld_phase);
+extern void ld_stop_phase (ld_phase);
+
 extern ld_config_type config;
 
 extern FILE * saved_script_handle;
diff --git a/ld/ld.texi b/ld/ld.texi
index b85d8103b9f..29bd0e1bb1e 100644
--- a/ld/ld.texi
+++ b/ld/ld.texi
@@ -2184,6 +2184,9 @@  Memory region         Used Size  Region Size  %age Used
              RAM:          32 B         2 GB      0.00%
 @end smallexample
 
+Note: if you want to find out about the memory usage of the linker
+itself, then the @option{--stats} option will do this.
+
 @cindex help
 @cindex usage
 @kindex --help
@@ -2706,10 +2709,76 @@  more than @var{count} relocations one output section will contain that
 many relocations.  @var{count} defaults to a value of 32768.
 
 @kindex --stats
-@item --stats
+@item --stats[=@var{filename}]
 Compute and display statistics about the operation of the linker, such
 as execution time and memory usage.
 
+If the optional @var{filename} argument is not supplied then only
+basic information is reported, and it is sent to the standard error
+output stream.  If the @var{filename} argument is supplied then
+extended information is written to the named file.  If @var{filename}
+is set to just the @var{-} symbol, then the extended information is
+sent to the standard output stream.  If the @var{filename} starts with
+@var{+} then the file is opened in append mode rather than overwrite
+mode.
+
+If the @option{-Map} option has been enabled then the information is
+also recorded in the map file as well.  Note: if both the
+@option{--stats} option and the @option{-Map} options have been given
+@var{filename} arguments and they match, then the information will
+only be written out once not twice.
+
+If the @code{LD_STATS} environment variable is defined then this
+behaves likes the @option{--stats} option.  If the variable's value is
+a string then this will used as the name of a file into which the
+information should be recorded.  Otherwise the information
+will be sent to the standard output stream.  Using the environment
+variable allows stats to be recorded without having to alter the
+linker's command line.  Note: if both the environment variable and the
+@option{--stats} option are used then the @option{--stats} option
+takes precedence.
+
+The extended information reported includes the cpu time used and, if
+the @var{getrusage()} system library call is available then memory use
+is recorded as well.  This information is reported for individual
+parts of the linking process which are referred to as @emph{phases}.
+In addition the information is also reported for a special phase
+called @emph{ALL} which covers the entire linking process.  Note that
+individual phases can contain or overlap with each other so it should
+not be assumed that the overall resources used by the linker is the
+sum of the resources used by the individual phases.
+
+In addition when extended information is being reported the linker
+version, command line arguments and linker start time are also
+included.  This makes it easier to handle the situation where multiple
+links are being invoked by a build system and to indentify exactly
+which arguments were responsible for producing the statistics that are
+reported.
+
+The extended output looks something like this:
+
+@smallexample
+Stats: linker version: (GNU Binutils) 2.44.50.20250401
+Stats: linker started: Wed Apr  2 09:36:41 2025
+Stats: args: ld -z norelro -z nomemory-seal -z no-separate-code -o a.out [...]
+
+Stats: phase               cpu time    memory      user time    system time    
+Stats: name                (microsec)   (KiB)      (seconds)      (seconds)    
+Stats: ALL                   390082    217740              0              0    
+Stats: ctf processing            12         0              0              0    
+Stats: string merge            1324         0              0              0    
+Stats: parsing                  349       288              0              0    
+Stats: plugins                    1         0              0              0    
+Stats: processing files      259616    214524              0              0    
+Stats: write                 116493         0              0              0    
+@end smallexample
+
+@kindex --no-stats
+@item --no-stats
+Disables the reporting of usage statistics, should it have been
+enabled via the @option{--stats} command line option or the
+@var{LD_STATS} environment variable.
+
 @kindex --sysroot=@var{directory}
 @item --sysroot=@var{directory}
 Use @var{directory} as the location of the sysroot, overriding the
@@ -4078,6 +4147,15 @@  If the PE/COFF specific @option{--insert-timestamp} is active and the
 timestamp value in this variable will be inserted into the COFF header
 instead of the current time.
 
+@kindex LD_STATS
+@cindex LD_STATS
+If the @code{LD_STATS} environment variable is defined then linker
+resource use information will be recorded, just as if the
+@option{--stats} option had been used.  If the @code{LD_STATS}
+variable has a string value then this will used as the name of a file
+into which the information should be stored.  Otherwise the information
+will be sent to the standard output stream.
+
 @c man end
 @end ifset
 
diff --git a/ld/ldlang.c b/ld/ldlang.c
index 0048dfa4911..0bb9e17d927 100644
--- a/ld/ldlang.c
+++ b/ld/ldlang.c
@@ -3807,6 +3807,8 @@  ldlang_open_ctf (void)
   int any_ctf = 0;
   int err;
 
+  ld_start_phase (PHASE_CTF);
+
   LANG_FOR_EACH_INPUT_STATEMENT (file)
     {
       asection *sect;
@@ -3844,17 +3846,23 @@  ldlang_open_ctf (void)
   if (!any_ctf)
     {
       ctf_output = NULL;
+      ld_stop_phase (PHASE_CTF);
       return;
     }
 
   if ((ctf_output = ctf_create (&err)) != NULL)
-    return;
+    {
+      ld_stop_phase (PHASE_CTF);
+      return;
+    }
 
   einfo (_("%P: warning: CTF output not created: `%s'\n"),
 	 ctf_errmsg (err));
 
   LANG_FOR_EACH_INPUT_STATEMENT (errfile)
     ctf_close (errfile->the_ctf);
+
+  ld_stop_phase (PHASE_CTF);
 }
 
 /* Merge together CTF sections.  After this, only the symtab-dependent
@@ -3869,6 +3877,8 @@  lang_merge_ctf (void)
   if (!ctf_output)
     return;
 
+  ld_start_phase (PHASE_CTF);
+
   output_sect = bfd_get_section_by_name (link_info.output_bfd, ".ctf");
 
   /* If the section was discarded, don't waste time merging.  */
@@ -3882,6 +3892,8 @@  lang_merge_ctf (void)
 	  ctf_close (file->the_ctf);
 	  file->the_ctf = NULL;
 	}
+
+      ld_stop_phase (PHASE_CTF);
       return;
     }
 
@@ -3924,6 +3936,8 @@  lang_merge_ctf (void)
     }
   /* Output any lingering errors that didn't come from ctf_link.  */
   lang_ctf_errs_warnings (ctf_output);
+
+  ld_stop_phase (PHASE_CTF);
 }
 
 /* Let the emulation acquire strings from the dynamic strtab to help it optimize
@@ -3932,7 +3946,9 @@  lang_merge_ctf (void)
 void
 ldlang_ctf_acquire_strings (struct elf_strtab_hash *dynstrtab)
 {
+  ld_start_phase (PHASE_CTF);
   ldemul_acquire_strings_for_ctf (ctf_output, dynstrtab);
+  ld_stop_phase (PHASE_CTF);
 }
 
 /* Inform the emulation about the addition of a new dynamic symbol, in BFD
@@ -3954,16 +3970,24 @@  lang_write_ctf (int late)
   if (!ctf_output)
     return;
 
+  ld_start_phase (PHASE_CTF);
+
   if (late)
     {
       /* Emit CTF late if this emulation says it can do so.  */
       if (ldemul_emit_ctf_early ())
-	return;
+	{
+	  ld_stop_phase (PHASE_CTF);
+	  return;
+	}
     }
   else
     {
       if (!ldemul_emit_ctf_early ())
-	return;
+	{
+	  ld_stop_phase (PHASE_CTF);
+	  return;
+	}
     }
 
   /* Inform the emulation that all the symbols that will be received have
@@ -3998,6 +4022,8 @@  lang_write_ctf (int late)
 
   LANG_FOR_EACH_INPUT_STATEMENT (file)
     file->the_ctf = NULL;
+
+  ld_stop_phase (PHASE_CTF);
 }
 
 /* Write out the CTF section late, if the emulation needs that.  */
@@ -8547,6 +8573,8 @@  lang_process (void)
     {
       asection *found;
 
+      ld_start_phase (PHASE_MERGE);
+
       /* Merge SEC_MERGE sections.  This has to be done after GC of
 	 sections, so that GCed sections are not merged, but before
 	 assigning dynamic symbols, since removing whole input sections
@@ -8554,6 +8582,8 @@  lang_process (void)
       if (!bfd_merge_sections (link_info.output_bfd, &link_info))
 	fatal (_("%P: bfd_merge_sections failed: %E\n"));
 
+      ld_stop_phase (PHASE_MERGE);
+
       /* Look for a text section and set the readonly attribute in it.  */
       found = bfd_get_section_by_name (link_info.output_bfd, ".text");
 
diff --git a/ld/ldlex.h b/ld/ldlex.h
index 999d0defc61..815da76a4c0 100644
--- a/ld/ldlex.h
+++ b/ld/ldlex.h
@@ -46,6 +46,7 @@  enum option_values
   OPTION_MAP,
   OPTION_NO_DEMANGLE,
   OPTION_NO_KEEP_MEMORY,
+  OPTION_NO_STATS,
   OPTION_NO_WARN_MISMATCH,
   OPTION_NO_WARN_SEARCH_MISMATCH,
   OPTION_NOINHIBIT_EXEC,
diff --git a/ld/ldmain.c b/ld/ldmain.c
index 54a834e42a6..91237a4baad 100644
--- a/ld/ldmain.c
+++ b/ld/ldmain.c
@@ -21,6 +21,7 @@ 
 
 #include "sysdep.h"
 #include "bfd.h"
+#include "bfdver.h"
 #include "safe-ctype.h"
 #include "libiberty.h"
 #include "bfdlink.h"
@@ -51,6 +52,10 @@ 
 
 #include <string.h>
 
+#if defined (HAVE_GETRUSAGE)
+#include <sys/resource.h>
+#endif
+
 #ifndef TARGET_SYSTEM_ROOT
 #define TARGET_SYSTEM_ROOT ""
 #endif
@@ -224,6 +229,10 @@  ld_cleanup (void)
       bfd_close_all_done (ibfd);
     }
 #if BFD_SUPPORTS_PLUGINS
+  /* Note - we do not call ld_plugin_start (PHASE_PLUGINS) here as this
+     function is only called when the linker is exiting - ie after any
+     stats may have been reported, and potentially in the middle of a
+     phase where we have already started recording plugin stats.  */
   plugin_call_cleanup ();
 #endif
   if (output_filename && delete_output_file_on_failure)
@@ -270,11 +279,305 @@  display_external_script (void)
   free (buf);
 }
 
+struct ld_phase_data
+{
+  const char *    name;
+
+  unsigned long   start;
+  unsigned long   duration;
+
+  bool            started;
+  bool            broken;
+
+#if defined (HAVE_GETRUSAGE)
+  struct rusage   begin;
+  struct rusage   use;
+#endif
+};
+
+static struct ld_phase_data phase_data [NUM_PHASES] =
+{
+  [PHASE_ALL]     = { .name = "ALL" },
+  [PHASE_CTF]     = { .name = "ctf processing" },
+  [PHASE_MERGE]   = { .name = "string merge" },
+  [PHASE_PARSE]   = { .name = "parsing" },
+  [PHASE_PLUGINS] = { .name = "plugins" },
+  [PHASE_PROCESS] = { .name = "processing files" },
+  [PHASE_WRITE]   = { .name = "write" },
+};
+
+void
+ld_start_phase (ld_phase phase)
+{
+  struct ld_phase_data * pd = phase_data + phase;
+
+  /* We record data even if config.stats_file is NULL.  This allows
+     us to record data about phases that start before the command line
+     arguments have been parsed.  ie PHASE_ALL and PHASE_PARSE.  */
+
+  /* Do not overwrite the fields if we have already started recording.  */
+  if (pd->started)
+    {
+      /* Since we do not queue phase starts and stops, if a phase is started
+	 multiple times there is a likelyhood that it will be stopped multiple
+	 times as well.  This is problematic as we will only record the data
+	 for the first time the phase stops and ignore all of the other stops.
+
+	 So let the user know.  Ideally real users will never actually see
+	 this message, and instead only developers who are adding new phase
+	 tracking code will ever encounter it.  */
+      einfo ("%P: --stats: phase %s started twice - data may be unreliable\n",
+	     pd->name);
+      return;
+    }
+
+  /* It is OK if other phases are also active at this point.
+     It just means that the phases overlap or that one phase is a sub-task
+     of another.  Since we record resources on a per-phase basis, this
+     should not matter.  */
+
+  pd->started = true;
+  pd->start = get_run_time ();
+
+#if defined (HAVE_GETRUSAGE)
+  /* Record the resource usage at the start of the phase.  */
+  struct rusage usage;
+
+  if (getrusage (RUSAGE_SELF, & usage) != 0)
+    /* FIXME: Complain ?  */
+    return;
+  
+  memcpy (& pd->begin, & usage, sizeof usage);
+#endif
+}
+
+void
+ld_stop_phase (ld_phase phase)
+{
+  struct ld_phase_data * pd = phase_data + phase;
+
+  if (!pd->started)
+    {
+      /* We set the broken flag to indicate that the data
+	 recorded for this phase is inconsistent.  */
+      pd->broken = true;
+      return;
+    }
+
+  pd->duration += get_run_time () - pd->start;
+  pd->started = false;
+
+#if defined (HAVE_GETRUSAGE)
+  struct rusage usage;
+
+  if (getrusage (RUSAGE_SELF, & usage) != 0)
+    /* FIXME: Complain ?  */
+    return;
+
+  if (phase == PHASE_ALL)
+    memcpy (& pd->use, & usage, sizeof usage);
+  else
+    {
+      struct timeval t;
+
+      /* For sub-phases we record the increase in specific fields.  */
+      /* FIXME: Most rusage{} fields appear to be irrelevent to when considering
+	 linker resource usage.  Currently we record maxrss and user and system
+	 cpu times.  Are there any other fields that might be useful ?  */
+
+#ifndef timeradd /* Macros copied from <sys/time.h>.  */
+#define timeradd(a, b, result)					\
+      do							\
+	{							\
+	  (result)->tv_sec = (a)->tv_sec + (b)->tv_sec;		\
+	  (result)->tv_usec = (a)->tv_usec + (b)->tv_usec;	\
+	  if ((result)->tv_usec >= 1000000)			\
+	    {							\
+	      ++(result)->tv_sec;				\
+	      (result)->tv_usec -= 1000000;			\
+	    }							\
+	}							\
+      while (0)
+#endif
+      
+#ifndef timersub
+#define timersub(a, b, result)					\
+      do							\
+	{							\
+	  (result)->tv_sec = (a)->tv_sec - (b)->tv_sec;		\
+	  (result)->tv_usec = (a)->tv_usec - (b)->tv_usec;	\
+	  if ((result)->tv_usec < 0)				\
+	    {							\
+	      --(result)->tv_sec;				\
+	      (result)->tv_usec += 1000000;			\
+	    }							\
+	}							\
+      while (0)
+#endif
+      
+      timersub (& usage.ru_utime, & pd->begin.ru_utime, & t);
+      timeradd (& pd->use.ru_utime, &t, & pd->use.ru_utime);
+
+      timersub (& usage.ru_stime, & pd->begin.ru_stime, & t);
+      timeradd (& pd->use.ru_stime, &t, & pd->use.ru_stime);
+		
+      if (pd->begin.ru_maxrss < usage.ru_maxrss)
+	pd->use.ru_maxrss += usage.ru_maxrss - pd->begin.ru_maxrss;
+#endif
+    }
+}
+
+static void
+report_phases (FILE * file, time_t * start, char ** argv)
+{
+  unsigned long i;
+
+  if (file == NULL)
+    return;
+
+  /* We might be writing to stdout, so make sure
+     that we do not have any pending error output.  */
+  fflush (stderr);
+
+  /* We do not translate "Stats" as we provide this as a key
+     word that can be searched for by grep and the like.  */
+#define STATS_PREFIX "Stats: "
+
+  fprintf (file, STATS_PREFIX "linker version: %s\n", BFD_VERSION_STRING);
+
+  /* No \n at the end of the string as ctime() provides its own.  */
+  fprintf (file, STATS_PREFIX "linker started: %s", ctime (start));
+
+  /* We include the linker command line arguments since
+     they can be hard to track down by other means.  */
+  if (argv != NULL)
+    {
+      fprintf (file, STATS_PREFIX "args: ");
+      for (i = 0; argv[i] != NULL; i++)
+	fprintf (file, "%s ", argv[i]);
+      fprintf (file, "\n\n");  /* Blank line to separate the args from the stats.  */
+    }
+
+  /* All of this song and dance with the column_info struct and printf
+     formatting is so that we can have a nicely formated table with regular
+     column spacing, whilst allowing for the column headers to be translated,
+     and coping nicely with extra long strings or numbers.  */
+  struct column_info
+  {
+    const char * header;
+    const char * sub_header;
+    int          width;
+    int          pad;
+  } columns[] =
+#define COLUMNS_FIELD(HEADER,SUBHEADER) \
+    { .header = N_( HEADER ), .sub_header = N_( SUBHEADER ) },
+  {
+    COLUMNS_FIELD ("phase", "name")
+    COLUMNS_FIELD ("cpu time", "(microsec)")
+#if defined (HAVE_GETRUSAGE)  
+    /* Note: keep these columns in sync with the
+       information recorded in ld_stop_phase().  */
+    COLUMNS_FIELD ("memory", "(KiB)")
+    COLUMNS_FIELD ("user time", "(seconds)")
+    COLUMNS_FIELD ("system time", "(seconds)")
+#endif
+  };
+
+#ifndef max
+#define max(A,B) ((A) < (B) ? (B) : (A))
+#endif
+
+  size_t maxwidth = 1;
+  for (i = 0; i < NUM_PHASES; i++)
+    maxwidth = max (maxwidth, strlen (phase_data[i].name));
+
+  fprintf (file, "%s", STATS_PREFIX);
+
+  for (i = 0; i < ARRAY_SIZE (columns); i++)
+    {
+      int padding;
+
+      if (i == 0)
+	columns[i].width = fprintf (file, "%-*s", (int) maxwidth, columns[i].header);
+      else
+	columns[i].width = fprintf (file, "%s", columns[i].header);
+      padding = columns[i].width % 8;
+      if (padding < 4)
+	padding = 4;
+      columns[i].pad = fprintf (file, "%*c", padding, ' ');
+    }
+
+  fprintf (file, "\n");
+
+  int bias = 0;
+#define COLUMN_ENTRY(VAL, FORMAT, N)					\
+  do									\
+    {									\
+      int l;								\
+      									\
+      if (N == 0) 							\
+	l = fprintf (file, "%-*" FORMAT, columns[N].width, VAL);	\
+      else								\
+	l = fprintf (file, "%*" FORMAT, columns[N].width - bias, VAL);	\
+      bias = 0;								\
+      if (l < columns[N].width)						\
+	l = columns[N].pad;						\
+      else if (l < columns[N].width + columns[N].pad)			\
+	l = columns[N].pad - (l - columns[N].width);			\
+      else								\
+	{								\
+	  bias = l - (columns[N].width + columns[N].pad);		\
+	  l = 0;							\
+	}								\
+      if (l)								\
+	fprintf (file, "%*c", l, ' ');					\
+    }									\
+  while (0)
+
+  fprintf (file, "%s", STATS_PREFIX);
+
+  for (i = 0; i < ARRAY_SIZE (columns); i++)
+    COLUMN_ENTRY (columns[i].sub_header, "s", i);
+
+  fprintf (file, "\n");
+
+  for (i = 0; i < NUM_PHASES; i++)
+    {
+      struct ld_phase_data * pd = phase_data + i;
+      /* This should not be needed...  */      
+      const char * name = pd->name ? pd->name : "<unnamed>";
+
+      if (pd->broken)
+	{
+	  fprintf (file, "%s %s: %s",
+		   STATS_PREFIX, name, _("WARNING: Data is unreliable!\n"));
+	  continue;
+	}
+
+      fprintf (file, "%s", STATS_PREFIX);
+
+      /* Care must be taken to keep the lines below in sync with
+	 entries in the columns_info array.
+	 FIXME: There ought to be a better way to do this...  */
+      COLUMN_ENTRY (name, "s", 0);
+      COLUMN_ENTRY (pd->duration, "ld", 1);
+#if defined (HAVE_GETRUSAGE)
+      COLUMN_ENTRY (pd->use.ru_maxrss, "ld", 2);
+      COLUMN_ENTRY (pd->use.ru_utime.tv_sec, "ld", 3);
+      COLUMN_ENTRY (pd->use.ru_stime.tv_sec, "ld", 4);
+#endif
+      fprintf (file, "\n");
+    }
+
+  fflush (file);
+}
+
 int
 main (int argc, char **argv)
 {
   char *emulation;
   long start_time = get_run_time ();
+  time_t start_seconds = time (NULL);
 
 #ifdef HAVE_LC_MESSAGES
   setlocale (LC_MESSAGES, "");
@@ -286,7 +589,23 @@  main (int argc, char **argv)
   program_name = argv[0];
   xmalloc_set_program_name (program_name);
 
+  /* Check the LD_STATS environment variable before parsing the command line
+     so that the --stats option, if used, can override the environment variable.  */
+  char * stats_filename;
+  if ((stats_filename = getenv ("LD_STATS")) != NULL)
+    {
+      if (ISPRINT (stats_filename[0]))
+	config.stats_filename = stats_filename;
+      else
+	config.stats_filename = "-";
+      config.stats = true;
+    }
+
+  ld_start_phase (PHASE_ALL);
+  ld_start_phase (PHASE_PARSE);
+  
   expandargv (&argc, &argv);
+  char ** saved_argv = dupargv (argv);
 
   if (bfd_init () != BFD_INIT_MAGIC)
     fatal (_("%P: fatal error: libbfd ABI mismatch\n"));
@@ -404,11 +723,17 @@  main (int argc, char **argv)
   if (config.hash_table_size != 0)
     bfd_hash_set_default_size (config.hash_table_size);
 
+  ld_stop_phase (PHASE_PARSE);
+  
 #if BFD_SUPPORTS_PLUGINS
+  ld_start_phase (PHASE_PLUGINS);
   /* Now all the plugin arguments have been gathered, we can load them.  */
   plugin_load_plugins ();
+  ld_stop_phase (PHASE_PLUGINS);
 #endif /* BFD_SUPPORTS_PLUGINS */
 
+  ld_start_phase (PHASE_PARSE);
+
   ldemul_set_symbols ();
 
   /* If we have not already opened and parsed a linker script,
@@ -531,7 +856,31 @@  main (int argc, char **argv)
       link_info.has_map_file = true;
     }
 
+  if (config.stats_filename != NULL)
+    {
+      if (config.map_filename != NULL
+	  && strcmp (config.stats_filename, config.map_filename) == 0)
+	config.stats_file = NULL;
+      else if (strcmp (config.stats_filename, "-") == 0)
+	config.stats_file = stdout;
+      else
+	{
+	  if (config.stats_filename[0] == '+')
+	    config.stats_file = fopen (config.stats_filename + 1, "a");
+	  else
+	    config.stats_file = fopen (config.stats_filename, "w");
+
+	  if (config.stats_file == NULL)
+	    einfo ("%P: Warning: failed to open resource record file: %s\n",
+		   config.stats_filename);
+	}
+    }
+
+  ld_stop_phase (PHASE_PARSE);
+
+  ld_start_phase (PHASE_PROCESS);
   lang_process ();
+  ld_stop_phase (PHASE_PROCESS);
 
   /* Print error messages for any missing symbols, for any warning
      symbols, and possibly multiple definitions.  */
@@ -558,7 +907,11 @@  main (int argc, char **argv)
   link_info.output_bfd->flags
     |= flags & bfd_applicable_file_flags (link_info.output_bfd);
 
+
+  ld_start_phase (PHASE_WRITE);
   ldwrite ();
+  ld_stop_phase (PHASE_WRITE);
+
 
   if (config.map_file != NULL)
     lang_map ();
@@ -653,19 +1006,38 @@  main (int argc, char **argv)
   if (config.emit_gnu_object_only)
     cmdline_emit_object_only_section ();
 
+  ld_stop_phase (PHASE_ALL);
+
   if (config.stats)
     {
-      long run_time = get_run_time () - start_time;
+      report_phases (config.map_file, & start_seconds, saved_argv);
+
+      if (config.stats_filename)
+	{
+	  report_phases (config.stats_file, & start_seconds, saved_argv);
+
+	  if (config.stats_file != stdout && config.stats_file != stderr)
+	    {
+	      fclose (config.stats_file);
+	      config.stats_file = NULL;
+	    }
+	}
+      else /* This is for backwards compatibility.  */
+	{
+	  long run_time = get_run_time () - start_time;
 
-      fflush (stdout);
-      fprintf (stderr, _("%s: total time in link: %ld.%06ld\n"),
-	       program_name, run_time / 1000000, run_time % 1000000);
-      fflush (stderr);
+	  fflush (stdout);
+	  fprintf (stderr, _("%s: total time in link: %ld.%06ld\n"),
+		   program_name, run_time / 1000000, run_time % 1000000);
+	  fflush (stderr);
+	}
     }
 
   /* Prevent ld_cleanup from deleting the output file.  */
   output_filename = NULL;
 
+  freeargv (saved_argv);
+
   xexit (0);
   return 0;
 }
@@ -942,8 +1314,11 @@  add_archive_element (struct bfd_link_info *info,
       && (!no_more_claiming
 	  || bfd_get_lto_type (abfd) != lto_fat_ir_object))
     {
+      ld_start_phase (PHASE_PLUGINS);
       /* We must offer this archive member to the plugins to claim.  */
       plugin_maybe_claim (input);
+      ld_stop_phase (PHASE_PLUGINS);
+
       if (input->flags.claimed)
 	{
 	  if (no_more_claiming)
diff --git a/ld/lexsup.c b/ld/lexsup.c
index 7de6e257ad0..bde20465835 100644
--- a/ld/lexsup.c
+++ b/ld/lexsup.c
@@ -499,8 +499,10 @@  static const struct ld_option ld_options[] =
   { {"split-by-reloc", optional_argument, NULL, OPTION_SPLIT_BY_RELOC},
     '\0', N_("[=COUNT]"), N_("Split output sections every COUNT relocs"),
     TWO_DASHES },
-  { {"stats", no_argument, NULL, OPTION_STATS},
-    '\0', NULL, N_("Print memory usage statistics"), TWO_DASHES },
+  { {"stats", optional_argument, NULL, OPTION_STATS},
+    '\0', NULL, N_("Print resource usage statistics"), TWO_DASHES },
+  { {"no-stats", optional_argument, NULL, OPTION_NO_STATS},
+    '\0', NULL, N_("Do not print resource usage statistics"), TWO_DASHES },
   { {"target-help", no_argument, NULL, OPTION_TARGET_HELP},
     '\0', NULL, N_("Display target specific options"), TWO_DASHES },
   { {"task-link", required_argument, NULL, OPTION_TASK_LINK},
@@ -1412,6 +1414,17 @@  parse_args (unsigned argc, char **argv)
 	  break;
 	case OPTION_STATS:
 	  config.stats = true;
+	  if (optarg)
+	    config.stats_filename = optarg;
+	  else
+	    {
+	      config.stats_filename = NULL;
+	      config.stats_file = stderr;
+	    }
+	  break;
+	case OPTION_NO_STATS:
+	  config.stats = false;
+	  config.stats_filename = NULL;
 	  break;
 	case OPTION_NO_SYMBOLIC:
 	  opt_symbolic = symbolic_unset;
diff --git a/ld/testsuite/ld-elf/sec64k.exp b/ld/testsuite/ld-elf/sec64k.exp
index 8dcb021c3f9..deb46d3281d 100644
--- a/ld/testsuite/ld-elf/sec64k.exp
+++ b/ld/testsuite/ld-elf/sec64k.exp
@@ -168,9 +168,9 @@  if [catch { set ofd [open "tmpdir/$test2.d" w] } x] {
     return
 }
 
-# too big for avr, d10v and msp
-# lack of fancy orphan section handling causes overlap on fr30 and iq2000
-# bfin and lm32 complain about relocations in read-only sections
+# Too big for avr, d10v and msp.
+# Lack of fancy orphan section handling causes overlap on fr30 and iq2000.
+# bfin and lm32 complain about relocations in read-only sections.
 if { ![istarget "d10v-*-*"]
      && ![istarget "avr-*-*"]
      && ![istarget "msp*-*-*"]
@@ -179,7 +179,13 @@  if { ![istarget "d10v-*-*"]
      && ![istarget "bfin-*-linux*"]
      && ![istarget "lm32-*-linux*"]
      && ![istarget "pru-*-*"] } {
+
+    # Create a 64ksec.d test control file...
+    
+    # List the input files.
     foreach sfile $sfiles { puts $ofd "#source: $sfile" }
+
+    # Add any needed linker command line options.
     if { [istarget spu*-*-*] } {
 	puts $ofd "#ld: --local-store 0:0"
     } elseif { [istarget "i?86-*-linux*"] || [istarget "x86_64-*-linux*"] } {
@@ -187,10 +193,20 @@  if { ![istarget "d10v-*-*"]
     } else {
 	puts $ofd "#ld:"
     }
-    #force z80 target to compile for eZ80 in ADL mode
+
+    # Enable the accumulation of internal linker statistics in a separate file.
+    # Enabled this way as you cannot have multiple #ld: options in a .d file.
+    # The + character causes the file to opened in append mode, so that multiple
+    # runs of this test will accumulate data over time.  Thus allowing regular
+    # testers to see changes in the performance of the linker.
+    puts $ofd "#ld_after_inputfiles: --stats=+tmpdir/$test2.stats"
+    
+    # Force z80 target to compile for eZ80 in ADL mode.
     if { [istarget "z80-*-*"] } then {
 	puts $ofd "#as: -ez80-adl"
     }
+
+    # Add a test of the linked binary.
     puts $ofd "#readelf: -W -wN -Ss"
     puts $ofd "There are 660.. section headers.*:"
     puts $ofd "#..."
@@ -199,6 +215,7 @@  if { ![istarget "d10v-*-*"]
     puts $ofd "  \\\[65279\\\] \\.foo\\.\[0-9\]+ .*"
     puts $ofd "  \\\[65280\\\] \\.foo\\.\[0-9\]+ .*"
     puts $ofd "#..."
+
     if { [is_elf_unused_section_symbols ] } {
 	puts $ofd " 660..: \[0-9a-f\]+\[ \]+0\[ \]+SECTION\[ \]+LOCAL\[ \]+DEFAULT\[ \]+660...*"
 	puts $ofd "#..."
@@ -209,6 +226,7 @@  if { ![istarget "d10v-*-*"]
 	puts $ofd " 66...: \[0-9a-f\]+\[ \]+0\[ \]+NOTYPE\[ \]+LOCAL\[ \]+DEFAULT\[ \]+660.. bar_66000$"
     }
     puts $ofd "#..."
+
     # Global symbols are not in "alphanumeric" order, so we just check
     # that the first and the last are present in any order (assuming no
     # duplicates).
@@ -217,9 +235,14 @@  if { ![istarget "d10v-*-*"]
     puts $ofd ".* (\[0-9\] foo_1|66... foo_66000)$"
     puts $ofd "#pass"
     close $ofd
+
+    # Now run the constructed test file.
     run_dump_test "tmpdir/$test2"
+
+    # Leave the test file around in case the user wants to examine it.
 }
 
+# Tidy up.
 for { set i 1 } { $i < $max_sec / $secs_per_file } { incr i } {
     catch "exec rm -f tmpdir/dump$i.o" status
 }
diff --git a/ld/testsuite/ld-scripts/map-address.exp b/ld/testsuite/ld-scripts/map-address.exp
index 2291302ae30..776fed4357f 100644
--- a/ld/testsuite/ld-scripts/map-address.exp
+++ b/ld/testsuite/ld-scripts/map-address.exp
@@ -130,19 +130,38 @@  if { [is_elf_format] } {
 	      $IMAGE_BASE tmpdir/map-address.o \
 	      -Map=tmpdir/map-locals.map --print-map-locals"]} {
 	fail $testname
-	return
-    }
 
-    if [is_remote host] then {
-	remote_upload host "tmpdir/map-locals.map"
-    }
+    } else {
 
-    # Some ELF targets do not preserve their local symbols.
-    setup_xfail "d30v-*-*" "dlx-*-*" "pj-*-*" "s12z-*-*" "xgate-*-*"
+	if [is_remote host] then {
+	    remote_upload host "tmpdir/map-locals.map"
+	}
+
+	# Some ELF targets do not preserve their local symbols.
+	setup_xfail "d30v-*-*" "dlx-*-*" "pj-*-*" "s12z-*-*" "xgate-*-*"
     
+	if {[regexp_diff \
+		 "tmpdir/map-locals.map" \
+		 "$srcdir/$subdir/map-locals.d"]} {
+	    fail $testname
+	} else {
+	    pass $testname
+	}
+    }
+}
+
+set testname "map with resource usage"
+
+if {![ld_link $ld tmpdir/map-address \
+	  "$LDFLAGS -T $srcdir/$subdir/map-address.t \
+	   $IMAGE_BASE tmpdir/map-address.o \
+	   -Map=tmpdir/map-locals.map \
+	   --stats=tmpdir/map-stats.map"]} {
+    fail $testname
+} else {
     if {[regexp_diff \
-	     "tmpdir/map-locals.map" \
-	     "$srcdir/$subdir/map-locals.d"]} {
+	     "tmpdir/map-stats.map" \
+	     "$srcdir/$subdir/map-stats.d"]} {
 	fail $testname
     } else {
 	pass $testname
--- /dev/null	2025-04-01 09:33:58.494830307 +0100
+++ current/ld/testsuite/ld-scripts/map-stats.d	2025-03-28 16:10:44.520133279 +0000
@@ -0,0 +1,5 @@ 
+#...
+Stats: phase.*
+Stats: name.*
+Stats: ALL.*
+#pass