diff --git a/benchmarks/000.microbenchmarks/010.sleep/config.json b/benchmarks/000.microbenchmarks/010.sleep/config.json index c7e8fc23..1121edc0 100644 --- a/benchmarks/000.microbenchmarks/010.sleep/config.json +++ b/benchmarks/000.microbenchmarks/010.sleep/config.json @@ -1,5 +1,5 @@ { "timeout": 120, "memory": 128, - "languages": ["python", "nodejs"] + "languages": ["python", "nodejs", "cpp"] } diff --git a/benchmarks/000.microbenchmarks/010.sleep/cpp/main.cpp b/benchmarks/000.microbenchmarks/010.sleep/cpp/main.cpp new file mode 100644 index 00000000..fe6f0709 --- /dev/null +++ b/benchmarks/000.microbenchmarks/010.sleep/cpp/main.cpp @@ -0,0 +1,22 @@ + +#include +#include +#include + +#include +#include + +Aws::Utils::Json::JsonValue function(Aws::Utils::Json::JsonView json) +{ + int sleep = json.GetInteger("sleep"); + + std::chrono::seconds timespan(sleep); + std::this_thread::sleep_for(timespan); + + //std::string res_json = "{ \"result\": " + std::to_string(sleep) + "}"; + //return aws::lambda_runtime::invocation_response::success(res_json, "application/json"); + Aws::Utils::Json::JsonValue val; + val.WithObject("result", std::to_string(sleep)); + return val; +} + diff --git a/benchmarks/wrappers/aws/cpp/handler.cpp b/benchmarks/wrappers/aws/cpp/handler.cpp new file mode 100644 index 00000000..233e6716 --- /dev/null +++ b/benchmarks/wrappers/aws/cpp/handler.cpp @@ -0,0 +1,73 @@ + +#include +#include +#include + +#include +#include +#include + +#include "utils.hpp" + +// Global variables that are retained across function invocations +bool cold_execution = true; +std::string container_id = ""; +std::string cold_start_var = ""; + +Aws::Utils::Json::JsonValue function(Aws::Utils::Json::JsonView req); + +aws::lambda_runtime::invocation_response handler(aws::lambda_runtime::invocation_request const &req) +{ + Aws::Utils::Json::JsonValue json(req.payload); + Aws::Utils::Json::JsonView json_view = json.View(); + // HTTP trigger with API Gateaway sends payload as a serialized JSON + // stored under key 'body' in the main JSON + // The SDK trigger converts everything for us + if(json_view.ValueExists("body")){ + Aws::Utils::Json::JsonValue parsed_body{json_view.GetString("body")}; + json = std::move(parsed_body); + json_view = json.View(); + } + + const auto begin = std::chrono::system_clock::now(); + auto ret = function(json.View()); + const auto end = std::chrono::system_clock::now(); + + Aws::Utils::Json::JsonValue body; + body.WithObject("result", ret); + + // Switch cold execution after the first one. + if(cold_execution) + cold_execution = false; + + auto b = std::chrono::duration_cast(begin.time_since_epoch()).count() / 1000.0 / 1000.0; + auto e = std::chrono::duration_cast(end.time_since_epoch()).count() / 1000.0 / 1000.0; + body.WithDouble("begin", b); + body.WithDouble("end", e); + body.WithDouble("results_time", e - b); + body.WithString("request_id", req.request_id); + body.WithBool("is_cold", cold_execution); + body.WithString("container_id", container_id); + body.WithString("cold_start_var", cold_start_var); + + Aws::Utils::Json::JsonValue final_result; + final_result.WithObject("body", body); + return aws::lambda_runtime::invocation_response::success(final_result.View().WriteReadable(), "application/json"); +} + +int main() +{ + Aws::SDKOptions options; + Aws::InitAPI(options); + + const char * cold_var = std::getenv("cold_start"); + if(cold_var) + cold_start_var = cold_var; + container_id = boost::uuids::to_string(boost::uuids::random_generator()()); + + aws::lambda_runtime::run_handler(handler); + + Aws::ShutdownAPI(options); + return 0; +} + diff --git a/benchmarks/wrappers/aws/cpp/key-value.cpp b/benchmarks/wrappers/aws/cpp/key-value.cpp new file mode 100644 index 00000000..3637a8c1 --- /dev/null +++ b/benchmarks/wrappers/aws/cpp/key-value.cpp @@ -0,0 +1,103 @@ + +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include "key-value.hpp" +#include "utils.hpp" + +KeyValue::KeyValue() +{ + Aws::Client::ClientConfiguration config; + //config.region = "eu-central-1"; + config.caFile = "/etc/pki/tls/certs/ca-bundle.crt"; + + char const TAG[] = "LAMBDA_ALLOC"; + auto credentialsProvider = Aws::MakeShared(TAG); + _client.reset(new Aws::DynamoDB::DynamoDBClient(credentialsProvider, config)); +} + +uint64_t KeyValue::download_file(Aws::String const &table, Aws::String const &key, + int &required_retries, double& read_units, bool with_backoff) +{ + Aws::DynamoDB::Model::GetItemRequest req; + + // Set up the request + req.SetTableName(table); + req.SetReturnConsumedCapacity(Aws::DynamoDB::Model::ReturnConsumedCapacity::TOTAL); + Aws::DynamoDB::Model::AttributeValue hashKey; + hashKey.SetS(key); + req.AddKey("key", hashKey); + + auto bef = timeSinceEpochMillisec(); + int retries = 0; + const int MAX_RETRIES = 1500; + + while (retries < MAX_RETRIES) { + auto get_result = _client->GetItem(req); + if (get_result.IsSuccess()) { + + // Reference the retrieved fields/values + auto result = get_result.GetResult(); + const Aws::Map& item = result.GetItem(); + if (item.size() > 0) { + uint64_t finishedTime = timeSinceEpochMillisec(); + + required_retries = retries; + // GetReadCapacityUnits returns 0? + read_units = result.GetConsumedCapacity().GetCapacityUnits(); + + return finishedTime - bef; + } + + } else { + retries += 1; + if(with_backoff) { + int sleep_time = retries; + if (retries > 100) { + sleep_time = retries * 2; + } + std::this_thread::sleep_for(std::chrono::milliseconds(sleep_time)); + } + } + } + return 0; +} + +uint64_t KeyValue::upload_file(Aws::String const &table, + Aws::String const &key, + double& write_units, + int size, unsigned char* pBuf) +{ + Aws::Utils::ByteBuffer buf(pBuf, size); + + Aws::DynamoDB::Model::PutItemRequest req; + req.SetTableName(table); + req.SetReturnConsumedCapacity(Aws::DynamoDB::Model::ReturnConsumedCapacity::TOTAL); + + Aws::DynamoDB::Model::AttributeValue av; + av.SetB(buf); + req.AddItem("data", av); + av.SetS(key); + req.AddItem("key", av); + + uint64_t bef = timeSinceEpochMillisec(); + const Aws::DynamoDB::Model::PutItemOutcome put_result = _client->PutItem(req); + if (!put_result.IsSuccess()) { + std::cout << put_result.GetError().GetMessage() << std::endl; + return 1; + } + auto result = put_result.GetResult(); + // GetWriteCapacityUnits returns 0? + write_units = result.GetConsumedCapacity().GetCapacityUnits(); + uint64_t finishedTime = timeSinceEpochMillisec(); + + return finishedTime - bef; +} diff --git a/benchmarks/wrappers/aws/cpp/key-value.hpp b/benchmarks/wrappers/aws/cpp/key-value.hpp new file mode 100644 index 00000000..e2fc48a3 --- /dev/null +++ b/benchmarks/wrappers/aws/cpp/key-value.hpp @@ -0,0 +1,31 @@ + +#include +#include +#include +#include + +#include +#include + +class KeyValue +{ + // non-copyable, non-movable + std::shared_ptr _client; +public: + + KeyValue(); + +uint64_t download_file(Aws::String const &bucket, + Aws::String const &key, + int& required_retries, + double& read_units, + bool with_backoff = false); + +uint64_t upload_file(Aws::String const &bucket, + Aws::String const &key, + double& write_units, + int size, + unsigned char* pBuf); + +}; + diff --git a/benchmarks/wrappers/aws/cpp/redis.cpp b/benchmarks/wrappers/aws/cpp/redis.cpp new file mode 100644 index 00000000..461dd3d6 --- /dev/null +++ b/benchmarks/wrappers/aws/cpp/redis.cpp @@ -0,0 +1,104 @@ + +#include + +#include + +#include "redis.hpp" +#include "utils.hpp" + +Redis::Redis(std::string redis_hostname, int redis_port) +{ + _context = redisConnect(redis_hostname.c_str(), redis_port); + if (_context == nullptr || _context->err) { + if (_context) { + std::cerr << "Redis Error: " << _context->errstr << '\n'; + } else { + std::cerr << "Can't allocate redis context\n"; + } + } +} + +bool Redis::is_initialized() +{ + return _context != nullptr; +} + +Redis::~Redis() +{ + redisFree(_context); +} + +uint64_t Redis::download_file(Aws::String const &key, + int &required_retries, bool with_backoff) +{ + std::string comm = "GET " + key; + + auto bef = timeSinceEpochMillisec(); + int retries = 0; + const int MAX_RETRIES = 1500; + + while (retries < MAX_RETRIES) { + + redisReply* reply = (redisReply*) redisCommand(_context, comm.c_str()); + + if (reply->type == REDIS_REPLY_NIL || reply->type == REDIS_REPLY_ERROR) { + + retries += 1; + if(with_backoff) { + int sleep_time = retries; + if (retries > 100) { + sleep_time = retries * 2; + } + std::this_thread::sleep_for(std::chrono::milliseconds(sleep_time)); + } + + } else { + + uint64_t finishedTime = timeSinceEpochMillisec(); + required_retries = retries; + + freeReplyObject(reply); + return finishedTime - bef; + + } + freeReplyObject(reply); + } + return 0; +} + +uint64_t Redis::upload_file(Aws::String const &key, + int size, char* pBuf) +{ + std::string comm = "SET " + key + " %b"; + + + uint64_t bef = timeSinceEpochMillisec(); + redisReply* reply = (redisReply*) redisCommand(_context, comm.c_str(), pBuf, size); + uint64_t finishedTime = timeSinceEpochMillisec(); + + if (reply->type == REDIS_REPLY_NIL || reply->type == REDIS_REPLY_ERROR) { + std::cerr << "Failed to write in Redis!" << std::endl; + abort(); + } + freeReplyObject(reply); + + return finishedTime - bef; +} + +uint64_t Redis::delete_file(std::string const &key) +{ + std::string comm = "DEL " + key; + + uint64_t bef = timeSinceEpochMillisec(); + redisReply* reply = (redisReply*) redisCommand(_context, comm.c_str()); + uint64_t finishedTime = timeSinceEpochMillisec(); + + if (reply->type == REDIS_REPLY_NIL || reply->type == REDIS_REPLY_ERROR) { + std::cerr << "Couldn't delete the key!" << '\n'; + abort(); + } + freeReplyObject(reply); + + return finishedTime - bef; +} + diff --git a/benchmarks/wrappers/aws/cpp/redis.hpp b/benchmarks/wrappers/aws/cpp/redis.hpp new file mode 100644 index 00000000..f30de43f --- /dev/null +++ b/benchmarks/wrappers/aws/cpp/redis.hpp @@ -0,0 +1,26 @@ + +#include +#include + +#include + +#include + +class Redis +{ + redisContext* _context; +public: + + Redis(std::string redis_hostname, int redis_port); + ~Redis(); + + bool is_initialized(); + + uint64_t download_file(Aws::String const &key, int &required_retries, bool with_backoff); + + uint64_t upload_file(Aws::String const &key, int size, char* pBuf); + + uint64_t delete_file(std::string const &key); + +}; + diff --git a/benchmarks/wrappers/aws/cpp/storage.cpp b/benchmarks/wrappers/aws/cpp/storage.cpp new file mode 100644 index 00000000..efc54b95 --- /dev/null +++ b/benchmarks/wrappers/aws/cpp/storage.cpp @@ -0,0 +1,90 @@ + +#include + +#include +#include +#include +#include + +#include + +#include "storage.hpp" +#include "utils.hpp" + +Storage Storage::get_client() +{ + Aws::Client::ClientConfiguration config; + //config.region = "eu-central-1"; + config.caFile = "/etc/pki/tls/certs/ca-bundle.crt"; + + std::cout << std::getenv("AWS_REGION") << std::endl; + + char const TAG[] = "LAMBDA_ALLOC"; + auto credentialsProvider = Aws::MakeShared(TAG); + Aws::S3::S3Client client(credentialsProvider, config); + return Storage(std::move(client)); +} + +uint64_t Storage::download_file(Aws::String const &bucket, Aws::String const &key, + int &required_retries, + bool report_dl_time) +{ + + + Aws::S3::Model::GetObjectRequest request; + request.WithBucket(bucket).WithKey(key); + auto bef = timeSinceEpochMillisec(); + + int retries = 0; + //const int MAX_RETRIES = 500; + const int MAX_RETRIES = 1500; + while (retries < MAX_RETRIES) { + auto outcome = this->_client.GetObject(request); + if (outcome.IsSuccess()) { + auto& s = outcome.GetResult().GetBody(); + uint64_t finishedTime = timeSinceEpochMillisec(); + // Perform NOP on result to prevent optimizations + std::stringstream ss; + ss << s.rdbuf(); + std::string first(" "); + ss.get(&first[0], 1); + required_retries = retries; + if (report_dl_time) { + return finishedTime - bef; + } else { + return finishedTime; + } + } else { + retries += 1; + //int sleep_time = retries; + //if (retries > 100) { + // sleep_time = retries * 2; + //} + //std::this_thread::sleep_for(std::chrono::milliseconds(sleep_time)); + } + } + return 0; + +} + +uint64_t Storage::upload_random_file(Aws::String const &bucket, + Aws::String const &key, + int size, char* pBuf) +{ + /** + * We use Boost's bufferstream to wrap the array as an IOStream. Usign a light-weight streambuf wrapper, as many solutions + * (e.g. https://stackoverflow.com/questions/13059091/creating-an-input-stream-from-constant-memory) on the internet + * suggest does not work because the S3 SDK relies on proper functioning tellp(), etc... (for instance to get the body length). + */ + const std::shared_ptr input_data = std::make_shared(pBuf, size); + + Aws::S3::Model::PutObjectRequest request; + request.WithBucket(bucket).WithKey(key); + request.SetBody(input_data); + uint64_t bef_upload = timeSinceEpochMillisec(); + Aws::S3::Model::PutObjectOutcome outcome = this->_client.PutObject(request); + if (!outcome.IsSuccess()) { + std::cerr << "Error: PutObject: " << outcome.GetError().GetMessage() << std::endl; + } + return bef_upload; +} diff --git a/benchmarks/wrappers/aws/cpp/storage.hpp b/benchmarks/wrappers/aws/cpp/storage.hpp new file mode 100644 index 00000000..2b548ef2 --- /dev/null +++ b/benchmarks/wrappers/aws/cpp/storage.hpp @@ -0,0 +1,28 @@ + +#include + +#include +#include + +class Storage +{ + Aws::S3::S3Client _client; +public: + + Storage(Aws::S3::S3Client && client): + _client(client) + {} + + static Storage get_client(); + + uint64_t download_file(Aws::String const &bucket, + Aws::String const &key, + int &required_retries, + bool report_dl_time); + + uint64_t upload_random_file(Aws::String const &bucket, + Aws::String const &key, + int size, char* pBuf); + +}; + diff --git a/benchmarks/wrappers/aws/cpp/utils.cpp b/benchmarks/wrappers/aws/cpp/utils.cpp new file mode 100644 index 00000000..d3d9207e --- /dev/null +++ b/benchmarks/wrappers/aws/cpp/utils.cpp @@ -0,0 +1,10 @@ + +#include "utils.hpp" + +uint64_t timeSinceEpochMillisec() +{ + auto now = std::chrono::high_resolution_clock::now(); + auto time = now.time_since_epoch(); + return std::chrono::duration_cast< std::chrono::microseconds >(time).count(); +} + diff --git a/benchmarks/wrappers/aws/cpp/utils.hpp b/benchmarks/wrappers/aws/cpp/utils.hpp new file mode 100644 index 00000000..0fff5f8a --- /dev/null +++ b/benchmarks/wrappers/aws/cpp/utils.hpp @@ -0,0 +1,5 @@ + +#include + +uint64_t timeSinceEpochMillisec(); + diff --git a/config/systems.json b/config/systems.json index c38f1233..6e88ff3b 100644 --- a/config/systems.json +++ b/config/systems.json @@ -62,6 +62,19 @@ "uuid": "3.4.0" } } + }, + "cpp": { + "base_images": { + "all": "amazon/aws-lambda-provided:al2.2022.04.27.09" + }, + "dependencies": [ + "runtime", "sdk", "boost", "hiredis" + ], + "versions": ["all"], + "images": ["build"], + "deployment": { + "files": [ "handler.cpp", "key-value.cpp", "key-value.hpp", "storage.cpp", "storage.hpp", "redis.hpp", "redis.cpp", "utils.cpp", "utils.hpp"] + } } } }, diff --git a/docker/aws/cpp/Dockerfile.build b/docker/aws/cpp/Dockerfile.build new file mode 100755 index 00000000..2699806e --- /dev/null +++ b/docker/aws/cpp/Dockerfile.build @@ -0,0 +1,32 @@ + +ARG BASE_REPOSITORY +ARG BASE_IMAGE +FROM ${BASE_REPOSITORY}:dependencies-sdk.aws.cpp.all as sdk +FROM ${BASE_REPOSITORY}:dependencies-boost.aws.cpp.all as boost +FROM ${BASE_REPOSITORY}:dependencies-hiredis.aws.cpp.all as hiredis +FROM ${BASE_REPOSITORY}:dependencies-runtime.aws.cpp.all as runtime + +FROM ${BASE_IMAGE} as builder + +RUN yum install -y cmake3 curl libcurl libcurl-devel git gcc gcc-c++ make tar gzip zip zlib-devel openssl-devel openssl-static +ENV GOSU_VERSION 1.14 +# https://github.com/tianon/gosu/releases/tag/1.14 +# key https://keys.openpgp.org/search?q=tianon%40debian.org +RUN curl -o /usr/local/bin/gosu -SL "https://github.com/tianon/gosu/releases/download/${GOSU_VERSION}/gosu-amd64" \ + && chmod +x /usr/local/bin/gosu +RUN mkdir -p /sebs/ +COPY docker/entrypoint.sh /sebs/entrypoint.sh +COPY docker/cpp_installer.sh /sebs/installer.sh +RUN chmod +x /sebs/entrypoint.sh +RUN chmod +x /sebs/installer.sh + +COPY --from=sdk /opt /opt +COPY --from=boost /opt /opt +COPY --from=runtime /opt /opt +COPY --from=hiredis /opt /opt + +# useradd and groupmod is installed in /usr/sbin which is not in PATH +ENV PATH=/usr/sbin:$PATH +CMD /bin/bash /sebs/installer.sh +ENTRYPOINT ["/sebs/entrypoint.sh"] + diff --git a/docker/aws/cpp/Dockerfile.dependencies-boost b/docker/aws/cpp/Dockerfile.dependencies-boost new file mode 100755 index 00000000..ab987b9e --- /dev/null +++ b/docker/aws/cpp/Dockerfile.dependencies-boost @@ -0,0 +1,19 @@ +ARG BASE_IMAGE +FROM ${BASE_IMAGE} as builder +ARG WORKERS +ENV WORKERS=${WORKERS} + + +RUN yum install -y cmake curl libcurl libcurl-devel git gcc gcc-c++ make tar gzip which python-devel +RUN curl -LO https://boostorg.jfrog.io/artifactory/main/release/1.79.0/source/boost_1_79_0.tar.gz\ + && tar -xf boost_1_79_0.tar.gz && cd boost_1_79_0\ + && echo "using gcc : : $(which gcc10-c++) ;" >> tools/build/src/user-config.jam\ + && ./bootstrap.sh --prefix=/opt\ + && ./b2 -j${WORKERS} --prefix=/opt cxxflags="-fPIC" link=static install +#RUN curl -LO https://boostorg.jfrog.io/artifactory/main/release/1.79.0/source/boost_1_79_0.tar.gz\ +# && tar -xf boost_1_79_0.tar.gz && cd boost_1_79_0 && ./bootstrap.sh --prefix=/opt\ +# && ./b2 -j${WORKERS} --prefix=/opt cxxflags="-fPIC" link=static install + +FROM ${BASE_IMAGE} + +COPY --from=builder /opt /opt diff --git a/docker/aws/cpp/Dockerfile.dependencies-hiredis b/docker/aws/cpp/Dockerfile.dependencies-hiredis new file mode 100755 index 00000000..4842a22d --- /dev/null +++ b/docker/aws/cpp/Dockerfile.dependencies-hiredis @@ -0,0 +1,12 @@ +ARG BASE_IMAGE +FROM ${BASE_IMAGE} as builder +ARG WORKERS +ENV WORKERS=${WORKERS} + +RUN yum install -y git make gcc gcc-c++ +RUN git clone https://github.com/redis/hiredis.git && cd hiredis\ + && PREFIX=/opt make -j${WORKERS} install + +FROM ${BASE_IMAGE} + +COPY --from=builder /opt /opt diff --git a/docker/aws/cpp/Dockerfile.dependencies-runtime b/docker/aws/cpp/Dockerfile.dependencies-runtime new file mode 100755 index 00000000..ed570ec5 --- /dev/null +++ b/docker/aws/cpp/Dockerfile.dependencies-runtime @@ -0,0 +1,14 @@ +ARG BASE_IMAGE +FROM ${BASE_IMAGE} as builder +ARG WORKERS +ENV WORKERS=${WORKERS} + + +RUN yum install -y cmake3 curl libcurl libcurl-devel git gcc gcc-c++ make tar gzip +RUN git clone https://github.com/awslabs/aws-lambda-cpp.git\ + && cmake3 -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DCMAKE_INSTALL_PREFIX=/opt -B aws-lambda-cpp/build -S aws-lambda-cpp\ + && cmake3 --build aws-lambda-cpp/build --parallel ${WORKERS} --target install + +FROM ${BASE_IMAGE} + +COPY --from=builder /opt /opt diff --git a/docker/aws/cpp/Dockerfile.dependencies-sdk b/docker/aws/cpp/Dockerfile.dependencies-sdk new file mode 100755 index 00000000..f1c44f53 --- /dev/null +++ b/docker/aws/cpp/Dockerfile.dependencies-sdk @@ -0,0 +1,14 @@ +ARG BASE_IMAGE +FROM ${BASE_IMAGE} as builder +ARG WORKERS +ENV WORKERS=${WORKERS} + +RUN yum install -y cmake3 curl libcurl libcurl-devel git gcc gcc-c++ make zlib-devel openssl-devel +RUN git clone --recurse-submodules https://github.com/aws/aws-sdk-cpp.git\ + && cd aws-sdk-cpp && mkdir build && cd build\ + && cmake3 -DCMAKE_BUILD_TYPE=Release -DBUILD_ONLY="s3;dynamodb;sqs" -DENABLE_TESTING=OFF -DCMAKE_INSTALL_PREFIX=/opt/ ..\ + && cmake3 --build . --parallel ${WORKERS} --target install + +FROM ${BASE_IMAGE} + +COPY --from=builder /opt /opt diff --git a/docker/aws/python/Dockerfile.build b/docker/aws/python/Dockerfile.build index 960fc300..f41752ed 100755 --- a/docker/aws/python/Dockerfile.build +++ b/docker/aws/python/Dockerfile.build @@ -4,7 +4,7 @@ ARG VERSION ENV PYTHON_VERSION=${VERSION} # useradd, groupmod -RUN yum install -y shadow-utils +RUN yum install -y cmake curl libcurl libcurl-devel ENV GOSU_VERSION 1.14 # https://github.com/tianon/gosu/releases/tag/1.14 # key https://keys.openpgp.org/search?q=tianon%40debian.org diff --git a/docker/cpp_installer.sh b/docker/cpp_installer.sh new file mode 100644 index 00000000..fe237d9b --- /dev/null +++ b/docker/cpp_installer.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +mkdir -p /mnt/function/build +cmake3 -DCMAKE_BUILD_TYPE=Release -S /mnt/function/ -B /mnt/function/build > /mnt/function/build/configuration.log +VERBOSE=1 cmake3 --build /mnt/function/build --target aws-lambda-package-benchmark > /mnt/function/build/compilation.log + diff --git a/sebs.py b/sebs.py index ce78036c..17a41c66 100755 --- a/sebs.py +++ b/sebs.py @@ -63,7 +63,7 @@ def simplified_common_params(func): @click.option( "--language", default=None, - type=click.Choice(["python", "nodejs"]), + type=click.Choice(["python", "nodejs", "cpp"]), help="Benchmark language", ) @click.option("--language-version", default=None, type=str, help="Benchmark language version") diff --git a/sebs/aws/aws.py b/sebs/aws/aws.py index 6c34af90..ef942d67 100644 --- a/sebs/aws/aws.py +++ b/sebs/aws/aws.py @@ -19,6 +19,7 @@ from sebs.faas.function import Function, ExecutionResult, Trigger, FunctionConfig from sebs.faas.storage import PersistentStorage from sebs.faas.system import System +from sebs.types import Language class AWS(System): @@ -125,42 +126,57 @@ def get_storage(self, replace_existing: bool = False) -> PersistentStorage: def package_code( self, directory: str, - language_name: str, + language: Language, language_version: str, benchmark: str, is_cached: bool, ) -> Tuple[str, int]: CONFIG_FILES = { - "python": ["handler.py", "requirements.txt", ".python_packages"], - "nodejs": ["handler.js", "package.json", "node_modules"], + Language.PYTHON: ["handler.py", "requirements.txt", ".python_packages"], + Language.NODEJS: ["handler.js", "package.json", "node_modules"], } - package_config = CONFIG_FILES[language_name] - function_dir = os.path.join(directory, "function") - os.makedirs(function_dir) - # move all files to 'function' except handler.py - for file in os.listdir(directory): - if file not in package_config: - file = os.path.join(directory, file) - shutil.move(file, function_dir) - - # FIXME: use zipfile - # create zip with hidden directory but without parent directory - execute("zip -qu -r9 {}.zip * .".format(benchmark), shell=True, cwd=directory) - benchmark_archive = "{}.zip".format(os.path.join(directory, benchmark)) - self.logging.info("Created {} archive".format(benchmark_archive)) - - bytes_size = os.path.getsize(os.path.join(directory, benchmark_archive)) - mbytes = bytes_size / 1024.0 / 1024.0 - self.logging.info("Zip archive size {:2f} MB".format(mbytes)) - - return os.path.join(directory, "{}.zip".format(benchmark)), bytes_size + + if language in [Language.PYTHON, Language.NODEJS]: + package_config = CONFIG_FILES[language] + function_dir = os.path.join(directory, "function") + os.makedirs(function_dir) + # move all files to 'function' except handler.py + for file in os.listdir(directory): + if file not in package_config: + file = os.path.join(directory, file) + shutil.move(file, function_dir) + + # FIXME: use zipfile + # create zip with hidden directory but without parent directory + execute("zip -qu -r9 {}.zip * .".format(benchmark), shell=True, cwd=directory) + benchmark_archive = "{}.zip".format(os.path.join(directory, benchmark)) + self.logging.info("Created {} archive".format(benchmark_archive)) + + bytes_size = os.path.getsize(os.path.join(directory, benchmark_archive)) + mbytes = bytes_size / 1024.0 / 1024.0 + self.logging.info("Zip archive size {:2f} MB".format(mbytes)) + + return os.path.join(directory, "{}.zip".format(benchmark)), bytes_size + elif language == Language.CPP: + + # lambda C++ runtime build scripts create the .zip file in build directory + benchmark_archive = os.path.join(directory, "build", "benchmark.zip") + self.logging.info("Created {} archive".format(benchmark_archive)) + + bytes_size = os.path.getsize(os.path.join(directory, benchmark_archive)) + mbytes = bytes_size / 1024.0 / 1024.0 + self.logging.info("Zip archive size {:2f} MB".format(mbytes)) + + return benchmark_archive, bytes_size + else: + raise NotImplementedError() def create_function(self, code_package: Benchmark, func_name: str) -> "LambdaFunction": package = code_package.code_location benchmark = code_package.benchmark - language = code_package.language_name + language = code_package.language language_runtime = code_package.language_version timeout = code_package.benchmark_config.timeout memory = code_package.benchmark_config.memory @@ -210,7 +226,7 @@ def create_function(self, code_package: Benchmark, func_name: str) -> "LambdaFun code_config = {"S3Bucket": code_bucket, "S3Key": code_package_name} ret = self.client.create_function( FunctionName=func_name, - Runtime="{}{}".format(language, language_runtime), + Runtime=self.cloud_runtime(language, language_runtime), Handler="handler.handler", Role=self.config.resources.lambda_role(self.session), MemorySize=memory, @@ -534,3 +550,11 @@ def wait_function_updated(self, func: LambdaFunction): waiter = self.client.get_waiter("function_updated_v2") waiter.wait(FunctionName=func.name) self.logging.info("Lambda function has been updated.") + + def cloud_runtime(self, language: Language, language_version: str): + if language in [Language.NODEJS, Language.PYTHON]: + return ("{}{}".format(language, language_version),) + elif language == Language.CPP: + return "provided.al2" + else: + raise NotImplementedError() diff --git a/sebs/azure/azure.py b/sebs/azure/azure.py index e957d693..4b590db2 100644 --- a/sebs/azure/azure.py +++ b/sebs/azure/azure.py @@ -20,6 +20,7 @@ from ..faas.function import Function, FunctionConfig, ExecutionResult from ..faas.storage import PersistentStorage from ..faas.system import System +from sebs.types import Language class Azure(System): @@ -117,7 +118,7 @@ def get_storage(self, replace_existing: bool = False) -> PersistentStorage: def package_code( self, directory: str, - language_name: str, + language: Language, language_version: str, benchmark: str, is_cached: bool, @@ -125,12 +126,12 @@ def package_code( # In previous step we ran a Docker container which installed packages # Python packages are in .python_packages because this is expected by Azure - EXEC_FILES = {"python": "handler.py", "nodejs": "handler.js"} + EXEC_FILES = {Language.PYTHON: "handler.py", Language.NODEJS: "handler.js"} CONFIG_FILES = { - "python": ["requirements.txt", ".python_packages"], - "nodejs": ["package.json", "node_modules"], + Language.PYTHON: ["requirements.txt", ".python_packages"], + Language.NODEJS: ["package.json", "node_modules"], } - package_config = CONFIG_FILES[language_name] + package_config = CONFIG_FILES[language] handler_dir = os.path.join(directory, "handler") os.makedirs(handler_dir) @@ -143,7 +144,7 @@ def package_code( # generate function.json # TODO: extension to other triggers than HTTP default_function_json = { - "scriptFile": EXEC_FILES[language_name], + "scriptFile": EXEC_FILES[language], "bindings": [ { "authLevel": "function", diff --git a/sebs/benchmark.py b/sebs/benchmark.py index 0c51b2cd..edcc4631 100644 --- a/sebs/benchmark.py +++ b/sebs/benchmark.py @@ -4,6 +4,7 @@ import os import shutil import subprocess +import textwrap from typing import Any, Callable, Dict, List, Tuple import docker @@ -11,12 +12,12 @@ from sebs.config import SeBSConfig from sebs.cache import Cache from sebs.utils import find_benchmark, project_absolute_path, LoggingBase +from sebs.types import Language from sebs.faas.storage import PersistentStorage from typing import TYPE_CHECKING if TYPE_CHECKING: from sebs.experiments.config import Config as ExperimentConfig - from sebs.faas.function import Language class BenchmarkConfig: @@ -137,7 +138,7 @@ def language_version(self): @property # noqa: A003 def hash(self): path = os.path.join(self.benchmark_path, self.language_name) - self._hash_value = Benchmark.hash_directory(path, self._deployment_name, self.language_name) + self._hash_value = Benchmark.hash_directory(path, self._deployment_name, self.language) return self._hash_value @hash.setter # noqa: A003 @@ -192,14 +193,19 @@ def __init__( """ @staticmethod - def hash_directory(directory: str, deployment: str, language: str): + def hash_directory(directory: str, deployment: str, language: Language): hash_sum = hashlib.md5() FILES = { - "python": ["*.py", "requirements.txt*"], - "nodejs": ["*.js", "package.json"], + Language.PYTHON: ["*.py", "requirements.txt*"], + Language.NODEJS: ["*.js", "package.json"], + Language.CPP: ["*.cpp", "*.hpp", "dependencies.json"], + } + WRAPPERS = { + Language.PYTHON: ["*.py"], + Language.NODEJS: ["*.js"], + Language.CPP: ["*.cpp", "*.hpp"], } - WRAPPERS = {"python": "*.py", "nodejs": "*.js"} NON_LANG_FILES = ["*.sh", "*.json"] selected_files = FILES[language] + NON_LANG_FILES for file_type in selected_files: @@ -208,13 +214,14 @@ def hash_directory(directory: str, deployment: str, language: str): with open(path, "rb") as opened_file: hash_sum.update(opened_file.read()) # wrappers - wrappers = project_absolute_path( - "benchmarks", "wrappers", deployment, language, WRAPPERS[language] - ) - for f in glob.glob(wrappers): - path = os.path.join(directory, f) - with open(path, "rb") as opened_file: - hash_sum.update(opened_file.read()) + for wrapper in WRAPPERS[language]: + wrappers = project_absolute_path( + "benchmarks", "wrappers", deployment, language.value, wrapper + ) + for f in glob.glob(wrappers): + path = os.path.join(directory, f) + with open(path, "rb") as opened_file: + hash_sum.update(opened_file.read()) return hash_sum.hexdigest() def serialize(self) -> dict: @@ -246,11 +253,12 @@ def query_cache(self): def copy_code(self, output_dir): FILES = { - "python": ["*.py", "requirements.txt*"], - "nodejs": ["*.js", "package.json"], + Language.PYTHON: ["*.py", "requirements.txt*"], + Language.NODEJS: ["*.js", "package.json"], + Language.CPP: ["*.cpp", "*.hpp", "dependencies.json"], } path = os.path.join(self.benchmark_path, self.language_name) - for file_type in FILES[self.language_name]: + for file_type in FILES[self.language]: for f in glob.glob(os.path.join(path, file_type)): shutil.copy2(os.path.join(path, f), output_dir) @@ -307,6 +315,48 @@ def add_deployment_package_nodejs(self, output_dir): with open(package_config, "w") as package_file: json.dump(package_json, package_file, indent=2) + def add_deployment_package_cpp(self, output_dir): + + # FIXME: Configure CMakeLists.txt dependencies + # FIXME: Configure for AWS - this should be generic + # FIXME: optional hiredis + cmake_script = """ + cmake_minimum_required(VERSION 3.9) + set(CMAKE_CXX_STANDARD 11) + project(benchmark LANGUAGES CXX) + add_executable( + ${PROJECT_NAME} "handler.cpp" "key-value.cpp" + "storage.cpp" "redis.cpp" "utils.cpp" "main.cpp" + ) + target_include_directories(${PROJECT_NAME} PRIVATE ".") + + target_compile_features(${PROJECT_NAME} PRIVATE "cxx_std_11") + target_compile_options(${PROJECT_NAME} PRIVATE "-Wall" "-Wextra") + + find_package(aws-lambda-runtime) + target_link_libraries(${PROJECT_NAME} PRIVATE AWS::aws-lambda-runtime) + + find_package(Boost REQUIRED) + target_include_directories(${PROJECT_NAME} PRIVATE ${Boost_INCLUDE_DIRS}) + target_link_libraries(${PROJECT_NAME} PRIVATE ${Boost_LIBRARIES}) + + find_package(AWSSDK COMPONENTS s3 dynamodb core) + target_link_libraries(${PROJECT_NAME} PUBLIC ${AWSSDK_LINK_LIBRARIES}) + + find_package(PkgConfig REQUIRED) + set(ENV{PKG_CONFIG_PATH} "/opt/lib/pkgconfig") + pkg_check_modules(HIREDIS REQUIRED IMPORTED_TARGET hiredis) + + target_include_directories(${PROJECT_NAME} PUBLIC PkgConfig::HIREDIS) + target_link_libraries(${PROJECT_NAME} PUBLIC PkgConfig::HIREDIS) + + # this line creates a target that packages your binary and zips it up + aws_lambda_package_target(${PROJECT_NAME}) + """ + build_script = os.path.join(output_dir, "CMakeLists.txt") + with open(build_script, "w") as script_file: + script_file.write(textwrap.dedent(cmake_script)) + def add_deployment_package(self, output_dir): from sebs.faas.function import Language @@ -314,6 +364,8 @@ def add_deployment_package(self, output_dir): self.add_deployment_package_python(output_dir) elif self.language == Language.NODEJS: self.add_deployment_package_nodejs(output_dir) + elif self.language == Language.CPP: + self.add_deployment_package_cpp(output_dir) else: raise NotImplementedError @@ -370,8 +422,12 @@ def install_dependencies(self, output_dir): } # run Docker container to install packages - PACKAGE_FILES = {"python": "requirements.txt", "nodejs": "package.json"} - file = os.path.join(output_dir, PACKAGE_FILES[self.language_name]) + PACKAGE_FILES = { + Language.PYTHON: "requirements.txt", + Language.NODEJS: "package.json", + Language.CPP: "CMakeLists.txt", + } + file = os.path.join(output_dir, PACKAGE_FILES[self.language]) if os.path.exists(file): try: self.logging.info( @@ -455,7 +511,7 @@ def install_dependencies(self, output_dir): self.logging.info("Docker build: {}".format(line)) except docker.errors.ContainerError as e: self.logging.error("Package build failed!") - self.logging.error(e) + self.logging.error(f"Stderr: {e.stderr.decode()}") self.logging.error(f"Docker mount volumes: {volumes}") raise e @@ -464,7 +520,7 @@ def recalculate_code_size(self): return self._code_size def build( - self, deployment_build_step: Callable[[str, str, str, str, bool], Tuple[str, int]] + self, deployment_build_step: Callable[[str, Language, str, str, bool], Tuple[str, int]] ) -> Tuple[bool, str]: # Skip build if files are up to date and user didn't enforce rebuild @@ -495,7 +551,7 @@ def build( self.install_dependencies(self._output_dir) self._code_location, self._code_size = deployment_build_step( os.path.abspath(self._output_dir), - self.language_name, + self.language, self.language_version, self.benchmark, self.is_cached, diff --git a/sebs/faas/function.py b/sebs/faas/function.py index 5b1bf748..87b01ae0 100644 --- a/sebs/faas/function.py +++ b/sebs/faas/function.py @@ -9,6 +9,7 @@ from enum import Enum from typing import Callable, Dict, List, Optional, Type, TypeVar # noqa +from sebs.types import Language, Architecture from sebs.benchmark import Benchmark from sebs.utils import LoggingBase @@ -208,6 +209,13 @@ def _http_invoke(self, payload: dict, url: str, verify_ssl: bool = True) -> Exec try: output = json.loads(data.getvalue()) + if "body" in output: + # AWS C++ trigger returns payload as a dictionary inside "body" + # but add a conversion step just in case + if isinstance(output["body"], dict): + output = output["body"] + else: + output = json.loads(output["body"]) if status_code != 200: self.logging.error("Invocation on URL {} failed!".format(url)) @@ -254,34 +262,6 @@ def deserialize(cached_config: dict) -> "Trigger": pass -class Language(Enum): - PYTHON = "python" - NODEJS = "nodejs" - - # FIXME: 3.7+ python with future annotations - @staticmethod - def deserialize(val: str) -> Language: - for member in Language: - if member.value == val: - return member - raise Exception(f"Unknown language type {member}") - - -class Architecture(Enum): - X86 = "x86" - ARM = "arm" - - def serialize(self) -> str: - return self.value - - @staticmethod - def deserialize(val: str) -> Architecture: - for member in Architecture: - if member.value == val: - return member - raise Exception(f"Unknown architecture type {member}") - - @dataclass class Runtime: @@ -293,8 +273,7 @@ def serialize(self) -> dict: @staticmethod def deserialize(config: dict) -> Runtime: - languages = {"python": Language.PYTHON, "nodejs": Language.NODEJS} - return Runtime(language=languages[config["language"]], version=config["version"]) + return Runtime(language=Language.deserialize(config["language"]), version=config["version"]) T = TypeVar("T", bound="FunctionConfig") diff --git a/sebs/faas/system.py b/sebs/faas/system.py index 64923255..80621fbc 100644 --- a/sebs/faas/system.py +++ b/sebs/faas/system.py @@ -11,6 +11,7 @@ from sebs.faas.function import Function, Trigger, ExecutionResult from sebs.faas.storage import PersistentStorage from sebs.utils import LoggingBase +from sebs.types import Language from .config import Config """ @@ -107,7 +108,7 @@ def get_storage(self, replace_existing: bool) -> PersistentStorage: def package_code( self, directory: str, - language_name: str, + language: Language, language_version: str, benchmark: str, is_cached: bool, diff --git a/sebs/gcp/gcp.py b/sebs/gcp/gcp.py index cd97ab9e..5c9ea1e5 100644 --- a/sebs/gcp/gcp.py +++ b/sebs/gcp/gcp.py @@ -23,6 +23,7 @@ from sebs.gcp.storage import GCPStorage from sebs.gcp.function import GCPFunction from sebs.utils import LoggingHandlers +from sebs.types import Language """ This class provides basic abstractions for the FaaS system. @@ -132,21 +133,21 @@ def format_function_name(func_name: str) -> str: def package_code( self, directory: str, - language_name: str, + language: Language, language_version: str, benchmark: str, is_cached: bool, ) -> Tuple[str, int]: CONFIG_FILES = { - "python": ["handler.py", ".python_packages"], - "nodejs": ["handler.js", "node_modules"], + Language.PYTHON: ["handler.py", ".python_packages"], + Language.NODEJS: ["handler.js", "node_modules"], } HANDLER = { - "python": ("handler.py", "main.py"), - "nodejs": ("handler.js", "index.js"), + Language.PYTHON: ("handler.py", "main.py"), + Language.NODEJS: ("handler.js", "index.js"), } - package_config = CONFIG_FILES[language_name] + package_config = CONFIG_FILES[language] function_dir = os.path.join(directory, "function") os.makedirs(function_dir) for file in os.listdir(directory): @@ -159,7 +160,7 @@ def package_code( requirements.close() # rename handler function.py since in gcp it has to be caled main.py - old_name, new_name = HANDLER[language_name] + old_name, new_name = HANDLER[language] old_path = os.path.join(directory, old_name) new_path = os.path.join(directory, new_name) shutil.move(old_path, new_path) diff --git a/sebs/openwhisk/openwhisk.py b/sebs/openwhisk/openwhisk.py index 0337a9bb..888cf308 100644 --- a/sebs/openwhisk/openwhisk.py +++ b/sebs/openwhisk/openwhisk.py @@ -15,6 +15,7 @@ from .config import OpenWhiskConfig from .function import OpenWhiskFunction, OpenWhiskFunctionConfig from ..config import SeBSConfig +from sebs.types import Language class OpenWhisk(System): @@ -112,7 +113,7 @@ def find_image(self, repository_name, image_tag) -> bool: def build_base_image( self, directory: str, - language_name: str, + language: Language, language_version: str, benchmark: str, is_cached: bool, @@ -133,7 +134,7 @@ def build_base_image( registry_name = self.config.resources.docker_registry repository_name = self.system_config.docker_repository() image_tag = self.system_config.benchmark_image_tag( - self.name(), benchmark, language_name, language_version + self.name(), benchmark, language.value, language_version ) if registry_name is not None: repository_name = f"{registry_name}/{repository_name}" @@ -159,7 +160,7 @@ def build_base_image( build_dir = os.path.join(directory, "docker") os.makedirs(build_dir) shutil.copy( - os.path.join(PROJECT_DIR, "docker", self.name(), language_name, "Dockerfile.function"), + os.path.join(PROJECT_DIR, "docker", self.name(), language.value, "Dockerfile.function"), os.path.join(build_dir, "Dockerfile"), ) @@ -171,7 +172,7 @@ def build_base_image( with open(os.path.join(build_dir, ".dockerignore"), "w") as f: f.write("Dockerfile") - builder_image = self.system_config.benchmark_base_images(self.name(), language_name)[ + builder_image = self.system_config.benchmark_base_images(self.name(), language.value)[ language_version ] self.logging.info(f"Build the benchmark base image {repository_name}:{image_tag}.") @@ -200,7 +201,7 @@ def build_base_image( def package_code( self, directory: str, - language_name: str, + language: Language, language_version: str, benchmark: str, is_cached: bool, @@ -208,15 +209,15 @@ def package_code( # Regardless of Docker image status, we need to create .zip file # to allow registration of function with OpenWhisk - self.build_base_image(directory, language_name, language_version, benchmark, is_cached) + self.build_base_image(directory, language, language_version, benchmark, is_cached) # We deploy Minio config in code package since this depends on local # deployment - it cannnot be a part of Docker image CONFIG_FILES = { - "python": ["__main__.py"], - "nodejs": ["index.js"], + Language.PYTHON: ["__main__.py"], + Language.NODEJS: ["index.js"], } - package_config = CONFIG_FILES[language_name] + package_config = CONFIG_FILES[language] benchmark_archive = os.path.join(directory, f"{benchmark}.zip") subprocess.run( diff --git a/sebs/types.py b/sebs/types.py index 43574337..e1927ee0 100644 --- a/sebs/types.py +++ b/sebs/types.py @@ -1,16 +1,45 @@ +from __future__ import annotations from enum import Enum class Platforms(str, Enum): - AWS = ("aws",) - AZURE = ("azure",) - GCP = ("gcp",) - LOCAL = ("local",) + AWS = "aws" + AZURE = "azure" + GCP = "gcp" + LOCAL = "local" OPENWHISK = "openwhisk" class Storage(str, Enum): - AWS_S3 = ("aws-s3",) - AZURE_BLOB_STORAGE = ("azure-blob-storage",) - GCP_STORAGE = ("google-cloud-storage",) + AWS_S3 = "aws-s3" + AZURE_BLOB_STORAGE = "azure-blob-storage" + GCP_STORAGE = "google-cloud-storage" MINIO = "minio" + + +class Language(str, Enum): + PYTHON = "python" + NODEJS = "nodejs" + CPP = "cpp" + + @staticmethod + def deserialize(val: str) -> Language: + for member in Language: + if member.value == val: + return member + raise Exception(f"Unknown language type {member}") + + +class Architecture(str, Enum): + X86 = "x86" + ARM = "arm" + + def serialize(self) -> str: + return self.value + + @staticmethod + def deserialize(val: str) -> Architecture: + for member in Architecture: + if member.value == val: + return member + raise Exception(f"Unknown architecture type {member}") diff --git a/tools/build_docker_images.py b/tools/build_docker_images.py index 8f1eb320..109b9ecd 100755 --- a/tools/build_docker_images.py +++ b/tools/build_docker_images.py @@ -12,8 +12,10 @@ parser.add_argument( "--deployment", default=None, choices=["local", "aws", "azure", "gcp"], action="store" ) -parser.add_argument("--type", default=None, choices=["build", "run", "manage"], action="store") -parser.add_argument("--language", default=None, choices=["python", "nodejs"], action="store") +parser.add_argument("--type", default=None, choices=["build", "dependencies", "run", "manage"], action="store") +parser.add_argument("--type-tag", default=None, type=str, action="store") +parser.add_argument("--language", default=None, choices=["python", "nodejs", "cpp"], action="store") +parser.add_argument('--parallel', default=1, type=int, action='store') args = parser.parse_args() config = json.load(open(os.path.join(PROJECT_DIR, "config", "systems.json"), "r")) client = docker.from_env() @@ -40,6 +42,8 @@ def build(image_type, system, language=None, version=None, version_name=None): # if we pass an integer, the build will fail with 'connection reset by peer' buildargs = { "VERSION": version, + 'WORKERS': str(args.parallel), + 'BASE_REPOSITORY': config["general"]["docker_repository"] } if version: buildargs["BASE_IMAGE"] = version_name @@ -50,7 +54,6 @@ def build(image_type, system, language=None, version=None, version_name=None): ) client.images.build(path=PROJECT_DIR, dockerfile=dockerfile, buildargs=buildargs, tag=target) - def build_language(system, language, language_config): configs = [] if "base_images" in language_config: @@ -74,6 +77,22 @@ def build_systems(system, system_config): build(args.type, system) else: print(f"Skipping manage image for {system}") + elif args.type == "dependencies": + if args.language: + if "dependencies" in system_config["languages"][args.language]: + language_config = system_config["languages"][args.language] + # for all dependencies + if args.type_tag: + # for all image versions + for version, base_image in language_config["base_images"].items(): + build(f"{args.type}-{args.type_tag}", system, args.language, version, base_image) + else: + for dep in system_config["languages"][args.language]["dependencies"]: + # for all image versions + for version, base_image in language_config["base_images"].items(): + build(f"{args.type}-{dep}", system, args.language, version, base_image) + else: + raise RuntimeError('Language must be specified for dependencies') else: if args.language: build_language(system, args.language, system_config["languages"][args.language])