/*
* Copyright (C) 2009 The Android Open Source Project
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
* OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
* AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
* OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
#include <cstdio>
#include <cstdlib>
#include <ctime>
#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <limits.h>
#include <string.h>
#include <sys/stat.h>
#include <linux/fadvise.h>
#include <unistd.h>
#include <fts.h>
#include "stopwatch.h"
#include "sysutil.h"
#include "testcase.h"
// Stress test for the sdcard. Use this to generate some load on the
// sdcard and collect performance statistics. The output is either a
// human readable report or the raw timing samples that can be
// processed using another tool.
//
// Needs debugfs:
// adb root;
// adb shell mount -t debugfs none /sys/kernel/debug
//
// The following tests are defined (value of the --test flag):
// write: Open a file write some random data and close.
// read: Open a file read it and close.
// read_write: Combine readers and writers.
// open_create: Open|create an non existing file.
//
// For each run you can control how many processes will run the test in
// parallel to simulate a real load (--procnb flag)
//
// For each process, the test selected will be executed many time to
// get a meaningful average/min/max (--iterations flag)
//
// Use --dump to also get the raw data.
//
// For read/write tests, size is the number of Kbytes to use.
//
// To build: mmm system/extras/tests/sdcard/Android.mk SDCARD_TESTS=1
//
// Examples:
// adb shell /system/bin/sdcard_perf_test --test=read --size=1000 --chunk-size=100 --procnb=1 --iterations=10 --dump > /tmp/data.txt
// adb shell /system/bin/sdcard_perf_test --test=write --size=1000 --chunk-size=100 --procnb=1 --iterations=100 --dump > /tmp/data.txt
//
// To watch the memory: cat /proc/meminfo
// If the phone crashes, look at /proc/last_kmsg on reboot.
//
// TODO: It would be cool if we could play with various fadvise()
// strategies in here to see how tweaking read-ahead changes things.
//
extern char *optarg;
extern int optind, opterr, optopt;
// TODO: No clue where fadvise is. Disabled for now.
#define FADVISE(fd, off, len, advice) (void)0
#ifndef min
#define min(a,b) (((a)>(b))?(b):(a))
#endif
namespace {
using android::kernelVersion;
using android::kMinKernelVersionBufferSize;
using android::schedFeatures;
using android::kMinSchedFeaturesBufferSize;
using android_test::StopWatch;
using android::writePidAndWaitForReply;
using android::waitForChildrenAndSignal;
using android::waitForChildrenOrExit;
using android_test::TestCase;
const char *kAppName = "sdcard_perf_test";
const char *kTestDir = "/sdcard/perf";
const bool kVerbose = false;
// Used by getopt to parse the command line.
struct option long_options[] = {
{"size", required_argument, 0, 's'},
{"chunk-size", required_argument, 0, 'S'},
{"depth", required_argument, 0, 'D'},
{"iterations", required_argument, 0, 'i'},
{"procnb", required_argument, 0, 'p'},
{"test", required_argument, 0, 't'},
{"dump", no_argument, 0, 'd'},
{"cpu-scaling", no_argument, 0, 'c'},
{"sync", required_argument, 0, 'f'},
{"truncate", no_argument, 0, 'e'},
{"no-new-fair-sleepers", no_argument, 0, 'z'},
{"no-normalized-sleepers", no_argument, 0, 'Z'},
{"fadvise", required_argument, 0, 'a'},
{"help", no_argument, 0, 'h'},
{0, 0, 0, 0},
};
void usage()
{
printf("sdcard_perf_test --test=write|read|read_write|open_create|traverse [options]\n\n"
" -t --test: Select the test.\n"
" -s --size: Size in kbytes of the data.\n"
" -S --chunk-size: Size of a chunk. Default to size ie 1 chunk.\n"
" Data will be written/read using that chunk size.\n"
" -D --depth: Depth of directory tree to create for traversal.\n",
" -i --iterations: Number of time a process should carry its task.\n"
" -p --procnb: Number of processes to use.\n"
" -d --dump: Print the raw timing on stdout.\n"
" -c --cpu-scaling: Enable cpu scaling.\n"
" -s --sync: fsync|sync Use fsync or sync in write test. Default: no sync call.\n"
" -e --truncate: Truncate to size the file on creation.\n"
" -z --no-new-fair-sleepers: Turn them off. You need to mount debugfs.\n"
" -Z --no-normalized-sleepers: Turn them off. You need to mount debugfs.\n"
" -a --fadvise: Specify an fadvise policy (not supported).\n"
);
}
// Print command line, pid, kernel version, OOM adj and scheduler.
void printHeader(int argc, char **argv, const TestCase& testCase)
{
printf("# Command: ");
for (int i = 0; i < argc; ++i)
{
printf("%s ", argv[i]);
}
printf("\n");
printf("# Pid: %d\n", getpid());
{
char buffer[kMinKernelVersionBufferSize] = {0, };
if (kernelVersion(buffer, sizeof(buffer)) > 0)
{
printf("# Kernel: %s", buffer);
}
}
// Earlier on, running this test was crashing the phone. It turned
// out that it was using too much memory but its oom_adj value was
// -17 which means disabled. -16 is the system_server and 0 is
// typically what applications run at. The issue is that adb runs
// at -17 and so is this test. We force oom_adj to 0 unless the
// oom_adj option has been used.
// TODO: We talked about adding an option to control oom_adj, not
// sure if we still need that.
int oomAdj = android::pidOutOfMemoryAdj();
printf("# Oom_adj: %d ", oomAdj);
if (oomAdj < 0)
{
android::setPidOutOfMemoryAdj(0);
printf("adjuted to %d", android::pidOutOfMemoryAdj());
}
printf("\n");
{
char buffer[kMinSchedFeaturesBufferSize] = {0, };
if (schedFeatures(buffer, sizeof(buffer)) > 0)
{
printf("# Sched features: %s", buffer);
}
}
printf("# Fadvise: %s\n", testCase.fadviseAsStr());
}
// Remove all the files under kTestDir and clear the caches.
void cleanup() {
android::resetDirectory(kTestDir);
android::syncAndDropCaches(); // don't pollute runs.
}
// @param argc, argv have a wild guess.
// @param[out] testCase Structure built from the cmd line args.
void parseCmdLine(int argc, char **argv, TestCase *testCase)\
{
int c;
while(true)
{
// getopt_long stores the option index here.
int option_index = 0;
c = getopt_long (argc, argv,
"hS:s:D:i:p:t:dcf:ezZa:",
long_options,
&option_index);
// Detect the end of the options.
if (c == -1) break;
switch (c)
{
case 's':
testCase->setDataSize(atoi(optarg) * 1024);
break;
case 'S':
testCase->setChunkSize(atoi(optarg) * 1024);
break;
case 'D': // tree depth
testCase->setTreeDepth(atoi(optarg));
break;
case 'i':
testCase->setIter(atoi(optarg));
printf("# Iterations: %d\n", testCase->iter());
break;
case 'p':
testCase->setNproc(atoi(optarg));
printf("# Proc nb: %d\n", testCase->nproc());
break;
case 't':
testCase->setTypeFromName(optarg);
printf("# Test name %s\n", testCase->name());
break;
case 'd':
testCase->setDump();
break;
case 'c':
printf("# Cpu scaling is on\n");
testCase->setCpuScaling();
break;
case 'f':
if (strcmp("sync", optarg) == 0) {
testCase->setSync(TestCase::SYNC);
} else if (strcmp("fsync", optarg) == 0) {
testCase->setSync(TestCase::FSYNC);
}
break;
case 'e': // e for empty
printf("# Will truncate to size\n");
testCase->setTruncateToSize();
break;
case 'z': // no new fair sleepers
testCase->setNewFairSleepers(false);
break;
case 'Z': // no normalized sleepers
testCase->setNormalizedSleepers(false);
break;
case 'a': // fadvise
testCase->setFadvise(optarg);
break;
case 'h':
usage();
exit(0);
default:
fprintf(stderr, "Unknown option %s\n", optarg);
exit(EXIT_FAILURE);
}
}
}
// ----------------------------------------------------------------------
// READ TEST
// Read a file. We use a new file each time to avoid any caching
// effect that would happen if we were reading the same file each
// time.
// @param chunk buffer large enough where the chunk read are written.
// @param idx iteration number.
// @param testCase has all the timers and paramters to run the test.
bool readData(char *const chunk, const int idx, TestCase *testCase)
{
char filename[80] = {'\0',};
sprintf(filename, "%s/file-%d-%d", kTestDir, idx, getpid());
testCase->openTimer()->start();
int fd = open(filename, O_RDONLY);
testCase->openTimer()->stop();
if (fd < 0)
{
fprintf(stderr, "Open read only failed.");
return false;
}
FADVISE(fd, 0, 0, testCase->fadvise());
size_t left = testCase->dataSize();
pid_t *pid = (pid_t*)chunk;
while (left > 0)
{
char *dest = chunk;
size_t chunk_size = testCase->chunkSize();
if (chunk_size > left)
{
chunk_size = left;
left = 0;
}
else
{
left -= chunk_size;
}
testCase->readTimer()->start();
while (chunk_size > 0)
{
ssize_t s = read(fd, dest, chunk_size);
if (s < 0)
{
fprintf(stderr, "Failed to read.\n");
close(fd);
return false;
}
chunk_size -= s;
dest += s;
}
testCase->readTimer()->stop();
}
close(fd);
if (testCase->pid() != *pid)
{
fprintf(stderr, "Wrong pid found @ read block %x != %x\n", testCase->pid(), *pid);
return false;
}
else
{
return true;
}
}
bool testRead(TestCase *testCase) {
// Setup the testcase by writting some dummy files.
const size_t size = testCase->dataSize();
size_t chunk_size = testCase->chunkSize();
char *const chunk = new char[chunk_size];
memset(chunk, 0xaa, chunk_size);
*((pid_t *)chunk) = testCase->pid(); // write our pid at the beginning of each chunk
size_t iter = testCase->iter();
// since readers are much faster we increase the number of
// iteration to last longer and have concurrent read/write
// thoughout the whole test.
if (testCase->type() == TestCase::READ_WRITE)
{
iter *= TestCase::kReadWriteFactor;
}
for (size_t i = 0; i < iter; ++i)
{
char filename[80] = {'\0',};
sprintf(filename, "%s/file-%d-%d", kTestDir, i, testCase->pid());
int fd = open(filename, O_RDWR | O_CREAT, S_IRWXU);
size_t left = size;
while (left > 0)
{
if (chunk_size > left)
{
chunk_size = left;
}
ssize_t written = write(fd, chunk, chunk_size);
if (written < 0)
{
fprintf(stderr, "Write failed %d %s.", errno, strerror(errno));
return false;
}
left -= written;
}
close(fd);
}
if (kVerbose) printf("Child %d all chunk written\n", testCase->pid());
android::syncAndDropCaches();
testCase->signalParentAndWait();
// Start the read test.
testCase->testTimer()->start();
for (size_t i = 0; i < iter; ++i)
{
if (!readData(chunk, i, testCase))
{
return false;
}
}
testCase->testTimer()->stop();
delete [] chunk;
return true;
}
// ----------------------------------------------------------------------
// WRITE TEST
bool writeData(const char *const chunk, const int idx, TestCase *testCase) {
char filename[80] = {'\0',};
sprintf(filename, "%s/file-%d-%d", kTestDir, idx, getpid());
testCase->openTimer()->start();
int fd = open(filename, O_RDWR | O_CREAT, S_IRWXU); // no O_TRUNC, see header comment
testCase->openTimer()->stop();
if (fd < 0)
{
fprintf(stderr, "Open write failed.");
return false;
}
FADVISE(fd, 0, 0, testCase->fadvise());
if (testCase->truncateToSize())
{
testCase->truncateTimer()->start();
ftruncate(fd, testCase->dataSize());
testCase->truncateTimer()->stop();
}
size_t left = testCase->dataSize();
while (left > 0)
{
const char *dest = chunk;
size_t chunk_size = testCase->chunkSize();
if (chunk_size > left)
{
chunk_size = left;
left = 0;
}
else
{
left -= chunk_size;
}
testCase->writeTimer()->start();
while (chunk_size > 0)
{
ssize_t s = write(fd, dest, chunk_size);
if (s < 0)
{
fprintf(stderr, "Failed to write.\n");
close(fd);
return false;
}
chunk_size -= s;
dest += s;
}
testCase->writeTimer()->stop();
}
if (TestCase::FSYNC == testCase->sync())
{
testCase->syncTimer()->start();
fsync(fd);
testCase->syncTimer()->stop();
}
else if (TestCase::SYNC == testCase->sync())
{
testCase->syncTimer()->start();
sync();
testCase->syncTimer()->stop();
}
close(fd);
return true;
}
bool testWrite(TestCase *testCase)
{
const size_t size = testCase->dataSize();
size_t chunk_size = testCase->chunkSize();
char *data = new char[chunk_size];
memset(data, 0xaa, chunk_size);
// TODO: write the pid at the beginning like in the write
// test. Have a mode where we check the write was correct.
testCase->signalParentAndWait();
testCase->testTimer()->start();
for (size_t i = 0; i < testCase->iter(); ++i)
{
if (!writeData(data, i, testCase))
{
return false;
}
}
testCase->testTimer()->stop();
delete [] data;
return true;
}
// ----------------------------------------------------------------------
// READ WRITE
// Mix of read and write test. Even PID run the write test. Odd PID
// the read test. Not fool proof but work most of the time.
bool testReadWrite(TestCase *testCase)
{
if (getpid() & 0x1) {
return testRead(testCase);
} else {
return testWrite(testCase);
}
}
// ----------------------------------------------------------------------
// OPEN CREATE TEST
bool testOpenCreate(TestCase *testCase)
{
char filename[80] = {'\0',};
testCase->signalParentAndWait();
testCase->testTimer()->start();
for (size_t i = 0; i < testCase->iter(); ++i)
{
sprintf(filename, "%s/file-%d-%d", kTestDir, i, testCase->pid());
int fd = open(filename, O_RDWR | O_CREAT, S_IRWXU);
FADVISE(fd, 0, 0, testCase->fadvise());
if (testCase->truncateToSize())
{
ftruncate(fd, testCase->dataSize());
}
if (fd < 0)
{
return false;
}
close(fd);
}
testCase->testTimer()->stop();
return true;
}
bool writeTestFile(TestCase *testCase, const char* filename) {
int fd = open(filename, O_RDWR | O_CREAT, S_IRWXU);
if (fd < 0) {
fprintf(stderr, "open() failed: %s\n", strerror(errno));
return false;
}
bool res = false;
char * const chunk = new char[testCase->chunkSize()];
memset(chunk, 0xaa, testCase->chunkSize());
size_t left = testCase->dataSize();
while (left > 0) {
char *dest = chunk;
size_t chunk_size = testCase->chunkSize();
if (chunk_size > left) {
chunk_size = left;
left = 0;
} else {
left -= chunk_size;
}
while (chunk_size > 0) {
ssize_t s = write(fd, dest, chunk_size);
if (s < 0) {
fprintf(stderr, "write() failed: %s\n", strerror(errno));
goto fail;
}
chunk_size -= s;
dest += s;
}
}
res = true;
fail:
close(fd);
delete[] chunk;
return res;
}
// ----------------------------------------------------------------------
// TRAVERSE
#define MAX_PATH 512
// Creates a directory tree that is both deep and wide, and times
// traversal using fts_open().
bool testTraverse(TestCase *testCase) {
char path[MAX_PATH];
char filepath[MAX_PATH];
strcpy(path, kTestDir);
// Generate a deep directory hierarchy
size_t depth = testCase->treeDepth();
for (size_t i = 0; i < depth; i++) {
// Go deeper by appending onto current path
snprintf(path + strlen(path), MAX_PATH - strlen(path), "/dir%d", i);
mkdir(path, S_IRWXU);
// Create some files at this depth
strcpy(filepath, path);
int pathlen = strlen(path);
char* nameStart = filepath + pathlen;
for (size_t j = 0; j < depth; j++) {
snprintf(nameStart, MAX_PATH - pathlen, "/file%d", j);
writeTestFile(testCase, filepath);
}
}
testCase->signalParentAndWait();
testCase->testTimer()->start();
// Now traverse structure
size_t iter = testCase->iter();
for (size_t i = 0; i < iter; i++) {
testCase->traverseTimer()->start();
FTS *ftsp;
if ((ftsp = fts_open((char **) &kTestDir, FTS_LOGICAL | FTS_XDEV, NULL)) == NULL) {
fprintf(stderr, "fts_open() failed: %s\n", strerror(errno));
return false;
}
// Count discovered files
int dirs = 0, files = 0;
FTSENT *curr;
while ((curr = fts_read(ftsp)) != NULL) {
switch (curr->fts_info) {
case FTS_D:
dirs++;
break;
case FTS_F:
files++;
break;
}
}
fts_close(ftsp);
testCase->traverseTimer()->stop();
int expectedDirs = depth + 1;
if (expectedDirs != dirs) {
fprintf(stderr, "expected %d dirs, but found %d\n", expectedDirs, dirs);
return false;
}
int expectedFiles = depth * depth;
if (expectedFiles != files) {
fprintf(stderr, "expected %d files, but found %d\n", expectedFiles, files);
return false;
}
}
testCase->testTimer()->stop();
return true;
}
} // anonymous namespace
int main(int argc, char **argv)
{
android_test::TestCase testCase(kAppName);
cleanup();
parseCmdLine(argc, argv, &testCase);
printHeader(argc, argv, testCase);
printf("# File size %d kbytes\n", testCase.dataSize() / 1024);
printf("# Chunk size %d kbytes\n", testCase.chunkSize() / 1024);
printf("# Sync: %s\n", testCase.syncAsStr());
if (!testCase.cpuScaling())
{
android::disableCpuScaling();
}
switch(testCase.type()) {
case TestCase::WRITE:
testCase.mTestBody = testWrite;
break;
case TestCase::READ:
testCase.mTestBody = testRead;
break;
case TestCase::OPEN_CREATE:
testCase.mTestBody = testOpenCreate;
break;
case TestCase::READ_WRITE:
testCase.mTestBody = testReadWrite;
break;
case TestCase::TRAVERSE:
testCase.mTestBody = testTraverse;
break;
default:
fprintf(stderr, "Unknown test type %s", testCase.name());
exit(EXIT_FAILURE);
}
testCase.createTimers();
bool ok;
ok = testCase.runTest();
cleanup();
if (!ok)
{
printf("error %d %s", errno, strerror(errno));
exit(EXIT_FAILURE);
}
else
{
exit(EXIT_SUCCESS);
}
}