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