Make gdb-add-index index concurrently.

Most linux machines can likely handle a few concurrent indexing jobs
for the shared objects in one binary. This modification uses SIGUSR1 and
subshells to simulate a semaphore with task queue that significantly
increases the speed of execution over a newly built set of binaries.

By default, this script executes 4 parallel indexing tasks, though the
concurrency be controlled by setting the INDEX_TASKS shell variable.

Here are bencharks from my z620, using a component build browser_test
binary as the target:

6 threads browser_threads:
   real    0m36.307s
   user    0m51.680s
   sys     0m13.150s

4 threads browser_threads:
   real   0m39.243s
   user   0m53.010s
   sys    0m12.230s

1 thread browser_threads:
   real    1m12.099s
   user    0m53.300s
   sys     0m13.580s

This was on an SSD. Benefit hit diminishing return after 4 threads and
may actually taper off sooner.

BUG=none
[email protected]

Review URL: https://codereview.chromium.org/23130007

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@217815 0039d316-1c4b-4281-b951-d872f2087c98
diff --git a/build/gdb-add-index b/build/gdb-add-index
index 4975532..0d66d8d 100755
--- a/build/gdb-add-index
+++ b/build/gdb-add-index
@@ -4,8 +4,84 @@
 # found in the LICENSE file.
 #
 # Saves the gdb index for a given binary and its shared library dependencies.
+#
+# This will run gdb index in parallel on a number of binaries using SIGUSR1
+# as the communication mechanism to simulate a semaphore. Because of the
+# nature of this technique, using "set -e" is very difficult. The SIGUSR1
+# terminates a "wait" with an error which we need to interpret.
+#
+# When modifying this code, most of the real logic is in the index_one_file
+# function. The rest is cleanup + sempahore plumbing.
 
-set -e
+# Cleanup temp directory and ensure all child jobs are dead-dead.
+function on_exit {
+  trap "" EXIT USR1  # Avoid reentrancy.
+
+  local jobs=$(jobs -p)
+  if [ -n "$jobs" ]; then
+    echo -n "Killing outstanding index jobs..."
+    kill -KILL $(jobs -p)
+    wait
+    echo "done"
+  fi
+
+  if [ -f "$DIRECTORY" ]; then
+    echo -n "Removing temp directory $DIRECTORY..."
+    rm -rf $DIRECTORY
+    echo done
+  fi
+}
+
+# Add index to one binary.
+function index_one_file {
+  local file=$1
+  local basename=$(basename "$file")
+
+  local readelf_out=$(readelf -S "$file")
+  if [[ $readelf_out =~ "gdb_index" ]]; then
+    echo "Skipped $basename -- already contains index."
+  else
+    local start=$(date +"%s%N")
+    echo "Adding index to $basename..."
+
+    gdb -batch "$file" -ex "save gdb-index $DIRECTORY" -ex "quit"
+    local index_file="$DIRECTORY/$basename.gdb-index"
+    if [ -f "$index_file" ]; then
+      objcopy --add-section .gdb_index="$index_file" \
+        --set-section-flags .gdb_index=readonly "$file" "$file"
+      local finish=$(date +"%s%N")
+      local elappsed=$(((finish - start)/1000000))
+      echo "   ...$basename indexed. [${elappsed}ms]"
+    else
+      echo "   ...$basename unindexable."
+    fi
+  fi
+}
+
+# Functions that when combined, concurrently index all files in FILES_TO_INDEX
+# array. The global FILES_TO_INDEX is declared in the main body of the script.
+function async_index {
+  # Start a background subshell to run the index command.
+  {
+    index_one_file $1
+    kill -SIGUSR1 $$  # $$ resolves to the parent script.
+    exit 129  # See comment above wait loop at bottom.
+  } &
+}
+
+CUR_FILE_NUM=0
+function index_next {
+  if (( CUR_FILE_NUM >= ${#FILES_TO_INDEX[@]} )); then
+    return
+  fi
+
+  async_index "${FILES_TO_INDEX[CUR_FILE_NUM]}"
+  ((CUR_FILE_NUM += 1)) || true
+}
+
+
+########
+### Main body of the script.
 
 if [[ ! $# == 1 ]]; then
   echo "Usage: $0 path-to-binary"
@@ -18,30 +94,38 @@
   exit 1
 fi
 
+# Ensure we cleanup on on exit.
+trap on_exit EXIT
+
 # We're good to go! Create temp directory for index files.
 DIRECTORY=$(mktemp -d)
 echo "Made temp directory $DIRECTORY."
 
-# Always remove directory on exit.
-trap "{ echo -n Removing temp directory $DIRECTORY...;
-  rm -rf $DIRECTORY; echo done; }" EXIT
-
-# Grab all the chromium shared library files.
-so_files=$(ldd "$FILENAME" 2>/dev/null \
+# Create array with the filename and all shared libraries that
+# have the same dirname. The dirname is a signal that these
+# shared libraries were part of the same build as the binary.
+declare -a FILES_TO_INDEX=($FILENAME
+ $(ldd "$FILENAME" 2>/dev/null \
   | grep $(dirname "$FILENAME") \
   | sed "s/.*[ \t]\(.*\) (.*/\1/")
+)
 
-# Add index to binary and the shared library dependencies.
-for file in "$FILENAME" $so_files; do
-  basename=$(basename "$file")
-  echo -n "Adding index to $basename..."
-  readelf_out=$(readelf -S "$file")
-  if [[ $readelf_out =~ "gdb_index" ]]; then
-    echo "already contains index. Skipped."
-  else
-    gdb -batch "$file" -ex "save gdb-index $DIRECTORY" -ex "quit"
-    objcopy --add-section .gdb_index="$DIRECTORY"/$basename.gdb-index \
-      --set-section-flags .gdb_index=readonly "$file" "$file"
-    echo "done."
-  fi
+# Start concurrent indexing.
+trap index_next USR1
+
+# 4 is an arbitrary default. When changing, remember we are likely IO bound
+# so basing this off the number of cores is not sensible.
+INDEX_TASKS=${INDEX_TASKS:-4}
+for ((i=0;i<${INDEX_TASKS};i++)); do
+  index_next
+done
+
+# Do a wait loop. Bash waits that terminate due a trap have an exit
+# code > 128. We also ensure that our subshell's "normal" exit occurs with
+# an exit code > 128. This allows us to do consider a > 128 exit code as
+# an indication that the loop should continue. Unfortunately, it also means
+# we cannot use set -e since technically the "wait" is failing.
+wait
+while (( $? > 128 )); do
+  wait
 done