| OLD | NEW |
| (Empty) |
| 1 // Copyright 2014 The Chromium Authors. All rights reserved. | |
| 2 // Use of this source code is governed by a BSD-style license that can be | |
| 3 // found in the LICENSE file. | |
| 4 package org.chromium.tools.binary_size; | |
| 5 | |
| 6 import java.io.BufferedReader; | |
| 7 import java.io.File; | |
| 8 import java.io.IOException; | |
| 9 import java.io.InputStream; | |
| 10 import java.io.InputStreamReader; | |
| 11 import java.io.Reader; | |
| 12 import java.nio.charset.Charset; | |
| 13 import java.util.HashMap; | |
| 14 import java.util.HashSet; | |
| 15 import java.util.Map; | |
| 16 import java.util.Queue; | |
| 17 import java.util.Random; | |
| 18 import java.util.Set; | |
| 19 import java.util.concurrent.ArrayBlockingQueue; | |
| 20 import java.util.concurrent.ConcurrentHashMap; | |
| 21 import java.util.concurrent.ConcurrentLinkedQueue; | |
| 22 import java.util.concurrent.ConcurrentMap; | |
| 23 import java.util.concurrent.CountDownLatch; | |
| 24 import java.util.concurrent.TimeUnit; | |
| 25 import java.util.concurrent.atomic.AtomicInteger; | |
| 26 import java.util.concurrent.atomic.AtomicReference; | |
| 27 | |
| 28 /** | |
| 29 * A pool of workers that run 'addr2line', looking up source locations for | |
| 30 * addresses that are fed into the pool via a queue. As address lookups | |
| 31 * complete, records are placed onto an output queue. | |
| 32 */ | |
| 33 class Addr2LineWorkerPool { | |
| 34 private static final Charset sAscii = Charset.forName("US-ASCII"); | |
| 35 private final Addr2LineWorker[] mWorkers; | |
| 36 private final ArrayBlockingQueue<Record> mRecordsIn = new ArrayBlockingQueue
<Record>(1000); | |
| 37 private final Queue<Record> mRecordsOut = new ConcurrentLinkedQueue<Record>(
); | |
| 38 private final CountDownLatch mCompletionLatch; | |
| 39 private final String mAddr2linePath; | |
| 40 private final String mLibraryPath; | |
| 41 private final boolean mDisambiguate; | |
| 42 private final boolean mDedupe; | |
| 43 private final String stripLocation; | |
| 44 private final ConcurrentMap<Long, Record> mAddressesSeen = | |
| 45 new ConcurrentHashMap<Long, Record>(100000, 0.75f, 32); | |
| 46 private volatile Map<String,String> mFileLookupTable = null; | |
| 47 private final AtomicInteger mDisambiguationSuccessCount = new AtomicInteger(
0); | |
| 48 private final AtomicInteger mDisambiguationFailureCount = new AtomicInteger(
0); | |
| 49 private final AtomicInteger mDedupeCount = new AtomicInteger(0); | |
| 50 | |
| 51 /** | |
| 52 * These are the suffixes of files that we are potentially interested in | |
| 53 * searching during symbol lookups when disambiguation is enabled. Anything | |
| 54 * that could theoretically end up being linked into the library file | |
| 55 * should be listed here. | |
| 56 * <p> | |
| 57 * IMPORTANT: All of these should be lowercase. When doing comparisons, | |
| 58 * always lowercase the file name before attempting the match. | |
| 59 */ | |
| 60 private static final String[] INTERESTING_FILE_ENDINGS = new String[]{ | |
| 61 ".c", ".cc", ".h", ".cp", ".cpp", ".cxx", ".c++", ".asm", ".inc", ".
s", ".hxx" | |
| 62 }; | |
| 63 | |
| 64 | |
| 65 Addr2LineWorkerPool(final int size, | |
| 66 final String addr2linePath, final String libraryPath, | |
| 67 final boolean disambiguate, final boolean dedupe) | |
| 68 throws IOException { | |
| 69 this.mAddr2linePath = addr2linePath; | |
| 70 this.mLibraryPath = libraryPath; | |
| 71 this.mDisambiguate = disambiguate; | |
| 72 this.mDedupe = dedupe; | |
| 73 | |
| 74 // Prepare disambiguation table if necessary. This must be done | |
| 75 // before launching the threads in the processing pool for visibility. | |
| 76 if (disambiguate) { | |
| 77 try { | |
| 78 createDisambiguationTable(); | |
| 79 } catch (IOException e) { | |
| 80 throw new RuntimeException("Can't create lookup table", e); | |
| 81 } | |
| 82 } | |
| 83 | |
| 84 // The library is in, e.g.: src/out/Release | |
| 85 // Convert all output paths to be relative to src. | |
| 86 // Strip everything up to and including "src/". | |
| 87 String canonical = new File(libraryPath).getCanonicalPath(); | |
| 88 int end = canonical.lastIndexOf("/src/"); | |
| 89 if (end < 0) { | |
| 90 // Shouldn't happen if the library exists. | |
| 91 throw new RuntimeException("Bad library path: " + libraryPath + | |
| 92 ". Library is expected to be within a build directory."); | |
| 93 } | |
| 94 stripLocation = canonical.substring(0, end + "/src/".length()); | |
| 95 | |
| 96 mWorkers = new Addr2LineWorker[size]; | |
| 97 mCompletionLatch = new CountDownLatch(size); | |
| 98 for (int x = 0; x < mWorkers.length; x++) { | |
| 99 mWorkers[x] = new Addr2LineWorker(); | |
| 100 } | |
| 101 } | |
| 102 | |
| 103 void submit(Record record) throws InterruptedException { | |
| 104 mRecordsIn.put(record); | |
| 105 } | |
| 106 | |
| 107 void allRecordsSubmitted() { | |
| 108 for (Addr2LineWorker worker : mWorkers) { | |
| 109 worker.stopIfQueueIsEmpty = true; | |
| 110 } | |
| 111 } | |
| 112 | |
| 113 boolean await(int amount, TimeUnit unit) throws InterruptedException { | |
| 114 return mCompletionLatch.await(amount, unit); | |
| 115 } | |
| 116 | |
| 117 /** | |
| 118 * @param value the base value | |
| 119 * @param percent absolute percentage to jitter by (in the range [0,100]) | |
| 120 * @return a value that is on average uniformly distributed within | |
| 121 * plus or minus <em>percent</em> of the base value. | |
| 122 */ | |
| 123 private static int jitter(final int value, final int percent) { | |
| 124 Random r = new Random(); | |
| 125 int delta = (r.nextBoolean() ? 1 : -1) * r.nextInt((percent * value) / 1
00); | |
| 126 return value + delta; | |
| 127 } | |
| 128 | |
| 129 | |
| 130 /** | |
| 131 * A class that encapsulates an addr2line process and the work that it | |
| 132 * needs to do. | |
| 133 */ | |
| 134 private class Addr2LineWorker { | |
| 135 // Our work queues | |
| 136 private final AtomicReference<Process> processRef = new AtomicReference<
Process>(); | |
| 137 private final Thread workerThread; | |
| 138 private volatile boolean stopIfQueueIsEmpty = false; | |
| 139 | |
| 140 /** | |
| 141 * After this many addresses have been processed, the addr2line process | |
| 142 * itself will be recycled. This prevents the addr2line process from | |
| 143 * getting too huge, which in turn allows more parallel addr2line | |
| 144 * processes to run. There is a balance to be achieved here, as | |
| 145 * creating a new addr2line process is very costly. A value of | |
| 146 * approximately 2000 appears, empirically, to be a good tradeoff | |
| 147 * on a modern machine; memory usage stays tolerable, and good | |
| 148 * throughput can be achieved. The value is jittered by +/- 10% so that | |
| 149 * the processes don't all restart at once. | |
| 150 */ | |
| 151 private final int processRecycleThreshold = jitter(2000, 10); | |
| 152 | |
| 153 private Addr2LineWorker() throws IOException { | |
| 154 this.processRef.set(createAddr2LineProcess()); | |
| 155 workerThread = new Thread(new Addr2LineTask(), "Addr2Line Worker"); | |
| 156 workerThread.setDaemon(true); | |
| 157 workerThread.start(); | |
| 158 } | |
| 159 | |
| 160 /** | |
| 161 * Builds a new addr2line process for use in this worker. | |
| 162 * @return the process | |
| 163 * @throws IOException if unable to create the process for any reason | |
| 164 */ | |
| 165 private Process createAddr2LineProcess() | |
| 166 throws IOException { | |
| 167 ProcessBuilder builder = new ProcessBuilder(mAddr2linePath, "-e", mL
ibraryPath, "-f"); | |
| 168 Process process = builder.start(); | |
| 169 return process; | |
| 170 } | |
| 171 | |
| 172 /** | |
| 173 * Reads records from the input queue and pipes addresses into | |
| 174 * addr2line, using the output to complete the record and pushing | |
| 175 * the record into the output queue. | |
| 176 */ | |
| 177 private class Addr2LineTask implements Runnable { | |
| 178 @Override | |
| 179 public void run() { | |
| 180 int processTaskCounter = 0; | |
| 181 InputStream inStream = processRef.get().getInputStream(); | |
| 182 Reader isr = new InputStreamReader(inStream); | |
| 183 BufferedReader reader = new BufferedReader(isr); | |
| 184 try { | |
| 185 while (true) { | |
| 186 // Check for a task. | |
| 187 final Record record = mRecordsIn.poll(1, TimeUnit.SECOND
S); | |
| 188 if (record == null) { | |
| 189 if (stopIfQueueIsEmpty) { | |
| 190 // All tasks have been submitted, so if the | |
| 191 // queue is empty then there is nothing left | |
| 192 // to do and it's ready to shut down | |
| 193 return; | |
| 194 } | |
| 195 continue; // else, queue starvation. Try again. | |
| 196 } | |
| 197 | |
| 198 // Avoid reprocessing previously-seen symbols if | |
| 199 // deduping is enabled | |
| 200 if (tryDedupe(record)) continue; | |
| 201 | |
| 202 // Create a new reader if the addr2line process is new | |
| 203 // or has been recycled. A single reader will be used | |
| 204 // for the entirety of the addr2line process' lifetime. | |
| 205 final Process process = processRef.get(); | |
| 206 if (inStream == null) { | |
| 207 inStream = process.getInputStream(); | |
| 208 isr = new InputStreamReader(inStream); | |
| 209 reader = new BufferedReader(isr); | |
| 210 } | |
| 211 | |
| 212 // Write the address to stdin of addr2line | |
| 213 process.getOutputStream().write(record.address.getBytes(
sAscii)); | |
| 214 process.getOutputStream().write('\n'); | |
| 215 process.getOutputStream().flush(); | |
| 216 | |
| 217 // Read the answer from addr2line. Example: | |
| 218 // ABGRToYRow_C | |
| 219 // /src/out/Release/../../third_party/libyuv/source/row_
common.cc:293 | |
| 220 final String name = reader.readLine(); | |
| 221 if (name == null || name.isEmpty()) { | |
| 222 stopIfQueueIsEmpty = true; | |
| 223 continue; | |
| 224 } | |
| 225 | |
| 226 String location = reader.readLine(); | |
| 227 if (location == null || location.isEmpty()) { | |
| 228 stopIfQueueIsEmpty = true; | |
| 229 continue; | |
| 230 } | |
| 231 | |
| 232 record.resolvedSuccessfully = !( | |
| 233 name.equals("??") && location.equals("??:0")); | |
| 234 | |
| 235 if (record.resolvedSuccessfully) { | |
| 236 // Keep the name from the initial NM dump. | |
| 237 // Some addr2line impls, such as the one for Android | |
| 238 // on ARM, seem to lose data here. | |
| 239 // Note that the location may also include a line | |
| 240 // discriminator, which maps to a part of a line. | |
| 241 // Not currently used. | |
| 242 record.location = processLocation(location);; | |
| 243 } | |
| 244 | |
| 245 // Check if there is more input on the stream. | |
| 246 // If there is then it is a serious processing | |
| 247 // error, and reading anything else would de-sync | |
| 248 // the input queue from the results being read. | |
| 249 if (inStream.available() > 0) { | |
| 250 throw new IllegalStateException( | |
| 251 "Alignment mismatch in output from address "
+ record.address); | |
| 252 } | |
| 253 | |
| 254 // Track stats and move record to output queue | |
| 255 processTaskCounter++; | |
| 256 mRecordsOut.add(record); | |
| 257 | |
| 258 // If the addr2line process has done too much work, | |
| 259 // kill it and start a new one to reduce memory | |
| 260 // pressure created by the pool. | |
| 261 if (processTaskCounter >= processRecycleThreshold) { | |
| 262 // Out with the old... | |
| 263 try { | |
| 264 processRef.get().destroy(); | |
| 265 } catch (Exception e) { | |
| 266 System.err.println("WARNING: zombie process"); | |
| 267 e.printStackTrace(); | |
| 268 } | |
| 269 // ... and in with the new! | |
| 270 try { | |
| 271 processRef.set(createAddr2LineProcess()); | |
| 272 } catch (IOException e) { | |
| 273 e.printStackTrace(); | |
| 274 } | |
| 275 processTaskCounter = 0; | |
| 276 inStream = null; // New readers need to be created n
ext iteration | |
| 277 } | |
| 278 } | |
| 279 } catch (RuntimeException e) { | |
| 280 throw e; | |
| 281 } catch (Exception e) { | |
| 282 e.printStackTrace(); | |
| 283 } finally { | |
| 284 try { | |
| 285 // Make an attempt to clean up. If we fail, there is | |
| 286 // nothing we can do beyond this. | |
| 287 processRef.get().destroy(); | |
| 288 } catch (Exception e) { | |
| 289 // There's nothing more we can do here. | |
| 290 } | |
| 291 mCompletionLatch.countDown(); | |
| 292 } | |
| 293 } | |
| 294 } | |
| 295 | |
| 296 /** | |
| 297 * Examines the location from a record and attempts to canonicalize | |
| 298 * it and strip off the common source root. | |
| 299 * @param location the location to process | |
| 300 * @return the canonicalized, stripped location or the original input | |
| 301 * string if the location cannot be canonicalized | |
| 302 */ | |
| 303 private String processLocation(String location) { | |
| 304 if (location.startsWith("/")) { | |
| 305 try { | |
| 306 location = new File(location).getCanonicalPath(); | |
| 307 } catch (IOException e) { | |
| 308 System.err.println("Unable to canonicalize path: " + locatio
n); | |
| 309 } | |
| 310 } else if (mDisambiguate) { | |
| 311 // Ambiguous path (only has the file name) | |
| 312 // Try dictionary lookup if that's enabled | |
| 313 final int indexOfColon = location.lastIndexOf(':'); | |
| 314 final String key; | |
| 315 final String line; | |
| 316 if (indexOfColon != -1) { | |
| 317 key = location.substring(0, indexOfColon); | |
| 318 line = location.substring(indexOfColon + 1); | |
| 319 } else { | |
| 320 key = location; | |
| 321 line = null; | |
| 322 } | |
| 323 final String found = mFileLookupTable.get(key); | |
| 324 if (found != null) { | |
| 325 mDisambiguationSuccessCount.incrementAndGet(); | |
| 326 location = found; | |
| 327 if (line != null) location = location + ":" + line; | |
| 328 } else { | |
| 329 mDisambiguationFailureCount.incrementAndGet(); | |
| 330 } | |
| 331 } | |
| 332 if (location.startsWith(stripLocation)) { | |
| 333 location = location.substring(stripLocation.length()); | |
| 334 } | |
| 335 return location; | |
| 336 } | |
| 337 | |
| 338 /** | |
| 339 * Attempts to dedupe a record using a cache of previously-seen | |
| 340 * addresses if and only if deduping is enabled. | |
| 341 * @param record the record to attempt deduping | |
| 342 * @return true if deduplication is enabled and the record references | |
| 343 * an address that has already been seen; otherwise false | |
| 344 */ | |
| 345 private boolean tryDedupe(Record record) { | |
| 346 if (mDedupe) { | |
| 347 long addressLong = Long.parseLong(record.address, 16); | |
| 348 Record existing = mAddressesSeen.get(addressLong); | |
| 349 if (existing != null) { | |
| 350 if (!existing.size.equals(record.size)) { | |
| 351 System.err.println("WARNING: Deduped address " + | |
| 352 record.address + " has a size mismatch, " + | |
| 353 existing.size + " != " + record.size); | |
| 354 } | |
| 355 mDedupeCount.incrementAndGet(); | |
| 356 return true; | |
| 357 } | |
| 358 if (mAddressesSeen.putIfAbsent(addressLong, record) != null) { | |
| 359 // putIfAbsent is used to ensure that we have | |
| 360 // an accurate dedupeCount; otherwise, two | |
| 361 // workers could insert the same record in | |
| 362 // parallel without realizing that one of them | |
| 363 // was actually a duplicate. | |
| 364 mDedupeCount.incrementAndGet(); | |
| 365 return true; | |
| 366 } | |
| 367 } | |
| 368 return false; | |
| 369 } | |
| 370 } | |
| 371 | |
| 372 // TODO(andrewhayden): Make this less Android-specific | |
| 373 private void createDisambiguationTable() throws IOException { | |
| 374 // Convert /src/out/Release/lib/*.so -> /src/out/Release | |
| 375 final File libraryOutputDirectory = new File(mLibraryPath) | |
| 376 .getParentFile().getParentFile().getCanonicalFile(); | |
| 377 | |
| 378 // Convert /src/out/Release -> /src | |
| 379 final File root = libraryOutputDirectory | |
| 380 .getParentFile().getParentFile().getCanonicalFile(); | |
| 381 | |
| 382 // There is no code at the root of Chromium. | |
| 383 // Ignore all the 'out' directories. | |
| 384 mFileLookupTable = new HashMap<String, String>(); | |
| 385 Set<String> dupes = new HashSet<String>(); | |
| 386 for (File file : root.listFiles()) { | |
| 387 if (file.isDirectory()) { | |
| 388 String name = file.getName(); | |
| 389 if (name.startsWith("out")) { | |
| 390 if (new File(file, "Release").exists() || new File(file, "De
bug").exists()) { | |
| 391 // It's an output directory, skip it - except for the | |
| 392 // 'obj' and 'gen' subdirectories that are siblings | |
| 393 // to the library file's parent dir, which is needed. | |
| 394 // Include those at the very end, since they are known. | |
| 395 continue; | |
| 396 } | |
| 397 } else if (name.startsWith(".")) { | |
| 398 // Skip dot directories: .git, .svn, etcetera. | |
| 399 continue; | |
| 400 } | |
| 401 findInterestingFiles(file, dupes); | |
| 402 } | |
| 403 } | |
| 404 | |
| 405 // Include directories that contain generated resources we are likely | |
| 406 // to encounter in the symbol table. | |
| 407 findInterestingFiles(new File(libraryOutputDirectory, "gen"), dupes); | |
| 408 findInterestingFiles(new File(libraryOutputDirectory, "obj"), dupes); | |
| 409 | |
| 410 // Any duplicates in the filesystem can't be used for disambiguation | |
| 411 // because it is not obvious which of the duplicates is the true source. | |
| 412 // Therefore, discard all files that have duplicate names. | |
| 413 for (String dupe : dupes) { | |
| 414 mFileLookupTable.remove(dupe); | |
| 415 } | |
| 416 } | |
| 417 | |
| 418 // TODO(andrewhayden): Could integrate with build system to know EXACTLY | |
| 419 // what is out there. This would avoid the need for the dupes set, which | |
| 420 // would make it possible to do much better deduping. | |
| 421 private void findInterestingFiles(File directory, Set<String> dupes) { | |
| 422 for (File file : directory.listFiles()) { | |
| 423 if (file.isDirectory() && file.canRead()) { | |
| 424 if (!file.getName().startsWith(".")) { | |
| 425 findInterestingFiles(file, dupes); | |
| 426 } | |
| 427 } else { | |
| 428 String name = file.getName(); | |
| 429 String normalized = name.toLowerCase(); | |
| 430 for (String ending : INTERESTING_FILE_ENDINGS) { | |
| 431 if (normalized.endsWith(ending)) { | |
| 432 String other = mFileLookupTable.put( | |
| 433 name, file.getAbsolutePath()); | |
| 434 if (other != null) dupes.add(name); | |
| 435 } | |
| 436 } | |
| 437 } | |
| 438 } | |
| 439 } | |
| 440 | |
| 441 /** | |
| 442 * Polls the output queue for the next record. | |
| 443 * @return the next record | |
| 444 */ | |
| 445 Record poll() { | |
| 446 return mRecordsOut.poll(); | |
| 447 } | |
| 448 | |
| 449 /** | |
| 450 * @return the number of ambiguous paths successfully disambiguated | |
| 451 */ | |
| 452 int getDisambiguationSuccessCount() { | |
| 453 return mDisambiguationSuccessCount.get(); | |
| 454 } | |
| 455 | |
| 456 /** | |
| 457 * @return the number of ambiguous paths that couldn't be disambiguated | |
| 458 */ | |
| 459 int getDisambiguationFailureCount() { | |
| 460 return mDisambiguationFailureCount.get(); | |
| 461 } | |
| 462 | |
| 463 /** | |
| 464 * @return the number of symbols deduped | |
| 465 */ | |
| 466 int getDedupeCount() { | |
| 467 return mDedupeCount.get(); | |
| 468 } | |
| 469 } | |
| OLD | NEW |