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

Commit 05e1ba2d authored by Manjeet Rulhania's avatar Manjeet Rulhania Committed by Android (Google) Code Review
Browse files

Merge "Schedule sqlite writes into the registry" into main

parents 20690484 9b1a4a5e
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -177,6 +177,8 @@ abstract class DiscreteOpsRegistry {
     */
    abstract void writeAndClearOldAccessHistory();

    void shutdown() {}

    /** Remove all discrete op events. */
    abstract void clearHistory();

+77 −36
Original line number Diff line number Diff line
@@ -57,13 +57,18 @@ import java.util.Set;
public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry {
    private static final String TAG = "DiscreteOpsSqlRegistry";

    private static final long DB_WRITE_INTERVAL = Duration.ofMinutes(10).toMillis();
    private static final long EXPIRED_ENTRY_DELETION_INTERVAL = Duration.ofHours(6).toMillis();

    // Event type handled by SqliteWriteHandler
    private static final int WRITE_DATABASE_RECURRING = 1;
    private static final int DELETE_EXPIRED_ENTRIES = 2;
    private static final int WRITE_DATABASE_CACHE_FULL = 3;

    private final Context mContext;
    private final DiscreteOpsDbHelper mDiscreteOpsDbHelper;
    private final SqliteWriteHandler mSqliteWriteHandler;
    private final DiscreteOpCache mDiscreteOpCache = new DiscreteOpCache(512);
    private static final long THREE_HOURS = Duration.ofHours(3).toMillis();
    private static final int WRITE_CACHE_EVICTED_OP_EVENTS = 1;
    private static final int DELETE_OLD_OP_EVENTS = 2;
    // Attribution chain id is used to identify an attribution source chain, This is
    // set for startOp only. PermissionManagerService resets this ID on device restart, so
    // we use previously persisted chain id as offset, and add it to chain id received from
@@ -83,6 +88,9 @@ public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry {
        mSqliteWriteHandler = new SqliteWriteHandler(thread.getLooper());
        mDiscreteOpsDbHelper = new DiscreteOpsDbHelper(context, databaseFile);
        mChainIdOffset = mDiscreteOpsDbHelper.getLargestAttributionChainId();
        mSqliteWriteHandler.sendEmptyMessageDelayed(WRITE_DATABASE_RECURRING, DB_WRITE_INTERVAL);
        mSqliteWriteHandler.sendEmptyMessageDelayed(DELETE_EXPIRED_ENTRIES,
                EXPIRED_ENTRY_DELETION_INTERVAL);
    }

    @Override
@@ -117,15 +125,14 @@ public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry {
    }

    @Override
    void writeAndClearOldAccessHistory() {
        // Let the sql impl also follow the same disk write frequencies as xml,
        // controlled by AppOpsService.
    void shutdown() {
        mSqliteWriteHandler.removeAllPendingMessages();
        mDiscreteOpsDbHelper.insertDiscreteOps(mDiscreteOpCache.getAllEventsAndClear());
        if (!mSqliteWriteHandler.hasMessages(DELETE_OLD_OP_EVENTS)) {
            if (mSqliteWriteHandler.sendEmptyMessageDelayed(DELETE_OLD_OP_EVENTS, THREE_HOURS)) {
                Slog.w(TAG, "DELETE_OLD_OP_EVENTS is not queued");
            }
    }

    @Override
    void writeAndClearOldAccessHistory() {
        // no-op
    }

    @Override
@@ -175,7 +182,7 @@ public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry {
            @Nullable String attributionTagFilter, int opFlagsFilter,
            Set<String> attributionExemptPkgs) {
        // flush the cache into database before read.
        writeAndClearOldAccessHistory();
        mDiscreteOpsDbHelper.insertDiscreteOps(mDiscreteOpCache.getAllEventsAndClear());
        boolean assembleChains = attributionExemptPkgs != null;
        IntArray opCodes = getAppOpCodes(filter, opNamesFilter);
        beginTimeMillis = Math.max(beginTimeMillis, Instant.now().minus(sDiscreteHistoryCutoff,
@@ -363,19 +370,58 @@ public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case WRITE_CACHE_EVICTED_OP_EVENTS:
                    List<DiscreteOp> opEvents = (List<DiscreteOp>) msg.obj;
                    mDiscreteOpsDbHelper.insertDiscreteOps(opEvents);
                    break;
                case DELETE_OLD_OP_EVENTS:
                case WRITE_DATABASE_RECURRING -> {
                    try {
                        List<DiscreteOp> evictedEvents;
                        synchronized (mDiscreteOpCache) {
                            evictedEvents = mDiscreteOpCache.evict();
                        }
                        mDiscreteOpsDbHelper.insertDiscreteOps(evictedEvents);
                    } finally {
                        mSqliteWriteHandler.sendEmptyMessageDelayed(WRITE_DATABASE_RECURRING,
                                DB_WRITE_INTERVAL);
                        // Schedule a cleanup to truncate older (before cutoff time) entries.
                        if (!mSqliteWriteHandler.hasMessages(DELETE_EXPIRED_ENTRIES)) {
                            mSqliteWriteHandler.sendEmptyMessageDelayed(DELETE_EXPIRED_ENTRIES,
                                    EXPIRED_ENTRY_DELETION_INTERVAL);
                        }
                    }
                }
                case DELETE_EXPIRED_ENTRIES -> {
                    long cutOffTimeStamp = System.currentTimeMillis() - sDiscreteHistoryCutoff;
                    mDiscreteOpsDbHelper.execSQL(
                            DiscreteOpsTable.DELETE_TABLE_DATA_BEFORE_ACCESS_TIME,
                            new Object[]{cutOffTimeStamp});
                    break;
                default:
                    throw new IllegalStateException("Unexpected value: " + msg.what);
                }
                case WRITE_DATABASE_CACHE_FULL -> {
                    try {
                        List<DiscreteOp> evictedEvents;
                        synchronized (mDiscreteOpCache) {
                            evictedEvents = mDiscreteOpCache.evict();
                            // if nothing to evict, just write the whole cache to database.
                            if (evictedEvents.isEmpty()
                                    && mDiscreteOpCache.size() >= mDiscreteOpCache.capacity()) {
                                evictedEvents.addAll(mDiscreteOpCache.mCache);
                                mDiscreteOpCache.clear();
                            }
                        }
                        mDiscreteOpsDbHelper.insertDiscreteOps(evictedEvents);
                    } finally {
                        // Just in case initial message is not scheduled.
                        if (!mSqliteWriteHandler.hasMessages(WRITE_DATABASE_RECURRING)) {
                            mSqliteWriteHandler.sendEmptyMessageDelayed(WRITE_DATABASE_RECURRING,
                                    DB_WRITE_INTERVAL);
                        }
                    }
                }
                default -> throw new IllegalStateException("Unexpected value: " + msg.what);
            }
        }

        void removeAllPendingMessages() {
            removeMessages(WRITE_DATABASE_RECURRING);
            removeMessages(DELETE_EXPIRED_ENTRIES);
            removeMessages(WRITE_DATABASE_CACHE_FULL);
        }
    }

@@ -390,6 +436,7 @@ public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry {
     * 4) During shutdown.
     */
    class DiscreteOpCache {
        private static final String TAG = "DiscreteOpCache";
        private final int mCapacity;
        private final ArraySet<DiscreteOp> mCache;

@@ -404,23 +451,9 @@ public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry {
                    return;
                }
                mCache.add(opEvent);

                if (mCache.size() >= mCapacity) {
                    if (DEBUG_LOG) {
                        Slog.i(TAG, "Current discrete ops cache size: " + mCache.size());
                    }
                    List<DiscreteOp> evictedEvents = evict();
                    if (DEBUG_LOG) {
                        Slog.i(TAG, "Evicted discrete ops size: " + evictedEvents.size());
                    }
                    // if nothing to evict, just write the whole cache to disk
                    if (evictedEvents.isEmpty()) {
                        Slog.w(TAG, "No discrete ops event is evicted, write cache to db.");
                        evictedEvents.addAll(mCache);
                        mCache.clear();
                    }
                    Message msg = mSqliteWriteHandler.obtainMessage(
                            WRITE_CACHE_EVICTED_OP_EVENTS, evictedEvents);
                    mSqliteWriteHandler.sendMessage(msg);
                    mSqliteWriteHandler.sendEmptyMessage(WRITE_DATABASE_CACHE_FULL);
                }
            }
        }
@@ -461,6 +494,14 @@ public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry {
            }
        }

        int size() {
            return mCache.size();
        }

        int capacity() {
            return mCapacity;
        }

        /**
         * Remove all entries from the cache.
         */
+1 −0
Original line number Diff line number Diff line
@@ -750,6 +750,7 @@ final class HistoricalRegistry {
        }
        // Do not call persistPendingHistory inside the memory lock, due to possible deadlock
        persistPendingHistory();
        mDiscreteRegistry.shutdown();
    }

    void persistPendingHistory() {
+2 −2
Original line number Diff line number Diff line
@@ -226,9 +226,9 @@ public class DiscreteAppOpSqlPersistenceTest {
        mDiscreteRegistry.recordDiscreteAccess(event2);
    }

    /** This clears in-memory cache and push records into the database. */
    private void flushDiscreteOpsToDatabase() {
        mDiscreteRegistry.writeAndClearOldAccessHistory();
        // This clears in-memory cache and push records from cache into the database.
        mDiscreteRegistry.shutdown();
    }

    /**
+2 −1
Original line number Diff line number Diff line
@@ -106,7 +106,8 @@ public class DiscreteOpsMigrationAndRollbackTest {
                    opEvent.getDuration(), opEvent.getAttributionFlags(),
                    (int) opEvent.getChainId(), DiscreteOpsRegistry.ACCESS_TYPE_NOTE_OP);
        }
        sqlRegistry.writeAndClearOldAccessHistory();
        // flush records from cache to the database.
        sqlRegistry.shutdown();
        assertThat(sqlRegistry.getAllDiscreteOps().size()).isEqualTo(RECORD_COUNT);
        assertThat(sqlRegistry.getLargestAttributionChainId()).isEqualTo(RECORD_COUNT);