Add perf test to measure duration of blob store digest computation.

+ Move BlobStoreTestUtils from cts/ to frameworks/base/tests/.

Bug: 148898557
Test: atest ./apct-tests/perftests/blobstore/src/com/android/perftests/blob/BlobStorePerfTests.java
Change-Id: I2de155d0c0c1fb602c57353ba4819bdc9cda8c0a
This commit is contained in:
Sudheer Shanka
2020-02-04 16:42:17 -08:00
parent e53e1ed253
commit d4ea5e142f
8 changed files with 602 additions and 0 deletions

View File

@@ -0,0 +1,28 @@
// Copyright (C) 2020 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.
android_test {
name: "BlobStorePerfTests",
srcs: ["src/**/*.java"],
static_libs: [
"BlobStoreTestUtils",
"androidx.test.rules",
"androidx.annotation_annotation",
"apct-perftests-utils",
"ub-uiautomator",
],
platform_apis: true,
test_suites: ["device-tests"],
certificate: "platform",
}

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2020 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.perftests.blob">
<application>
<uses-library android:name="android.test.runner" />
</application>
<instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
android:targetPackage="com.android.perftests.blob"/>
</manifest>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2020 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.
-->
<configuration description="Runs BlobStorePerfTests metric instrumentation.">
<option name="test-suite-tag" value="apct" />
<option name="test-suite-tag" value="apct-metric-instrumentation" />
<target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
<option name="cleanup-apks" value="true" />
<option name="test-file-name" value="BlobStorePerfTests.apk" />
</target_preparer>
<test class="com.android.tradefed.testtype.AndroidJUnitTest" >
<option name="package" value="com.android.perftests.blob" />
<option name="hidden-api-checks" value="false"/>
</test>
</configuration>

View File

@@ -0,0 +1,120 @@
/*
* Copyright (C) 2020 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.perftests.blob;
import android.app.Instrumentation;
import android.app.UiAutomation;
import android.os.ParcelFileDescriptor;
import android.perftests.utils.TraceMarkParser;
import android.perftests.utils.TraceMarkParser.TraceMarkSlice;
import android.support.test.uiautomator.UiDevice;
import android.util.Log;
import androidx.test.platform.app.InstrumentationRegistry;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.List;
import java.util.function.BiConsumer;
// Copy of com.android.frameworks.perftests.am.util.AtraceUtils. TODO: avoid this duplication.
public class AtraceUtils {
private static final String TAG = "AtraceUtils";
private static final boolean VERBOSE = true;
private static final String ATRACE_START = "atrace --async_start -b %d -c %s";
private static final String ATRACE_DUMP = "atrace --async_dump";
private static final String ATRACE_STOP = "atrace --async_stop";
private static final int DEFAULT_ATRACE_BUF_SIZE = 1024;
private UiAutomation mAutomation;
private static AtraceUtils sUtils = null;
private boolean mStarted = false;
private AtraceUtils(Instrumentation instrumentation) {
mAutomation = instrumentation.getUiAutomation();
}
public static AtraceUtils getInstance(Instrumentation instrumentation) {
if (sUtils == null) {
sUtils = new AtraceUtils(instrumentation);
}
return sUtils;
}
/**
* @param categories The list of the categories to trace, separated with space.
*/
public void startTrace(String categories) {
synchronized (this) {
if (mStarted) {
throw new IllegalStateException("atrace already started");
}
runShellCommand(String.format(
ATRACE_START, DEFAULT_ATRACE_BUF_SIZE, categories));
mStarted = true;
}
}
public void stopTrace() {
synchronized (this) {
mStarted = false;
runShellCommand(ATRACE_STOP);
}
}
private String runShellCommand(String cmd) {
try {
return UiDevice.getInstance(
InstrumentationRegistry.getInstrumentation()).executeShellCommand(cmd);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* @param parser The function that can accept the buffer of atrace dump and parse it.
* @param handler The parse result handler
*/
public void performDump(TraceMarkParser parser,
BiConsumer<String, List<TraceMarkSlice>> handler) {
parser.reset();
try {
if (VERBOSE) {
Log.i(TAG, "Collecting atrace dump...");
}
writeDataToBuf(mAutomation.executeShellCommand(ATRACE_DUMP), parser);
} catch (IOException e) {
Log.e(TAG, "Error in reading dump", e);
}
parser.forAllSlices(handler);
}
// The given file descriptor here will be closed by this function
private void writeDataToBuf(ParcelFileDescriptor pfDescriptor,
TraceMarkParser parser) throws IOException {
InputStream inputStream = new ParcelFileDescriptor.AutoCloseInputStream(pfDescriptor);
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
String line;
while ((line = reader.readLine()) != null) {
parser.visit(line);
}
}
}
}

View File

@@ -0,0 +1,146 @@
/*
* Copyright 2020 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.perftests.blob;
import android.app.blob.BlobStoreManager;
import android.content.Context;
import android.perftests.utils.ManualBenchmarkState;
import android.perftests.utils.PerfManualStatusReporter;
import android.perftests.utils.TraceMarkParser;
import android.perftests.utils.TraceMarkParser.TraceMarkSlice;
import android.support.test.uiautomator.UiDevice;
import androidx.test.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry;
import com.android.utils.blob.DummyBlobData;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
@LargeTest
@RunWith(Parameterized.class)
public class BlobStorePerfTests {
// From frameworks/native/cmds/atrace/atrace.cpp
private static final String ATRACE_CATEGORY_SYSTEM_SERVER = "ss";
// From f/b/apex/blobstore/service/java/com/android/server/blob/BlobStoreSession.java
private static final String ATRACE_COMPUTE_DIGEST_PREFIX = "computeBlobDigest-";
private Context mContext;
private BlobStoreManager mBlobStoreManager;
private AtraceUtils mAtraceUtils;
private ManualBenchmarkState mState;
@Rule
public PerfManualStatusReporter mPerfManualStatusReporter = new PerfManualStatusReporter();
@Parameterized.Parameter(0)
public int fileSizeInMb;
@Parameterized.Parameters(name = "{0}MB")
public static Collection<Object[]> getParameters() {
return Arrays.asList(new Object[][] {
{ 25 },
{ 50 },
{ 100 },
{ 200 },
});
}
@Before
public void setUp() {
mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
mBlobStoreManager = (BlobStoreManager) mContext.getSystemService(
Context.BLOB_STORE_SERVICE);
mAtraceUtils = AtraceUtils.getInstance(InstrumentationRegistry.getInstrumentation());
mState = mPerfManualStatusReporter.getBenchmarkState();
}
@After
public void tearDown() {
// TODO: Add a blob_store shell command to trigger idle maintenance to avoid hardcoding
// job id like this.
// From BlobStoreConfig.IDLE_JOB_ID = 191934935.
runShellCommand("cmd jobscheduler run -f android 191934935");
}
@Test
public void testComputeDigest() throws Exception {
mAtraceUtils.startTrace(ATRACE_CATEGORY_SYSTEM_SERVER);
try {
final List<Long> durations = new ArrayList<>();
final DummyBlobData blobData = prepareDataBlob(fileSizeInMb);
final TraceMarkParser parser = new TraceMarkParser(
line -> line.name.startsWith(ATRACE_COMPUTE_DIGEST_PREFIX));
while (mState.keepRunning(durations)) {
commitBlob(blobData);
durations.clear();
collectDigestDurationsFromTrace(parser, durations);
// get and delete blobId
}
} finally {
mAtraceUtils.stopTrace();
}
}
private void collectDigestDurationsFromTrace(TraceMarkParser parser, List<Long> durations) {
mAtraceUtils.performDump(parser, (key, slices) -> {
for (TraceMarkSlice slice : slices) {
durations.add(TimeUnit.MICROSECONDS.toNanos(slice.getDurationInMicroseconds()));
}
});
}
private DummyBlobData prepareDataBlob(int fileSizeInMb) throws Exception {
final DummyBlobData blobData = new DummyBlobData(mContext,
fileSizeInMb * 1024 * 1024 /* bytes */);
blobData.prepare();
return blobData;
}
private void commitBlob(DummyBlobData blobData) throws Exception {
final long sessionId = mBlobStoreManager.createSession(blobData.getBlobHandle());
try (BlobStoreManager.Session session = mBlobStoreManager.openSession(sessionId)) {
blobData.writeToSession(session);
final CompletableFuture<Integer> callback = new CompletableFuture<>();
session.commit(mContext.getMainExecutor(), callback::complete);
// Ignore commit callback result.
callback.get();
}
}
private String runShellCommand(String cmd) {
try {
return UiDevice.getInstance(
InstrumentationRegistry.getInstrumentation()).executeShellCommand(cmd);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -21,6 +21,7 @@ import static android.app.blob.XmlTags.ATTR_PACKAGE;
import static android.app.blob.XmlTags.ATTR_UID;
import static android.app.blob.XmlTags.TAG_ACCESS_MODE;
import static android.app.blob.XmlTags.TAG_BLOB_HANDLE;
import static android.os.Trace.TRACE_TAG_SYSTEM_SERVER;
import static android.system.OsConstants.O_CREAT;
import static android.system.OsConstants.O_RDONLY;
import static android.system.OsConstants.O_RDWR;
@@ -41,6 +42,7 @@ import android.os.FileUtils;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.os.RevocableFileDescriptor;
import android.os.Trace;
import android.os.storage.StorageManager;
import android.system.ErrnoException;
import android.system.Os;
@@ -382,9 +384,13 @@ class BlobStoreSession extends IBlobStoreSession.Stub {
void verifyBlobData() {
byte[] actualDigest = null;
try {
Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER,
"computeBlobDigest-i" + mSessionId + "-l" + getSessionFile().length());
actualDigest = FileUtils.digest(getSessionFile(), mBlobHandle.algorithm);
} catch (IOException | NoSuchAlgorithmException e) {
Slog.e(TAG, "Error computing the digest", e);
} finally {
Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER);
}
synchronized (mSessionLock) {
if (actualDigest != null && Arrays.equals(actualDigest, mBlobHandle.digest)) {

View File

@@ -0,0 +1,20 @@
// Copyright (C) 2020 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.
java_library {
name: "BlobStoreTestUtils",
srcs: ["src/**/*.java"],
static_libs: ["truth-prebuilt"],
platform_apis: true
}

View File

@@ -0,0 +1,227 @@
/*
* Copyright 2020 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.utils.blob;
import static com.google.common.truth.Truth.assertThat;
import android.app.blob.BlobHandle;
import android.app.blob.BlobStoreManager;
import android.content.Context;
import android.os.FileUtils;
import android.os.ParcelFileDescriptor;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.nio.file.Files;
import java.security.MessageDigest;
import java.util.Random;
import java.util.concurrent.TimeUnit;
public class DummyBlobData {
private static final long DEFAULT_SIZE_BYTES = 10 * 1024L * 1024L;
private static final int BUFFER_SIZE_BYTES = 16 * 1024;
private final Context mContext;
private final Random mRandom;
private final File mFile;
private final long mFileSize;
private final String mLabel;
byte[] mFileDigest;
long mExpiryTimeMs;
public DummyBlobData(Context context) {
this(context, new Random(0), "blob_" + System.nanoTime());
}
public DummyBlobData(Context context, long fileSize) {
this(context, fileSize, new Random(0), "blob_" + System.nanoTime(), "Test label");
}
public DummyBlobData(Context context, Random random, String fileName) {
this(context, DEFAULT_SIZE_BYTES, random, fileName, "Test label");
}
public DummyBlobData(Context context, Random random, String fileName, String label) {
this(context, DEFAULT_SIZE_BYTES, random, fileName, label);
}
public DummyBlobData(Context context, long fileSize, Random random, String fileName,
String label) {
mContext = context;
mRandom = random;
mFile = new File(mContext.getFilesDir(), fileName);
mFileSize = fileSize;
mLabel = label;
}
public void prepare() throws Exception {
try (RandomAccessFile file = new RandomAccessFile(mFile, "rw")) {
writeRandomData(file, mFileSize);
}
mFileDigest = FileUtils.digest(mFile, "SHA-256");
mExpiryTimeMs = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1);
}
public BlobHandle getBlobHandle() throws Exception {
return BlobHandle.createWithSha256(createSha256Digest(mFile), mLabel,
mExpiryTimeMs, "test_tag");
}
public long getFileSize() throws Exception {
return mFileSize;
}
public long getExpiryTimeMillis() {
return mExpiryTimeMs;
}
public void delete() {
mFile.delete();
}
public void writeToSession(BlobStoreManager.Session session) throws Exception {
writeToSession(session, 0, mFileSize);
}
public void writeToSession(BlobStoreManager.Session session,
long offsetBytes, long lengthBytes) throws Exception {
try (FileInputStream in = new FileInputStream(mFile)) {
in.getChannel().position(offsetBytes);
try (FileOutputStream out = new ParcelFileDescriptor.AutoCloseOutputStream(
session.openWrite(offsetBytes, lengthBytes))) {
copy(in, out, lengthBytes);
}
}
}
public void writeToFd(FileDescriptor fd, long offsetBytes, long lengthBytes) throws Exception {
try (FileInputStream in = new FileInputStream(mFile)) {
in.getChannel().position(offsetBytes);
try (FileOutputStream out = new FileOutputStream(fd)) {
copy(in, out, lengthBytes);
}
}
}
private void copy(InputStream in, OutputStream out, long lengthBytes) throws Exception {
final byte[] buffer = new byte[BUFFER_SIZE_BYTES];
long bytesWrittern = 0;
while (bytesWrittern < lengthBytes) {
final int toWrite = (bytesWrittern + buffer.length <= lengthBytes)
? buffer.length : (int) (lengthBytes - bytesWrittern);
in.read(buffer, 0, toWrite);
out.write(buffer, 0, toWrite);
bytesWrittern += toWrite;
}
}
public void readFromSessionAndVerifyBytes(BlobStoreManager.Session session,
long offsetBytes, int lengthBytes) throws Exception {
final byte[] expectedBytes = new byte[lengthBytes];
try (FileInputStream in = new FileInputStream(mFile)) {
read(in, expectedBytes, offsetBytes, lengthBytes);
}
final byte[] actualBytes = new byte[lengthBytes];
try (FileInputStream in = new ParcelFileDescriptor.AutoCloseInputStream(
session.openWrite(0L, 0L))) {
read(in, actualBytes, offsetBytes, lengthBytes);
}
assertThat(actualBytes).isEqualTo(expectedBytes);
}
private void read(FileInputStream in, byte[] buffer,
long offsetBytes, int lengthBytes) throws Exception {
in.getChannel().position(offsetBytes);
in.read(buffer, 0, lengthBytes);
}
public void readFromSessionAndVerifyDigest(BlobStoreManager.Session session)
throws Exception {
readFromSessionAndVerifyDigest(session, 0, mFile.length());
}
public void readFromSessionAndVerifyDigest(BlobStoreManager.Session session,
long offsetBytes, long lengthBytes) throws Exception {
final byte[] actualDigest;
try (FileInputStream in = new ParcelFileDescriptor.AutoCloseInputStream(
session.openWrite(0L, 0L))) {
actualDigest = createSha256Digest(in, offsetBytes, lengthBytes);
}
assertThat(actualDigest).isEqualTo(mFileDigest);
}
public void verifyBlob(ParcelFileDescriptor pfd) throws Exception {
final byte[] actualDigest;
try (FileInputStream in = new ParcelFileDescriptor.AutoCloseInputStream(pfd)) {
actualDigest = FileUtils.digest(in, "SHA-256");
}
assertThat(actualDigest).isEqualTo(mFileDigest);
}
private byte[] createSha256Digest(FileInputStream in, long offsetBytes, long lengthBytes)
throws Exception {
final MessageDigest digest = MessageDigest.getInstance("SHA-256");
in.getChannel().position(offsetBytes);
final byte[] buffer = new byte[BUFFER_SIZE_BYTES];
long bytesRead = 0;
while (bytesRead < lengthBytes) {
int toRead = (bytesRead + buffer.length <= lengthBytes)
? buffer.length : (int) (lengthBytes - bytesRead);
toRead = in.read(buffer, 0, toRead);
digest.update(buffer, 0, toRead);
bytesRead += toRead;
}
return digest.digest();
}
private byte[] createSha256Digest(File file) throws Exception {
final MessageDigest digest = MessageDigest.getInstance("SHA-256");
try (BufferedInputStream in = new BufferedInputStream(
Files.newInputStream(file.toPath()))) {
final byte[] buffer = new byte[BUFFER_SIZE_BYTES];
int bytesRead;
while ((bytesRead = in.read(buffer)) > 0) {
digest.update(buffer, 0, bytesRead);
}
}
return digest.digest();
}
private void writeRandomData(RandomAccessFile file, long fileSize)
throws Exception {
long bytesWritten = 0;
final byte[] buffer = new byte[BUFFER_SIZE_BYTES];
while (bytesWritten < fileSize) {
mRandom.nextBytes(buffer);
final int toWrite = (bytesWritten + buffer.length <= fileSize)
? buffer.length : (int) (fileSize - bytesWritten);
file.seek(bytesWritten);
file.write(buffer, 0, toWrite);
bytesWritten += toWrite;
}
}
}