1 // Copyright 2018 The Chromium Authors 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.base.task; 6 7 import static java.util.concurrent.TimeUnit.SECONDS; 8 9 import androidx.annotation.VisibleForTesting; 10 11 import org.chromium.build.BuildConfig; 12 13 import java.lang.reflect.Field; 14 import java.util.HashMap; 15 import java.util.Map; 16 import java.util.concurrent.ArrayBlockingQueue; 17 import java.util.concurrent.BlockingQueue; 18 import java.util.concurrent.RejectedExecutionException; 19 import java.util.concurrent.ThreadFactory; 20 import java.util.concurrent.ThreadPoolExecutor; 21 import java.util.concurrent.TimeUnit; 22 import java.util.concurrent.atomic.AtomicInteger; 23 24 class ChromeThreadPoolExecutor extends ThreadPoolExecutor { 25 private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors(); 26 27 // Core pool is still used despite allowCoreThreadTimeOut(true) being called - while the core 28 // pool can still timeout, the thread pool will still start up threads more aggressively while 29 // under the CORE_POOL_SIZE. 30 private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4)); 31 private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1; 32 private static final int KEEP_ALIVE_SECONDS = 30; 33 34 private static final ThreadFactory sThreadFactory = 35 new ThreadFactory() { 36 private final AtomicInteger mCount = new AtomicInteger(1); 37 38 @Override 39 public Thread newThread(Runnable r) { 40 return new Thread(r, "CrAsyncTask #" + mCount.getAndIncrement()); 41 } 42 }; 43 44 private static final BlockingQueue<Runnable> sPoolWorkQueue = 45 new ArrayBlockingQueue<Runnable>(128); 46 47 // May have to be lowered if we are not capturing any Runnable sources. 48 private static final int RUNNABLE_WARNING_COUNT = 32; 49 ChromeThreadPoolExecutor()50 ChromeThreadPoolExecutor() { 51 this( 52 CORE_POOL_SIZE, 53 MAXIMUM_POOL_SIZE, 54 KEEP_ALIVE_SECONDS, 55 SECONDS, 56 sPoolWorkQueue, 57 sThreadFactory); 58 } 59 60 @VisibleForTesting ChromeThreadPoolExecutor( int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory)61 ChromeThreadPoolExecutor( 62 int corePoolSize, 63 int maximumPoolSize, 64 long keepAliveTime, 65 TimeUnit unit, 66 BlockingQueue<Runnable> workQueue, 67 ThreadFactory threadFactory) { 68 super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory); 69 allowCoreThreadTimeOut(true); 70 } 71 72 @SuppressWarnings("NoAndroidAsyncTaskCheck") getClassName(Runnable runnable)73 private static String getClassName(Runnable runnable) { 74 Class blamedClass = runnable.getClass(); 75 try { 76 if (blamedClass == AsyncTask.NamedFutureTask.class) { 77 blamedClass = ((AsyncTask.NamedFutureTask) runnable).getBlamedClass(); 78 } else if (blamedClass.getEnclosingClass() == android.os.AsyncTask.class) { 79 // This gets the AsyncTask that produced the runnable. 80 Field field = blamedClass.getDeclaredField("this$0"); 81 field.setAccessible(true); 82 blamedClass = field.get(runnable).getClass(); 83 } 84 } catch (NoSuchFieldException e) { 85 if (BuildConfig.ENABLE_ASSERTS) { 86 throw new RuntimeException(e); 87 } 88 } catch (IllegalAccessException e) { 89 if (BuildConfig.ENABLE_ASSERTS) { 90 throw new RuntimeException(e); 91 } 92 } 93 return blamedClass.getName(); 94 } 95 getNumberOfClassNameOccurrencesInQueue()96 private Map<String, Integer> getNumberOfClassNameOccurrencesInQueue() { 97 Map<String, Integer> counts = new HashMap<>(); 98 Runnable[] copiedQueue = getQueue().toArray(new Runnable[0]); 99 for (Runnable runnable : copiedQueue) { 100 String className = getClassName(runnable); 101 int count = counts.containsKey(className) ? counts.get(className) : 0; 102 counts.put(className, count + 1); 103 } 104 return counts; 105 } 106 findClassNamesWithTooManyRunnables(Map<String, Integer> counts)107 private String findClassNamesWithTooManyRunnables(Map<String, Integer> counts) { 108 // We only show the classes over RUNNABLE_WARNING_COUNT appearances so that these 109 // crashes group up together in the reporting dashboards. If we were to print all 110 // the Runnables or their counts, this would fragment the reporting, with one for 111 // each unique set of Runnables/counts. 112 StringBuilder classesWithTooManyRunnables = new StringBuilder(); 113 for (Map.Entry<String, Integer> entry : counts.entrySet()) { 114 if (entry.getValue() > RUNNABLE_WARNING_COUNT) { 115 classesWithTooManyRunnables.append(entry.getKey()).append(' '); 116 } 117 } 118 if (classesWithTooManyRunnables.length() == 0) { 119 return "NO CLASSES FOUND"; 120 } 121 return classesWithTooManyRunnables.toString(); 122 } 123 124 @Override execute(Runnable r)125 public void execute(Runnable r) { 126 try { 127 super.execute(r); 128 } catch (RejectedExecutionException e) { 129 Map<String, Integer> counts = getNumberOfClassNameOccurrencesInQueue(); 130 131 throw new RejectedExecutionException( 132 "Prominent classes in AsyncTask: " + findClassNamesWithTooManyRunnables(counts), 133 e); 134 } 135 } 136 } 137