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

Commit 1ba0c757 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "move heap dump from activity to test" into main

parents 984aa88a dbd06d0d
Loading
Loading
Loading
Loading
+0 −109
Original line number Diff line number Diff line
@@ -17,120 +17,11 @@
package android.app.memory.testhelper;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.os.Debug;
import android.os.SystemClock;
import android.util.Log;

import org.apache.harmony.dalvik.ddmc.DdmVmInternal;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;

public class EmptyActivity extends Activity {
    private static final String TAG = "AppMemoryTest";

    // A local string, whose value must be visible in the heap dump.  If the value is not found,
    // the heap dump is not being created correctly.
    private String mSentinelString;

    // A large array that is used to calibrate the heap profile walker.
    private int[] mExtraBytes;

    // Configure for a heap dump

    private static void configureForHeap() {
        DdmVmInternal.setRecentAllocationsTrackingEnabled(true);
    }

    // Configure allocation tracking in the constructor.
    public EmptyActivity() {
        configureForHeap();
    }

    @Override
    public void onCreate(Bundle icicle) {
        super.onCreate(icicle);

        Intent intent = getIntent();
        if (intent == null) {
            Log.e(TAG, "no intent to process");
            throw new RuntimeException("no intent to process");
        }
        String dumpPath = intent.getStringExtra("dump-path");
        if (dumpPath == null) {
            Log.e(TAG, "no dump path available");
            throw new RuntimeException("no dump path available");
        }
        long soakDelay = intent.getLongExtra("soak-delay", -1);
        if (soakDelay < 0) {
            Log.e(TAG, "no soak delay available");
            throw new RuntimeException("no soak delay available");
        }
        int extraSize = intent.getIntExtra("extra-size", 0);
        if (extraSize > 0) {
            mExtraBytes = new int[extraSize];
        }

        // Create a sentinel string.  If "a sample string" is not found in the heap dump, something
        // is wrong.
        StringBuilder b;
        b = new StringBuilder();
        b.append("a").append(" sample").append(" string");
        mSentinelString = b.toString();

        // Delay for the soak time.  The purpose is to give the background tasks time to
        // stabilize.
        final long alarm = SystemClock.uptimeMillis() + soakDelay;
        while (SystemClock.uptimeMillis() < alarm) {
            try {
                Thread.sleep(alarm - SystemClock.uptimeMillis());
            } catch (InterruptedException e) {
                Log.e(TAG, "interrupted during wait for boot-up");
            }
        }

        final long  allocated = android.os.Debug.getGlobalAllocSize();

        try {
            Debug.dumpHprofData(dumpPath + ".hprof");
        } catch (IOException e) {
            Log.e(TAG, "failed to create hprof " + dumpPath + ": ", e);
        }

        // The remaining logic occurs after the heap dump.
        Log.i(TAG, "heap dump=" + dumpPath + " soak-delay=" + soakDelay
                + " extra-size=" + extraSize);

        File statFile = new File(dumpPath + ".stats");
        try (PrintWriter out = new PrintWriter(statFile)) {
            // Write a header that identifies the start of the file.  The current time is used by
            // report generators to sequence statistics files from multiple runs on the same
            // build.
            out.format("memory.testhelper.runtime %d\n", System.currentTimeMillis() / 1000);
            out.format("art.gc.bytes-allocated %d\n", allocated);
            out.format("extra-size.length %d\n", (mExtraBytes != null) ? mExtraBytes.length : 0);
            out.format("sentinel.string %s\n", mSentinelString);
        } catch (FileNotFoundException e) {
            Log.e(TAG, "cannot write stats file", e);
            throw new RuntimeException(e.toString());
        }

        // It is not necessary to throw for the following errors.  The main test app will detect
        // that the result files (statistics and heap dump) are not readable and will fail the test.
        if (!statFile.setReadable(true, false)) {
            Log.e(TAG, "failed to make stats file readable");
        } else if (!statFile.setWritable(true, false)) {
            Log.e(TAG, "failed to make stats file writable");
        }

        // Make the heap dump world-readable so that the main test can read it.
        File dumpFile = new File(dumpPath + ".hprof");
        if (!dumpFile.setReadable(true, false)) {
            Log.e(TAG, "failed to make dump readable");
        }
    }
}
+43 −48
Original line number Diff line number Diff line
@@ -16,8 +16,8 @@

package android.app.memory.tests;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertNotNull;

import android.app.Activity;
import android.app.Instrumentation;
@@ -36,13 +36,9 @@ import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;

@RunWith(Parameterized.class)
public class AppMemoryTest {
@@ -104,6 +100,7 @@ public class AppMemoryTest {
    @After
    public void tearDown() {
        runShellCommandWithResult("setprop debug.allocTracker.stackDepth 16");
        runShellCommandWithResult("am force-stop " + HELPER);
        uninstallHelper();
    }

@@ -140,66 +137,64 @@ public class AppMemoryTest {
        // Compute the suffix for this particular test.  The suffix is just the amount of extra
        // memory divided by 1024.
        final String suffix = String.format("-%04d", extra / 1024);

        final String path = mRootPath + "jheap" + suffix;
        final String profilePath = mRootPath + "jheap" + suffix + ".hprof";
        final File profileFile = new File(profilePath);

        // Where the dump will be found.  First verify that it is not present.
        final File profile = new File(path + ".hprof");
        assertFalse(profile.exists());
        // Ensure clean state
        profileFile.delete();

        String cmd = new String("am start-activity -S --track-allocation ")
                     + String.format("-n %s/.EmptyActivity ", HELPER)
                     + String.format("--es dump-path %s ", path)
                     + String.format("--el soak-delay %d ", delay)
                     + String.format("--ei extra-size %d", extra);
        // -S stops any existing activity before starting new, ensuring cold start
        // -W waits until activity has drawn its first frame
        String cmd = new String("am start-activity -S -W --track-allocation ")
                     + String.format("-n %s/.EmptyActivity ", HELPER);

        Log.i(TAG, "starting helper activity");
        Log.i(TAG, cmd);
        runShellCommandWithResult(cmd);

        Log.i(TAG, "waiting for helper activity");
        // Wait up to 120 seconds for the profile to be created.
        for (int i = 0; i < 120; i++) {
            if (profile.exists() && profile.canRead()) break;
            Thread.sleep(1000);
        }
        // without this sleep, pidof errors out, meaning maybe when we wait with -W
        // perhaps when the first frame is drawn the pid is still not assigned?
        // also serves as a soak delay
        Thread.sleep(15 * 1000);

        // get PID of the helper app
        String pid = runShellCommandWithResult("pidof " + HELPER).trim();
        assertNotNull("Could not get PID for package " + HELPER, pid);
        Log.i(TAG, "Got PID for " + HELPER + ": " + pid);
        assertTrue("PID is not a valid number: " + pid, pid.matches("\\d+"));
        Log.i(TAG, "Got PID for " + HELPER + ": " + pid);

        // trigger the heap dump
        cmd = String.format("am dumpheap %s %s", pid, profilePath);
        Log.i(TAG, "Executing heap dump command: " + cmd);
        runShellCommandWithResult(cmd);

        // Now verify that the dump was created and is readable.
        assertTrue("heap profile does not exist", profile.exists());
        assertTrue("heap profile is not readable", profile.canRead());

        // Read the summary file.  Let exceptions roll up and fail the test.
        HashMap<String, String> appStats = new HashMap<>();
        try (BufferedReader ifile = new BufferedReader(new FileReader(path + ".stats"))) {
            String line;
            while ((line = ifile.readLine()) != null) {
                String[] keyval = line.split(" ");
                if (keyval.length == 2) {
                    appStats.put(keyval[0], keyval[1]);
                }
            }
        }
        // make file readable
        runShellCommandWithResult("chmod 666 " + profilePath);

        // verify dump exists and readable
        assertTrue("Heap profile does not exist: " + profilePath, profileFile.exists());
        assertTrue("Heap profile is not readable: " + profilePath, profileFile.canRead());
        Log.i(TAG, "Heap dump successfully created at: " + profilePath);

        // without this sleep, am dumpheap creates a file but it is still writing to it.
        // if Profile() reads it too early, then readVersion() triggers an overflow error.
        Thread.sleep(15 * 1000);

        // Extract metrics and report them.
        Profile p = new Profile(profile);

        // Extract metrics and report them
        Profile p = new Profile(profileFile);
        final long p_allocated = p.size();
        final long d_allocated = Long.parseLong(appStats.get("art.gc.bytes-allocated"));
        // Log the sizes for debugging purposes.
        Log.i(TAG, String.format("allocated p:%d d:%d", p_allocated, d_allocated));

        try (FileWriter ofile = new FileWriter(path + ".stats", true)) {
            ofile.write("profile.heap " + p_allocated + "\n");
        }
        // Log the parsed size for debugging purposes.
        Log.i(TAG, "Profile-parsed heap size (PSize): " + p_allocated);

        // Send metrics to the automation system.
        Bundle stats = new Bundle();
        String key;
        key = "PSize" + suffix;
        String key = "PSize" + suffix;
        stats.putLong(key, p_allocated);
        stats.putString(Instrumentation.REPORT_KEY_STREAMRESULT, key + ": " + p_allocated);
        key = "DSize" + suffix;
        stats.putLong(key, d_allocated);
        stats.putString(Instrumentation.REPORT_KEY_STREAMRESULT, key + ": " + d_allocated);
        InstrumentationRegistry.getInstrumentation().sendStatus(Activity.RESULT_OK, stats);
    }