Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit d1128537 authored by Lee Shombert's avatar Lee Shombert Committed by Android (Google) Code Review
Browse files

Merge "Record resource access times"

parents 9f7005c5 3e4d9f20
Loading
Loading
Loading
Loading
+289 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.content.res;

import android.annotation.NonNull;
import android.annotation.Nullable;

import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.ParcelFileDescriptor;
import android.os.Process;
import android.os.SystemClock;
import android.text.TextUtils;
import android.util.Log;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.FastPrintWriter;

import java.io.FileOutputStream;
import java.io.PrintWriter;
import java.util.Arrays;

/**
 * Provides access to the resource timers without intruding on other classes.
 * @hide
 */
public final class ResourceTimer {
    private static final String TAG = "ResourceTimer";

    // Enable metrics in this process.  The flag may be set false in some processes.  The flag is
    // never set true at runtime, so setting it false here disables the feature entirely.
    private static boolean sEnabled = true;

    // Set true for incremental metrics (the counters are reset after every fetch).  Set false for
    // cumulative metrics (the counters are never reset and accumulate values for the lifetime of
    // the process).
    private static boolean sIncrementalMetrics = true;

    // Set true to enable some debug behavior.
    private static boolean ENABLE_DEBUG = false;

    // The global lock.
    private static final Object sLock = new Object();

    // The singleton cache manager
    private static ResourceTimer sManager;

    // The handler for the timeouts.
    private static Handler mHandler;

    // The time at which the process started.
    private final static long sProcessStart = SystemClock.uptimeMillis();

    // Metrics are published at offsets from the process start.  Processes publish at five minutes
    // and one hour.  Thereafter, metrics are published every 12 hours.  The values in this array
    // are in units of minutes.
    private static final long[] sPublicationPoints = new long[]{ 5, 60, 60 * 12 };

    // The index of the latest publication point.
    private static int sCurrentPoint;

    /**
     * The runtime timer configuration.
     */
    private static class Config {
        // The number of timers in the runtime
        int maxTimer;
        // The number of histogram buckets per timer.
        int maxBuckets;
        // The number of "largest" values per timer.
        int maxLargest;
        // A string label for each timer.  This array has maxTimer elements.
        String[] timers;
    }

    /**
     * The timer information that is pulled from the native runtime.  All times have units of ns.
     */
    private static class Timer {
        int count;
        long total;
        int mintime;
        int maxtime;
        int[] largest;
        int[] percentile;
        @Override
        public String toString() {
            return TextUtils.formatSimple("%d:%d:%d:%d", count, total, mintime, maxtime);
        }
    }

    /**
     * A singleton Config.  This is initialized when the timers are started.
     */
    @GuardedBy("sLock")
    private static Config sConfig;

    /**
     * A singleton Summary object that is refilled from the native side.  The length of the array
     * is the number of timers that can be fetched.  nativeGetTimers() will fill the array to the
     * smaller of the length of the array or the actual number of timers in the runtime.  The
     * actual number of timers in the run time is returned by the function.
     */
    @GuardedBy("sLock")
    private static Timer[] sTimers;

    /**
     * The time at which the local timer array was last updated.  This has the same units as
     * sProcessStart; the difference is the process run time.
     */
    @GuardedBy("sLock")
    private static long sLastUpdated = 0;

    // The constructor is private.  Use the factory to get a hold of the manager.
    private ResourceTimer() {
        throw new RuntimeException("ResourceTimer constructor");
    }

    /**
     * Start the manager.  This runs a periodic job that collects and publishes timer values.
     * This is not part of the constructor only because the looper failicities might not be
     * available at the beginning of time.
     */
    public static void start() {
        synchronized (sLock) {
            if (!sEnabled) {
                return;
            }
            if (mHandler != null) {
                // Nothing to be done.  The job has already been started.
                return;
            }
            if (Looper.getMainLooper() == null) {
                throw new RuntimeException("ResourceTimer started too early");
            }
            mHandler = new Handler(Looper.getMainLooper()) {
                    @Override
                    public void handleMessage(Message msg) {
                        ResourceTimer.handleMessage(msg);
                    }
                };

            // Initialize the container that holds information from the native runtime.  The
            // container is built according to the dimensions returned by the native layer.
            sConfig = new Config();
            nativeEnableTimers(sConfig);
            sTimers = new Timer[sConfig.maxTimer];
            for (int i = 0; i < sTimers.length; i++) {
                sTimers[i] = new Timer();
                sTimers[i].percentile = new int[sConfig.maxBuckets];
                sTimers[i].largest = new int[sConfig.maxLargest];
            }

            sCurrentPoint = 0;
            startTimer();
        }
    }

    /**
     * Handle a refresh message.  Publish the metrics and start a timer for the next publication.
     * The message parameter is unused.
     */
    private static void handleMessage(Message msg) {
        synchronized (sLock) {
            publish();
            startTimer();
        }
    }

    /**
     * Start a timer to the next publication point.  Publication points are referenced from
     * process start.
     */
    @GuardedBy("sLock")
    private static void startTimer() {
        // The delay is in minutes.
        long delay;
        if (sCurrentPoint < sPublicationPoints.length) {
            delay = sPublicationPoints[sCurrentPoint];
        } else {
            // Repeat with the final publication point.
            delay = sCurrentPoint * sPublicationPoints[sPublicationPoints.length - 1];
        }
        // Convert minutes to milliseconds.
        delay *= 60 * 1000;
        // If debug is enabled, convert hours down to minutes.
        if (ENABLE_DEBUG) {
            delay /= 60;
        }
        mHandler.sendEmptyMessageAtTime(0, sProcessStart + delay);
    }

    /**
     * Update the local copy of the timers.  The current time is saved as well.
     */
    @GuardedBy("sLock")
    private static void update(boolean reset) {
        nativeGetTimers(sTimers, reset);
        sLastUpdated = SystemClock.uptimeMillis();
    }

    /**
     * Retrieve the accumulated timer information, reset the native counters, and advance the
     * publication point.
     */
    @GuardedBy("sLock")
    private static void publish() {
        update(true);
        // Log the number of records read.  This happens a few times a day.
        for (int i = 0; i < sTimers.length; i++) {
            if (sTimers[i].count > 0) {
                Log.i(TAG, TextUtils.formatSimple("%s count=%d pvalues=%s",
                                sConfig.timers[i], sTimers[i].count,
                                packedString(sTimers[i].percentile)));
            }
        }
        sCurrentPoint++;
    }

    /**
     * Given an int[], return a string that is formatted as "a,b,c" with no spaces.
     */
    private static String packedString(int[] a) {
        return Arrays.toString(a).replaceAll("[\\]\\[ ]", "");
    }

    /**
     * Update the metrics information and dump it.
     * @hide
     */
    public static void dumpTimers(@NonNull ParcelFileDescriptor pfd, @Nullable String[] args) {
        FileOutputStream fout = new FileOutputStream(pfd.getFileDescriptor());
        PrintWriter pw = new FastPrintWriter(fout);
        synchronized (sLock) {
            if (!sEnabled || (sConfig == null)) {
                pw.println("  Timers are not enabled in this process");
                pw.flush();
                return;
            }
        }

        // Look for the --refresh switch.  If the switch is present, then sTimers is updated.
        // Otherwise, the current value of sTimers is displayed.
        boolean refresh = Arrays.asList(args).contains("-refresh");

        synchronized (sLock) {
            update(refresh);
            long runtime = sLastUpdated - sProcessStart;
            pw.format("  config runtime=%d proc=%s\n", runtime, Process.myProcessName());
            for (int i = 0; i < sTimers.length; i++) {
                Timer t = sTimers[i];
                if (t.count != 0) {
                    String name = sConfig.timers[i];
                    pw.format("  stats timer=%s cnt=%d avg=%d min=%d max=%d pval=%s "
                        + "largest=%s\n",
                        name, t.count, t.total / t.count, t.mintime, t.maxtime,
                        packedString(t.percentile),
                        packedString(t.largest));
                }
            }
        }
        pw.flush();
    }

    // Enable (or disabled) the runtime timers.  Note that timers are disabled by default.  This
    // retrieves the runtime timer configuration that are taking effect
    private static native int nativeEnableTimers(@NonNull Config config);

    // Retrieve the timers from the native layer.  If reset is true, the timers are reset after
    // being fetched.  The method returns the number of timers that are defined in the runtime
    // layer.  The stats array is filled out to the smaller of its actual size and the number of
    // runtime timers; it never overflows.
    private static native int nativeGetTimers(@NonNull Timer[] stats, boolean reset);
}
+1 −0
Original line number Diff line number Diff line
@@ -209,6 +209,7 @@ cc_library_shared {
                "android_content_res_ApkAssets.cpp",
                "android_content_res_ObbScanner.cpp",
                "android_content_res_Configuration.cpp",
                "android_content_res_ResourceTimer.cpp",
                "android_security_Scrypt.cpp",
                "com_android_internal_content_om_OverlayConfig.cpp",
                "com_android_internal_net_NetworkUtilsInternal.cpp",
+2 −0
Original line number Diff line number Diff line
@@ -117,6 +117,7 @@ extern int register_android_util_MemoryIntArray(JNIEnv* env);
extern int register_android_content_StringBlock(JNIEnv* env);
extern int register_android_content_XmlBlock(JNIEnv* env);
extern int register_android_content_res_ApkAssets(JNIEnv* env);
extern int register_android_content_res_ResourceTimer(JNIEnv* env);
extern int register_android_graphics_BLASTBufferQueue(JNIEnv* env);
extern int register_android_graphics_SurfaceTexture(JNIEnv* env);
extern int register_android_view_DisplayEventReceiver(JNIEnv* env);
@@ -1506,6 +1507,7 @@ static const RegJNIRec gRegJNI[] = {
        REG_JNI(register_android_content_StringBlock),
        REG_JNI(register_android_content_XmlBlock),
        REG_JNI(register_android_content_res_ApkAssets),
        REG_JNI(register_android_content_res_ResourceTimer),
        REG_JNI(register_android_text_AndroidCharacter),
        REG_JNI(register_android_text_Hyphenator),
        REG_JNI(register_android_view_InputDevice),
+129 −0
Original line number Diff line number Diff line
/*
 * Copyright 2022, The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <jni.h>
#include <core_jni_helpers.h>
#include <utils/misc.h>
#include <androidfw/ResourceTimer.h>

namespace android {

// ----------------------------------------------------------------------------

static struct {
  jfieldID maxTimer;
  jfieldID maxBuckets;
  jfieldID maxLargest;
  jfieldID timers;
} gConfigOffsets;

static struct {
  jfieldID count;
  jfieldID total;
  jfieldID mintime;
  jfieldID maxtime;
  jfieldID largest;
  jfieldID percentile;
} gTimerOffsets;

// ----------------------------------------------------------------------------


static int NativeGetTimers(JNIEnv* env, jobject /*clazz*/, jobjectArray timer, jboolean reset) {
  size_t size = ResourceTimer::counterSize;
  if (jsize st = env->GetArrayLength(timer); st < size) {
    // Shrink the size to the minimum of the available counters and the available space.
    size = st;
  }
  for (size_t i = 0; i < size; i++) {
    ResourceTimer::Timer src;
    ResourceTimer::copy(i, src, reset);
    jobject dst = env->GetObjectArrayElement(timer, i);
    env->SetIntField(dst, gTimerOffsets.count, src.count);
    if (src.count == 0) {
      continue;
    }

    src.compute();
    env->SetIntField(dst, gTimerOffsets.count, src.count);
    env->SetLongField(dst, gTimerOffsets.total, src.total);
    env->SetIntField(dst, gTimerOffsets.mintime, src.mintime);
    env->SetIntField(dst, gTimerOffsets.maxtime, src.maxtime);
    jintArray percentile =
        reinterpret_cast<jintArray>(env->GetObjectField(dst, gTimerOffsets.percentile));
    env->SetIntArrayRegion(percentile, 0, 1, &src.pvalues.p50.nominal);
    env->SetIntArrayRegion(percentile, 1, 1, &src.pvalues.p90.nominal);
    env->SetIntArrayRegion(percentile, 2, 1, &src.pvalues.p95.nominal);
    env->SetIntArrayRegion(percentile, 3, 1, &src.pvalues.p99.nominal);
    jintArray largest =
        reinterpret_cast<jintArray>(env->GetObjectField(dst, gTimerOffsets.largest));
    env->SetIntArrayRegion(largest, 0, ResourceTimer::Timer::MaxLargest, src.largest);
  }
  return size;
}

static jstring counterName(JNIEnv *env, int counter) {
  char const *s = ResourceTimer::toString(static_cast<ResourceTimer::Counter>(counter));
  return env->NewStringUTF(s);
}

static int NativeEnableTimers(JNIEnv* env, jobject /*clazz*/, jobject config) {
  ResourceTimer::enable();

  env->SetIntField(config, gConfigOffsets.maxTimer, ResourceTimer::counterSize);
  env->SetIntField(config, gConfigOffsets.maxBuckets, 4);       // Number of ints in PValues
  env->SetIntField(config, gConfigOffsets.maxLargest, ResourceTimer::Timer::MaxLargest);

  jclass str = env->FindClass("java/lang/String");
  jstring empty = counterName(env, 0);
  jobjectArray timers = env->NewObjectArray(ResourceTimer::counterSize, str, empty);
  for (int i = 0; i < ResourceTimer::counterSize; i++) {
    env->SetObjectArrayElement(timers, i, counterName(env, i));
  }
  env->SetObjectField(config, gConfigOffsets.timers, timers);
  return 0;
}

// ----------------------------------------------------------------------------

// JNI registration.
static const JNINativeMethod gResourceTimerMethods[] = {
  {"nativeEnableTimers", "(Landroid/content/res/ResourceTimer$Config;)I",
   (void *) NativeEnableTimers},
  {"nativeGetTimers", "([Landroid/content/res/ResourceTimer$Timer;Z)I",
   (void *) NativeGetTimers},
};

int register_android_content_res_ResourceTimer(JNIEnv* env) {
  jclass config = FindClassOrDie(env, "android/content/res/ResourceTimer$Config");
  gConfigOffsets.maxTimer = GetFieldIDOrDie(env, config, "maxTimer", "I");
  gConfigOffsets.maxBuckets = GetFieldIDOrDie(env, config, "maxBuckets", "I");
  gConfigOffsets.maxLargest = GetFieldIDOrDie(env, config, "maxLargest", "I");
  gConfigOffsets.timers = GetFieldIDOrDie(env, config, "timers", "[Ljava/lang/String;");

  jclass timers = FindClassOrDie(env, "android/content/res/ResourceTimer$Timer");
  gTimerOffsets.count = GetFieldIDOrDie(env, timers, "count", "I");
  gTimerOffsets.total = GetFieldIDOrDie(env, timers, "total", "J");
  gTimerOffsets.mintime = GetFieldIDOrDie(env, timers, "mintime", "I");
  gTimerOffsets.maxtime = GetFieldIDOrDie(env, timers, "maxtime", "I");
  gTimerOffsets.largest = GetFieldIDOrDie(env, timers, "largest", "[I");
  gTimerOffsets.percentile = GetFieldIDOrDie(env, timers, "percentile", "[I");

  return RegisterMethodsOrDie(env, "android/content/res/ResourceTimer", gResourceTimerMethods,
                              NELEM(gResourceTimerMethods));
}

}; // namespace android
+2 −0
Original line number Diff line number Diff line
@@ -71,6 +71,7 @@ cc_library {
        "misc.cpp",
        "ObbFile.cpp",
        "PosixUtils.cpp",
        "ResourceTimer.cpp",
        "ResourceTypes.cpp",
        "ResourceUtils.cpp",
        "StreamingZipInflater.cpp",
@@ -173,6 +174,7 @@ cc_test {
        "tests/Idmap_test.cpp",
        "tests/LoadedArsc_test.cpp",
        "tests/Locale_test.cpp",
        "tests/ResourceTimer_test.cpp",
        "tests/ResourceUtils_test.cpp",
        "tests/ResTable_test.cpp",
        "tests/Split_test.cpp",
Loading