1 /* 2 * Copyright (C) 2019 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 package android.device.collectors; 17 18 import android.device.collectors.util.SendToInstrumentation; 19 import android.os.Bundle; 20 import android.os.SystemClock; 21 import android.util.Log; 22 import androidx.annotation.VisibleForTesting; 23 24 import com.android.helpers.ICollectorHelper; 25 26 import java.io.BufferedWriter; 27 import java.io.File; 28 import java.io.FileWriter; 29 import java.io.IOException; 30 import java.nio.file.Path; 31 import java.nio.file.Paths; 32 import java.util.HashMap; 33 import java.util.Map; 34 import java.util.UUID; 35 36 import org.junit.runner.Description; 37 import org.junit.runner.Result; 38 39 /** 40 * Extend this class for a periodic metric collection which relies on ICollectorHelper to collect 41 * metrics and dump the time-series in csv format. In case of system crashes, the time series up to 42 * the point where the crash happened will still be stored. 43 * 44 * In case of running tests with Tradefed file pulller, use the option 45 * {@link file-puller-log-collector:directory-keys} from {{@link FilePullerLogCollector} to 46 * specify the directory path under which the output file should be pulled from (i.e. 47 * <external_storage>/test_results, where <external_storage> is /sdcard for Android phones and 48 * /storage/emulated/10 for Android Auto), instead of using 49 * {@link file-puller-log-collector:pull-pattern-keys}. 50 */ 51 public class ScheduledRunCollectionListener<T extends Number> extends ScheduledRunMetricListener { 52 private static final String LOG_TAG = ScheduledRunCollectionListener.class.getSimpleName(); 53 private static final String TIME_SERIES_PREFIX = "time_series_"; 54 @VisibleForTesting public static final String OUTPUT_ROOT = "test_results"; 55 @VisibleForTesting public static final String OUTPUT_FILE_PATH = "%s_time_series_path"; 56 57 @VisibleForTesting 58 public static final String TIME_SERIES_HEADER = 59 String.format("%-20s,%-100s,%-20s", "time", "metric_key", "value"); 60 61 private static final String TIME_SERIES_BODY = "%-20d,%-100s,%-20s"; 62 @VisibleForTesting public static final String MEAN_SUFFIX = "-mean"; 63 @VisibleForTesting public static final String MAX_SUFFIX = "-max"; 64 @VisibleForTesting public static final String MIN_SUFFIX = "-min"; 65 66 protected ICollectorHelper<T> mHelper; 67 private TimeSeriesCsvWriter mTimeSeriesCsvWriter; 68 private TimeSeriesStatistics mTimeSeriesStatistics; 69 private long mStartTime; 70 ScheduledRunCollectionListener()71 public ScheduledRunCollectionListener() {} 72 73 @VisibleForTesting ScheduledRunCollectionListener(Bundle argsBundle, ICollectorHelper helper)74 ScheduledRunCollectionListener(Bundle argsBundle, ICollectorHelper helper) { 75 super(argsBundle); 76 mHelper = helper; 77 } 78 79 /** 80 * Write a time-series in csv format to the given destination under external storage as an 81 * unpivoted table like: 82 * 83 * time ,metric_key ,value 84 * 0 ,metric1 ,5 85 * 0 ,metric2 ,10 86 * 0 ,metric3 ,15 87 * 1000 ,metric1 ,6 88 * 1000 ,metric2 ,11 89 * 1000 ,metric3 ,16 90 */ 91 private class TimeSeriesCsvWriter { 92 private File mDestFile; 93 private boolean mIsHeaderWritten = false; 94 TimeSeriesCsvWriter(Path destination)95 private TimeSeriesCsvWriter(Path destination) { 96 // Create parent directory if it doesn't exist. 97 File destDir = createAndEmptyDirectory(destination.getParent().toString()); 98 mDestFile = new File(destDir, destination.getFileName().toString()); 99 } 100 write(Map<String, T> dataPoint, long timeStamp)101 private void write(Map<String, T> dataPoint, long timeStamp) { 102 try (BufferedWriter writer = new BufferedWriter(new FileWriter(mDestFile, true))) { 103 if (!mIsHeaderWritten) { 104 writer.append(TIME_SERIES_HEADER); 105 writer.append("\n"); 106 mIsHeaderWritten = true; 107 } 108 109 for (String key : dataPoint.keySet()) { 110 writer.append( 111 String.format(TIME_SERIES_BODY, timeStamp, key, dataPoint.get(key))); 112 writer.append("\n"); 113 } 114 } catch (IOException e) { 115 Log.e( 116 LOG_TAG, 117 String.format("Fail to output time series due to : %s.", e.getMessage())); 118 } 119 } 120 } 121 122 private class TimeSeriesStatistics { 123 Map<String, T> minMap = new HashMap<>(); 124 Map<String, T> maxMap = new HashMap<>(); 125 Map<String, Double> sumMap = new HashMap<>(); 126 Map<String, Long> countMap = new HashMap<>(); 127 update(Map<String, T> dataPoint)128 private void update(Map<String, T> dataPoint) { 129 for (String key : dataPoint.keySet()) { 130 T value = dataPoint.get(key); 131 // Add / replace min. 132 minMap.computeIfPresent(key, (k, v) -> compareAsDouble(value, v) == -1 ? value : v); 133 minMap.computeIfAbsent(key, k -> value); 134 // Add / replace max. 135 maxMap.computeIfPresent(key, (k, v) -> compareAsDouble(value, v) == 1 ? value : v); 136 maxMap.computeIfAbsent(key, k -> value); 137 // Add / update sum. 138 sumMap.put(key, value.doubleValue() + sumMap.getOrDefault(key, 0.)); 139 // Add / update count. 140 countMap.put(key, 1 + countMap.getOrDefault(key, 0L)); 141 } 142 } 143 getStatistics()144 private Map<String, String> getStatistics() { 145 Map<String, String> res = new HashMap<>(); 146 for (String key : minMap.keySet()) { 147 res.put(key + MIN_SUFFIX, minMap.get(key).toString()); 148 } 149 for (String key : maxMap.keySet()) { 150 res.put(key + MAX_SUFFIX, maxMap.get(key).toString()); 151 } 152 for (String key : sumMap.keySet()) { 153 if (countMap.containsKey(key)) { 154 double mean = sumMap.get(key) / countMap.get(key); 155 res.put(key + MEAN_SUFFIX, Double.toString(mean)); 156 } 157 } 158 return res; 159 } 160 161 /** Compare to Number objects. Return -1 if the n1 < n2; 0 if n1 == n2; 1 if n1 > n2. */ compareAsDouble(Number n1, Number n2)162 private int compareAsDouble(Number n1, Number n2) { 163 Double d1 = Double.valueOf(n1.doubleValue()); 164 Double d2 = Double.valueOf(n2.doubleValue()); 165 return d1.compareTo(d2); 166 } 167 } 168 169 /** {@inheritDoc} */ 170 @Override onStart(DataRecord runData, Description description)171 void onStart(DataRecord runData, Description description) { 172 setupAdditionalArgs(); 173 Path path = 174 Paths.get( 175 OUTPUT_ROOT, 176 getClass().getSimpleName(), 177 String.format( 178 "%s%s-%d.csv", 179 TIME_SERIES_PREFIX, 180 getClass().getSimpleName(), 181 UUID.randomUUID().hashCode())); 182 mTimeSeriesCsvWriter = new TimeSeriesCsvWriter(path); 183 mTimeSeriesStatistics = new TimeSeriesStatistics(); 184 mStartTime = SystemClock.uptimeMillis(); 185 mHelper.startCollecting(); 186 // Send to stdout the path where the time-series files will be stored. 187 Bundle filePathBundle = new Bundle(); 188 filePathBundle.putString( 189 String.format(OUTPUT_FILE_PATH, getClass().getSimpleName()), 190 mTimeSeriesCsvWriter.mDestFile.toString()); 191 SendToInstrumentation.sendBundle(getInstrumentation(), filePathBundle); 192 } 193 194 /** {@inheritDoc} */ 195 @Override onEnd(DataRecord runData, Result result)196 void onEnd(DataRecord runData, Result result) { 197 mHelper.stopCollecting(); 198 for (Map.Entry<String, String> entry : mTimeSeriesStatistics.getStatistics().entrySet()) { 199 runData.addStringMetric(entry.getKey(), entry.getValue()); 200 } 201 } 202 203 /** {@inheritDoc} */ 204 @Override collect(DataRecord runData, Description description)205 public void collect(DataRecord runData, Description description) throws InterruptedException { 206 long timeStamp = SystemClock.uptimeMillis() - mStartTime; 207 Map<String, T> dataPoint = mHelper.getMetrics(); 208 mTimeSeriesCsvWriter.write(dataPoint, timeStamp); 209 mTimeSeriesStatistics.update(dataPoint); 210 } 211 212 /** 213 * To add listener specific extra args implement this method in the sub class and add the 214 * listener specific args. 215 */ setupAdditionalArgs()216 public void setupAdditionalArgs() { 217 // NO-OP by default 218 } 219 createHelperInstance(ICollectorHelper helper)220 protected void createHelperInstance(ICollectorHelper helper) { 221 mHelper = helper; 222 } 223 } 224 225