chromium/content/public/android/java/src/org/chromium/content/browser/ChildProcessConnectionMetrics.java

// Copyright 2022 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.content.browser;

import androidx.annotation.VisibleForTesting;
import androidx.collection.ArraySet;

import org.chromium.base.ApplicationState;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.ThreadUtils;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.process_launcher.ChildProcessConnection;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;

import java.util.Random;
import java.util.Set;

/**
 * Tracks {@link ChildProcessConnection} importance and binding state for snaboxed connections. Uses
 * this information to emit metrics comparing the actual {@link ChildBindingState} of a connection
 * to the
 * {@link ChildBindingState} that would be applied based on the {@link ChildProcessImportance} in
 * {@link ChildProcessLauncherHelperImpl#setPriority} if {@link BindingManager} was not running.
 *
 * This class enforces that it is only used on the launcher thread other than during init.
 */
public class ChildProcessConnectionMetrics {
    @VisibleForTesting private static final long INITIAL_EMISSION_DELAY_MS = 60 * 1000; // 1 min.
    private static final long REGULAR_EMISSION_DELAY_MS = 5 * 60 * 1000; // 5 min.

    private static ChildProcessConnectionMetrics sInstance;

    // Whether the main application is currently brought to the foreground.
    private boolean mApplicationInForegroundOnUiThread;
    private BindingManager mBindingManager;

    private final Set<ChildProcessConnection> mConnections = new ArraySet<>();
    private final Random mRandom = new Random();
    private final Runnable mEmitMetricsRunnable;

    @VisibleForTesting
    ChildProcessConnectionMetrics() {
        mEmitMetricsRunnable =
                () -> {
                    emitMetrics();
                    postEmitMetrics(REGULAR_EMISSION_DELAY_MS);
                };
    }

    public static ChildProcessConnectionMetrics getInstance() {
        assert LauncherThread.runningOnLauncherThread();
        if (sInstance == null) {
            sInstance = new ChildProcessConnectionMetrics();
            sInstance.registerActivityStateListenerAndStartEmitting();
        }
        return sInstance;
    }

    public void setBindingManager(BindingManager bindingManager) {
        assert LauncherThread.runningOnLauncherThread();
        mBindingManager = bindingManager;
    }

    public void addConnection(ChildProcessConnection connection) {
        assert LauncherThread.runningOnLauncherThread();
        mConnections.add(connection);
    }

    public void removeConnection(ChildProcessConnection connection) {
        assert LauncherThread.runningOnLauncherThread();
        mConnections.remove(connection);
    }

    /**
     * Generate a Poisson distributed time delay.
     * @param meanTimeMs the mean time of the delay.
     */
    private long getTimeDelayMs(long meanTimeMs) {
        return Math.round(-1 * Math.log(mRandom.nextDouble()) * (double) meanTimeMs);
    }

    private void postEmitMetrics(long meanDelayMs) {
        // No thread affinity.
        LauncherThread.postDelayed(mEmitMetricsRunnable, getTimeDelayMs(meanDelayMs));
    }

    private void startEmitting() {
        assert ThreadUtils.runningOnUiThread();
        postEmitMetrics(INITIAL_EMISSION_DELAY_MS);
    }

    private void cancelEmitting() {
        assert ThreadUtils.runningOnUiThread();
        LauncherThread.post(
                () -> {
                    LauncherThread.removeCallbacks(mEmitMetricsRunnable);
                });
    }

    private void registerActivityStateListenerAndStartEmitting() {
        PostTask.postTask(
                TaskTraits.UI_DEFAULT,
                () -> {
                    assert ThreadUtils.runningOnUiThread();
                    mApplicationInForegroundOnUiThread =
                            ApplicationStatus.getStateForApplication()
                                            == ApplicationState.HAS_RUNNING_ACTIVITIES
                                    || ApplicationStatus.getStateForApplication()
                                            == ApplicationState.HAS_PAUSED_ACTIVITIES;

                    ApplicationStatus.registerApplicationStateListener(
                            newState -> {
                                switch (newState) {
                                    case ApplicationState.UNKNOWN:
                                        break;
                                    case ApplicationState.HAS_RUNNING_ACTIVITIES:
                                    case ApplicationState.HAS_PAUSED_ACTIVITIES:
                                        if (!mApplicationInForegroundOnUiThread) onForegrounded();
                                        break;
                                    default:
                                        if (mApplicationInForegroundOnUiThread) onBackgrounded();
                                        break;
                                }
                            });
                    if (mApplicationInForegroundOnUiThread) {
                        startEmitting();
                    }
                });
    }

    private void onForegrounded() {
        assert ThreadUtils.runningOnUiThread();
        mApplicationInForegroundOnUiThread = true;
        startEmitting();
    }

    private void onBackgrounded() {
        assert ThreadUtils.runningOnUiThread();
        mApplicationInForegroundOnUiThread = false;
        cancelEmitting();
    }

    private boolean bindingManagerHasExclusiveVisibleBinding(ChildProcessConnection connection) {
        if (mBindingManager != null) {
            return mBindingManager.hasExclusiveVisibleBinding(connection);
        }
        return false;
    }

    // These metrics are only emitted in the foreground.
    @VisibleForTesting
    void emitMetrics() {
        assert LauncherThread.runningOnLauncherThread();

        // Binding counts from all connections.
        int strongBindingCount = 0;
        int visibleBindingCount = 0;
        int notPerceptibleBindingCount = 0;
        int waivedBindingCount = 0;

        // Bindings from BindingManager which could be waived.
        int waivableBindingCount = 0;

        // Visible and waived connections if BindingManager didn't exist.
        int contentVisibleBindingCount = 0;
        int contentWaivedBindingCount = 0;

        if (mBindingManager != null) {
            waivableBindingCount = mBindingManager.getExclusiveBindingCount();
        }

        for (ChildProcessConnection connection : mConnections) {
            if (connection.isStrongBindingBound()) {
                strongBindingCount++;
            } else if (connection.isVisibleBindingBound()) {
                visibleBindingCount++;
                if (bindingManagerHasExclusiveVisibleBinding(connection)) {
                    contentWaivedBindingCount++;
                } else {
                    contentVisibleBindingCount++;
                }
            } else if (connection.isNotPerceptibleBindingBound()) {
                notPerceptibleBindingCount++;
                contentWaivedBindingCount++;
            } else {
                waivedBindingCount++;
                contentWaivedBindingCount++;
            }
        }

        assert strongBindingCount
                        + visibleBindingCount
                        + notPerceptibleBindingCount
                        + waivedBindingCount
                == mConnections.size();
        final int totalConnections = mConnections.size();

        // Count 100 is sufficient here as we are limited to 100 live sandboxed services. See
        // ChildConnectionAllocator.MAX_VARIABLE_ALLOCATED.

        // Actual effecting binding state counts.
        RecordHistogram.recordCount100Histogram(
                "Android.ChildProcessBinding.TotalConnections", totalConnections);
        RecordHistogram.recordCount100Histogram(
                "Android.ChildProcessBinding.StrongConnections", strongBindingCount);
        RecordHistogram.recordCount100Histogram(
                "Android.ChildProcessBinding.VisibleConnections", visibleBindingCount);
        RecordHistogram.recordCount100Histogram(
                "Android.ChildProcessBinding.NotPerceptibleConnections",
                notPerceptibleBindingCount);
        RecordHistogram.recordCount100Histogram(
                "Android.ChildProcessBinding.WaivedConnections", waivedBindingCount);

        // Metrics if BindingManager wasn't running.
        RecordHistogram.recordCount100Histogram(
                "Android.ChildProcessBinding.ContentVisibleConnections",
                contentVisibleBindingCount);
        RecordHistogram.recordCount100Histogram(
                "Android.ChildProcessBinding.ContentWaivedConnections", contentWaivedBindingCount);
        RecordHistogram.recordCount100Histogram(
                "Android.ChildProcessBinding.WaivableConnections", waivableBindingCount);
    }
}