chromium/base/android/java/src/org/chromium/base/task/ChromeThreadPoolExecutor.java

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.base.task;

import static java.util.concurrent.TimeUnit.SECONDS;

import androidx.annotation.VisibleForTesting;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

class ChromeThreadPoolExecutor extends ThreadPoolExecutor {
    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();

    // Core pool is still used despite allowCoreThreadTimeOut(true) being called - while the core
    // pool can still timeout, the thread pool will still start up threads more aggressively while
    // under the CORE_POOL_SIZE.
    private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
    private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
    private static final int KEEP_ALIVE_SECONDS = 30;

    private static final ThreadFactory sThreadFactory =
            new ThreadFactory() {
                private final AtomicInteger mCount = new AtomicInteger(1);

                @Override
                public Thread newThread(Runnable r) {
                    return new Thread(r, "CrAsyncTask #" + mCount.getAndIncrement());
                }
            };

    private static final BlockingQueue<Runnable> sPoolWorkQueue =
            new ArrayBlockingQueue<Runnable>(128);

    // May have to be lowered if we are not capturing any Runnable sources.
    private static final int RUNNABLE_WARNING_COUNT = 32;

    ChromeThreadPoolExecutor() {
        this(
                CORE_POOL_SIZE,
                MAXIMUM_POOL_SIZE,
                KEEP_ALIVE_SECONDS,
                SECONDS,
                sPoolWorkQueue,
                sThreadFactory);
    }

    @VisibleForTesting
    ChromeThreadPoolExecutor(
            int corePoolSize,
            int maximumPoolSize,
            long keepAliveTime,
            TimeUnit unit,
            BlockingQueue<Runnable> workQueue,
            ThreadFactory threadFactory) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
        allowCoreThreadTimeOut(true);
    }

    @SuppressWarnings("NoAndroidAsyncTaskCheck")
    private static String getClassName(Runnable runnable) {
        Class blamedClass = runnable.getClass();
        if (blamedClass == AsyncTask.NamedFutureTask.class) {
                blamedClass = ((AsyncTask.NamedFutureTask) runnable).getBlamedClass();
            } else if (blamedClass.getEnclosingClass() == android.os.AsyncTask.class) {
            blamedClass = android.os.AsyncTask.class;
        }
        return blamedClass.getName();
    }

    private Map<String, Integer> getNumberOfClassNameOccurrencesInQueue() {
        Map<String, Integer> counts = new HashMap<>();
        Runnable[] copiedQueue = getQueue().toArray(new Runnable[0]);
        for (Runnable runnable : copiedQueue) {
            String className = getClassName(runnable);
            int count = counts.containsKey(className) ? counts.get(className) : 0;
            counts.put(className, count + 1);
        }
        return counts;
    }

    private String findClassNamesWithTooManyRunnables(Map<String, Integer> counts) {
        // We only show the classes over RUNNABLE_WARNING_COUNT appearances so that these
        // crashes group up together in the reporting dashboards. If we were to print all
        // the Runnables or their counts, this would fragment the reporting, with one for
        // each unique set of Runnables/counts.
        StringBuilder classesWithTooManyRunnables = new StringBuilder();
        for (Map.Entry<String, Integer> entry : counts.entrySet()) {
            if (entry.getValue() > RUNNABLE_WARNING_COUNT) {
                classesWithTooManyRunnables.append(entry.getKey()).append(' ');
            }
        }
        if (classesWithTooManyRunnables.length() == 0) {
            return "NO CLASSES FOUND";
        }
        return classesWithTooManyRunnables.toString();
    }

    @Override
    public void execute(Runnable r) {
        try {
            super.execute(r);
        } catch (RejectedExecutionException e) {
            Map<String, Integer> counts = getNumberOfClassNameOccurrencesInQueue();

            throw new RejectedExecutionException(
                    "Prominent classes in AsyncTask: " + findClassNamesWithTooManyRunnables(counts),
                    e);
        }
    }
}