// Copyright (c) 2013-2014 Sandstorm Development Group, Inc. and contributors // Licensed under the MIT License: // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. #include <sys/types.h> #include <sys/time.h> #include <sys/resource.h> #include <sys/wait.h> #include <sys/stat.h> #include <inttypes.h> #include <string> #include <stdio.h> #include <unistd.h> #include <string.h> #include <iostream> #include <iomanip> using namespace std; namespace capnp { namespace benchmark { namespace runner { struct Times { uint64_t real; uint64_t user; uint64_t sys; uint64_t cpu() { return user + sys; } Times operator-(const Times& other) { Times result; result.real = real - other.real; result.user = user - other.user; result.sys = sys - other.sys; return result; } }; uint64_t asNanosecs(const struct timeval& tv) { return (uint64_t)tv.tv_sec * 1000000000 + (uint64_t)tv.tv_usec * 1000; } Times currentTimes() { Times result; struct rusage self, children; getrusage(RUSAGE_SELF, &self); getrusage(RUSAGE_CHILDREN, &children); struct timeval real; gettimeofday(&real, nullptr); result.real = asNanosecs(real); result.user = asNanosecs(self.ru_utime) + asNanosecs(children.ru_utime); result.sys = asNanosecs(self.ru_stime) + asNanosecs(children.ru_stime); return result; } struct TestResult { uint64_t objectSize; uint64_t messageSize; Times time; }; enum class Product { CAPNPROTO, PROTOBUF, NULLCASE }; enum class TestCase { EVAL, CATRANK, CARSALES }; const char* testCaseName(TestCase testCase) { switch (testCase) { case TestCase::EVAL: return "eval"; case TestCase::CATRANK: return "catrank"; case TestCase::CARSALES: return "carsales"; } // Can't get here. return nullptr; } enum class Mode { OBJECTS, OBJECT_SIZE, BYTES, PIPE_SYNC, PIPE_ASYNC }; enum class Reuse { YES, NO }; enum class Compression { NONE, PACKED, SNAPPY }; TestResult runTest(Product product, TestCase testCase, Mode mode, Reuse reuse, Compression compression, uint64_t iters) { char* argv[6]; string progName; switch (product) { case Product::CAPNPROTO: progName = "capnproto-"; break; case Product::PROTOBUF: progName = "protobuf-"; break; case Product::NULLCASE: progName = "null-"; break; } progName += testCaseName(testCase); argv[0] = strdup(progName.c_str()); switch (mode) { case Mode::OBJECTS: argv[1] = strdup("object"); break; case Mode::OBJECT_SIZE: argv[1] = strdup("object-size"); break; case Mode::BYTES: argv[1] = strdup("bytes"); break; case Mode::PIPE_SYNC: argv[1] = strdup("pipe"); break; case Mode::PIPE_ASYNC: argv[1] = strdup("pipe-async"); break; } switch (reuse) { case Reuse::YES: argv[2] = strdup("reuse"); break; case Reuse::NO: argv[2] = strdup("no-reuse"); break; } switch (compression) { case Compression::NONE: argv[3] = strdup("none"); break; case Compression::PACKED: argv[3] = strdup("packed"); break; case Compression::SNAPPY: argv[3] = strdup("snappy"); break; } char itersStr[64]; sprintf(itersStr, "%llu", (long long unsigned int)iters); argv[4] = itersStr; argv[5] = nullptr; // Make pipe for child to write throughput. int childPipe[2]; if (pipe(childPipe) < 0) { perror("pipe"); exit(1); } // Spawn the child process. struct timeval start, end; gettimeofday(&start, nullptr); pid_t child = fork(); if (child == 0) { close(childPipe[0]); dup2(childPipe[1], STDOUT_FILENO); close(childPipe[1]); execv(argv[0], argv); exit(1); } close(childPipe[1]); for (int i = 0; i < 4; i++) { free(argv[i]); } // Read throughput number written to child's stdout. FILE* input = fdopen(childPipe[0], "r"); long long unsigned int throughput; if (fscanf(input, "%lld", &throughput) != 1) { fprintf(stderr, "Child didn't write throughput to stdout."); } char buffer[1024]; while (fgets(buffer, sizeof(buffer), input) != nullptr) { // Loop until EOF. } fclose(input); // Wait for child exit. int status; struct rusage usage; wait4(child, &status, 0, &usage); gettimeofday(&end, nullptr); // Calculate results. TestResult result; result.objectSize = mode == Mode::OBJECT_SIZE ? throughput : 0; result.messageSize = mode == Mode::OBJECT_SIZE ? 0 : throughput; result.time.real = asNanosecs(end) - asNanosecs(start); result.time.user = asNanosecs(usage.ru_utime); result.time.sys = asNanosecs(usage.ru_stime); return result; } void reportTableHeader() { cout << setw(40) << left << "Test" << setw(10) << right << "obj size" << setw(10) << right << "I/O bytes" << setw(10) << right << "wall ns" << setw(10) << right << "user ns" << setw(10) << right << "sys ns" << endl; cout << setfill('=') << setw(90) << "" << setfill(' ') << endl; } void reportResults(const char* name, uint64_t iters, TestResult results) { cout << setw(40) << left << name << setw(10) << right << (results.objectSize / iters) << setw(10) << right << (results.messageSize / iters) << setw(10) << right << (results.time.real / iters) << setw(10) << right << (results.time.user / iters) << setw(10) << right << (results.time.sys / iters) << endl; } void reportComparisonHeader() { cout << setw(40) << left << "Measure" << setw(15) << right << "Protobuf" << setw(15) << right << "Cap'n Proto" << setw(15) << right << "Improvement" << endl; cout << setfill('=') << setw(85) << "" << setfill(' ') << endl; } void reportOldNewComparisonHeader() { cout << setw(40) << left << "Measure" << setw(15) << right << "Old" << setw(15) << right << "New" << setw(15) << right << "Improvement" << endl; cout << setfill('=') << setw(85) << "" << setfill(' ') << endl; } class Gain { public: Gain(double oldValue, double newValue) : amount(newValue / oldValue) {} void writeTo(std::ostream& os) { if (amount < 2) { double percent = (amount - 1) * 100; os << (int)(percent + 0.5) << "%"; } else { os << fixed << setprecision(2) << amount << "x"; } } private: double amount; }; ostream& operator<<(ostream& os, Gain gain) { gain.writeTo(os); return os; } void reportComparison(const char* name, double base, double protobuf, double capnproto, uint64_t iters) { cout << setw(40) << left << name << setw(14) << right << Gain(base, protobuf) << setw(14) << right << Gain(base, capnproto); // Since smaller is better, the "improvement" is the "gain" from capnproto to protobuf. cout << setw(14) << right << Gain(capnproto - base, protobuf - base) << endl; } void reportComparison(const char* name, const char* unit, double protobuf, double capnproto, uint64_t iters) { cout << setw(40) << left << name << setw(15-strlen(unit)) << fixed << right << setprecision(2) << (protobuf / iters) << unit << setw(15-strlen(unit)) << fixed << right << setprecision(2) << (capnproto / iters) << unit; // Since smaller is better, the "improvement" is the "gain" from capnproto to protobuf. cout << setw(14) << right << Gain(capnproto, protobuf) << endl; } void reportIntComparison(const char* name, const char* unit, uint64_t protobuf, uint64_t capnproto, uint64_t iters) { cout << setw(40) << left << name << setw(15-strlen(unit)) << right << (protobuf / iters) << unit << setw(15-strlen(unit)) << right << (capnproto / iters) << unit; // Since smaller is better, the "improvement" is the "gain" from capnproto to protobuf. cout << setw(14) << right << Gain(capnproto, protobuf) << endl; } size_t fileSize(const std::string& name) { struct stat stats; if (stat(name.c_str(), &stats) < 0) { perror(name.c_str()); exit(1); } return stats.st_size; } int main(int argc, char* argv[]) { char* path = argv[0]; char* slashpos = strrchr(path, '/'); char origDir[1024]; if (getcwd(origDir, sizeof(origDir)) == nullptr) { perror("getcwd"); return 1; } if (slashpos != nullptr) { *slashpos = '\0'; if (chdir(path) < 0) { perror("chdir"); return 1; } *slashpos = '/'; } TestCase testCase = TestCase::CATRANK; Mode mode = Mode::PIPE_SYNC; Compression compression = Compression::NONE; uint64_t iters = 1; const char* oldDir = nullptr; for (int i = 1; i < argc; i++) { string arg = argv[i]; if (isdigit(argv[i][0])) { iters = strtoul(argv[i], nullptr, 0); } else if (arg == "async") { mode = Mode::PIPE_ASYNC; } else if (arg == "inmem") { mode = Mode::BYTES; } else if (arg == "eval") { testCase = TestCase::EVAL; } else if (arg == "carsales") { testCase = TestCase::CARSALES; } else if (arg == "snappy") { compression = Compression::SNAPPY; } else if (arg == "-c") { ++i; if (i == argc) { fprintf(stderr, "-c requires argument.\n"); return 1; } oldDir = argv[i]; } else { fprintf(stderr, "Unknown option: %s\n", argv[i]); return 1; } } // Scale iterations to something reasonable for each case. switch (testCase) { case TestCase::EVAL: iters *= 100000; break; case TestCase::CATRANK: iters *= 1000; break; case TestCase::CARSALES: iters *= 20000; break; } cout << "Running " << iters << " iterations of "; switch (testCase) { case TestCase::EVAL: cout << "calculator"; break; case TestCase::CATRANK: cout << "CatRank"; break; case TestCase::CARSALES: cout << "car sales"; break; } cout << " example case with:" << endl; switch (mode) { case Mode::OBJECTS: case Mode::OBJECT_SIZE: // Can't happen. break; case Mode::BYTES: cout << "* in-memory I/O" << endl; cout << " * with client and server in the same thread" << endl; break; case Mode::PIPE_SYNC: cout << "* pipe I/O" << endl; cout << " * with client and server in separate processes" << endl; cout << " * client waits for each response before sending next request" << endl; break; case Mode::PIPE_ASYNC: cout << "* pipe I/O" << endl; cout << " * with client and server in separate processes" << endl; cout << " * client sends as many simultaneous requests as it can" << endl; break; } switch (compression) { case Compression::NONE: cout << "* no compression" << endl; break; case Compression::PACKED: cout << "* de-zero packing for Cap'n Proto" << endl; cout << "* standard packing for Protobuf" << endl; break; case Compression::SNAPPY: cout << "* Snappy compression" << endl; break; } cout << endl; reportTableHeader(); TestResult nullCase = runTest( Product::NULLCASE, testCase, Mode::OBJECT_SIZE, Reuse::YES, compression, iters); reportResults("Theoretical best pass-by-object", iters, nullCase); TestResult protobufBase = runTest( Product::PROTOBUF, testCase, Mode::OBJECTS, Reuse::YES, compression, iters); protobufBase.objectSize = runTest( Product::PROTOBUF, testCase, Mode::OBJECT_SIZE, Reuse::YES, compression, iters).objectSize; reportResults("Protobuf pass-by-object", iters, protobufBase); TestResult capnpBase = runTest( Product::CAPNPROTO, testCase, Mode::OBJECTS, Reuse::YES, compression, iters); capnpBase.objectSize = runTest( Product::CAPNPROTO, testCase, Mode::OBJECT_SIZE, Reuse::YES, compression, iters).objectSize; reportResults("Cap'n Proto pass-by-object", iters, capnpBase); TestResult nullCaseNoReuse = runTest( Product::NULLCASE, testCase, Mode::OBJECT_SIZE, Reuse::NO, compression, iters); reportResults("Theoretical best w/o object reuse", iters, nullCaseNoReuse); TestResult protobufNoReuse = runTest( Product::PROTOBUF, testCase, Mode::OBJECTS, Reuse::NO, compression, iters); protobufNoReuse.objectSize = runTest( Product::PROTOBUF, testCase, Mode::OBJECT_SIZE, Reuse::NO, compression, iters).objectSize; reportResults("Protobuf w/o object reuse", iters, protobufNoReuse); TestResult capnpNoReuse = runTest( Product::CAPNPROTO, testCase, Mode::OBJECTS, Reuse::NO, compression, iters); capnpNoReuse.objectSize = runTest( Product::CAPNPROTO, testCase, Mode::OBJECT_SIZE, Reuse::NO, compression, iters).objectSize; reportResults("Cap'n Proto w/o object reuse", iters, capnpNoReuse); TestResult protobuf = runTest( Product::PROTOBUF, testCase, mode, Reuse::YES, compression, iters); protobuf.objectSize = protobufBase.objectSize; reportResults("Protobuf I/O", iters, protobuf); TestResult capnp = runTest( Product::CAPNPROTO, testCase, mode, Reuse::YES, compression, iters); capnp.objectSize = capnpBase.objectSize; reportResults("Cap'n Proto I/O", iters, capnp); TestResult capnpPacked = runTest( Product::CAPNPROTO, testCase, mode, Reuse::YES, Compression::PACKED, iters); capnpPacked.objectSize = capnpBase.objectSize; reportResults("Cap'n Proto packed I/O", iters, capnpPacked); size_t protobufBinarySize = fileSize("protobuf-" + std::string(testCaseName(testCase))); size_t capnpBinarySize = fileSize("capnproto-" + std::string(testCaseName(testCase))); size_t protobufCodeSize = fileSize(std::string(testCaseName(testCase)) + ".pb.cc") + fileSize(std::string(testCaseName(testCase)) + ".pb.h"); size_t capnpCodeSize = fileSize(std::string(testCaseName(testCase)) + ".capnp.c++") + fileSize(std::string(testCaseName(testCase)) + ".capnp.h"); size_t protobufObjSize = fileSize(std::string(testCaseName(testCase)) + ".pb.o"); size_t capnpObjSize = fileSize(std::string(testCaseName(testCase)) + ".capnp.o"); TestResult oldNullCase; TestResult oldNullCaseNoReuse; TestResult oldCapnpBase; TestResult oldCapnpNoReuse; TestResult oldCapnp; TestResult oldCapnpPacked; size_t oldCapnpBinarySize = 0; size_t oldCapnpCodeSize = 0; size_t oldCapnpObjSize = 0; if (oldDir != nullptr) { if (chdir(origDir) < 0) { perror("chdir"); return 1; } if (chdir(oldDir) < 0) { perror(oldDir); return 1; } oldNullCase = runTest( Product::NULLCASE, testCase, Mode::OBJECT_SIZE, Reuse::YES, compression, iters); reportResults("Old theoretical best pass-by-object", iters, nullCase); oldCapnpBase = runTest( Product::CAPNPROTO, testCase, Mode::OBJECTS, Reuse::YES, compression, iters); oldCapnpBase.objectSize = runTest( Product::CAPNPROTO, testCase, Mode::OBJECT_SIZE, Reuse::YES, compression, iters) .objectSize; reportResults("Old Cap'n Proto pass-by-object", iters, oldCapnpBase); oldNullCaseNoReuse = runTest( Product::NULLCASE, testCase, Mode::OBJECT_SIZE, Reuse::NO, compression, iters); reportResults("Old theoretical best w/o object reuse", iters, oldNullCaseNoReuse); oldCapnpNoReuse = runTest( Product::CAPNPROTO, testCase, Mode::OBJECTS, Reuse::NO, compression, iters); oldCapnpNoReuse.objectSize = runTest( Product::CAPNPROTO, testCase, Mode::OBJECT_SIZE, Reuse::NO, compression, iters).objectSize; reportResults("Old Cap'n Proto w/o object reuse", iters, oldCapnpNoReuse); oldCapnp = runTest( Product::CAPNPROTO, testCase, mode, Reuse::YES, compression, iters); oldCapnp.objectSize = oldCapnpBase.objectSize; reportResults("Old Cap'n Proto I/O", iters, oldCapnp); oldCapnpPacked = runTest( Product::CAPNPROTO, testCase, mode, Reuse::YES, Compression::PACKED, iters); oldCapnpPacked.objectSize = oldCapnpBase.objectSize; reportResults("Old Cap'n Proto packed I/O", iters, oldCapnpPacked); oldCapnpBinarySize = fileSize("capnproto-" + std::string(testCaseName(testCase))); oldCapnpCodeSize = fileSize(std::string(testCaseName(testCase)) + ".capnp.c++") + fileSize(std::string(testCaseName(testCase)) + ".capnp.h"); oldCapnpObjSize = fileSize(std::string(testCaseName(testCase)) + ".capnp.o"); } cout << endl; reportComparisonHeader(); reportComparison("memory overhead (vs ideal)", nullCase.objectSize, protobufBase.objectSize, capnpBase.objectSize, iters); reportComparison("memory overhead w/o object reuse", nullCaseNoReuse.objectSize, protobufNoReuse.objectSize, capnpNoReuse.objectSize, iters); reportComparison("object manipulation time (us)", "", ((int64_t)protobufBase.time.user - (int64_t)nullCase.time.user) / 1000.0, ((int64_t)capnpBase.time.user - (int64_t)nullCase.time.user) / 1000.0, iters); reportComparison("object manipulation time w/o reuse (us)", "", ((int64_t)protobufNoReuse.time.user - (int64_t)nullCaseNoReuse.time.user) / 1000.0, ((int64_t)capnpNoReuse.time.user - (int64_t)nullCaseNoReuse.time.user) / 1000.0, iters); reportComparison("I/O time (us)", "", ((int64_t)protobuf.time.user - (int64_t)protobufBase.time.user) / 1000.0, ((int64_t)capnp.time.user - (int64_t)capnpBase.time.user) / 1000.0, iters); reportComparison("packed I/O time (us)", "", ((int64_t)protobuf.time.user - (int64_t)protobufBase.time.user) / 1000.0, ((int64_t)capnpPacked.time.user - (int64_t)capnpBase.time.user) / 1000.0, iters); reportIntComparison("message size (bytes)", "", protobuf.messageSize, capnp.messageSize, iters); reportIntComparison("packed message size (bytes)", "", protobuf.messageSize, capnpPacked.messageSize, iters); reportComparison("binary size (KiB)", "", protobufBinarySize / 1024.0, capnpBinarySize / 1024.0, 1); reportComparison("generated code size (KiB)", "", protobufCodeSize / 1024.0, capnpCodeSize / 1024.0, 1); reportComparison("generated obj size (KiB)", "", protobufObjSize / 1024.0, capnpObjSize / 1024.0, 1); if (oldDir != nullptr) { cout << endl; reportOldNewComparisonHeader(); reportComparison("memory overhead", oldNullCase.objectSize, oldCapnpBase.objectSize, capnpBase.objectSize, iters); reportComparison("memory overhead w/o object reuse", oldNullCaseNoReuse.objectSize, oldCapnpNoReuse.objectSize, capnpNoReuse.objectSize, iters); reportComparison("object manipulation time (us)", "", ((int64_t)oldCapnpBase.time.user - (int64_t)oldNullCase.time.user) / 1000.0, ((int64_t)capnpBase.time.user - (int64_t)oldNullCase.time.user) / 1000.0, iters); reportComparison("object manipulation time w/o reuse (us)", "", ((int64_t)oldCapnpNoReuse.time.user - (int64_t)oldNullCaseNoReuse.time.user) / 1000.0, ((int64_t)capnpNoReuse.time.user - (int64_t)oldNullCaseNoReuse.time.user) / 1000.0, iters); reportComparison("I/O time (us)", "", ((int64_t)oldCapnp.time.user - (int64_t)oldCapnpBase.time.user) / 1000.0, ((int64_t)capnp.time.user - (int64_t)capnpBase.time.user) / 1000.0, iters); reportComparison("packed I/O time (us)", "", ((int64_t)oldCapnpPacked.time.user - (int64_t)oldCapnpBase.time.user) / 1000.0, ((int64_t)capnpPacked.time.user - (int64_t)capnpBase.time.user) / 1000.0, iters); reportIntComparison("message size (bytes)", "", oldCapnp.messageSize, capnp.messageSize, iters); reportIntComparison("packed message size (bytes)", "", oldCapnpPacked.messageSize, capnpPacked.messageSize, iters); reportComparison("binary size (KiB)", "", oldCapnpBinarySize / 1024.0, capnpBinarySize / 1024.0, 1); reportComparison("generated code size (KiB)", "", oldCapnpCodeSize / 1024.0, capnpCodeSize / 1024.0, 1); reportComparison("generated obj size (KiB)", "", oldCapnpObjSize / 1024.0, capnpObjSize / 1024.0, 1); } return 0; } } // namespace runner } // namespace benchmark } // namespace capnp int main(int argc, char* argv[]) { return capnp::benchmark::runner::main(argc, argv); }