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