chromium/content/public/android/junit/src/org/chromium/content/browser/ChildProcessConnectionMetricsUnitTest.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 android.content.ComponentName;
import android.os.Build;

import androidx.test.filters.SmallTest;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;

import org.chromium.base.ChildBindingState;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.process_launcher.ChildProcessConnection;
import org.chromium.base.process_launcher.TestChildProcessConnection;
import org.chromium.base.test.BaseRobolectricTestRunner;

import java.util.LinkedList;

/** Unit test for {@link ChildProcessConnectionMetrics}. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE, sdk = Build.VERSION_CODES.Q)
public class ChildProcessConnectionMetricsUnitTest {
    private LinkedList<ChildProcessConnection> mRanking;
    private BindingManager mBindingManager;
    private ChildProcessConnectionMetrics mConnectionMetrics;

    @Before
    public void setUp() {
        LauncherThread.setCurrentThreadAsLauncherThread();
        mRanking = new LinkedList<ChildProcessConnection>();
        mBindingManager =
                new BindingManager(
                        RuntimeEnvironment.application, BindingManager.NO_MAX_SIZE, mRanking);
        mConnectionMetrics = new ChildProcessConnectionMetrics();
        mConnectionMetrics.setBindingManager(mBindingManager);
    }

    @Test
    @SmallTest
    public void testEmitMetricsNoConnections() {
        setupBindingType(false);
        ChildProcessConnection connection = createMockConnection(ChildBindingState.STRONG);
        mConnectionMetrics.addConnection(connection);
        removeConnection(connection);

        mConnectionMetrics.emitMetrics();
        Assert.assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.ChildProcessBinding.TotalConnections", 0));
        Assert.assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.ChildProcessBinding.StrongConnections", 0));
        Assert.assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.ChildProcessBinding.VisibleConnections", 0));
        Assert.assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.ChildProcessBinding.NotPerceptibleConnections", 0));
        Assert.assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.ChildProcessBinding.WaivedConnections", 0));

        Assert.assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.ChildProcessBinding.ContentVisibleConnections", 0));
        Assert.assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.ChildProcessBinding.ContentWaivedConnections", 0));

        Assert.assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.ChildProcessBinding.WaivableConnections", 0));
    }

    @Test
    @SmallTest
    public void testEmitMetrics_BindingManagerUsesNotPerceptible() {
        setupBindingType(true);
        mConnectionMetrics.addConnection(createMockConnection(ChildBindingState.STRONG));
        mConnectionMetrics.addConnection(createMockConnection(ChildBindingState.VISIBLE));
        mConnectionMetrics.addConnection(createMockConnection(ChildBindingState.WAIVED));
        mConnectionMetrics.addConnection(createMockConnection(ChildBindingState.WAIVED));
        mConnectionMetrics.addConnection(createMockConnection(ChildBindingState.WAIVED));
        ChildProcessConnection lowestRankingConnection =
                createMockConnection(ChildBindingState.WAIVED);
        mConnectionMetrics.addConnection(lowestRankingConnection);
        setLowestRanking(lowestRankingConnection);

        mConnectionMetrics.emitMetrics();
        Assert.assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.ChildProcessBinding.TotalConnections", 6));
        Assert.assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.ChildProcessBinding.StrongConnections", 1));
        Assert.assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.ChildProcessBinding.VisibleConnections", 1));
        Assert.assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.ChildProcessBinding.NotPerceptibleConnections", 3));
        Assert.assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.ChildProcessBinding.WaivedConnections", 1));

        Assert.assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.ChildProcessBinding.ContentVisibleConnections", 1));
        Assert.assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.ChildProcessBinding.ContentWaivedConnections", 4));

        Assert.assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.ChildProcessBinding.WaivableConnections", 3));
    }

    @Test
    @SmallTest
    public void testEmitMetrics_BindingManagerUsesVisible() {
        setupBindingType(false);
        mConnectionMetrics.addConnection(createMockConnection(ChildBindingState.STRONG));
        mConnectionMetrics.addConnection(createMockConnection(ChildBindingState.VISIBLE));
        ChildProcessConnection lowestRankingConnection =
                createMockConnection(ChildBindingState.WAIVED);
        mConnectionMetrics.addConnection(lowestRankingConnection);
        mConnectionMetrics.addConnection(createMockConnection(ChildBindingState.WAIVED));
        mConnectionMetrics.addConnection(createMockConnection(ChildBindingState.WAIVED));
        mConnectionMetrics.addConnection(createMockConnection(ChildBindingState.WAIVED));
        setLowestRanking(lowestRankingConnection);

        mConnectionMetrics.emitMetrics();

        Assert.assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.ChildProcessBinding.TotalConnections", 6));
        Assert.assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.ChildProcessBinding.StrongConnections", 1));
        Assert.assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.ChildProcessBinding.VisibleConnections", 4));
        Assert.assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.ChildProcessBinding.NotPerceptibleConnections", 0));
        Assert.assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.ChildProcessBinding.WaivedConnections", 1));

        Assert.assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.ChildProcessBinding.ContentVisibleConnections", 1));
        Assert.assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.ChildProcessBinding.ContentWaivedConnections", 4));

        Assert.assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.ChildProcessBinding.WaivableConnections", 3));
    }

    @Test
    @SmallTest
    public void testEmitMetricsWithUpdate() {
        setupBindingType(false);
        ChildProcessConnection lowestRankingConnection =
                createMockConnection(ChildBindingState.VISIBLE);
        mConnectionMetrics.addConnection(lowestRankingConnection);
        ChildProcessConnection highestRankingConnection =
                createMockConnection(ChildBindingState.VISIBLE);
        mConnectionMetrics.addConnection(highestRankingConnection);
        setLowestRanking(lowestRankingConnection);
        mConnectionMetrics.emitMetrics();

        Assert.assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.ChildProcessBinding.TotalConnections", 2));
        Assert.assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.ChildProcessBinding.StrongConnections", 0));
        Assert.assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.ChildProcessBinding.VisibleConnections", 2));
        Assert.assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.ChildProcessBinding.NotPerceptibleConnections", 0));
        Assert.assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.ChildProcessBinding.WaivedConnections", 0));

        Assert.assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.ChildProcessBinding.ContentVisibleConnections", 2));
        Assert.assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.ChildProcessBinding.ContentWaivedConnections", 0));

        Assert.assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.ChildProcessBinding.WaivableConnections", 0));

        updateContentBinding(lowestRankingConnection, ChildBindingState.WAIVED);
        updateContentBinding(highestRankingConnection, ChildBindingState.WAIVED);
        mConnectionMetrics.emitMetrics();

        Assert.assertEquals(
                2,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.ChildProcessBinding.TotalConnections", 2));
        Assert.assertEquals(
                2,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.ChildProcessBinding.StrongConnections", 0));
        Assert.assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.ChildProcessBinding.VisibleConnections", 1));
        Assert.assertEquals(
                2,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.ChildProcessBinding.NotPerceptibleConnections", 0));
        Assert.assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.ChildProcessBinding.WaivedConnections", 1));

        Assert.assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.ChildProcessBinding.ContentVisibleConnections", 0));
        Assert.assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.ChildProcessBinding.ContentWaivedConnections", 2));

        Assert.assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "Android.ChildProcessBinding.WaivableConnections", 1));
    }

    /**
     * Create a mock connection with the specified content binding state and a BindingManager
     * binding.
     */
    private ChildProcessConnection createMockConnection(
            @ChildBindingState int contentBindingState) {
        ChildProcessConnection connection =
                new TestChildProcessConnection(
                        new ComponentName("pkg", "cls"),
                        /* bindToCallerCheck= */ false,
                        /* bindAsExternalService= */ false,
                        /* serviceBundle= */ null);
        connection.start(/* useStrongBinding= */ false, /* serviceCallback= */ null);
        if (contentBindingState == ChildBindingState.STRONG) {
            connection.addStrongBinding();
            connection.removeVisibleBinding();
        } else if (contentBindingState == ChildBindingState.WAIVED) {
            connection.removeVisibleBinding();
        }
        mBindingManager.addConnection(connection);
        return connection;
    }

    /**
     * Change the current binding state of a mock {@code connection} to {@code contentBindingState}.
     */
    private void updateContentBinding(
            ChildProcessConnection connection, @ChildBindingState int contentBindingState) {
        final boolean needsStrongBinding = contentBindingState == ChildBindingState.STRONG;
        final boolean hasContentStrongBinding = connection.isStrongBindingBound();

        final boolean lowestRank = mRanking.size() == 1 && mRanking.get(0) == connection;
        final boolean needsVisibleBinding = contentBindingState == ChildBindingState.VISIBLE;
        final boolean hasContentVisibleBinding =
                ((lowestRank || BindingManager.useNotPerceptibleBinding())
                                && connection.isVisibleBindingBound())
                        || connection.getVisibleBindingCount() == 2;

        if (needsStrongBinding && !hasContentStrongBinding) {
            connection.addStrongBinding();
        } else if (!needsStrongBinding && hasContentStrongBinding) {
            connection.removeStrongBinding();
        }

        if (needsVisibleBinding && !hasContentVisibleBinding) {
            connection.addVisibleBinding();
        } else if (!needsVisibleBinding && hasContentVisibleBinding) {
            connection.removeVisibleBinding();
        }
    }

    /** Make the supplied connection the lowest ranking. */
    private void setLowestRanking(ChildProcessConnection connection) {
        if (mRanking.size() == 1) {
            mBindingManager.removeConnection(mRanking.get(0));
            mBindingManager.addConnection(mRanking.get(0));
        }
        mRanking.clear();
        if (connection != null) {
            mRanking.add(connection);
        }
        mBindingManager.rankingChanged();
    }

    private void removeConnection(ChildProcessConnection connection) {
        mConnectionMetrics.removeConnection(connection);
        mBindingManager.removeConnection(connection);
    }

    private void setupBindingType(boolean useNotPerceptibleBinding) {
        BindingManager.setUseNotPerceptibleBindingForTesting(useNotPerceptibleBinding);
        if (useNotPerceptibleBinding) {
            Assert.assertTrue(BindingManager.useNotPerceptibleBinding());
            return;
        }
        Assert.assertFalse(BindingManager.useNotPerceptibleBinding());
    }
}