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.providers.media.metrics;
18 
19 import static com.android.providers.media.MediaProviderStatsLog.TRANSCODING_DATA;
20 
21 import android.app.StatsManager;
22 import android.util.StatsEvent;
23 
24 import com.android.internal.annotations.VisibleForTesting;
25 
26 import java.util.ArrayList;
27 import java.util.Collections;
28 import java.util.List;
29 import java.util.Random;
30 
31 /**
32  * Stores metrics for transcode sessions to be shared with statsd.
33  */
34 final class TranscodeMetrics {
35     private static final List<TranscodingStatsData> TRANSCODING_STATS_DATA = new ArrayList<>();
36 
37     // PLEASE update these if there's a change in the proto message, per the limit set in
38     // StatsEvent#MAX_PULL_PAYLOAD_SIZE
39     private static final int STATS_DATA_SAMPLE_LIMIT = 300;
40     private static final int STATS_DATA_COUNT_HARD_LIMIT = 500;  // for safety
41 
42     // Total data save requests we've received for one statsd pull cycle.
43     // This can be greater than TRANSCODING_STATS_DATA.size() since we might not add all the
44     // incoming data because of the hard limit on the size.
45     private static int sTotalStatsDataCount = 0;
46 
pullStatsEvents()47     static List<StatsEvent> pullStatsEvents() {
48         synchronized (TRANSCODING_STATS_DATA) {
49             if (TRANSCODING_STATS_DATA.size() > STATS_DATA_SAMPLE_LIMIT) {
50                 doRandomSampling();
51             }
52 
53             List<StatsEvent> result = getStatsEvents();
54             resetStatsData();
55             return result;
56         }
57     }
58 
getStatsEvents()59     private static List<StatsEvent> getStatsEvents() {
60         synchronized (TRANSCODING_STATS_DATA) {
61             List<StatsEvent> result = new ArrayList<>();
62             StatsEvent event;
63             int dataCountToFill = Math.min(TRANSCODING_STATS_DATA.size(), STATS_DATA_SAMPLE_LIMIT);
64             for (int i = 0; i < dataCountToFill; ++i) {
65                 TranscodingStatsData statsData = TRANSCODING_STATS_DATA.get(i);
66                 event = StatsEvent.newBuilder().setAtomId(TRANSCODING_DATA)
67                         .writeString(statsData.mRequestorPackage)
68                         .writeInt(statsData.mAccessType)
69                         .writeLong(statsData.mFileSizeBytes)
70                         .writeInt(statsData.mTranscodeResult)
71                         .writeLong(statsData.mTranscodeDurationMillis)
72                         .writeLong(statsData.mFileDurationMillis)
73                         .writeLong(statsData.mFrameRate)
74                         .writeInt(statsData.mAccessReason).build();
75 
76                 result.add(event);
77             }
78             return result;
79         }
80     }
81 
82     /**
83      * The random samples would get collected in the first {@code STATS_DATA_SAMPLE_LIMIT} positions
84      * inside {@code TRANSCODING_STATS_DATA}
85      */
doRandomSampling()86     private static void doRandomSampling() {
87         Random random = new Random(System.currentTimeMillis());
88 
89         synchronized (TRANSCODING_STATS_DATA) {
90             for (int i = 0; i < STATS_DATA_SAMPLE_LIMIT; ++i) {
91                 int randomIndex = random.nextInt(TRANSCODING_STATS_DATA.size() - i /* bound */)
92                         + i;
93                 Collections.swap(TRANSCODING_STATS_DATA, i, randomIndex);
94             }
95         }
96     }
97 
98     @VisibleForTesting
resetStatsData()99     static void resetStatsData() {
100         synchronized (TRANSCODING_STATS_DATA) {
101             TRANSCODING_STATS_DATA.clear();
102             sTotalStatsDataCount = 0;
103         }
104     }
105 
106     /** Saves the statsd data that'd eventually be shared in the pull callback. */
107     @VisibleForTesting
saveStatsData(TranscodingStatsData transcodingStatsData)108     static void saveStatsData(TranscodingStatsData transcodingStatsData) {
109         checkAndLimitStatsDataSizeAfterAddition(transcodingStatsData);
110     }
111 
checkAndLimitStatsDataSizeAfterAddition( TranscodingStatsData transcodingStatsData)112     private static void checkAndLimitStatsDataSizeAfterAddition(
113             TranscodingStatsData transcodingStatsData) {
114         synchronized (TRANSCODING_STATS_DATA) {
115             ++sTotalStatsDataCount;
116 
117             if (TRANSCODING_STATS_DATA.size() < STATS_DATA_COUNT_HARD_LIMIT) {
118                 TRANSCODING_STATS_DATA.add(transcodingStatsData);
119                 return;
120             }
121 
122             // Depending on how much transcoding we are doing, we might end up accumulating a lot of
123             // data by the time statsd comes back with the pull callback.
124             // We don't want to just keep growing our memory usage.
125             // So we simply randomly choose an element to remove with equal likeliness.
126             Random random = new Random(System.currentTimeMillis());
127             int replaceIndex = random.nextInt(sTotalStatsDataCount /* bound */);
128 
129             if (replaceIndex < STATS_DATA_COUNT_HARD_LIMIT) {
130                 TRANSCODING_STATS_DATA.set(replaceIndex, transcodingStatsData);
131             }
132         }
133     }
134 
135     @VisibleForTesting
getSavedStatsDataCount()136     static int getSavedStatsDataCount() {
137         return TRANSCODING_STATS_DATA.size();
138     }
139 
140     @VisibleForTesting
getTotalStatsDataCount()141     static int getTotalStatsDataCount() {
142         return sTotalStatsDataCount;
143     }
144 
145     @VisibleForTesting
getStatsDataCountHardLimit()146     static int getStatsDataCountHardLimit() {
147         return STATS_DATA_COUNT_HARD_LIMIT;
148     }
149 
150     @VisibleForTesting
getStatsDataSampleLimit()151     static int getStatsDataSampleLimit() {
152         return STATS_DATA_SAMPLE_LIMIT;
153     }
154 
155     /** This is the data to populate the proto shared with statsd. */
156     static final class TranscodingStatsData {
157         private final String mRequestorPackage;
158         private final short mAccessType;
159         private final long mFileSizeBytes;
160         private final short mTranscodeResult;
161         private final long mTranscodeDurationMillis;
162         private final long mFileDurationMillis;
163         private final long mFrameRate;
164         private final short mAccessReason;
165 
TranscodingStatsData(String requestorPackage, int accessType, long fileSizeBytes, int transcodeResult, long transcodeDurationMillis, long videoDurationMillis, long frameRate, short transcodeReason)166         TranscodingStatsData(String requestorPackage, int accessType, long fileSizeBytes,
167                 int transcodeResult, long transcodeDurationMillis,
168                 long videoDurationMillis, long frameRate, short transcodeReason) {
169             mRequestorPackage = requestorPackage;
170             mAccessType = (short) accessType;
171             mFileSizeBytes = fileSizeBytes;
172             mTranscodeResult = (short) transcodeResult;
173             mTranscodeDurationMillis = transcodeDurationMillis;
174             mFileDurationMillis = videoDurationMillis;
175             mFrameRate = frameRate;
176             mAccessReason = transcodeReason;
177         }
178     }
179 }
180