#!/usr/bin/env python # Copyright The containerd Authors. # 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. # The MIT License (MIT) # # Copyright (c) 2015 Tintri # # 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. import os, sys, subprocess, select, random, time, json, tempfile, shutil, itertools TMP_DIR = tempfile.mkdtemp() LEGACY_MODE = "legacy" ESTARGZ_NOOPT_MODE = "estargz-noopt" ESTARGZ_MODE = "estargz" ZSTDCHUNKED_MODE = "zstdchunked" DEFAULT_OPTIMIZER = "ctr-remote image optimize --oci" DEFAULT_PULLER = "nerdctl image pull" DEFAULT_PUSHER = "nerdctl image push" DEFAULT_TAGGER = "nerdctl image tag" BENCHMARKOUT_MARK = "BENCHMARK_OUTPUT: " CTR="ctr" PODMAN="podman" def exit(status): # cleanup shutil.rmtree(TMP_DIR) sys.exit(status) def tmp_path(): tmp_path.nxt += 1 return os.path.join(TMP_DIR, str(tmp_path.nxt)) tmp_path.nxt = 0 def tmp_copy(src): dst = tmp_path() if os.path.isdir(src): shutil.copytree(src, dst) else: shutil.copyfile(src, dst) return dst def genargs_for_optimization(arg): if arg == None or arg == "": return "" else: return '-args \'["%s"]\'' % arg.replace('"', '\\\"').replace('\'', '\'"\'"\'') def format_repo(mode, repository, name): if mode == ESTARGZ_MODE: return "%s/%s-esgz" % (repository, name) elif mode == ESTARGZ_NOOPT_MODE: return "%s/%s-esgz-noopt" % (repository, name) elif mode == ZSTDCHUNKED_MODE: return "%s/%s-zstdchunked" % (repository, name) else: return "%s/%s-org" % (repository, name) class RunArgs: def __init__(self, env={}, arg='', stdin='', stdin_sh='sh', waitline='', mount=[]): self.env = env self.arg = arg self.stdin = stdin self.stdin_sh = stdin_sh self.waitline = waitline self.mount = mount class Bench: def __init__(self, name, category='other'): self.name = name self.category = category def __str__(self): return json.dumps(self.__dict__) class BenchRunner: ECHO_HELLO = set(['alpine:3.15.3', 'nixos/nix:2.3.12', 'fedora:35', 'ubuntu:22.04']) CMD_ARG_WAIT = {'rethinkdb:2.4.1': RunArgs(waitline='Server ready'), 'glassfish:4.1-jdk8': RunArgs(waitline='Running GlassFish'), 'drupal:9.3.9': RunArgs(waitline='apache2 -D FOREGROUND'), 'jenkins:2.60.3': RunArgs(waitline='Jenkins is fully up and running'), 'redis:6.2.6': RunArgs(waitline='Ready to accept connections'), 'tomcat:10.1.0-jdk17-openjdk-bullseye': RunArgs(waitline='Server startup'), 'postgres:14.2': RunArgs(waitline='database system is ready to accept connections', env={'POSTGRES_PASSWORD': 'abc'}), 'mariadb:10.7.3': RunArgs(waitline='mysqld: ready for connections', env={'MYSQL_ROOT_PASSWORD': 'abc'}), 'wordpress:5.9.2': RunArgs(waitline='apache2 -D FOREGROUND'), 'php:8.1.4-apache-bullseye': RunArgs(waitline='apache2 -D FOREGROUND'), 'rabbitmq:3.9.14': RunArgs(waitline='Server startup complete'), 'elasticsearch:8.1.1': RunArgs(waitline='"started"', mount=[('elasticsearch/elasticsearch.yml', '/usr/share/elasticsearch/config/elasticsearch.yml')]), } CMD_STDIN = {'php:8.1.4': RunArgs(stdin='php -r "echo \\\"hello\\n\\\";"; exit\n'), 'gcc:11.2.0': RunArgs(stdin='cd /src; gcc main.c; ./a.out; exit\n', mount=[('gcc', '/src')]), 'golang:1.18': RunArgs(stdin='cd /go/src; go run main.go; exit\n', mount=[('go', '/go/src')]), 'jruby:9.3.4': RunArgs(stdin='jruby -e "puts \\\"hello\\\""; exit\n'), 'r-base:4.1.3': RunArgs(stdin='sprintf("hello")\nq()\n', stdin_sh='R --no-save'), } CMD_ARG = {'perl:5.34.1': RunArgs(arg='perl -e \'print("hello\\n")\''), 'python:3.10': RunArgs(arg='python -c \'print("hello")\''), 'pypy:3.9': RunArgs(arg='pypy3 -c \'print("hello")\''), 'node:17.8.0': RunArgs(arg='node -e \'console.log("hello")\''), } # complete listing ALL = dict([(b.name, b) for b in [Bench('alpine:3.15.3', 'distro'), Bench('fedora:35', 'distro'), Bench('ubuntu:22.04', 'distro'), Bench('rethinkdb:2.4.1', 'database'), Bench('postgres:14.2', 'database'), Bench('redis:6.2.6', 'database'), Bench('mariadb:10.7.3', 'database'), Bench('python:3.10', 'language'), Bench('golang:1.18', 'language'), Bench('gcc:11.2.0', 'language'), Bench('jruby:9.3.4', 'language'), Bench('perl:5.34.1', 'language'), Bench('php:8.1.4', 'language'), Bench('pypy:3.9', 'language'), Bench('r-base:4.1.3', 'language'), Bench('drupal:9.3.9'), Bench('jenkins:2.60.3'), Bench('node:17.8.0'), Bench('tomcat:10.1.0-jdk17-openjdk-bullseye', 'web-server'), Bench('wordpress:5.9.2', 'web-server'), Bench('php:8.1.4-apache-bullseye', 'web-server'), Bench('rabbitmq:3.9.14'), Bench('elasticsearch:8.1.1'), # needs "--srcrepository=docker.io" Bench('nixos/nix:2.3.12'), ]]) def __init__(self, repository='docker.io/library', srcrepository='docker.io/library', mode=LEGACY_MODE, optimizer=DEFAULT_OPTIMIZER, puller=DEFAULT_PULLER, pusher=DEFAULT_PUSHER, runtime="containerd", profile=0): if runtime == "containerd": self.controller = ContainerdController(mode == ESTARGZ_NOOPT_MODE or mode == ESTARGZ_MODE or mode == ZSTDCHUNKED_MODE) elif runtime == "podman": self.controller = PodmanController() else: print ('Unknown runtime mode: '+runtime) exit(1) self.repository = repository self.srcrepository = srcrepository self.mode = mode self.optimizer = optimizer self.puller = puller self.pusher = pusher profile = int(profile) if profile > 0: self.controller.start_profile(profile) def cleanup(self, cid, bench): self.controller.cleanup(cid, self.fully_qualify(bench.name)) def fully_qualify(self, repo): return format_repo(self.mode, self.repository, repo) def run_task(self, cid): cmd = self.controller.task_start_cmd(cid) startrun = time.time() rc = os.system(cmd) runtime = time.time() - startrun assert(rc == 0) return runtime def run_echo_hello(self, image, cid): cmd = self.controller.create_echo_hello_cmd(image, cid) startcreate = time.time() rc = os.system(cmd) createtime = time.time() - startcreate assert(rc == 0) return createtime, self.run_task(cid) def run_cmd_arg(self, image, cid, runargs): assert(len(runargs.mount) == 0) cmd = self.controller.create_cmd_arg_cmd(image, cid, runargs) startcreate = time.time() rc = os.system(cmd) createtime = time.time() - startcreate assert(rc == 0) return createtime, self.run_task(cid) def run_cmd_arg_wait(self, image, cid, runargs): cmd = self.controller.create_cmd_arg_wait_cmd(image, cid, runargs) startcreate = time.time() rc = os.system(cmd) createtime = time.time() - startcreate assert(rc == 0) cmd = self.controller.task_start_cmd(cid) runtime = 0 startrun = time.time() # line buffer output p = subprocess.Popen(cmd, shell=True, bufsize=1, stderr=subprocess.STDOUT, stdout=subprocess.PIPE) while True: l = p.stdout.readline().decode("utf-8") if l == '': continue print ('out: ' + l.strip()) # are we done? if l.find(runargs.waitline) >= 0: runtime = time.time() - startrun # cleanup print ('DONE') cmd = self.controller.task_kill_cmd(cid) rc = os.system(cmd) assert(rc == 0) break p.wait() return createtime, runtime def run_cmd_stdin(self, image, cid, runargs): cmd = self.controller.create_cmd_stdin_cmd(image, cid, runargs) startcreate = time.time() rc = os.system(cmd) createtime = time.time() - startcreate assert(rc == 0) cmd = self.controller.task_start_cmd(cid) startrun = time.time() p = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) print (runargs.stdin) out, _ = p.communicate(runargs.stdin.encode()) runtime = time.time() - startrun print (out) assert(p.returncode == 0) return createtime, runtime def run(self, bench, cid): name = bench.name image = self.fully_qualify(bench.name) print ("Pulling the image...") pullcmd = self.controller.pull_cmd(image) startpull = time.time() rc = os.system(pullcmd) assert(rc == 0) pulltime = time.time() - startpull runtime = 0 createtime = 0 if name in BenchRunner.ECHO_HELLO: createtime, runtime = self.run_echo_hello(image=image, cid=cid) elif name in BenchRunner.CMD_ARG: createtime, runtime = self.run_cmd_arg(image=image, cid=cid, runargs=BenchRunner.CMD_ARG[name]) elif name in BenchRunner.CMD_ARG_WAIT: createtime, runtime = self.run_cmd_arg_wait(image=image, cid=cid, runargs=BenchRunner.CMD_ARG_WAIT[name]) elif name in BenchRunner.CMD_STDIN: createtime, runtime = self.run_cmd_stdin(image=image, cid=cid, runargs=BenchRunner.CMD_STDIN[name]) else: print ('Unknown bench: '+name) exit(1) return pulltime, createtime, runtime def convert_echo_hello(self, src, dest, option): period=10 cmd = ('%s %s -cni -period %s -entrypoint \'["/bin/sh", "-c"]\' -args \'["echo hello"]\' %s %s' % (self.optimizer, option, period, src, dest)) print (cmd) rc = os.system(cmd) assert(rc == 0) def convert_cmd_arg(self, src, dest, runargs, option): period = 30 assert(len(runargs.mount) == 0) entry = "" if runargs.arg != "": # FIXME: this is naive... entry = '-entrypoint \'["/bin/sh", "-c"]\'' cmd = ('%s %s -cni -period %s %s %s %s %s' % (self.optimizer, option, period, entry, genargs_for_optimization(runargs.arg), src, dest)) print (cmd) rc = os.system(cmd) assert(rc == 0) def convert_cmd_arg_wait(self, src, dest, runargs, option): mounts = '' for a,b in runargs.mount: a = os.path.join(os.path.dirname(os.path.abspath(__file__)), a) a = tmp_copy(a) mounts += '--mount type=bind,src=%s,dst=%s,options=rbind ' % (a,b) period = 90 env = ' '.join(['-env %s=%s' % (k,v) for k,v in runargs.env.items()]) cmd = ('%s %s -cni -period %s %s %s %s %s %s' % (self.optimizer, option, period, mounts, env, genargs_for_optimization(runargs.arg), src, dest)) print (cmd) rc = os.system(cmd) assert(rc == 0) def convert_cmd_stdin(self, src, dest, runargs, option): mounts = '' for a,b in runargs.mount: a = os.path.join(os.path.dirname(os.path.abspath(__file__)), a) a = tmp_copy(a) mounts += '--mount type=bind,src=%s,dst=%s,options=rbind ' % (a,b) period = 60 cmd = ('%s %s -i -cni -period %s %s -entrypoint \'["/bin/sh", "-c"]\' %s %s %s' % (self.optimizer, option, period, mounts, genargs_for_optimization(runargs.stdin_sh), src, dest)) print (cmd) p = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE) print (runargs.stdin) out,_ = p.communicate(runargs.stdin.encode()) print (out) p.wait() assert(p.returncode == 0) def push_img(self, dest): cmd = '%s %s' % (self.pusher, dest) print (cmd) rc = os.system(cmd) assert(rc == 0) def pull_img(self, src): cmd = '%s %s' % (self.puller, src) print (cmd) rc = os.system(cmd) assert(rc == 0) def copy_img(self, src, dest): cmd = 'crane copy %s %s' % (src, dest) print (cmd) rc = os.system(cmd) assert(rc == 0) def convert_and_push_img(self, src, dest): self.pull_img(src) cmd = '%s --no-optimize %s %s' % (self.optimizer, src, dest) print (cmd) rc = os.system(cmd) assert(rc == 0) self.push_img(dest) def optimize_img(self, name, src, dest, option): self.pull_img(src) if name in BenchRunner.ECHO_HELLO: self.convert_echo_hello(src=src, dest=dest, option=option) elif name in BenchRunner.CMD_ARG: self.convert_cmd_arg(src=src, dest=dest, runargs=BenchRunner.CMD_ARG[name], option=option) elif name in BenchRunner.CMD_ARG_WAIT: self.convert_cmd_arg_wait(src=src, dest=dest, runargs=BenchRunner.CMD_ARG_WAIT[name], option=option) elif name in BenchRunner.CMD_STDIN: self.convert_cmd_stdin(src=src, dest=dest, runargs=BenchRunner.CMD_STDIN[name], option=option) else: print ('Unknown bench: '+name) exit(1) self.push_img(dest) def prepare(self, bench): name = bench.name src = '%s/%s' % (self.srcrepository, name) self.copy_img(src=src, dest=format_repo(LEGACY_MODE, self.repository, name)) self.convert_and_push_img(src=src, dest=format_repo(ESTARGZ_NOOPT_MODE, self.repository, name)) self.optimize_img(name=name, src=src, dest=format_repo(ESTARGZ_MODE, self.repository, name), option="") self.optimize_img(name=name, src=src, dest=format_repo(ZSTDCHUNKED_MODE, self.repository, name), option="--zstdchunked") def operation(self, op, bench, cid): if op == 'run': return self.run(bench, cid) elif op == 'prepare': self.prepare(bench) return 0, 0, 0 else: print ('Unknown operation: '+op) exit(1) class ContainerdController: def __init__(self, is_lazypull=False): self.is_lazypull = is_lazypull def pull_cmd(self, image): base_cmd = "%s i pull" % CTR if self.is_lazypull: base_cmd = "ctr-remote i rpull" cmd = '%s %s' % (base_cmd, image) print (cmd) return cmd def create_echo_hello_cmd(self, image, cid): snapshotter_opt = "" if self.is_lazypull: snapshotter_opt = "--snapshotter=stargz" cmd = '%s c create --net-host %s -- %s %s echo hello' % (CTR, snapshotter_opt, image, cid) print (cmd) return cmd def create_cmd_arg_cmd(self, image, cid, runargs): snapshotter_opt = "" if self.is_lazypull: snapshotter_opt = "--snapshotter=stargz" cmd = '%s c create --net-host %s ' % (CTR, snapshotter_opt) cmd += '-- %s %s ' % (image, cid) cmd += runargs.arg print (cmd) return cmd def create_cmd_arg_wait_cmd(self, image, cid, runargs): snapshotter_opt = "" if self.is_lazypull: snapshotter_opt = "--snapshotter=stargz" env = ' '.join(['--env %s=%s' % (k,v) for k,v in runargs.env.items()]) cmd = '%s c create --net-host %s %s '% (CTR, snapshotter_opt, env) for a,b in runargs.mount: a = os.path.join(os.path.dirname(os.path.abspath(__file__)), a) a = tmp_copy(a) cmd += '--mount type=bind,src=%s,dst=%s,options=rbind ' % (a,b) cmd += '-- %s %s %s' % (image, cid, runargs.arg) print (cmd) return cmd def create_cmd_stdin_cmd(self, image, cid, runargs): snapshotter_opt = "" if self.is_lazypull: snapshotter_opt = "--snapshotter=stargz" cmd = '%s c create --net-host %s ' % (CTR, snapshotter_opt) for a,b in runargs.mount: a = os.path.join(os.path.dirname(os.path.abspath(__file__)), a) a = tmp_copy(a) cmd += '--mount type=bind,src=%s,dst=%s,options=rbind ' % (a,b) cmd += '-- %s %s ' % (image, cid) if runargs.stdin_sh: cmd += runargs.stdin_sh # e.g., sh -c print (cmd) return cmd def task_start_cmd(self, cid): cmd = '%s t start %s' % (CTR, cid) print (cmd) return cmd def task_kill_cmd(self, cid): cmd = '%s t kill -s 9 %s' % (CTR, cid) print (cmd) return cmd def cleanup(self, name, image): print ("Cleaning up environment...") cmd = '%s t kill -s 9 %s' % (CTR, name) print (cmd) rc = os.system(cmd) # sometimes containers already exit. we ignore the failure. cmd = '%s c rm %s' % (CTR, name) print (cmd) rc = os.system(cmd) assert(rc == 0) cmd = '%s image rm %s' % (CTR, image) print (cmd) rc = os.system(cmd) assert(rc == 0) def start_profile(self, seconds): out = open('/tmp/hello-bench-output/profile-%d.out' % (random.randint(1,1000000)), 'w') p = subprocess.Popen( [CTR, 'pprof', '-d', '/run/containerd-stargz-grpc/debug.sock', 'profile', '-s', '%ds' % (seconds)], stdout=out ) class PodmanController: def pull_cmd(self, image): cmd = '%s pull docker://%s' % (PODMAN, image) print (cmd) return cmd def create_echo_hello_cmd(self, image, cid): cmd = '%s create --name %s docker://%s echo hello' % (PODMAN, cid, image) print (cmd) return cmd def create_cmd_arg_cmd(self, image, cid, runargs): cmd = '%s create --name %s docker://%s ' % (PODMAN, cid, image) cmd += runargs.arg print (cmd) return cmd def create_cmd_arg_wait_cmd(self, image, cid, runargs): env = ' '.join(['--env %s=%s' % (k,v) for k,v in runargs.env.items()]) cmd = '%s create %s --name %s '% (PODMAN, env, cid) for a,b in runargs.mount: a = os.path.join(os.path.dirname(os.path.abspath(__file__)), a) a = tmp_copy(a) cmd += '--mount type=bind,src=%s,dst=%s ' % (a,b) cmd += ' docker://%s %s ' % (image, runargs.arg) print (cmd) return cmd def create_cmd_stdin_cmd(self, image, cid, runargs): cmd = '%s create -i ' % PODMAN for a,b in runargs.mount: a = os.path.join(os.path.dirname(os.path.abspath(__file__)), a) a = tmp_copy(a) cmd += '--mount type=bind,src=%s,dst=%s ' % (a,b) cmd += '--name %s docker://%s ' % (cid, image) if runargs.stdin_sh: cmd += runargs.stdin_sh # e.g., sh -c print (cmd) return cmd def task_start_cmd(self, cid): cmd = '%s start -a %s' % (PODMAN, cid) print (cmd) return cmd def task_kill_cmd(self, cid): cmd = '%s kill -s 9 %s' % (PODMAN, cid) print (cmd) return cmd def cleanup(self, name, image): print ("Cleaning up environment...") cmd = '%s kill -s 9 %s' % (PODMAN, name) print (cmd) rc = os.system(cmd) # sometimes containers already exit. we ignore the failure. cmd = '%s rm %s' % (PODMAN, name) print (cmd) rc = os.system(cmd) assert(rc == 0) cmd = '%s image rm %s' % (PODMAN, image) print (cmd) rc = os.system(cmd) assert(rc == 0) cmd = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../reboot_store.sh') print (cmd) rc = os.system(cmd) assert(rc == 0) def main(): if len(sys.argv) == 1: print ('Usage: bench.py [OPTIONS] [BENCHMARKS]') print ('OPTIONS:') print ('--repository=') print ('--srcrepository=') print ('--all') print ('--list') print ('--list-json') print ('--experiments') print ('--profile=') print ('--runtime') print ('--op=(prepare|run)') print ('--mode=(%s|%s|%s)' % (LEGACY_MODE, ESTARGZ_NOOPT_MODE, ESTARGZ_MODE, ZSTDCHUNKED_MODE)) exit(1) benches = [] kvargs = {} # parse args for arg in sys.argv[1:]: if arg.startswith('--'): parts = arg[2:].split('=') if len(parts) == 2: kvargs[parts[0]] = parts[1] elif parts[0] == 'all': benches.extend(BenchRunner.ALL.values()) elif parts[0] == 'list': template = '%-16s\t%-20s' print (template % ('CATEGORY', 'NAME')) for b in sorted(BenchRunner.ALL.values(), key=lambda b:(b.category, b.name)): print (template % (b.category, b.name)) elif parts[0] == 'list-json': print (json.dumps([b.__dict__ for b in BenchRunner.ALL.values()])) else: benches.append(BenchRunner.ALL[arg]) op = kvargs.pop('op', 'run') trytimes = int(kvargs.pop('experiments', '1')) if not op == "run": trytimes = 1 experiments = range(trytimes) profile = int(kvargs.get('profile', 0)) if profile > 0: experiments = itertools.count(start=0) # run benchmarks runner = BenchRunner(**kvargs) for bench in benches: cid = '%s_bench_%d' % (bench.name.replace(':', '-').replace('/', '-'), random.randint(1,1000000)) elapsed_times = [] pull_times = [] create_times = [] run_times = [] for_start = time.time() for i in experiments: start = time.time() pulltime, createtime, runtime = runner.operation(op, bench, cid) elapsed = time.time() - start if op == "run": runner.cleanup(cid, bench) elapsed_times.append(elapsed) pull_times.append(pulltime) create_times.append(createtime) run_times.append(runtime) print ('ITERATION %s:' % i) print ('elapsed %s' % elapsed) print ('pull %s' % pulltime) print ('create %s' % createtime) print ('run %s' % runtime) if profile > 0 and time.time() - for_start > profile: break row = {'mode':'%s' % runner.mode, 'repo':bench.name, 'bench':bench.name, 'elapsed':sum(elapsed_times) / len(elapsed_times), 'elapsed_pull':sum(pull_times) / len(pull_times), 'elapsed_create':sum(create_times) / len(create_times), 'elapsed_run':sum(run_times) / len(run_times)} js = json.dumps(row) print ('%s%s,' % (BENCHMARKOUT_MARK, js)) sys.stdout.flush() if __name__ == '__main__': main() exit(0)