1 /*
2  * Copyright (C) 2022 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 android.content.res;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 
22 import android.app.AppProtoEnums;
23 import android.os.Handler;
24 import android.os.Looper;
25 import android.os.Message;
26 import android.os.ParcelFileDescriptor;
27 import android.os.Process;
28 import android.os.SystemClock;
29 import android.text.TextUtils;
30 import android.util.Log;
31 
32 import com.android.internal.annotations.GuardedBy;
33 import com.android.internal.util.FastPrintWriter;
34 import com.android.internal.util.FrameworkStatsLog;
35 
36 import java.io.FileOutputStream;
37 import java.io.PrintWriter;
38 import java.util.Arrays;
39 
40 /**
41  * Provides access to the resource timers without intruding on other classes.
42  * @hide
43  */
44 public final class ResourceTimer {
45     private static final String TAG = "ResourceTimer";
46 
47     // Enable metrics in this process.  The flag may be set false in some processes.  The flag is
48     // never set true at runtime, so setting it false here disables the feature entirely.
49     private static boolean sEnabled = true;
50 
51     // Set true for incremental metrics (the counters are reset after every fetch).  Set false for
52     // cumulative metrics (the counters are never reset and accumulate values for the lifetime of
53     // the process).
54     private static boolean sIncrementalMetrics = true;
55 
56     // Set true to enable some debug behavior.
57     private static boolean ENABLE_DEBUG = false;
58 
59     // The global lock.
60     private static final Object sLock = new Object();
61 
62     // The singleton cache manager
63     private static ResourceTimer sManager;
64 
65     // The handler for the timeouts.
66     private static Handler mHandler;
67 
68     // The time at which the process started.
69     private final static long sProcessStart = SystemClock.uptimeMillis();
70 
71     // Metrics are published at offsets from the process start.  Processes publish at five minutes
72     // and one hour.  Thereafter, metrics are published every 12 hours.  The values in this array
73     // are in units of minutes.
74     private static final long[] sPublicationPoints = new long[]{ 5, 60, 60 * 12 };
75 
76     // The index of the latest publication point.
77     private static int sCurrentPoint;
78 
79     /**
80      * The runtime timer configuration.
81      */
82     private static class Config {
83         // The number of timers in the runtime
84         int maxTimer;
85         // The number of histogram buckets per timer.
86         int maxBuckets;
87         // The number of "largest" values per timer.
88         int maxLargest;
89         // A string label for each timer.  This array has maxTimer elements.
90         String[] timers;
91     }
92 
93     /**
94      * The timer information that is pulled from the native runtime.  All times have units of ns.
95      */
96     private static class Timer {
97         int count;
98         long total;
99         int mintime;
100         int maxtime;
101         int[] largest;
102         int[] percentile;
103         @Override
toString()104         public String toString() {
105             return TextUtils.formatSimple("%d:%d:%d:%d", count, total, mintime, maxtime);
106         }
107     }
108 
109     /**
110      * A singleton Config.  This is initialized when the timers are started.
111      */
112     @GuardedBy("sLock")
113     private static Config sConfig;
114 
115     /**
116      * This array contains the statsd enum associated with each timer entry.  A value of NONE (0)
117      * means that the entry should not be logged to statsd.  (This would be the case for timers
118      * that are created for temporary debugging.)
119      */
120     @GuardedBy("sLock")
121     private static int[] sApiMap;
122 
123     /**
124      * A singleton Summary object that is refilled from the native side.  The length of the array
125      * is the number of timers that can be fetched.  nativeGetTimers() will fill the array to the
126      * smaller of the length of the array or the actual number of timers in the runtime.  The
127      * actual number of timers in the run time is returned by the function.
128      */
129     @GuardedBy("sLock")
130     private static Timer[] sTimers;
131 
132     /**
133      * The time at which the local timer array was last updated.  This has the same units as
134      * sProcessStart; the difference is the process run time.
135      */
136     @GuardedBy("sLock")
137     private static long sLastUpdated = 0;
138 
139     // The constructor is private.  Use the factory to get a hold of the manager.
ResourceTimer()140     private ResourceTimer() {
141         throw new RuntimeException("ResourceTimer constructor");
142     }
143 
144     /**
145      * Start the manager.  This runs a periodic job that collects and publishes timer values.
146      * This is not part of the constructor only because the looper failicities might not be
147      * available at the beginning of time.
148      */
start()149     public static void start() {
150         synchronized (sLock) {
151             if (!sEnabled) {
152                 return;
153             }
154             if (mHandler != null) {
155                 // Nothing to be done.  The job has already been started.
156                 return;
157             }
158             if (Looper.getMainLooper() == null) {
159                 throw new RuntimeException("ResourceTimer started too early");
160             }
161             mHandler = new Handler(Looper.getMainLooper()) {
162                     @Override
163                     public void handleMessage(Message msg) {
164                         ResourceTimer.handleMessage(msg);
165                     }
166                 };
167 
168             // Initialize the container that holds information from the native runtime.  The
169             // container is built according to the dimensions returned by the native layer.
170             sConfig = new Config();
171             nativeEnableTimers(sConfig);
172             sTimers = new Timer[sConfig.maxTimer];
173             for (int i = 0; i < sTimers.length; i++) {
174                 sTimers[i] = new Timer();
175                 sTimers[i].percentile = new int[sConfig.maxBuckets];
176                 sTimers[i].largest = new int[sConfig.maxLargest];
177             }
178             // Map the values returned from the runtime to statsd enumerals  The runtime may
179             // return timers that are not meant to be logged via statsd.  Such timers are mapped
180             // to RESOURCE_API_NONE.
181             sApiMap = new int[sConfig.maxTimer];
182             for (int i = 0; i < sApiMap.length; i++) {
183                 if (sConfig.timers[i].equals("GetResourceValue")) {
184                     sApiMap[i] = AppProtoEnums.RESOURCE_API_GET_VALUE;
185                 } else if (sConfig.timers[i].equals("RetrieveAttributes")) {
186                     sApiMap[i] = AppProtoEnums.RESOURCE_API_RETRIEVE_ATTRIBUTES;
187                 } else {
188                     sApiMap[i] = AppProtoEnums.RESOURCE_API_NONE;
189                 }
190             }
191 
192             sCurrentPoint = 0;
193             startTimer();
194         }
195     }
196 
197     /**
198      * Handle a refresh message.  Publish the metrics and start a timer for the next publication.
199      * The message parameter is unused.
200      */
handleMessage(Message msg)201     private static void handleMessage(Message msg) {
202         synchronized (sLock) {
203             publish();
204             startTimer();
205         }
206     }
207 
208     /**
209      * Start a timer to the next publication point.  Publication points are referenced from
210      * process start.
211      */
212     @GuardedBy("sLock")
startTimer()213     private static void startTimer() {
214         // The delay is in minutes.
215         long delay;
216         if (sCurrentPoint < sPublicationPoints.length) {
217             delay = sPublicationPoints[sCurrentPoint];
218         } else {
219             // Repeat with the final publication point.
220             final long repeated = sPublicationPoints[sPublicationPoints.length - 1];
221             final int prelude = sPublicationPoints.length - 1;
222             delay = (sCurrentPoint - prelude) * repeated;
223         }
224         // Convert minutes to milliseconds.
225         delay *= 60 * 1000;
226         // If debug is enabled, convert hours down to minutes.
227         if (ENABLE_DEBUG) {
228             delay /= 60;
229         }
230         mHandler.sendEmptyMessageAtTime(0, sProcessStart + delay);
231     }
232 
233     /**
234      * Update the local copy of the timers.  The current time is saved as well.
235      */
236     @GuardedBy("sLock")
update(boolean reset)237     private static void update(boolean reset) {
238         nativeGetTimers(sTimers, reset);
239         sLastUpdated = SystemClock.uptimeMillis();
240     }
241 
242     /**
243      * Retrieve the accumulated timer information, reset the native counters, and advance the
244      * publication point.
245      */
246     @GuardedBy("sLock")
publish()247     private static void publish() {
248         update(true);
249         // Log the number of records read.  This happens a few times a day.
250         for (int i = 0; i < sTimers.length; i++) {
251             var timer = sTimers[i];
252             if (timer.count > 0) {
253                 Log.i(TAG, TextUtils.formatSimple("%s count=%d pvalues=%s",
254                                 sConfig.timers[i], timer.count, packedString(timer.percentile)));
255                 if (sApiMap[i] != AppProtoEnums.RESOURCE_API_NONE) {
256                     FrameworkStatsLog.write(FrameworkStatsLog.RESOURCE_API_INFO,
257                             sApiMap[i],
258                             timer.count, timer.total,
259                             timer.percentile[0], timer.percentile[1],
260                             timer.percentile[2], timer.percentile[3],
261                             timer.largest[0], timer.largest[1], timer.largest[2],
262                             timer.largest[3], timer.largest[4]);
263                 }
264             }
265         }
266         sCurrentPoint++;
267     }
268 
269     /**
270      * Given an int[], return a string that is formatted as "a,b,c" with no spaces.
271      */
packedString(int[] a)272     private static String packedString(int[] a) {
273         return Arrays.toString(a).replaceAll("[\\]\\[ ]", "");
274     }
275 
276     /**
277      * Update the metrics information and dump it.
278      * @hide
279      */
dumpTimers(@onNull ParcelFileDescriptor pfd, @Nullable String[] args)280     public static void dumpTimers(@NonNull ParcelFileDescriptor pfd, @Nullable String[] args) {
281         FileOutputStream fout = new FileOutputStream(pfd.getFileDescriptor());
282         PrintWriter pw = new FastPrintWriter(fout);
283         synchronized (sLock) {
284             if (!sEnabled || (sConfig == null)) {
285                 pw.println("  Timers are not enabled in this process");
286                 pw.flush();
287                 return;
288             }
289         }
290 
291         // Look for the --refresh switch.  If the switch is present, then sTimers is updated.
292         // Otherwise, the current value of sTimers is displayed.
293         boolean refresh = Arrays.asList(args).contains("-refresh");
294 
295         synchronized (sLock) {
296             update(refresh);
297             long runtime = sLastUpdated - sProcessStart;
298             pw.format("  config runtime=%d proc=%s\n", runtime, Process.myProcessName());
299             for (int i = 0; i < sTimers.length; i++) {
300                 Timer t = sTimers[i];
301                 if (t.count != 0) {
302                     String name = sConfig.timers[i];
303                     pw.format("  stats timer=%s cnt=%d avg=%d min=%d max=%d pval=%s "
304                         + "largest=%s\n",
305                         name, t.count, t.total / t.count, t.mintime, t.maxtime,
306                         packedString(t.percentile),
307                         packedString(t.largest));
308                 }
309             }
310         }
311         pw.flush();
312     }
313 
314     // Enable (or disabled) the runtime timers.  Note that timers are disabled by default.  This
315     // retrieves the runtime timer configuration that are taking effect
nativeEnableTimers(@onNull Config config)316     private static native int nativeEnableTimers(@NonNull Config config);
317 
318     // Retrieve the timers from the native layer.  If reset is true, the timers are reset after
319     // being fetched.  The method returns the number of timers that are defined in the runtime
320     // layer.  The stats array is filled out to the smaller of its actual size and the number of
321     // runtime timers; it never overflows.
nativeGetTimers(@onNull Timer[] stats, boolean reset)322     private static native int nativeGetTimers(@NonNull Timer[] stats, boolean reset);
323 }
324