Index: tools/binary_size/java/src/org/chromium/tools/binary_size/Addr2LineWorkerPool.java |
diff --git a/tools/binary_size/java/src/org/chromium/tools/binary_size/Addr2LineWorkerPool.java b/tools/binary_size/java/src/org/chromium/tools/binary_size/Addr2LineWorkerPool.java |
new file mode 100644 |
index 0000000000000000000000000000000000000000..03d6e5886031917f4e71af6df3eea7a889f86cff |
--- /dev/null |
+++ b/tools/binary_size/java/src/org/chromium/tools/binary_size/Addr2LineWorkerPool.java |
@@ -0,0 +1,419 @@ |
+// Copyright 2014 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+package org.chromium.tools.binary_size; |
+ |
+import java.io.BufferedReader; |
+import java.io.File; |
+import java.io.IOException; |
+import java.io.InputStream; |
+import java.io.InputStreamReader; |
+import java.io.Reader; |
+import java.nio.charset.Charset; |
+import java.util.HashMap; |
+import java.util.HashSet; |
+import java.util.Map; |
+import java.util.Queue; |
+import java.util.Random; |
+import java.util.Set; |
+import java.util.concurrent.ArrayBlockingQueue; |
+import java.util.concurrent.ConcurrentHashMap; |
+import java.util.concurrent.ConcurrentLinkedQueue; |
+import java.util.concurrent.ConcurrentMap; |
+import java.util.concurrent.CountDownLatch; |
+import java.util.concurrent.TimeUnit; |
+import java.util.concurrent.atomic.AtomicInteger; |
+import java.util.concurrent.atomic.AtomicReference; |
+ |
+class Addr2LineWorkerPool { |
+ private static final Charset ASCII = Charset.forName("US-ASCII"); |
+ private final Addr2LineWorker[] workers; |
Peter Beverloo
2014/01/08 16:06:23
nit: Java files in Chromium follow the Android cod
Andrew Hayden (chromium.org)
2014/01/08 21:04:10
I'll refactor at the same time that I fix the pyth
|
+ private final ArrayBlockingQueue<Record> recordsIn = new ArrayBlockingQueue<Record>(1000); |
+ private final Queue<Record> recordsOut = new ConcurrentLinkedQueue<Record>(); |
+ private final CountDownLatch completionLatch; |
+ private final String addr2linePath; |
+ private final String libraryPath; |
+ private final boolean disambiguate; |
+ private final boolean dedupe; |
+ private final String stripLocation; |
+ private final ConcurrentMap<Long, Record> addressesSeen = |
+ new ConcurrentHashMap<Long, Record>(100000, 0.75f, 32); |
+ private volatile Map<String,String> fileLookupTable = null; |
+ private final AtomicInteger disambiguationSuccessCount = new AtomicInteger(0); |
+ private final AtomicInteger disambiguationFailureCount = new AtomicInteger(0); |
+ private final AtomicInteger numDeduped = new AtomicInteger(0); |
Peter Beverloo
2014/01/08 16:06:23
s/numDeduped/dedupeCount/ for consistency (most no
Andrew Hayden (chromium.org)
2014/01/08 21:04:10
Done.
|
+ |
+ Addr2LineWorkerPool(final int size, |
+ final String addr2linePath, final String libraryPath, |
+ final boolean disambiguate, final boolean dedupe) |
+ throws IOException { |
+ this.addr2linePath = addr2linePath; |
+ this.libraryPath = libraryPath; |
+ this.disambiguate = disambiguate; |
+ this.dedupe = dedupe; |
+ |
+ // Prepare disambiguation table if necessary. This must be done |
+ // before launching the threads in the processing pool for visibility. |
+ if (disambiguate) { |
+ try { |
+ createLookupTable(); |
+ } catch (IOException e) { |
+ throw new RuntimeException("Can't create lookup table", e); |
+ } |
+ } |
+ |
+ // library is in, e.g.: src/out/Release |
+ // We want to convert all output paths to be relative to src, |
bulach
2014/01/08 15:04:00
nit: I had reviewers asking to not use pronouns..
Andrew Hayden (chromium.org)
2014/01/08 21:04:10
Done.
|
+ // so what we'll strip is everything up to and including "src/". |
+ String canonical = new File(libraryPath).getCanonicalPath(); |
+ int end = canonical.lastIndexOf("/src/"); |
+ if (end < 0) { |
+ // Shouldn't happen if the library exists. |
+ throw new RuntimeException("Bad library path: " + libraryPath |
+ + ". Library is expected to be within a build directory."); |
bulach
2014/01/08 15:04:00
nit: + operator in the previous line
Andrew Hayden (chromium.org)
2014/01/08 21:04:10
Done.
|
+ } |
+ stripLocation = canonical.substring(0, end + "/src/".length()); |
+ |
+ workers = new Addr2LineWorker[size]; |
+ completionLatch = new CountDownLatch(size); |
+ for (int x = 0; x < workers.length; x++) { |
+ workers[x] = new Addr2LineWorker(); |
+ } |
+ } |
+ |
+ void submit(Record record) throws InterruptedException { |
+ recordsIn.put(record); |
+ } |
+ |
+ void allRecordsSubmitted() { |
+ for (Addr2LineWorker worker : workers) { |
+ worker.stopIfQueueIsEmpty = true; |
+ } |
+ } |
+ |
+ boolean await(int amount, TimeUnit unit) throws InterruptedException { |
+ return completionLatch.await(amount, unit); |
+ } |
+ |
+ /** |
+ * @param value the base value |
+ * @param percent absolute percentage to jitter by (in the range [0,100]) |
+ * @return a value that is on average uniformly distributed within |
+ * plus or minus <em>percent</em> of the base value. |
+ */ |
+ private static int jitter(final int value, final float percent) { |
bulach
2014/01/08 15:04:00
nit: s/float/int/
|
+ Random r = new Random(); |
+ int delta = (r.nextBoolean() ? 1 : -1) * r.nextInt((int) (percent / 100f * value)); |
bulach
2014/01/08 15:04:00
nit: isn't the cast equivalent to just do it in in
Andrew Hayden (chromium.org)
2014/01/08 21:04:10
Done.
|
+ return value + delta; |
+ } |
+ |
+ |
+ /** |
+ * A class that encapsulates an addr2line process and the work that it |
+ * needs to do. |
+ */ |
+ private class Addr2LineWorker { |
+ // Our work queues |
+ private final AtomicReference<Process> processRef = new AtomicReference<Process>(); |
+ private final Thread workerThread; |
+ private volatile boolean stopIfQueueIsEmpty = false; |
+ |
+ /** |
+ * After this many addresses have been processed, the addr2line process |
+ * itself will be recycled. This prevents the addr2line process from |
+ * getting too huge, which in turn allows more parallel addr2line |
+ * processes to run. There is a balance to be achieved here, as |
+ * creating a new addr2line process is very costly. A value of |
+ * approximately 2000 appears, empirically, to be a good tradeoff |
+ * on a modern machine; memory usage stays tolerable, and good |
+ * throughput can be achieved. We jitter this value by +/- 10% so that |
bulach
2014/01/08 15:04:00
nit: ditto about "we".
// Jitter this value by +/-
Andrew Hayden (chromium.org)
2014/01/08 21:04:10
Done.
|
+ * we don't end up with all the processes restarting at once. |
+ */ |
+ private final int processRecycleThreshold = jitter(2000, 10f); |
+ |
+ private Addr2LineWorker() throws IOException { |
+ this.processRef.set(createAddr2LineProcess()); |
+ workerThread = new Thread(new Addr2LineTask(), "Addr2Line Worker"); |
+ workerThread.setDaemon(true); |
+ workerThread.start(); |
+ } |
+ |
+ /** |
+ * Builds a new addr2line process for use in this worker. |
+ * @return the process |
+ * @throws IOException if unable to create the process for any reason |
+ */ |
+ private final Process createAddr2LineProcess() |
Peter Beverloo
2014/01/08 16:06:23
The "final" here declares that subclasses of Addr2
Andrew Hayden (chromium.org)
2014/01/08 21:04:10
Old habit. I tend to make things final and open th
|
+ throws IOException { |
+ ProcessBuilder builder = new ProcessBuilder(addr2linePath, "-e", libraryPath, "-f"); |
+ Process process = builder.start(); |
+ return process; |
+ } |
+ |
+ /** |
+ * Reads records from the input queue and pipes addresses into |
+ * addr2line, using the output to complete the record and pushing |
+ * the record into the output queue. |
+ */ |
+ private class Addr2LineTask implements Runnable { |
+ @Override |
+ public void run() { |
+ int processTaskCounter = 0; |
+ InputStream inStream = processRef.get().getInputStream(); |
+ Reader isr = new InputStreamReader(inStream); |
+ BufferedReader reader = new BufferedReader(isr); |
+ try { |
+ while (true) { |
+ // Check for a task. |
+ final Record record = recordsIn.poll(1, TimeUnit.SECONDS); |
+ if (record == null) { |
+ if (stopIfQueueIsEmpty) { |
+ // All tasks have been submitted, so if the |
+ // queue is empty then there is nothing left |
+ // to do and we are ready to shut down |
+ return; |
+ } |
+ continue; // else, we are just starving. Try again. |
+ } |
+ |
+ // Check cache |
Peter Beverloo
2014/01/08 16:06:23
nit: inconsistent "cache" vs. "dedupe" terminology
Andrew Hayden (chromium.org)
2014/01/08 21:04:10
Done.
|
+ if (dedupe) { |
+ long addressLong = Long.parseLong(record.address, 16); |
+ Record existing = addressesSeen.get(addressLong); |
+ if (existing != null) { |
+ if (!existing.size.equals(record.size)) { |
+ System.err.println("WARNING: Deduped address " + |
+ record.address + " has a size mismatch, " + |
+ existing.size + " != " + record.size); |
+ } |
+ numDeduped.incrementAndGet(); |
+ continue; |
+ } else { |
bulach
2014/01/08 15:04:00
nit: the previous "if" will always continue, so ca
Andrew Hayden (chromium.org)
2014/01/08 21:04:10
Done.
|
+ if (addressesSeen.putIfAbsent(addressLong, record) != null) { |
Peter Beverloo
2014/01/08 16:06:23
Am I missing some subtlety of putIfAbsent here, or
Andrew Hayden (chromium.org)
2014/01/08 21:04:10
We could but the dedupe count could then be incorr
|
+ numDeduped.incrementAndGet(); |
+ continue; |
+ } |
+ } |
+ } |
+ |
+ // If the addr2line process is new (or recycled), |
Peter Beverloo
2014/01/08 16:06:23
nit: Why the odd character offset for line breaks
Andrew Hayden (chromium.org)
2014/01/08 21:04:10
I ocassionally start new sentences on their own li
|
+ // we have to create new readers. |
+ // We cache these for the lifetime of the addr2line |
+ // process. |
+ final Process process = processRef.get(); |
+ if (inStream == null) { |
+ inStream = process.getInputStream(); |
+ isr = new InputStreamReader(inStream); |
+ reader = new BufferedReader(isr); |
+ } |
+ |
+ // Write the address to stdin of addr2line |
+ process.getOutputStream().write(record.address.getBytes(ASCII)); |
+ process.getOutputStream().write('\n'); |
+ process.getOutputStream().flush(); |
+ |
+ // Read the answer from addr2line. Example: |
+ // ABGRToYRow_C |
+ // /src/out/Release/../../third_party/libyuv/source/row_common.cc:293 |
+ final String name = reader.readLine(); |
+ if (name == null || name.isEmpty()) { |
+ stopIfQueueIsEmpty = true; |
+ continue; |
+ } |
+ |
+ String location = reader.readLine(); |
+ if (location == null || location.isEmpty()) { |
+ stopIfQueueIsEmpty = true; |
+ continue; |
+ } |
Peter Beverloo
2014/01/08 16:06:23
This method is very long, we should try to split i
Andrew Hayden (chromium.org)
2014/01/08 21:04:10
I have pulled out two chunks: deduplication and lo
|
+ |
+ record.resolvedSuccessfully = !( |
+ name.equals("??") && location.equals("??:0")); |
+ |
+ if (location.startsWith("/")) { |
+ try { |
+ location = new File(location).getCanonicalPath(); |
+ } catch (IOException e) { |
+ System.err.println("Unable to canonicalize path: " + location); |
+ } |
+ } else if (disambiguate) { |
+ // Ambiguous path (only has the file name) |
+ // Try dictionary lookup if that's enabled |
+ final int indexOfColon = location.lastIndexOf(':'); |
+ final String key; |
+ final String line; |
+ if (indexOfColon != -1) { |
+ key = location.substring(0, indexOfColon); |
+ line = location.substring(indexOfColon + 1); |
+ } else { |
+ key = location; |
+ line = null; |
+ } |
+ final String found = fileLookupTable.get(key); |
+ if (found != null) { |
+ disambiguationSuccessCount.incrementAndGet(); |
+ location = found; |
+ if (line != null) location = location + ":" + line; |
+ } else { |
+ disambiguationFailureCount.incrementAndGet(); |
+ } |
+ } |
+ if (location.startsWith(stripLocation)) { |
+ location = location.substring(stripLocation.length()); |
+ } |
+ |
+ if (record.resolvedSuccessfully) { |
+ // Keep the name from the initial NM dump. |
+ // Some addr2line impls, such as the one for Android |
+ // on ARM, seem to lose data here. |
+ // Note that the location may also include a line |
+ // discriminator, which maps to a part of a line. |
+ // We don't currently care about this. |
+ record.location = location; |
+ } |
+ |
+ // Check if there is more input on the stream. |
+ // If there is we have made a serious processing |
+ // error, and reading anything else would de-sync |
+ // the input queue from the results being read. |
+ if (inStream.available() > 0) { |
+ throw new IllegalStateException( |
+ "Alignment mismatch in output from address " + record.address); |
+ } |
+ |
+ // Track stats and move record to output queue |
+ processTaskCounter++; |
+ recordsOut.add(record); |
+ |
+ // If the addr2line process has done too much work, |
+ // kill it and start a new one to reduce memory |
+ // pressure created by the pool. |
+ if (processTaskCounter >= processRecycleThreshold) { |
+ // Out with the old... |
+ try { |
+ processRef.get().destroy(); |
+ } catch (Exception e) { |
+ System.err.println("warning: zombie process"); |
Peter Beverloo
2014/01/08 16:06:23
s/warning/WARNING/ to match line 185.
|
+ e.printStackTrace(System.err); |
+ } |
+ // ... and in with the new! |
+ try { |
+ processRef.set(createAddr2LineProcess()); |
+ } catch (IOException e) { |
+ e.printStackTrace(); |
Peter Beverloo
2014/01/08 16:06:23
Why do we write the Exception above (line 297) to
Andrew Hayden (chromium.org)
2014/01/08 21:04:10
Whoops. Actually, the default with no argument pas
|
+ } |
+ processTaskCounter = 0; |
+ inStream = null; // new readers need to be created next iteration |
+ } |
+ } |
+ } catch (Exception e) { |
+ e.printStackTrace(); |
+ } finally { |
+ try { |
+ // ensure process is dead |
+ processRef.get().destroy(); |
+ } catch (Exception e) { |
+ // ignore |
Peter Beverloo
2014/01/08 16:06:23
nit: Clarify why it's ok to ignore this. An empty
Andrew Hayden (chromium.org)
2014/01/08 21:04:10
Added comments.
|
+ } |
+ completionLatch.countDown(); |
+ } |
+ } |
+ } |
+ } |
+ |
+ private void createLookupTable() throws IOException { |
+ // Convert /src/out/Release/lib/libchromeview.so -> /src/out/Release |
+ final File libraryOutputDirectory = new File(libraryPath) |
+ .getParentFile().getParentFile().getCanonicalFile(); |
Peter Beverloo
2014/01/08 16:06:23
This is rather hard-coded for usage on Android. Ar
Andrew Hayden (chromium.org)
2014/01/08 21:04:10
Added TODO. It's not clear to me how else to do th
|
+ |
+ // Convert /src/out/Release -> /src |
+ final File root = libraryOutputDirectory |
+ .getParentFile().getParentFile().getCanonicalFile(); |
+ |
+ // there is no code at the root of Chromium. |
+ // ignore all the 'out' directories. |
Peter Beverloo
2014/01/08 16:06:23
nit: Sentences start with capitals.
Andrew Hayden (chromium.org)
2014/01/08 21:04:10
Done.
|
+ fileLookupTable = new HashMap<String, String>(); |
+ Set<String> dupes = new HashSet<String>(); |
+ for (File file : root.listFiles()) { |
+ if (file.isDirectory()) { |
+ boolean skip = false; |
+ String name = file.getName(); |
+ if (name.startsWith("out")) { |
+ if (new File(file, "Release").exists() || new File(file, "Debug").exists()) { |
+ // It's an output directory, skip it - except for the |
+ // 'obj' and 'gen' subdirectories that are siblings |
+ // to the library file's parent dir, which we need. |
+ // We'll include those at the very end, since we know |
+ // exactly what they are. |
+ skip = true; |
Peter Beverloo
2014/01/08 16:06:23
Rather than using |skip| here, you can just use th
Andrew Hayden (chromium.org)
2014/01/08 21:04:10
Done.
|
+ } |
+ } else if (name.startsWith(".")) { |
+ // Skip things like .git, .svn, ... |
Peter Beverloo
2014/01/08 16:06:23
micro nit: "Skip directories such as .git, .svn, e
Andrew Hayden (chromium.org)
2014/01/08 21:04:10
Done.
|
+ skip = true; |
+ } |
+ if (!skip) { |
+ findSourceFiles(file, dupes); |
+ } |
+ } |
+ } |
+ |
+ // Include magic directories. |
Peter Beverloo
2014/01/08 16:06:23
nit: "Include directories which are likely to cont
Andrew Hayden (chromium.org)
2014/01/08 21:04:10
Done.
|
+ findSourceFiles(new File(libraryOutputDirectory, "gen"), dupes); |
+ findSourceFiles(new File(libraryOutputDirectory, "obj"), dupes); |
+ |
+ // Any duplicates in the filesystem can't be used for disambiguation |
+ // because we don't know which of them is the true source. |
+ // Thus we must discard all duplicates. |
+ for (String dupe : dupes) { |
+ fileLookupTable.remove(dupe); |
+ } |
+ } |
+ |
+ // TODO(andrewhayden): Could integrate with build system to know EXACTLY |
+ // what is out there. This would avoid the need for the dupes set, which |
+ // would make it possible to do much better deduping. |
+ private final void findSourceFiles(File directory, Set<String> dupes) { |
+ for (File file : directory.listFiles()) { |
+ if (file.isDirectory() && file.canRead()) { |
+ if (!file.getName().startsWith(".")) { |
+ findSourceFiles(file, dupes); |
+ } |
+ } else { |
+ String name = file.getName(); |
+ if (name.endsWith(".c") || name.endsWith(".cc") |
+ || name.endsWith(".h") || name.endsWith(".cpp")) { |
Peter Beverloo
2014/01/08 16:06:23
Does it help to include assembly files here (.asm
Andrew Hayden (chromium.org)
2014/01/08 21:04:10
I don't know, but I'm not opposed. A bit of greppi
|
+ if (fileLookupTable.put(name, file.getAbsolutePath()) != null) dupes.add(name); |
+ } |
+ } |
+ } |
+ } |
+ |
+ /** |
+ * Polls the output queue for the next record. |
+ * @return the next record |
+ */ |
+ Record poll() { |
+ return recordsOut.poll(); |
+ } |
+ |
+ /** |
+ * @return the number of ambiguous paths successfully disambiguated |
+ */ |
+ int getDisambiguationSuccessCount() { |
+ return disambiguationSuccessCount.get(); |
+ } |
+ |
+ /** |
+ * @return the number of ambiguous paths that couldn't be disambiguated |
+ */ |
+ int getDisambiguationFailureCount() { |
+ return disambiguationFailureCount.get(); |
+ } |
+ |
+ /** |
+ * @return the number of symbols deduped |
+ */ |
+ int getDedupeCount() { |
+ return numDeduped.get(); |
+ } |
+} |