Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit fa93d465 authored by Kai Li's avatar Kai Li
Browse files

Introduce ContentCaptureService metric batching.

This change adds batching of metrics inside ContentCaptureService. Metrics will only be flushed when over a threshold, which will decrease the number of binder calls to the service implementation.

Exact metrics flushing timing:

1) Any Appeared/Disappeared/TextChanged events count over the threshold (if any new content capture events)
2) Session paused (e.g. FG Activity changes)
3) Session finished (e.g. Activity destoryed)
4) Service disconnected (e.g. AiAi crashed, which should be a super rare case)

Bug: 433614684
Flag: android.view.contentcapture.flags.reduce_binder_transaction_enabled
Test: atest android.view.contentcapture.LoginTest
Test: manually tested via Android Data Hub
Change-Id: If7262ede6300031429468b1612c3f3a1082e8361

Change-Id: If7262ede6300031429468b1612c3f3a1082e8361
parent b7d7662f
Loading
Loading
Loading
Loading
+51 −0
Original line number Diff line number Diff line
@@ -18,21 +18,31 @@ package android.view.contentcapture;
import static android.view.contentcapture.CustomTestActivity.VIEW_TYPE_CUSTOM_VIEW;
import static android.view.contentcapture.CustomTestActivity.VIEW_TYPE_TEXT_VIEW;
import static com.android.compatibility.common.util.ActivitiesWatcher.ActivityLifecycle.DESTROYED;
import static com.google.common.truth.Truth.assertThat;

import android.content.Intent;
import android.os.RemoteCallback;
import android.perftests.utils.BenchmarkState;
import android.platform.test.annotations.RequiresFlagsEnabled;
import android.platform.test.flag.junit.CheckFlagsRule;
import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.service.contentcapture.ContentCaptureService;
import android.view.View;
import android.view.contentcapture.flags.Flags;
import androidx.test.filters.LargeTest;

import com.android.compatibility.common.util.ActivitiesWatcher.ActivityWatcher;
import com.android.perftests.contentcapture.R;

import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;

@LargeTest
public class LoginTest extends AbstractContentCapturePerfTestCase {
    @Rule
    public final CheckFlagsRule mCheckFlagsRule =
            DeviceFlagsValueProvider.createCheckFlagsRule();

    @Test
    public void testLaunchActivity() throws Throwable {
@@ -300,4 +310,45 @@ public class LoginTest extends AbstractContentCapturePerfTestCase {
            state.resumeTiming();
        }
    }

    @Test
    @RequiresFlagsEnabled(Flags.FLAG_REDUCE_BINDER_TRANSACTION_ENABLED)
    public void testBatchFlushMetrics_flagEnabled() throws Throwable {
        // Arrange
        MyContentCaptureService service = enableService();
        CustomTestActivity activity =
                launchActivity(
                        R.layout.test_export_virtual_assist_node_activity, 3, VIEW_TYPE_TEXT_VIEW);
        View groupRootView = activity.findViewById(R.id.group_root_view);
        int sessionId = groupRootView.getContentCaptureSession().getId();
        int expectedFlushCount = 2;
        int expectedViewAppearedCount = 4;  // 3 TextViews + 1 container
        int eventTimeoutMs = 10000;
        BenchmarkState state = mPerfStatusReporter.getBenchmarkState();

        // Act
        while (state.keepRunning()) {
            state.pauseTiming();
            service.clearEvents();
            ContentCaptureService.PendingMetrics pendingMetrics =
                    service.mPendingMetrics.get(sessionId);
            if (pendingMetrics != null) {
                pendingMetrics.getMetrics().reset();
            }
            sInstrumentation.runOnMainSync(() -> groupRootView.setVisibility(View.GONE));
            sInstrumentation.waitForIdleSync();
            state.resumeTiming();

            sInstrumentation.runOnMainSync(() -> groupRootView.setVisibility(View.VISIBLE));
            sInstrumentation.waitForIdleSync();
            state.pauseTiming();
            service.waitForFlushEvents(expectedFlushCount, eventTimeoutMs);

            // Assert
            assertThat(service.getAppearedCount()).isEqualTo(expectedViewAppearedCount);
            assertThat(service.mPendingMetrics.get(sessionId).getMetrics().viewAppearedCount)
                    .isEqualTo(expectedViewAppearedCount);
            state.resumeTiming();
        }
    }
}
+37 −2
Original line number Diff line number Diff line
@@ -49,6 +49,7 @@ public class MyContentCaptureService extends ContentCaptureService {
    private final Condition eventsChanged = lock.newCondition();
    private final List<ContentCaptureEvent> mCapturedEvents = new ArrayList<>();
    private int appearedCount = 0;
    private int flushCount = 0;
    @NonNull
    public static ServiceWatcher setServiceWatcher() {
        if (sServiceWatcher != null) {
@@ -137,6 +138,8 @@ public class MyContentCaptureService extends ContentCaptureService {
            mCapturedEvents.add(event);
            if (event.getType() == ContentCaptureEvent.TYPE_VIEW_APPEARED) {
                appearedCount++;
            } else if (event.getType() == ContentCaptureEvent.TYPE_SESSION_FLUSH) {
                flushCount++;
            }
            eventsChanged.signalAll();
        } finally {
@@ -153,6 +156,7 @@ public class MyContentCaptureService extends ContentCaptureService {
        try {
            mCapturedEvents.clear();
            appearedCount = 0;
            flushCount = 0;
        } finally {
            lock.unlock();
        }
@@ -177,19 +181,50 @@ public class MyContentCaptureService extends ContentCaptureService {
        }
    }

    public int getFlushCount() {
        lock.lock();
        try {
            return flushCount;
        } finally {
            lock.unlock();
        }
    }

    public boolean waitForAppearedEvents(
            int expectedCount, long timeoutMillis) throws InterruptedException {
        return waitForEvents(expectedCount, timeoutMillis, ContentCaptureEvent.TYPE_VIEW_APPEARED);
    }

    public boolean waitForFlushEvents(
            int expectedCount, long timeoutMillis) throws InterruptedException {
        return waitForEvents(expectedCount, timeoutMillis, ContentCaptureEvent.TYPE_SESSION_FLUSH);
    }

    private boolean waitForEvents(int expectedCount, long timeoutMillis, int type)
            throws InterruptedException {
        long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeoutMillis);
        lock.lock();
        try {
            while (appearedCount < expectedCount) {
            while (true) {
                final int actualCount;
                if (type == ContentCaptureEvent.TYPE_VIEW_APPEARED) {
                    actualCount = appearedCount;
                } else if (type == ContentCaptureEvent.TYPE_SESSION_FLUSH) {
                    actualCount = flushCount;
                } else {
                    return false;
                }

                if (actualCount >= expectedCount) {
                    return true;
                }

                long remainingNanos = deadline - System.nanoTime();
                if (remainingNanos <= 0) {
                    return false;
                }
                eventsChanged.await(remainingNanos, TimeUnit.NANOSECONDS);
            }
            return true;
        } finally {
            lock.unlock();
        }
+199 −0
Original line number Diff line number Diff line
@@ -22,9 +22,12 @@ import static android.view.contentcapture.ContentCaptureHelper.sVerbose;
import static android.view.contentcapture.ContentCaptureHelper.toList;
import static android.view.contentcapture.ContentCaptureManager.NO_SESSION_ID;

import static android.view.contentcapture.flags.Flags.reduceBinderTransactionEnabled;

import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;

import android.annotation.CallSuper;
import android.annotation.MainThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
@@ -44,6 +47,7 @@ import android.os.Process;
import android.os.RemoteException;
import android.util.Log;
import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseIntArray;
import android.view.contentcapture.ContentCaptureCondition;
import android.view.contentcapture.ContentCaptureContext;
@@ -55,6 +59,7 @@ import android.view.contentcapture.DataRemovalRequest;
import android.view.contentcapture.DataShareRequest;
import android.view.contentcapture.IContentCaptureDirectManager;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.IResultReceiver;
import com.android.internal.util.FrameworkStatsLog;

@@ -143,6 +148,23 @@ public abstract class ContentCaptureService extends Service {
     */
    public static final String ASSIST_CONTENT_ACTIVITY_START_KEY = "activity_start_assist_content";

    /**
     * The threshold for the number of metrics to be flushed before flushing the pending metrics.
     *
     * <p>For example, if the threshold is 10,000, then the metrics will be flushed when there are
     * 10,000 or more pending view appeared or disappeared metrics.
     */
    private static final int METRICS_FLUSH_THRESHOLD = 10_000;

    /**
     * Holds metrics that are waiting to be flushed.
     *
     * <p>Key is the session id.
     *
     * @hide
     */
    @VisibleForTesting
    public final SparseArray<PendingMetrics> mPendingMetrics = new SparseArray<>();

    private final LocalDataShareAdapterResourceManager mDataShareAdapterResourceManager =
            new LocalDataShareAdapterResourceManager();
@@ -508,6 +530,12 @@ public abstract class ContentCaptureService extends Service {

    private void handleOnDisconnected() {
        onDisconnected();
        if (reduceBinderTransactionEnabled()) {
            if (mPendingMetrics.size() != 0) {
                Log.i(TAG, "Flushing " + mPendingMetrics.size() + " pending metrics on disconnect");
                flushAllPendingMetrics();
            }
        }
        mContentCaptureServiceCallback = null;
        mContentProtectionAllowlistCallback = null;
    }
@@ -536,9 +564,21 @@ public abstract class ContentCaptureService extends Service {
        setClientState(clientReceiver, stateFlags, mContentCaptureClientInterface.asBinder());
    }

    @MainThread
    private void handleSendEvents(int uid,
            @NonNull ParceledListSlice<ContentCaptureEvent> parceledEvents, int reason,
            @Nullable ContentCaptureOptions options) {
        if (reduceBinderTransactionEnabled()) {
            handleSendEventsWithBatching(uid, parceledEvents, reason, options);
        } else {
            handleSendEventsNoBatching(uid, parceledEvents, reason, options);
        }
    }

    @MainThread
    private void handleSendEventsNoBatching(int uid,
            @NonNull ParceledListSlice<ContentCaptureEvent> parceledEvents, int reason,
            @Nullable ContentCaptureOptions options) {
        final List<ContentCaptureEvent> events = parceledEvents.getList();
        if (events.isEmpty()) {
            Log.w(TAG, "handleSendEvents() received empty list of events");
@@ -601,6 +641,92 @@ public abstract class ContentCaptureService extends Service {
        writeFlushMetrics(lastSessionId, activityComponent, metrics, options, reason);
    }

    @MainThread
    private void handleSendEventsWithBatching(int uid,
            @NonNull ParceledListSlice<ContentCaptureEvent> parceledEvents, int reason,
            @Nullable ContentCaptureOptions options) {
        final List<ContentCaptureEvent> events = parceledEvents.getList();
        if (events.isEmpty()) {
            Log.w(TAG, "handleSendEventsWithBatching() received empty list of events");
            return;
        }

        int lastSessionId = NO_SESSION_ID;
        ContentCaptureSessionId sessionId = null;

        for (int i = 0; i < events.size(); i++) {
            final ContentCaptureEvent event = events.get(i);
            if (!handleIsRightCallerFor(event, uid)) continue;

            final int sessionIdInt = event.getSessionId();
            if (sessionIdInt != lastSessionId) {
                sessionId = new ContentCaptureSessionId(sessionIdInt);
                lastSessionId = sessionIdInt;
            }

            final int eventType = event.getType();
            PendingMetrics pendingMetrics = mPendingMetrics.get(sessionIdInt);
            if (pendingMetrics == null) {
                pendingMetrics = new PendingMetrics();
                mPendingMetrics.put(sessionIdInt, pendingMetrics);
            }
            final ContentCaptureContext clientContext = event.getContentCaptureContext();
            pendingMetrics.update(
                    clientContext != null ? clientContext.getActivityComponent() : null,
                    options,
                    reason);

            boolean shouldFlushMetrics = false;
            switch (eventType) {
                case ContentCaptureEvent.TYPE_SESSION_STARTED:
                    clientContext.setParentSessionId(event.getParentSessionId());
                    mSessionUids.put(sessionIdInt, uid);
                    onCreateContentCaptureSession(clientContext, sessionId);
                    pendingMetrics.metrics.sessionStarted++;
                    break;
                case ContentCaptureEvent.TYPE_SESSION_FINISHED:
                    mSessionUids.delete(sessionIdInt);
                    onDestroyContentCaptureSession(sessionId);
                    pendingMetrics.metrics.sessionFinished++;
                    // Flush immediately when session is finished, regardless of threshold.
                    shouldFlushMetrics = true;
                    break;
                case ContentCaptureEvent.TYPE_SESSION_PAUSED:
                    onContentCaptureEvent(sessionId, event);
                    // Flush immediately when session is paused, regardless of threshold.
                    shouldFlushMetrics = true;
                    break;
                case ContentCaptureEvent.TYPE_VIEW_APPEARED:
                    onContentCaptureEvent(sessionId, event);
                    pendingMetrics.metrics.viewAppearedCount++;
                    shouldFlushMetrics = isOverThreshold(pendingMetrics.metrics);
                    break;
                case ContentCaptureEvent.TYPE_VIEW_DISAPPEARED:
                    onContentCaptureEvent(sessionId, event);
                    pendingMetrics.metrics.viewDisappearedCount++;
                    shouldFlushMetrics = isOverThreshold(pendingMetrics.metrics);
                    break;
                case ContentCaptureEvent.TYPE_VIEW_TEXT_CHANGED:
                    onContentCaptureEvent(sessionId, event);
                    pendingMetrics.metrics.viewTextChangedCount++;
                    shouldFlushMetrics = isOverThreshold(pendingMetrics.metrics);
                    break;
                default:
                    onContentCaptureEvent(sessionId, event);
            }

            if (shouldFlushMetrics) {
                flushMetricsForSession(sessionIdInt);
            }
        }
    }

    private static boolean isOverThreshold(@NonNull FlushMetrics metrics) {
        return metrics.viewAppearedCount >= METRICS_FLUSH_THRESHOLD
                || metrics.viewDisappearedCount >= METRICS_FLUSH_THRESHOLD
                || metrics.viewTextChangedCount >= METRICS_FLUSH_THRESHOLD;
    }

    private void handleOnLoginDetected(
            int uid, @NonNull ParceledListSlice<ContentCaptureEvent> parceledEvents) {
        if (uid != Process.SYSTEM_UID) {
@@ -638,6 +764,9 @@ public abstract class ContentCaptureService extends Service {

    private void handleFinishSession(int sessionId) {
        mSessionUids.delete(sessionId);
        if (reduceBinderTransactionEnabled()) {
            flushMetricsForSession(sessionId);
        }
        onDestroyContentCaptureSession(new ContentCaptureSessionId(sessionId));
    }

@@ -744,6 +873,47 @@ public abstract class ContentCaptureService extends Service {
        }
    }

    @MainThread
    private void flushAllPendingMetrics() {
        if (mPendingMetrics.size() == 0) {
            if (sDebug) {
                Log.d(TAG, "flushAllPendingMetrics() - nothing to flush");
            }
            return;
        }

        if (sDebug) {
            Log.d(TAG, "Flushing metrics for " + mPendingMetrics.size() + " sessions");
        }

        for (int i = 0; i < mPendingMetrics.size(); i++) {
            final int sessionId = mPendingMetrics.keyAt(i);
            final PendingMetrics pendingMetrics = mPendingMetrics.valueAt(i);
            writeFlushMetrics(sessionId, pendingMetrics.activityComponent, pendingMetrics.metrics,
                    pendingMetrics.options, pendingMetrics.flushReason);
        }

        mPendingMetrics.clear();
    }

    @MainThread
    private void flushMetricsForSession(int sessionId) {
        int index = mPendingMetrics.indexOfKey(sessionId);
        if (index < 0) {
            return;
        }
        final PendingMetrics pendingMetrics = mPendingMetrics.get(sessionId);
        mPendingMetrics.removeAt(index);
        if (pendingMetrics == null) {
            return;
        }
        if (sDebug) {
            Log.d(TAG, "Flushing metrics for session " + sessionId);
        }
        writeFlushMetrics(sessionId, pendingMetrics.activityComponent, pendingMetrics.metrics,
                pendingMetrics.options, pendingMetrics.flushReason);
    }

    /**
     * Logs the metrics for content capture events flushing.
     */
@@ -763,6 +933,35 @@ public abstract class ContentCaptureService extends Service {
        }
    }

    /**
     * Container for metrics that are batched before being flushed.
     *
     * @hide
     */
    @VisibleForTesting
    public static class PendingMetrics {
        private final FlushMetrics metrics = new FlushMetrics();
        @Nullable private ComponentName activityComponent;
        @Nullable private ContentCaptureOptions options;
        private int flushReason;

        private void update(
                @Nullable ComponentName activityComponent,
                @Nullable ContentCaptureOptions options,
                int flushReason) {
            if (this.activityComponent == null && activityComponent != null) {
                this.activityComponent = activityComponent;
            }
            this.options = options;
            this.flushReason = flushReason;
        }

        @VisibleForTesting
        public FlushMetrics getMetrics() {
            return metrics;
        }
    }

    private static class DataShareReadAdapterDelegate extends IDataShareReadAdapter.Stub {

        private final WeakReference<LocalDataShareAdapterResourceManager> mResourceManagerReference;
+6 −0
Original line number Diff line number Diff line
@@ -52,3 +52,9 @@ flag {
    bug: "430421182"
}

flag {
    name: "reduce_binder_transaction_enabled"
    namespace: "ailabs"
    description: "Feature flag to reduce binder transaction for content capture, by batching metrics logging and reducing the number of flush events"
    bug: "433614684"
}