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