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