Output a linker map file for official builds

I intend to use the map file for binary size analysis.

Map file is gzipped, and is about 20MB for Android.
Adds ~2.5 seconds onto a 30 second link on my z620.

BUG=681694

Review-Url: https://codereview.chromium.org/2726983004
Cr-Original-Commit-Position: refs/heads/master@{#455482}
Cr-Mirrored-From: https://chromium.googlesource.com/chromium/src
Cr-Mirrored-Commit: 7d275da6c61c065510dcbcbd237636891d4b435a
diff --git a/android/BUILD.gn b/android/BUILD.gn
index ad7b762..9b3b162 100644
--- a/android/BUILD.gn
+++ b/android/BUILD.gn
@@ -25,6 +25,9 @@
     toolchain_args = invoker.toolchain_args
     toolchain_args.current_os = "android"
 
+    # Output linker map files for binary size analysis.
+    enable_linker_map = true
+
     # Make our manually injected libs relative to the build dir.
     _ndk_lib =
         rebase_path(invoker.sysroot + "/" + invoker.lib_dir, root_build_dir)
diff --git a/gcc_link_wrapper.py b/gcc_link_wrapper.py
index c589fe3..0e256fa 100755
--- a/gcc_link_wrapper.py
+++ b/gcc_link_wrapper.py
@@ -15,6 +15,8 @@
 import subprocess
 import sys
 
+import wrapper_utils
+
 
 # When running on a Windows host and using a toolchain whose tools are
 # actually wrapper scripts (i.e. .bat files on Windows) rather than binary
@@ -37,9 +39,12 @@
                       help='The strip binary to run',
                       metavar='PATH')
   parser.add_argument('--unstripped-file',
-                      required=True,
                       help='Executable file produced by linking command',
                       metavar='FILE')
+  parser.add_argument('--map-file',
+                      help=('Use --Wl,-Map to generate a map file. Will be '
+                            'gzipped if extension ends with .gz'),
+                      metavar='FILE')
   parser.add_argument('--output',
                       required=True,
                       help='Final output executable file',
@@ -51,7 +56,8 @@
   # Work-around for gold being slow-by-default. http://crbug.com/632230
   fast_env = dict(os.environ)
   fast_env['LC_ALL'] = 'C'
-  result = subprocess.call(CommandToRun(args.command), env=fast_env)
+  result = wrapper_utils.RunLinkWithOptionalMapFile(args.command, env=fast_env,
+                                                    map_file=args.map_file)
   if result != 0:
     return result
 
diff --git a/gcc_solink_wrapper.py b/gcc_solink_wrapper.py
index 426f9d6..7efc490 100755
--- a/gcc_solink_wrapper.py
+++ b/gcc_solink_wrapper.py
@@ -78,6 +78,10 @@
                       required=True,
                       help='Output table-of-contents file',
                       metavar='FILE')
+  parser.add_argument('--map-file',
+                      help=('Use --Wl,-Map to generate a map file. Will be '
+                            'gzipped if extension ends with .gz'),
+                      metavar='FILE')
   parser.add_argument('--output',
                       required=True,
                       help='Final output shared object file',
@@ -99,8 +103,10 @@
         whitelist_candidates, args.resource_whitelist)
 
   # First, run the actual link.
-  result = subprocess.call(
-      wrapper_utils.CommandToRun(args.command), env=fast_env)
+  command = wrapper_utils.CommandToRun(args.command)
+  result = wrapper_utils.RunLinkWithOptionalMapFile(command, env=fast_env,
+                                                    map_file=args.map_file)
+
   if result != 0:
     return result
 
diff --git a/gcc_toolchain.gni b/gcc_toolchain.gni
index b319806..21985f8 100644
--- a/gcc_toolchain.gni
+++ b/gcc_toolchain.gni
@@ -214,6 +214,9 @@
       extra_ldflags = ""
     }
 
+    enable_linker_map =
+        defined(invoker.enable_linker_map) && invoker.enable_linker_map
+
     # These library switches can apply to all tools below.
     lib_switch = "-l"
     lib_dir_switch = "-L"
@@ -319,18 +322,27 @@
 
       link_command = "$ld -shared {{ldflags}}${extra_ldflags} -o \"$unstripped_sofile\" -Wl,-soname=\"$soname\" @\"$rspfile\""
 
+      # Generate a map file to be used for binary size analysis.
+      # Map file adds ~10% to the link time on a z620.
+      # With target_os="android", libchrome.so.map.gz is ~20MB.
+      map_switch = ""
+      if (enable_linker_map && is_official_build) {
+        map_file = "$unstripped_sofile.map.gz"
+        map_switch = " --map-file \"$map_file\""
+      }
+
       assert(defined(readelf), "to solink you must have a readelf")
       assert(defined(nm), "to solink you must have an nm")
       strip_switch = ""
       if (defined(invoker.strip)) {
-        strip_switch = "--strip=${invoker.strip}"
+        strip_switch = "--strip=${invoker.strip} "
       }
 
       # This needs a Python script to avoid using a complex shell command
       # requiring sh control structures, pipelines, and POSIX utilities.
       # The host might not have a POSIX shell and utilities (e.g. Windows).
       solink_wrapper = rebase_path("//build/toolchain/gcc_solink_wrapper.py")
-      command = "$python_path \"$solink_wrapper\" --readelf=\"$readelf\" --nm=\"$nm\" $strip_switch --sofile=\"$unstripped_sofile\" --tocfile=\"$tocfile\" --output=\"$sofile\"$whitelist_flag -- $link_command"
+      command = "$python_path \"$solink_wrapper\" --readelf=\"$readelf\" --nm=\"$nm\" $strip_switch--sofile=\"$unstripped_sofile\" --tocfile=\"$tocfile\"$map_switch --output=\"$sofile\"$whitelist_flag -- $link_command"
 
       rspfile_content = "-Wl,--whole-archive {{inputs}} {{solibs}} -Wl,--no-whole-archive $solink_libs_section_prefix {{libs}} $solink_libs_section_postfix"
 
@@ -365,6 +377,9 @@
       if (sofile != unstripped_sofile) {
         outputs += [ unstripped_sofile ]
       }
+      if (defined(map_file)) {
+        outputs += [ map_file ]
+      }
       link_output = sofile
       depend_output = tocfile
     }
@@ -433,12 +448,25 @@
         unstripped_outfile = "{{root_out_dir}}/exe.unstripped/$exename"
       }
 
-      command = "$ld {{ldflags}}${extra_ldflags} -o \"$unstripped_outfile\" -Wl,--start-group @\"$rspfile\" {{solibs}} -Wl,--end-group $libs_section_prefix {{libs}} $libs_section_postfix"
-      if (defined(invoker.strip)) {
-        link_wrapper =
-            rebase_path("//build/toolchain/gcc_link_wrapper.py", root_build_dir)
-        command = "$python_path \"$link_wrapper\" --strip=\"${invoker.strip}\" --unstripped-file=\"$unstripped_outfile\" --output=\"$outfile\" -- $command"
+      # Generate a map file to be used for binary size analysis.
+      # Map file adds ~10% to the link time on a z620.
+      # With target_os="android", libchrome.so.map.gz is ~20MB.
+      map_switch = ""
+      if (enable_linker_map && is_official_build) {
+        map_file = "$unstripped_outfile.map.gz"
+        map_switch = " --map-file \"$map_file\""
       }
+
+      link_command = "$ld {{ldflags}}${extra_ldflags} -o \"$unstripped_outfile\" -Wl,--start-group @\"$rspfile\" {{solibs}} -Wl,--end-group $libs_section_prefix {{libs}} $libs_section_postfix"
+
+      strip_switch = ""
+      if (defined(invoker.strip)) {
+        strip_switch = " --strip=\"${invoker.strip}\" --unstripped-file=\"$unstripped_outfile\""
+      }
+
+      link_wrapper =
+          rebase_path("//build/toolchain/gcc_link_wrapper.py", root_build_dir)
+      command = "$python_path \"$link_wrapper\" --output=\"$outfile\"$strip_switch$map_switch -- $link_command"
       description = "LINK $outfile"
       rspfile_content = "{{inputs}}"
       outputs = [
@@ -450,6 +478,9 @@
       if (defined(invoker.link_outputs)) {
         outputs += invoker.link_outputs
       }
+      if (defined(map_file)) {
+        outputs += [ map_file ]
+      }
     }
 
     # These two are really entirely generic, but have to be repeated in
@@ -511,7 +542,11 @@
     ar = "${toolprefix}ar"
     nm = "${toolprefix}nm"
 
-    forward_variables_from(invoker, [ "strip" ])
+    forward_variables_from(invoker,
+                           [
+                             "enable_linker_map",
+                             "strip",
+                           ])
 
     toolchain_args = {
       if (defined(invoker.toolchain_args)) {
diff --git a/linux/BUILD.gn b/linux/BUILD.gn
index 86cd7da..3be5c36 100644
--- a/linux/BUILD.gn
+++ b/linux/BUILD.gn
@@ -58,6 +58,9 @@
 }
 
 clang_toolchain("clang_x86") {
+  # Output linker map files for binary size analysis.
+  enable_linker_map = true
+
   toolchain_args = {
     current_cpu = "x86"
     current_os = "linux"
@@ -89,6 +92,9 @@
   ar = "ar"
   ld = cxx
 
+  # Output linker map files for binary size analysis.
+  enable_linker_map = true
+
   toolchain_args = {
     current_cpu = "x86"
     current_os = "linux"
@@ -97,6 +103,9 @@
 }
 
 clang_toolchain("clang_x64") {
+  # Output linker map files for binary size analysis.
+  enable_linker_map = true
+
   toolchain_args = {
     current_cpu = "x64"
     current_os = "linux"
@@ -128,6 +137,9 @@
   ar = "ar"
   ld = cxx
 
+  # Output linker map files for binary size analysis.
+  enable_linker_map = true
+
   toolchain_args = {
     current_cpu = "x64"
     current_os = "linux"
diff --git a/wrapper_utils.py b/wrapper_utils.py
index 467d85d..f76192e 100644
--- a/wrapper_utils.py
+++ b/wrapper_utils.py
@@ -4,16 +4,31 @@
 
 """Helper functions for gcc_toolchain.gni wrappers."""
 
+import gzip
 import os
 import re
 import subprocess
 import shlex
+import shutil
 import sys
+import threading
 
 _BAT_PREFIX = 'cmd /c call '
 _WHITELIST_RE = re.compile('whitelisted_resource_(?P<resource_id>[0-9]+)')
 
 
+def _GzipThenDelete(src_path, dest_path):
+  # Results for Android map file with GCC on a z620:
+  # Uncompressed: 207MB
+  # gzip -9: 16.4MB, takes 8.7 seconds.
+  # gzip -1: 21.8MB, takes 2.0 seconds.
+  # Piping directly from the linker via -print-map (or via -Map with a fifo)
+  # adds a whopping 30-45 seconds!
+  with open(src_path, 'rb') as f_in, gzip.GzipFile(dest_path, 'wb', 1) as f_out:
+    shutil.copyfileobj(f_in, f_out)
+  os.unlink(src_path)
+
+
 def CommandToRun(command):
   """Generates commands compatible with Windows.
 
@@ -36,6 +51,37 @@
   return command
 
 
+def RunLinkWithOptionalMapFile(command, env=None, map_file=None):
+  """Runs the given command, adding in -Wl,-Map when |map_file| is given.
+
+  Also takes care of gzipping when |map_file| ends with .gz.
+
+  Args:
+    command: List of arguments comprising the command.
+    env: Environment variables.
+    map_file: Path to output map_file.
+
+  Returns:
+    The exit code of running |command|.
+  """
+  tmp_map_path = None
+  if map_file and map_file.endswith('.gz'):
+    tmp_map_path = map_file + '.tmp'
+    command.append('-Wl,-Map,' + tmp_map_path)
+  elif map_file:
+    command.append('-Wl,-Map,' + map_file)
+
+  result = subprocess.call(command, env=env)
+
+  if tmp_map_path and result == 0:
+    threading.Thread(
+        target=lambda: _GzipThenDelete(tmp_map_path, map_file)).start()
+  elif tmp_map_path and os.path.exists(tmp_map_path):
+    os.unlink(tmp_map_path)
+
+  return result
+
+
 def ResolveRspLinks(inputs):
   """Return a list of files contained in a response file.