• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.appsearch.stats;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.app.appsearch.exceptions.AppSearchException;
22 import android.content.Context;
23 import android.os.Process;
24 import android.os.SystemClock;
25 import android.util.ArrayMap;
26 import android.util.Log;
27 import android.util.SparseIntArray;
28 
29 import com.android.internal.annotations.GuardedBy;
30 import com.android.internal.annotations.VisibleForTesting;
31 import com.android.server.appsearch.AppSearchConfig;
32 import com.android.server.appsearch.external.localstorage.AppSearchLogger;
33 import com.android.server.appsearch.external.localstorage.stats.CallStats;
34 import com.android.server.appsearch.external.localstorage.stats.InitializeStats;
35 import com.android.server.appsearch.external.localstorage.stats.OptimizeStats;
36 import com.android.server.appsearch.external.localstorage.stats.PutDocumentStats;
37 import com.android.server.appsearch.external.localstorage.stats.RemoveStats;
38 import com.android.server.appsearch.external.localstorage.stats.SearchStats;
39 import com.android.server.appsearch.util.PackageUtil;
40 
41 import java.io.UnsupportedEncodingException;
42 import java.security.MessageDigest;
43 import java.security.NoSuchAlgorithmException;
44 import java.util.Map;
45 import java.util.Objects;
46 import java.util.Random;
47 
48 /**
49  * Logger Implementation for pushed atoms.
50  *
51  * <p>This class is thread-safe.
52  *
53  * @hide
54  */
55 public final class PlatformLogger implements AppSearchLogger {
56     private static final String TAG = "AppSearchPlatformLogger";
57 
58     // Context of the user we're logging for.
59     private final Context mUserContext;
60 
61     // Manager holding the configuration flags
62     private final AppSearchConfig mConfig;
63 
64     private final Random mRng = new Random();
65     private final Object mLock = new Object();
66 
67     /**
68      * SparseArray to track how many stats we skipped due to
69      * {@link AppSearchConfig#getCachedMinTimeIntervalBetweenSamplesMillis()}.
70      *
71      * <p> We can have correct extrapolated number by adding those counts back when we log
72      * the same type of stats next time. E.g. the true count of an event could be estimated as:
73      * SUM(sampling_interval * (num_skipped_sample + 1)) as est_count
74      *
75      * <p>The key to the SparseArray is {@link CallStats.CallType}
76      */
77     @GuardedBy("mLock")
78     private final SparseIntArray mSkippedSampleCountLocked =
79             new SparseIntArray();
80 
81     /**
82      * Map to cache the packageUid for each package.
83      *
84      * <p>It maps packageName to packageUid.
85      *
86      * <p>The entry will be removed whenever the app gets uninstalled
87      */
88     @GuardedBy("mLock")
89     private final Map<String, Integer> mPackageUidCacheLocked =
90             new ArrayMap<>();
91 
92     /**
93      * Elapsed time for last stats logged from boot in millis
94      */
95     @GuardedBy("mLock")
96     private long mLastPushTimeMillisLocked = 0;
97 
98     /**
99      * Helper class to hold platform specific stats for statsd.
100      */
101     static final class ExtraStats {
102         // UID for the calling package of the stats.
103         final int mPackageUid;
104         // sampling interval for the call type of the stats.
105         final int mSamplingInterval;
106         // number of samplings skipped before the current one for the same call type.
107         final int mSkippedSampleCount;
108 
ExtraStats(int packageUid, int samplingInterval, int skippedSampleCount)109         ExtraStats(int packageUid, int samplingInterval, int skippedSampleCount) {
110             mPackageUid = packageUid;
111             mSamplingInterval = samplingInterval;
112             mSkippedSampleCount = skippedSampleCount;
113         }
114     }
115 
116     /**
117      * Constructor
118      */
PlatformLogger( @onNull Context userContext, @NonNull AppSearchConfig config)119     public PlatformLogger(
120             @NonNull Context userContext,
121             @NonNull AppSearchConfig config) {
122         mUserContext = Objects.requireNonNull(userContext);
123         mConfig = Objects.requireNonNull(config);
124     }
125 
126     /** Logs {@link CallStats}. */
127     @Override
logStats(@onNull CallStats stats)128     public void logStats(@NonNull CallStats stats) {
129         Objects.requireNonNull(stats);
130         synchronized (mLock) {
131             if (shouldLogForTypeLocked(stats.getCallType())) {
132                 logStatsImplLocked(stats);
133             }
134         }
135     }
136 
137     /** Logs {@link PutDocumentStats}. */
138     @Override
logStats(@onNull PutDocumentStats stats)139     public void logStats(@NonNull PutDocumentStats stats) {
140         Objects.requireNonNull(stats);
141         synchronized (mLock) {
142             if (shouldLogForTypeLocked(CallStats.CALL_TYPE_PUT_DOCUMENT)) {
143                 logStatsImplLocked(stats);
144             }
145         }
146     }
147 
148     @Override
logStats(@onNull InitializeStats stats)149     public void logStats(@NonNull InitializeStats stats) {
150         Objects.requireNonNull(stats);
151         synchronized (mLock) {
152             if (shouldLogForTypeLocked(CallStats.CALL_TYPE_INITIALIZE)) {
153                 logStatsImplLocked(stats);
154             }
155         }
156     }
157 
158     @Override
logStats(@onNull SearchStats stats)159     public void logStats(@NonNull SearchStats stats) {
160         Objects.requireNonNull(stats);
161         synchronized (mLock) {
162             if (shouldLogForTypeLocked(CallStats.CALL_TYPE_SEARCH)) {
163                 logStatsImplLocked(stats);
164             }
165         }
166     }
167 
168     @Override
logStats(@onNull RemoveStats stats)169     public void logStats(@NonNull RemoveStats stats) {
170         // TODO(b/173532925): Log stats
171     }
172 
173     @Override
logStats(@onNull OptimizeStats stats)174     public void logStats(@NonNull OptimizeStats stats) {
175         Objects.requireNonNull(stats);
176         synchronized (mLock) {
177             if (shouldLogForTypeLocked(CallStats.CALL_TYPE_OPTIMIZE)) {
178                 logStatsImplLocked(stats);
179             }
180         }
181     }
182 
183     /**
184      * Removes cached UID for package.
185      *
186      * @return removed UID for the package, or {@code INVALID_UID} if package was not previously
187      * cached.
188      */
removeCachedUidForPackage(@onNull String packageName)189     public int removeCachedUidForPackage(@NonNull String packageName) {
190         // TODO(b/173532925) This needs to be called when we get PACKAGE_REMOVED intent
191         Objects.requireNonNull(packageName);
192         synchronized (mLock) {
193             Integer uid = mPackageUidCacheLocked.remove(packageName);
194             return uid != null ? uid : Process.INVALID_UID;
195         }
196     }
197 
198     @GuardedBy("mLock")
logStatsImplLocked(@onNull CallStats stats)199     private void logStatsImplLocked(@NonNull CallStats stats) {
200         mLastPushTimeMillisLocked = SystemClock.elapsedRealtime();
201         ExtraStats extraStats = createExtraStatsLocked(stats.getPackageName(), stats.getCallType());
202         String database = stats.getDatabase();
203         try {
204             int hashCodeForDatabase = calculateHashCodeMd5(database);
205             AppSearchStatsLog.write(AppSearchStatsLog.APP_SEARCH_CALL_STATS_REPORTED,
206                     extraStats.mSamplingInterval,
207                     extraStats.mSkippedSampleCount,
208                     extraStats.mPackageUid,
209                     hashCodeForDatabase,
210                     stats.getStatusCode(),
211                     stats.getTotalLatencyMillis(),
212                     stats.getCallType(),
213                     stats.getEstimatedBinderLatencyMillis(),
214                     stats.getNumOperationsSucceeded(),
215                     stats.getNumOperationsFailed());
216         } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
217             // TODO(b/184204720) report hashing error to statsd
218             //  We need to set a special value(e.g. 0xFFFFFFFF) for the hashing of the database,
219             //  so in the dashboard we know there is some error for hashing.
220             //
221             // Something is wrong while calculating the hash code for database
222             // this shouldn't happen since we always use "MD5" and "UTF-8"
223             if (database != null) {
224                 Log.e(TAG, "Error calculating hash code for database " + database, e);
225             }
226         }
227     }
228 
229     @GuardedBy("mLock")
logStatsImplLocked(@onNull PutDocumentStats stats)230     private void logStatsImplLocked(@NonNull PutDocumentStats stats) {
231         mLastPushTimeMillisLocked = SystemClock.elapsedRealtime();
232         ExtraStats extraStats = createExtraStatsLocked(
233                 stats.getPackageName(), CallStats.CALL_TYPE_PUT_DOCUMENT);
234         String database = stats.getDatabase();
235         try {
236             int hashCodeForDatabase = calculateHashCodeMd5(database);
237             AppSearchStatsLog.write(AppSearchStatsLog.APP_SEARCH_PUT_DOCUMENT_STATS_REPORTED,
238                     extraStats.mSamplingInterval,
239                     extraStats.mSkippedSampleCount,
240                     extraStats.mPackageUid,
241                     hashCodeForDatabase,
242                     stats.getStatusCode(),
243                     stats.getTotalLatencyMillis(),
244                     stats.getGenerateDocumentProtoLatencyMillis(),
245                     stats.getRewriteDocumentTypesLatencyMillis(),
246                     stats.getNativeLatencyMillis(),
247                     stats.getNativeDocumentStoreLatencyMillis(),
248                     stats.getNativeIndexLatencyMillis(),
249                     stats.getNativeIndexMergeLatencyMillis(),
250                     stats.getNativeDocumentSizeBytes(),
251                     stats.getNativeNumTokensIndexed(),
252                     stats.getNativeExceededMaxNumTokens());
253         } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
254             // TODO(b/184204720) report hashing error to statsd
255             //  We need to set a special value(e.g. 0xFFFFFFFF) for the hashing of the database,
256             //  so in the dashboard we know there is some error for hashing.
257             //
258             // Something is wrong while calculating the hash code for database
259             // this shouldn't happen since we always use "MD5" and "UTF-8"
260             if (database != null) {
261                 Log.e(TAG, "Error calculating hash code for database " + database, e);
262             }
263         }
264     }
265 
266     @GuardedBy("mLock")
logStatsImplLocked(@onNull SearchStats stats)267     private void logStatsImplLocked(@NonNull SearchStats stats) {
268         mLastPushTimeMillisLocked = SystemClock.elapsedRealtime();
269         ExtraStats extraStats = createExtraStatsLocked(stats.getPackageName(),
270                 CallStats.CALL_TYPE_SEARCH);
271         String database = stats.getDatabase();
272         try {
273             int hashCodeForDatabase = calculateHashCodeMd5(database);
274             AppSearchStatsLog.write(AppSearchStatsLog.APP_SEARCH_QUERY_STATS_REPORTED,
275                     extraStats.mSamplingInterval,
276                     extraStats.mSkippedSampleCount,
277                     extraStats.mPackageUid,
278                     hashCodeForDatabase,
279                     stats.getStatusCode(),
280                     stats.getTotalLatencyMillis(),
281                     stats.getRewriteSearchSpecLatencyMillis(),
282                     stats.getRewriteSearchResultLatencyMillis(),
283                     stats.getVisibilityScope(),
284                     stats.getNativeLatencyMillis(),
285                     stats.getTermCount(),
286                     stats.getQueryLength(),
287                     stats.getFilteredNamespaceCount(),
288                     stats.getFilteredSchemaTypeCount(),
289                     stats.getRequestedPageSize(),
290                     stats.getCurrentPageReturnedResultCount(),
291                     stats.isFirstPage(),
292                     stats.getParseQueryLatencyMillis(),
293                     stats.getRankingStrategy(),
294                     stats.getScoredDocumentCount(),
295                     stats.getScoringLatencyMillis(),
296                     stats.getRankingLatencyMillis(),
297                     stats.getDocumentRetrievingLatencyMillis(),
298                     stats.getResultWithSnippetsCount());
299         } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
300             // TODO(b/184204720) report hashing error to statsd
301             //  We need to set a special value(e.g. 0xFFFFFFFF) for the hashing of the database,
302             //  so in the dashboard we know there is some error for hashing.
303             //
304             // Something is wrong while calculating the hash code for database
305             // this shouldn't happen since we always use "MD5" and "UTF-8"
306             if (database != null) {
307                 Log.e(TAG, "Error calculating hash code for database " + database, e);
308             }
309         }
310     }
311 
312     @GuardedBy("mLock")
logStatsImplLocked(@onNull InitializeStats stats)313     private void logStatsImplLocked(@NonNull InitializeStats stats) {
314         mLastPushTimeMillisLocked = SystemClock.elapsedRealtime();
315         ExtraStats extraStats = createExtraStatsLocked(/*packageName=*/ null,
316                 CallStats.CALL_TYPE_INITIALIZE);
317         AppSearchStatsLog.write(AppSearchStatsLog.APP_SEARCH_INITIALIZE_STATS_REPORTED,
318                 extraStats.mSamplingInterval,
319                 extraStats.mSkippedSampleCount,
320                 extraStats.mPackageUid,
321                 stats.getStatusCode(),
322                 stats.getTotalLatencyMillis(),
323                 stats.hasDeSync(),
324                 stats.getPrepareSchemaAndNamespacesLatencyMillis(),
325                 stats.getPrepareVisibilityStoreLatencyMillis(),
326                 stats.getNativeLatencyMillis(),
327                 stats.getDocumentStoreRecoveryCause(),
328                 stats.getIndexRestorationCause(),
329                 stats.getSchemaStoreRecoveryCause(),
330                 stats.getDocumentStoreRecoveryLatencyMillis(),
331                 stats.getIndexRestorationLatencyMillis(),
332                 stats.getSchemaStoreRecoveryLatencyMillis(),
333                 stats.getDocumentStoreDataStatus(),
334                 stats.getDocumentCount(),
335                 stats.getSchemaTypeCount(),
336                 stats.hasReset(),
337                 stats.getResetStatusCode());
338     }
339 
340     @GuardedBy("mLock")
logStatsImplLocked(@onNull OptimizeStats stats)341     private void logStatsImplLocked(@NonNull OptimizeStats stats) {
342         mLastPushTimeMillisLocked = SystemClock.elapsedRealtime();
343         ExtraStats extraStats = createExtraStatsLocked(/*packageName=*/ null,
344                 CallStats.CALL_TYPE_OPTIMIZE);
345         AppSearchStatsLog.write(AppSearchStatsLog.APP_SEARCH_OPTIMIZE_STATS_REPORTED,
346                 extraStats.mSamplingInterval,
347                 extraStats.mSkippedSampleCount,
348                 stats.getStatusCode(),
349                 stats.getTotalLatencyMillis(),
350                 stats.getNativeLatencyMillis(),
351                 stats.getDocumentStoreOptimizeLatencyMillis(),
352                 stats.getIndexRestorationLatencyMillis(),
353                 stats.getOriginalDocumentCount(),
354                 stats.getDeletedDocumentCount(),
355                 stats.getExpiredDocumentCount(),
356                 stats.getStorageSizeBeforeBytes(),
357                 stats.getStorageSizeAfterBytes(),
358                 stats.getTimeSinceLastOptimizeMillis());
359     }
360 
361     /**
362      * Calculate the hash code as an integer by returning the last four bytes of its MD5.
363      *
364      * @param str a string
365      * @return hash code as an integer. returns -1 if str is null.
366      * @throws AppSearchException if either algorithm or encoding does not exist.
367      */
368     @VisibleForTesting
369     @NonNull
calculateHashCodeMd5(@ullable String str)370     static int calculateHashCodeMd5(@Nullable String str) throws
371             NoSuchAlgorithmException, UnsupportedEncodingException {
372         if (str == null) {
373             // Just return -1 if caller doesn't have database name
374             // For some stats like globalQuery, databaseName can be null.
375             // Since in atom it is an integer, we have to return something here.
376             return -1;
377         }
378 
379         MessageDigest md = MessageDigest.getInstance("MD5");
380         md.update(str.getBytes(/*charsetName=*/ "UTF-8"));
381         byte[] digest = md.digest();
382 
383         // Since MD5 generates 16 bytes digest, we don't need to check the length here to see
384         // if it is smaller than sizeof(int)(4).
385         //
386         // We generate the same value as BigInteger(digest).intValue().
387         // BigInteger takes bytes[] and treat it as big endian. And its intValue() would get the
388         // lower 4 bytes. So here we take the last 4 bytes and treat them as big endian.
389         return (digest[12] & 0xFF) << 24
390                 | (digest[13] & 0xFF) << 16
391                 | (digest[14] & 0xFF) << 8
392                 | (digest[15] & 0xFF);
393     }
394 
395     /**
396      * Creates {@link ExtraStats} to hold additional information generated for logging.
397      *
398      * <p>This method is called by most of logStatsImplLocked functions to reduce code
399      * duplication.
400      */
401     // TODO(b/173532925) Once we add CTS test for logging atoms and can inspect the result, we can
402     // remove this @VisibleForTesting and directly use PlatformLogger.logStats to test sampling and
403     // rate limiting.
404     @VisibleForTesting
405     @GuardedBy("mLock")
406     @NonNull
createExtraStatsLocked(@ullable String packageName, @CallStats.CallType int callType)407     ExtraStats createExtraStatsLocked(@Nullable String packageName,
408             @CallStats.CallType int callType) {
409         int packageUid = Process.INVALID_UID;
410         if (packageName != null) {
411             packageUid = getPackageUidAsUserLocked(packageName);
412         }
413 
414         // The sampling ratio here might be different from the one used in
415         // shouldLogForTypeLocked if there is a config change in the middle.
416         // Since it is only one sample, we can just ignore this difference.
417         // Or we can retrieve samplingRatio at beginning and pass along
418         // as function parameter, but it will make code less cleaner with some duplication.
419         int samplingInterval = getSamplingIntervalFromConfig(callType);
420         int skippedSampleCount = mSkippedSampleCountLocked.get(callType,
421                 /*valueOfKeyIfNotFound=*/ 0);
422         mSkippedSampleCountLocked.put(callType, 0);
423 
424         return new ExtraStats(packageUid, samplingInterval, skippedSampleCount);
425     }
426 
427     /**
428      * Checks if this stats should be logged.
429      *
430      * <p>It won't be logged if it is "sampled" out, or it is too close to the previous logged
431      * stats.
432      */
433     @GuardedBy("mLock")
434     // TODO(b/173532925) Once we add CTS test for logging atoms and can inspect the result, we can
435     // remove this @VisibleForTesting and directly use PlatformLogger.logStats to test sampling and
436     // rate limiting.
437     @VisibleForTesting
shouldLogForTypeLocked(@allStats.CallType int callType)438     boolean shouldLogForTypeLocked(@CallStats.CallType int callType) {
439         int samplingInterval = getSamplingIntervalFromConfig(callType);
440         // Sampling
441         if (!shouldSample(samplingInterval)) {
442             return false;
443         }
444 
445         // Rate limiting
446         // Check the timestamp to see if it is too close to last logged sample
447         long currentTimeMillis = SystemClock.elapsedRealtime();
448         if (mLastPushTimeMillisLocked
449                 > currentTimeMillis - mConfig.getCachedMinTimeIntervalBetweenSamplesMillis()) {
450             int count = mSkippedSampleCountLocked.get(callType, /*valueOfKeyIfNotFound=*/ 0);
451             ++count;
452             mSkippedSampleCountLocked.put(callType, count);
453             return false;
454         }
455 
456         return true;
457     }
458 
459     /**
460      * Checks if the stats should be "sampled"
461      *
462      * @param samplingInterval sampling interval
463      * @return if the stats should be sampled
464      */
shouldSample(int samplingInterval)465     private boolean shouldSample(int samplingInterval) {
466         if (samplingInterval <= 0) {
467             return false;
468         }
469 
470         return mRng.nextInt((int) samplingInterval) == 0;
471     }
472 
473     /**
474      * Finds the UID of the {@code packageName}. Returns {@link Process#INVALID_UID} if unable to
475      * find the UID.
476      */
477     @GuardedBy("mLock")
getPackageUidAsUserLocked(@onNull String packageName)478     private int getPackageUidAsUserLocked(@NonNull String packageName) {
479         Integer packageUid = mPackageUidCacheLocked.get(packageName);
480         if (packageUid == null) {
481             packageUid = PackageUtil.getPackageUid(mUserContext, packageName);
482             if (packageUid != Process.INVALID_UID) {
483                 mPackageUidCacheLocked.put(packageName, packageUid);
484             }
485         }
486         return packageUid;
487     }
488 
489     /** Returns sampling ratio for stats type specified form {@link AppSearchConfig}. */
getSamplingIntervalFromConfig(@allStats.CallType int statsType)490     private int getSamplingIntervalFromConfig(@CallStats.CallType int statsType) {
491         switch (statsType) {
492             case CallStats.CALL_TYPE_PUT_DOCUMENTS:
493             case CallStats.CALL_TYPE_GET_DOCUMENTS:
494             case CallStats.CALL_TYPE_REMOVE_DOCUMENTS_BY_ID:
495             case CallStats.CALL_TYPE_REMOVE_DOCUMENTS_BY_SEARCH:
496                 return mConfig.getCachedSamplingIntervalForBatchCallStats();
497             case CallStats.CALL_TYPE_PUT_DOCUMENT:
498                 return mConfig.getCachedSamplingIntervalForPutDocumentStats();
499             case CallStats.CALL_TYPE_INITIALIZE:
500                 return mConfig.getCachedSamplingIntervalForInitializeStats();
501             case CallStats.CALL_TYPE_SEARCH:
502                 return mConfig.getCachedSamplingIntervalForSearchStats();
503             case CallStats.CALL_TYPE_GLOBAL_SEARCH:
504                 return mConfig.getCachedSamplingIntervalForGlobalSearchStats();
505             case CallStats.CALL_TYPE_OPTIMIZE:
506                 return mConfig.getCachedSamplingIntervalForOptimizeStats();
507             case CallStats.CALL_TYPE_UNKNOWN:
508             case CallStats.CALL_TYPE_SET_SCHEMA:
509             case CallStats.CALL_TYPE_GET_DOCUMENT:
510             case CallStats.CALL_TYPE_REMOVE_DOCUMENT_BY_ID:
511             case CallStats.CALL_TYPE_FLUSH:
512             case CallStats.CALL_TYPE_REMOVE_DOCUMENT_BY_SEARCH:
513                 // TODO(b/173532925) Some of them above will have dedicated sampling ratio config
514             default:
515                 return mConfig.getCachedSamplingIntervalDefault();
516         }
517     }
518 
519     //
520     // Functions below are used for tests only
521     //
522     @VisibleForTesting
523     @GuardedBy("mLock")
setLastPushTimeMillisLocked(long lastPushElapsedTimeMillis)524     void setLastPushTimeMillisLocked(long lastPushElapsedTimeMillis) {
525         mLastPushTimeMillisLocked = lastPushElapsedTimeMillis;
526     }
527 }
528