Migrate pkvm_perf_test.py dex2oat64 and crosvm memory footprint

collection during compilation to AOSP.

Changes includes :
- Align with aosp/2189638 memory footprint collection solution
- Fork a thread to poll memory footprint before execute complilation
  command
- For each metrics, get the max value during the compilation
- Change to MetricsProcessor to calculate metrics stats that introduced
  in aosp/2200561

Bug: 236799228
Test: atest ComposBenchmarkApp
Change-Id: Ib5138dc7336f07937d2f1121a0fe995bbca5f583
This commit is contained in:
guile 2022-09-06 08:53:59 +08:00
parent 32721cd357
commit 551f7d3ad4
5 changed files with 292 additions and 95 deletions

View File

@ -12,6 +12,7 @@ android_test {
"androidx.test.runner", "androidx.test.runner",
"androidx.test.ext.junit", "androidx.test.ext.junit",
"MicrodroidDeviceTestHelper", "MicrodroidDeviceTestHelper",
"MicrodroidTestHelper",
"truth-prebuilt", "truth-prebuilt",
], ],
platform_apis: true, platform_apis: true,

View File

@ -26,8 +26,11 @@ import android.os.Bundle;
import android.os.ParcelFileDescriptor; import android.os.ParcelFileDescriptor;
import android.util.Log; import android.util.Log;
import com.android.microdroid.test.common.MetricsProcessor;
import com.android.microdroid.test.common.ProcessUtil;
import com.android.microdroid.test.device.MicrodroidDeviceTestBase; import com.android.microdroid.test.device.MicrodroidDeviceTestBase;
import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@ -40,11 +43,15 @@ import java.sql.Timestamp;
import java.text.DateFormat; import java.text.DateFormat;
import java.text.ParseException; import java.text.ParseException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@RunWith(JUnit4.class) @RunWith(JUnit4.class)
public class ComposBenchmark extends MicrodroidDeviceTestBase { public class ComposBenchmark extends MicrodroidDeviceTestBase {
private static final String TAG = "ComposBenchmark"; private static final String TAG = "ComposBenchmark";
@ -53,100 +60,106 @@ public class ComposBenchmark extends MicrodroidDeviceTestBase {
private static final double NANOS_IN_SEC = 1_000_000_000.0; private static final double NANOS_IN_SEC = 1_000_000_000.0;
private static final String METRIC_PREFIX = "avf_perf/compos/"; private static final String METRIC_PREFIX = "avf_perf/compos/";
private final MetricsProcessor mMetricsProcessor = new MetricsProcessor(METRIC_PREFIX);
private Instrumentation mInstrumentation; private Instrumentation mInstrumentation;
@Before @Before
public void setup() { public void setup() {
mInstrumentation = getInstrumentation(); mInstrumentation = getInstrumentation();
mInstrumentation.getUiAutomation().adoptShellPermissionIdentity();
} }
private void reportMetric(String name, String unit, double[] values) { @After
double sum = 0; public void tearDown() throws Exception {
double squareSum = 0; mInstrumentation.getUiAutomation().dropShellPermissionIdentity();
double min = Double.MAX_VALUE;
double max = Double.MIN_VALUE;
for (double val : values) {
sum += val;
squareSum += val * val;
min = val < min ? val : min;
max = val > max ? val : max;
}
double average = sum / values.length;
double variance = squareSum / values.length - average * average;
double stdev = Math.sqrt(variance);
Bundle bundle = new Bundle();
bundle.putDouble(METRIC_PREFIX + name + "_average_" + unit, average);
bundle.putDouble(METRIC_PREFIX + name + "_min_" + unit, min);
bundle.putDouble(METRIC_PREFIX + name + "_max_" + unit, max);
bundle.putDouble(METRIC_PREFIX + name + "_stdev_" + unit, stdev);
mInstrumentation.sendStatus(0, bundle);
}
public byte[] executeCommandBlocking(String command) {
try (
InputStream is = new ParcelFileDescriptor.AutoCloseInputStream(
getInstrumentation().getUiAutomation().executeShellCommand(command));
ByteArrayOutputStream out = new ByteArrayOutputStream()
) {
byte[] buf = new byte[BUFFER_SIZE];
int length;
while ((length = is.read(buf)) >= 0) {
out.write(buf, 0, length);
}
return out.toByteArray();
} catch (IOException e) {
Log.e(TAG, "Error executing: " + command, e);
return null;
}
}
public String executeCommand(String command)
throws InterruptedException, IOException {
getInstrumentation().getUiAutomation()
.adoptShellPermissionIdentity();
byte[] output = executeCommandBlocking(command);
getInstrumentation().getUiAutomation()
.dropShellPermissionIdentity();
if (output == null) {
throw new RuntimeException("Failed to run the command.");
} else {
String stdout = new String(output, "UTF-8");
Log.i(TAG, "Get stdout : " + stdout);
return stdout;
}
} }
@Test @Test
public void testGuestCompileTime() throws InterruptedException, IOException { public void testHostCompileTime() throws Exception {
assume().withMessage("Skip on CF; too slow").that(isCuttlefish()).isFalse(); final String command = "/apex/com.android.art/bin/odrefresh --force-compile";
final String command = "/apex/com.android.compos/bin/composd_cmd test-compile"; final List<Double> compileTimes = new ArrayList<>(ROUND_COUNT);
// The mapping is <memory metrics name> -> <all rounds value list>.
double[] compileTime = new double[ROUND_COUNT]; // EX : pss -> [10, 20, 30, ........]
final Map<String, List<Long>> processMemory = new HashMap<>();
for (int round = 0; round < ROUND_COUNT; ++round) { for (int round = 0; round < ROUND_COUNT; ++round) {
GetMetricsRunnable getMetricsRunnable =
new GetMetricsRunnable("dex2oat64", processMemory);
Thread threadGetMetrics = new Thread(getMetricsRunnable);
threadGetMetrics.start();
Timestamp beforeCompileLatestTime = getLatestDex2oatSuccessTime();
Long compileStartTime = System.nanoTime();
executeCommand(command);
Long compileEndTime = System.nanoTime();
Timestamp afterCompileLatestTime = getLatestDex2oatSuccessTime();
assertTrue(afterCompileLatestTime != null);
assertTrue(
beforeCompileLatestTime == null
|| beforeCompileLatestTime.before(afterCompileLatestTime));
double elapsedSec = (compileEndTime - compileStartTime) / NANOS_IN_SEC;
Log.i(TAG, "Compile time in host took " + elapsedSec + "s");
getMetricsRunnable.stop();
Log.i(TAG, "Waits for thread finish");
threadGetMetrics.join();
Log.i(TAG, "Thread is finish");
compileTimes.add(elapsedSec);
}
reportMetric("host_compile_time", "s", compileTimes);
reportAggregatedMetric(processMemory, "host_compile_dex2oat64_", "kB");
}
@Test
public void testGuestCompileTime() throws Exception {
assume().withMessage("Skip on CF; too slow").that(isCuttlefish()).isFalse();
final String command = "/apex/com.android.compos/bin/composd_cmd test-compile";
final List<Double> compileTimes = new ArrayList<>(ROUND_COUNT);
// The mapping is <memory metrics name> -> <all rounds value list>.
// EX : pss -> [10, 20, 30, ........]
final Map<String, List<Long>> processMemory = new HashMap<>();
for (int round = 0; round < ROUND_COUNT; ++round) {
GetMetricsRunnable getMetricsRunnable = new GetMetricsRunnable("crosvm", processMemory);
Thread threadGetMetrics = new Thread(getMetricsRunnable);
threadGetMetrics.start();
Long compileStartTime = System.nanoTime(); Long compileStartTime = System.nanoTime();
String output = executeCommand(command); String output = executeCommand(command);
Long compileEndTime = System.nanoTime(); Long compileEndTime = System.nanoTime();
Pattern pattern = Pattern.compile("All Ok"); Pattern pattern = Pattern.compile("All Ok");
Matcher matcher = pattern.matcher(output); Matcher matcher = pattern.matcher(output);
assertTrue(matcher.find()); assertTrue(matcher.find());
double elapsedSec = (compileEndTime - compileStartTime) / NANOS_IN_SEC;
Log.i(TAG, "Compile time in guest took " + elapsedSec + "s");
getMetricsRunnable.stop();
compileTime[round] = (compileEndTime - compileStartTime) / NANOS_IN_SEC; Log.i(TAG, "Waits for thread finish");
threadGetMetrics.join();
Log.i(TAG, "Thread is finish");
compileTimes.add(elapsedSec);
} }
reportMetric("guest_compile_time", "s", compileTime); reportMetric("guest_compile_time", "s", compileTimes);
reportAggregatedMetric(processMemory, "guest_compile_crosvm_", "kB");
} }
private Timestamp getLatestDex2oatSuccessTime() private Timestamp getLatestDex2oatSuccessTime()
throws InterruptedException, IOException, ParseException { throws InterruptedException, IOException, ParseException {
final String command = "logcat -d -e dex2oat"; final String command = "logcat -d -e dex2oat";
String output = executeCommand(command); String output = executeCommand(command);
String latestTime = null; String latestTime = null;
@ -170,29 +183,106 @@ public class ComposBenchmark extends MicrodroidDeviceTestBase {
return timeStampDate; return timeStampDate;
} }
@Test private void reportMetric(String name, String unit, List<? extends Number> values) {
public void testHostCompileTime() Log.d(TAG, "Report metric " + name + "(" + unit + ") : " + values.toString());
throws InterruptedException, IOException, ParseException { Map<String, Double> stats = mMetricsProcessor.computeStats(values, name, unit);
Bundle bundle = new Bundle();
final String command = "/apex/com.android.art/bin/odrefresh --force-compile"; for (Map.Entry<String, Double> entry : stats.entrySet()) {
bundle.putDouble(entry.getKey(), entry.getValue());
double[] compileTime = new double[ROUND_COUNT]; }
mInstrumentation.sendStatus(0, bundle);
for (int round = 0; round < ROUND_COUNT; ++round) {
Timestamp beforeCompileLatestTime = getLatestDex2oatSuccessTime();
Long compileStartTime = System.nanoTime();
String output = executeCommand(command);
Long compileEndTime = System.nanoTime();
Timestamp afterCompileLatestTime = getLatestDex2oatSuccessTime();
assertTrue(afterCompileLatestTime != null);
assertTrue(beforeCompileLatestTime == null
|| beforeCompileLatestTime.before(afterCompileLatestTime));
compileTime[round] = (compileEndTime - compileStartTime) / NANOS_IN_SEC;
} }
reportMetric("host_compile_time", "s", compileTime); private void reportAggregatedMetric(
Map<String, List<Long>> processMemory, String prefix, String unit) {
processMemory.forEach((k, v) -> reportMetric(prefix + k, unit, v));
} }
private byte[] executeCommandBlocking(String command) {
try (InputStream is =
new ParcelFileDescriptor.AutoCloseInputStream(
mInstrumentation.getUiAutomation().executeShellCommand(command));
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
byte[] buf = new byte[BUFFER_SIZE];
int length;
while ((length = is.read(buf)) >= 0) {
out.write(buf, 0, length);
}
return out.toByteArray();
} catch (IOException e) {
Log.e(TAG, "Error executing: " + command, e);
return null;
}
}
private String executeCommand(String command) {
try {
byte[] output = executeCommandBlocking(command);
if (output == null) {
throw new RuntimeException("Failed to run the command.");
} else {
String stdout = new String(output, "UTF-8");
Log.i(TAG, "Get stdout : " + stdout);
return stdout;
}
} catch (Exception e) {
throw new RuntimeException("Error executing: " + command + " , Exception: " + e);
}
}
private class GetMetricsRunnable implements Runnable {
private final String mProcessName;
private Map<String, List<Long>> mProcessMemory;
private AtomicBoolean mStop = new AtomicBoolean(false);
GetMetricsRunnable(String processName, Map<String, List<Long>> processMemory) {
this.mProcessName = processName;
this.mProcessMemory = processMemory;
}
void stop() {
mStop.set(true);
}
public void run() {
while (!mStop.get()) {
try {
updateProcessMemory(mProcessName, mProcessMemory);
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
} catch (Exception e) {
Log.e(TAG, "Get exception : " + e);
throw new RuntimeException(e);
}
}
}
}
private void updateProcessMemory(String processName, Map<String, List<Long>> processMemory)
throws Exception {
for (Map.Entry<Integer, String> process :
ProcessUtil.getProcessMap(this::executeCommand).entrySet()) {
int pId = process.getKey();
String pName = process.getValue();
if (pName.equalsIgnoreCase(processName)) {
for (Map.Entry<String, Long> stat :
ProcessUtil.getProcessSmapsRollup(pId, this::executeCommand).entrySet()) {
Log.i(
TAG,
"Get running process "
+ pName
+ " metrics : "
+ stat.getKey().toLowerCase()
+ '-'
+ stat.getValue());
processMemory
.computeIfAbsent(stat.getKey().toLowerCase(), k -> new ArrayList<>())
.add(stat.getValue());
}
}
}
}
} }

View File

@ -33,18 +33,21 @@ public final class MetricsProcessor {
* a {@link Map} with the corresponding keys equal to [mPrefix + name + * a {@link Map} with the corresponding keys equal to [mPrefix + name +
* _[min|max|average|stdev]_ + unit]. * _[min|max|average|stdev]_ + unit].
*/ */
public Map<String, Double> computeStats(List<Double> metrics, String name, String unit) { public Map<String, Double> computeStats(List<? extends Number> metrics, String name,
String unit) {
double sum = 0; double sum = 0;
double min = Double.MAX_VALUE; double min = Double.MAX_VALUE;
double max = Double.MIN_VALUE; double max = Double.MIN_VALUE;
for (double d : metrics) { for (Number metric : metrics) {
double d = metric.doubleValue();
sum += d; sum += d;
if (min > d) min = d; if (min > d) min = d;
if (max < d) max = d; if (max < d) max = d;
} }
double avg = sum / metrics.size(); double avg = sum / metrics.size();
double sqSum = 0; double sqSum = 0;
for (double d : metrics) { for (Number metric : metrics) {
double d = metric.doubleValue();
sqSum += (d - avg) * (d - avg); sqSum += (d - avg) * (d - avg);
} }
double stdDev = Math.sqrt(sqSum / (metrics.size() - 1)); double stdDev = Math.sqrt(sqSum / (metrics.size() - 1));

View File

@ -0,0 +1,78 @@
/*
* 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 com.android.microdroid.test.common;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
/** This class provides process utility for both device tests and host tests. */
public final class ProcessUtil {
/** Gets metrics key and values mapping of specified process id */
public static Map<String, Long> getProcessSmapsRollup(
int pid, Function<String, String> shellExecutor) throws IOException {
String path = "/proc/" + pid + "/smaps_rollup";
return parseMemoryInfo(skipFirstLine(shellExecutor.apply("cat " + path + " || true")));
}
/** Gets process id and process name mapping of the device */
public static Map<Integer, String> getProcessMap(Function<String, String> shellExecutor)
throws IOException {
Map<Integer, String> processMap = new HashMap<>();
for (String ps : skipFirstLine(shellExecutor.apply("ps -Ao PID,NAME")).split("\n")) {
// Each line is '<pid> <name>'.
// EX : 11424 dex2oat64
ps = ps.trim();
if (ps.length() == 0) {
continue;
}
int space = ps.indexOf(" ");
String pName = ps.substring(space + 1);
int pId = Integer.parseInt(ps.substring(0, space));
processMap.put(pId, pName);
}
return processMap;
}
// To ensures that only one object is created at a time.
private ProcessUtil() {}
private static Map<String, Long> parseMemoryInfo(String file) {
Map<String, Long> stats = new HashMap<>();
for (String line : file.split("[\r\n]+")) {
line = line.trim();
if (line.length() == 0) {
continue;
}
// Each line is '<metrics>: <number> kB'.
// EX : Pss_Anon: 70712 kB
if (line.endsWith(" kB")) line = line.substring(0, line.length() - 3);
String[] elems = line.split(":");
stats.put(elems[0].trim(), Long.parseLong(elems[1].trim()));
}
return stats;
}
private static String skipFirstLine(String str) {
int index = str.indexOf("\n");
return (index < 0) ? "" : str.substring(index + 1);
}
}

View File

@ -713,6 +713,11 @@ public class MicrodroidTestCase extends MicrodroidHostTestCaseBase {
} }
} }
// TODO(b/6184548): to be replaced by ProcessUtil
/**
* @deprecated use ProcessUtil instead.
*/
@Deprecated
private Map<String, Long> parseMemInfo(String file) { private Map<String, Long> parseMemInfo(String file) {
Map<String, Long> stats = new HashMap<>(); Map<String, Long> stats = new HashMap<>();
file.lines().forEach(line -> { file.lines().forEach(line -> {
@ -725,11 +730,21 @@ public class MicrodroidTestCase extends MicrodroidHostTestCaseBase {
return stats; return stats;
} }
// TODO(b/6184548): to be replaced by ProcessUtil
/**
* @deprecated use ProcessUtil instead.
*/
@Deprecated
private String skipFirstLine(String str) { private String skipFirstLine(String str) {
int index = str.indexOf("\n"); int index = str.indexOf("\n");
return (index < 0) ? "" : str.substring(index + 1); return (index < 0) ? "" : str.substring(index + 1);
} }
// TODO(b/6184548): to be replaced by ProcessUtil
/**
* @deprecated use ProcessUtil instead.
*/
@Deprecated
private List<ProcessInfo> getRunningProcessesList() { private List<ProcessInfo> getRunningProcessesList() {
List<ProcessInfo> list = new ArrayList<ProcessInfo>(); List<ProcessInfo> list = new ArrayList<ProcessInfo>();
skipFirstLine(runOnMicrodroid("ps", "-Ao", "PID,NAME")).lines().forEach(ps -> { skipFirstLine(runOnMicrodroid("ps", "-Ao", "PID,NAME")).lines().forEach(ps -> {
@ -744,10 +759,20 @@ public class MicrodroidTestCase extends MicrodroidHostTestCaseBase {
return list; return list;
} }
// TODO(b/6184548): to be replaced by ProcessUtil
/**
* @deprecated use ProcessUtil instead.
*/
@Deprecated
private Map<String, Long> getProcMemInfo() { private Map<String, Long> getProcMemInfo() {
return parseMemInfo(runOnMicrodroid("cat", "/proc/meminfo")); return parseMemInfo(runOnMicrodroid("cat", "/proc/meminfo"));
} }
// TODO(b/6184548): to be replaced by ProcessUtil
/**
* @deprecated use ProcessUtil instead.
*/
@Deprecated
private Map<String, Long> getProcSmapsRollup(int pid) { private Map<String, Long> getProcSmapsRollup(int pid) {
String path = "/proc/" + pid + "/smaps_rollup"; String path = "/proc/" + pid + "/smaps_rollup";
return parseMemInfo(skipFirstLine(runOnMicrodroid("cat", path, "||", "true"))); return parseMemInfo(skipFirstLine(runOnMicrodroid("cat", path, "||", "true")));