1 /*
2  * Copyright (C) 2017 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 libcore.heapdumper;
18 
19 import android.app.Activity;
20 import android.app.Instrumentation;
21 import android.os.Bundle;
22 import android.os.Environment;
23 import android.util.Log;
24 
25 import java.io.File;
26 import java.io.FileOutputStream;
27 import java.io.IOException;
28 import java.io.OutputStream;
29 
30 /**
31  * An abstract base class for an {@link Instrumentation} that takes some measurement, performs some
32  * action, and then takes the measurement again, all without launching any activities.
33  *
34  * <p>The metric to be collected is defined by the concrete subclass's implementation of
35  * {@link #takeMeasurement}. The action to be performed is determined by an invocation argument.
36  *
37  * <p>The instrumentation should be invoked with two arguments:
38  * <ul>
39  *     <li>one called {@code dumpdir} which gives the name of a directory to put the dumps in,
40  *     relative to the public external storage directory;
41  *     <li>one called {@code action} which gives the name of an {@link Actions} value to run between
42  *     the two measurements.
43  * </ul>
44  *
45  * <p>If there is a problem, it will try to create a file called {@code error} in the output
46  * directory, containing a failure message.
47  */
48 public abstract class AbstractMetricInstrumentation extends Instrumentation {
49 
50     private static final String TAG = "AbstractMetricInstrumentation";
51 
52     private File mOutputDirectory;
53 
54     @Override
onCreate(Bundle icicle)55     public void onCreate(Bundle icicle) {
56         mOutputDirectory = resolveOutputDirectory(icicle);
57         try {
58             Runnable mAction = loadAction(icicle);
59             takeMeasurement("before");
60             mAction.run();
61             takeMeasurement("after");
62         } catch (Exception e) {
63             recordException(e);
64         }
65         super.onCreate(icicle);
66         finish(Activity.RESULT_OK, new Bundle());
67     }
68 
69     /**
70      * Takes a measurement, including the given label in the filename of the output.
71      */
takeMeasurement(String label)72     protected abstract void takeMeasurement(String label) throws IOException;
73 
74     /**
75      * Returns a {@link File} in the correct output directory with the given relative filename.
76      */
resolveRelativeOutputFilename(String relativeOutputFilename)77     protected final File resolveRelativeOutputFilename(String relativeOutputFilename) {
78         return new File(mOutputDirectory, relativeOutputFilename);
79     }
80 
81     /**
82      * Does its best to force as much garbage as possible to be collected.
83      */
tryRemoveGarbage()84     protected final void tryRemoveGarbage() {
85         Runtime runtime = Runtime.getRuntime();
86         // Do a GC run.
87         runtime.gc();
88         // Run finalizers for any objects pending finalization.
89         runtime.runFinalization();
90         // Do another GC run, for objects made eligible for collection by the finalization process.
91         runtime.gc();
92     }
93 
94     /**
95      * Resolves the directory to use for output, based on the arguments in the bundle.
96      */
resolveOutputDirectory(Bundle icicle)97     private static File resolveOutputDirectory(Bundle icicle) {
98         String relativeDirectoryName = icicle.getString("dumpdir");
99         if (relativeDirectoryName == null) {
100             throw new IllegalArgumentException(
101                     "Instrumentation invocation missing dumpdir argument");
102         }
103         if (!Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
104             throw new IllegalStateException("External storage unavailable");
105         }
106         File dir = Environment.getExternalStoragePublicDirectory(relativeDirectoryName);
107         if (!dir.isDirectory()) {
108             throw new IllegalArgumentException(
109                     "Instrumentation invocation's dumpdir argument is not a directory: "
110                             + dir.getAbsolutePath());
111         }
112         return dir;
113     }
114 
115     /**
116      * Returns the {@link Runnable} to run between measurements, based on the arguments in the
117      * bundle.
118      */
loadAction(Bundle icicle)119     private static Runnable loadAction(Bundle icicle) {
120         String name = icicle.getString("action");
121         if (name == null) {
122             throw new IllegalArgumentException(
123                     "Instrumentation invocation missing action argument");
124         }
125         return Actions.valueOf(name);
126     }
127 
128     /**
129      * Write an {@code error} file into {@link #mOutputDirectory} containing the message of the
130      * exception.
131      */
recordException(Exception e)132     private void recordException(Exception e) {
133         Log.e(TAG, "Exception while taking measurements", e);
134         String contents = e.getMessage();
135         File errorFile = new File(mOutputDirectory, "error");
136         try {
137             try (OutputStream errorStream = new FileOutputStream(errorFile)) {
138                 errorStream.write(contents.getBytes("UTF-8"));
139             }
140         } catch (IOException e2) {
141             throw new RuntimeException("Exception writing error file!", e2);
142         }
143     }
144 }
145