[binutils-gdb] Add optional filename argument to the linker's --stats option, allowing extra resource use informati

Nick Clifton nickc@sourceware.org
Wed Apr 2 10:06:38 GMT 2025


https://sourceware.org/git/gitweb.cgi?p=binutils-gdb.git;h=c4fce3ef2927843f74df2b3c10f4347be6ff606f

commit c4fce3ef2927843f74df2b3c10f4347be6ff606f
Author: Nick Clifton <nickc@redhat.com>
Date:   Wed Apr 2 10:56:16 2025 +0100

    Add optional filename argument to the linker's --stats option, allowing extra resource use information to be reported.

Diff:
---
 ld/NEWS                                 |   9 +
 ld/config.in                            |   3 +
 ld/configure                            |   2 +-
 ld/configure.ac                         |   2 +-
 ld/emultempl/ppc64elf.em                |   5 +-
 ld/ld.h                                 |  37 +++
 ld/ld.texi                              |  80 ++++++-
 ld/ldlang.c                             |  36 ++-
 ld/ldlex.h                              |   1 +
 ld/ldmain.c                             | 385 +++++++++++++++++++++++++++++++-
 ld/lexsup.c                             |  17 +-
 ld/testsuite/ld-elf/sec64k.exp          |  31 ++-
 ld/testsuite/ld-scripts/map-address.exp |  37 ++-
 ld/testsuite/ld-scripts/map-stats.d     |   5 +
 14 files changed, 622 insertions(+), 28 deletions(-)

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
diff --git a/ld/testsuite/ld-scripts/map-stats.d b/ld/testsuite/ld-scripts/map-stats.d
new file mode 100644
index 00000000000..ba9adf8798a
--- /dev/null
+++ b/ld/testsuite/ld-scripts/map-stats.d
@@ -0,0 +1,5 @@
+#...
+Stats: phase.*
+Stats: name.*
+Stats: ALL.*
+#pass


More information about the Binutils-cvs mailing list