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