blob: e1268e2ecbbef8c608c2e06b62f0202043b91237 [file] [log] [blame]
Jan Tattermuschb2758442016-03-28 09:32:20 -07001#!/usr/bin/env python2.7
2# Copyright 2016, Google Inc.
3# All rights reserved.
4#
5# Redistribution and use in source and binary forms, with or without
6# modification, are permitted provided that the following conditions are
7# met:
8#
9# * Redistributions of source code must retain the above copyright
10# notice, this list of conditions and the following disclaimer.
11# * Redistributions in binary form must reproduce the above
12# copyright notice, this list of conditions and the following disclaimer
13# in the documentation and/or other materials provided with the
14# distribution.
15# * Neither the name of Google Inc. nor the names of its
16# contributors may be used to endorse or promote products derived from
17# this software without specific prior written permission.
18#
19# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
31"""Run performance tests locally or remotely."""
32
33import argparse
Jan Tattermuschbb1a4532016-03-30 18:04:01 -070034import itertools
Jan Tattermuschb2758442016-03-28 09:32:20 -070035import jobset
36import multiprocessing
37import os
38import subprocess
39import sys
40import tempfile
41import time
42import uuid
Craig Tillerd92d5c52016-04-04 13:49:29 -070043import performance.scenario_config as scenario_config
Jan Tattermuschb2758442016-03-28 09:32:20 -070044
45
46_ROOT = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), '../..'))
47os.chdir(_ROOT)
48
49
50_REMOTE_HOST_USERNAME = 'jenkins'
51
52
Jan Tattermuschb2758442016-03-28 09:32:20 -070053class QpsWorkerJob:
54 """Encapsulates a qps worker server job."""
55
Jan Tattermuschbb1a4532016-03-30 18:04:01 -070056 def __init__(self, spec, language, host_and_port):
Jan Tattermuschb2758442016-03-28 09:32:20 -070057 self._spec = spec
Jan Tattermuschbb1a4532016-03-30 18:04:01 -070058 self.language = language
Jan Tattermuschb2758442016-03-28 09:32:20 -070059 self.host_and_port = host_and_port
60 self._job = jobset.Job(spec, bin_hash=None, newline_on_success=True, travis=True, add_env={})
61
62 def is_running(self):
63 """Polls a job and returns True if given job is still running."""
64 return self._job.state(jobset.NoCache()) == jobset._RUNNING
65
66 def kill(self):
67 return self._job.kill()
68
69
Jan Tattermuschbb1a4532016-03-30 18:04:01 -070070def create_qpsworker_job(language, shortname=None,
71 port=10000, remote_host=None):
Jan Tattermuschb2758442016-03-28 09:32:20 -070072 # TODO: support more languages
Jan Tattermuschbb1a4532016-03-30 18:04:01 -070073 cmdline = language.worker_cmdline() + ['--driver_port=%s' % port]
Jan Tattermuschb2758442016-03-28 09:32:20 -070074 if remote_host:
75 user_at_host = '%s@%s' % (_REMOTE_HOST_USERNAME, remote_host)
Jan Tattermuschbb1a4532016-03-30 18:04:01 -070076 cmdline = ['ssh',
77 str(user_at_host),
78 'cd ~/performance_workspace/grpc/ && %s' % ' '.join(cmdline)]
Jan Tattermuschb2758442016-03-28 09:32:20 -070079 host_and_port='%s:%s' % (remote_host, port)
80 else:
81 host_and_port='localhost:%s' % port
82
83 jobspec = jobset.JobSpec(
Jan Tattermuschbb1a4532016-03-30 18:04:01 -070084 cmdline=cmdline,
85 shortname=shortname,
86 timeout_seconds=15*60)
87 return QpsWorkerJob(jobspec, language, host_and_port)
Jan Tattermuschb2758442016-03-28 09:32:20 -070088
89
90def create_scenario_jobspec(scenario_name, driver_args, workers, remote_host=None):
91 """Runs one scenario using QPS driver."""
92 # setting QPS_WORKERS env variable here makes sure it works with SSH too.
93 cmd = 'QPS_WORKERS="%s" bins/opt/qps_driver ' % ','.join(workers)
94 cmd += ' '.join(driver_args)
95 if remote_host:
96 user_at_host = '%s@%s' % (_REMOTE_HOST_USERNAME, remote_host)
97 cmd = 'ssh %s "cd ~/performance_workspace/grpc/ && %s"' % (user_at_host, cmd)
98
99 return jobset.JobSpec(
100 cmdline=[cmd],
101 shortname='qps_driver.%s' % scenario_name,
102 timeout_seconds=3*60,
103 shell=True,
104 verbose_success=True)
105
106
107def archive_repo():
108 """Archives local version of repo including submodules."""
109 # TODO: also archive grpc-go and grpc-java repos
110 archive_job = jobset.JobSpec(
111 cmdline=['tar', '-cf', '../grpc.tar', '../grpc/'],
112 shortname='archive_repo',
113 timeout_seconds=3*60)
114
115 jobset.message('START', 'Archiving local repository.', do_newline=True)
116 num_failures, _ = jobset.run(
117 [archive_job], newline_on_success=True, maxjobs=1)
118 if num_failures == 0:
119 jobset.message('SUCCESS',
120 'Archive with local repository create successfully.',
121 do_newline=True)
122 else:
123 jobset.message('FAILED', 'Failed to archive local repository.',
124 do_newline=True)
125 sys.exit(1)
126
127
128def prepare_remote_hosts(hosts):
129 """Prepares remote hosts."""
130 prepare_jobs = []
131 for host in hosts:
132 user_at_host = '%s@%s' % (_REMOTE_HOST_USERNAME, host)
133 prepare_jobs.append(
134 jobset.JobSpec(
135 cmdline=['tools/run_tests/performance/remote_host_prepare.sh'],
136 shortname='remote_host_prepare.%s' % host,
137 environ = {'USER_AT_HOST': user_at_host},
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700138 timeout_seconds=5*60))
Jan Tattermuschb2758442016-03-28 09:32:20 -0700139 jobset.message('START', 'Preparing remote hosts.', do_newline=True)
140 num_failures, _ = jobset.run(
141 prepare_jobs, newline_on_success=True, maxjobs=10)
142 if num_failures == 0:
143 jobset.message('SUCCESS',
144 'Remote hosts ready to start build.',
145 do_newline=True)
146 else:
147 jobset.message('FAILED', 'Failed to prepare remote hosts.',
148 do_newline=True)
149 sys.exit(1)
150
151
Craig Tillerd92d5c52016-04-04 13:49:29 -0700152def build_on_remote_hosts(hosts, languages=scenario_config.LANGUAGES.keys(), build_local=False):
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700153 """Builds performance worker on remote hosts (and maybe also locally)."""
Jan Tattermuschb2758442016-03-28 09:32:20 -0700154 build_timeout = 15*60
155 build_jobs = []
156 for host in hosts:
157 user_at_host = '%s@%s' % (_REMOTE_HOST_USERNAME, host)
158 build_jobs.append(
159 jobset.JobSpec(
Craig Tiller7797e3f2016-04-01 07:41:05 -0700160 cmdline=['tools/run_tests/performance/remote_host_build.sh'] + languages,
Jan Tattermuschb2758442016-03-28 09:32:20 -0700161 shortname='remote_host_build.%s' % host,
162 environ = {'USER_AT_HOST': user_at_host, 'CONFIG': 'opt'},
163 timeout_seconds=build_timeout))
164 if build_local:
165 # Build locally as well
166 build_jobs.append(
167 jobset.JobSpec(
Craig Tiller7797e3f2016-04-01 07:41:05 -0700168 cmdline=['tools/run_tests/performance/build_performance.sh'] + languages,
Jan Tattermuschb2758442016-03-28 09:32:20 -0700169 shortname='local_build',
170 environ = {'CONFIG': 'opt'},
171 timeout_seconds=build_timeout))
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700172 jobset.message('START', 'Building.', do_newline=True)
Jan Tattermuschb2758442016-03-28 09:32:20 -0700173 num_failures, _ = jobset.run(
174 build_jobs, newline_on_success=True, maxjobs=10)
175 if num_failures == 0:
176 jobset.message('SUCCESS',
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700177 'Built successfully.',
Jan Tattermuschb2758442016-03-28 09:32:20 -0700178 do_newline=True)
179 else:
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700180 jobset.message('FAILED', 'Build failed.',
Jan Tattermuschb2758442016-03-28 09:32:20 -0700181 do_newline=True)
182 sys.exit(1)
183
184
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700185def start_qpsworkers(languages, worker_hosts):
Jan Tattermuschb2758442016-03-28 09:32:20 -0700186 """Starts QPS workers as background jobs."""
187 if not worker_hosts:
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700188 # run two workers locally (for each language)
Jan Tattermuschb2758442016-03-28 09:32:20 -0700189 workers=[(None, 10000), (None, 10010)]
190 elif len(worker_hosts) == 1:
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700191 # run two workers on the remote host (for each language)
Jan Tattermuschb2758442016-03-28 09:32:20 -0700192 workers=[(worker_hosts[0], 10000), (worker_hosts[0], 10010)]
193 else:
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700194 # run one worker per each remote host (for each language)
Jan Tattermuschb2758442016-03-28 09:32:20 -0700195 workers=[(worker_host, 10000) for worker_host in worker_hosts]
196
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700197 return [create_qpsworker_job(language,
198 shortname= 'qps_worker_%s_%s' % (language,
199 worker_idx),
200 port=worker[1] + language.worker_port_offset(),
Jan Tattermuschb2758442016-03-28 09:32:20 -0700201 remote_host=worker[0])
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700202 for language in languages
203 for worker_idx, worker in enumerate(workers)]
Jan Tattermuschb2758442016-03-28 09:32:20 -0700204
205
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700206def create_scenarios(languages, workers_by_lang, remote_host=None):
Jan Tattermuschb2758442016-03-28 09:32:20 -0700207 """Create jobspecs for scenarios to run."""
208 scenarios = []
209 for language in languages:
210 for scenario_name, driver_args in language.scenarios().iteritems():
211 scenario = create_scenario_jobspec(scenario_name,
212 driver_args,
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700213 workers_by_lang[str(language)],
Jan Tattermuschb2758442016-03-28 09:32:20 -0700214 remote_host=remote_host)
215 scenarios.append(scenario)
216
217 # the very last scenario requests shutting down the workers.
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700218 all_workers = [worker
219 for workers in workers_by_lang.values()
220 for worker in workers]
Jan Tattermuschb2758442016-03-28 09:32:20 -0700221 scenarios.append(create_scenario_jobspec('quit_workers',
222 ['--quit=true'],
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700223 all_workers,
Jan Tattermuschb2758442016-03-28 09:32:20 -0700224 remote_host=remote_host))
225 return scenarios
226
227
228def finish_qps_workers(jobs):
229 """Waits for given jobs to finish and eventually kills them."""
230 retries = 0
231 while any(job.is_running() for job in jobs):
232 for job in qpsworker_jobs:
233 if job.is_running():
234 print 'QPS worker "%s" is still running.' % job.host_and_port
235 if retries > 10:
236 print 'Killing all QPS workers.'
237 for job in jobs:
238 job.kill()
239 retries += 1
240 time.sleep(3)
241 print 'All QPS workers finished.'
242
243
244argp = argparse.ArgumentParser(description='Run performance tests.')
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700245argp.add_argument('-l', '--language',
Craig Tillerd92d5c52016-04-04 13:49:29 -0700246 choices=['all'] + sorted(scenario_config.LANGUAGES.keys()),
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700247 nargs='+',
248 default=['all'],
249 help='Languages to benchmark.')
Jan Tattermuschb2758442016-03-28 09:32:20 -0700250argp.add_argument('--remote_driver_host',
251 default=None,
252 help='Run QPS driver on given host. By default, QPS driver is run locally.')
253argp.add_argument('--remote_worker_host',
254 nargs='+',
255 default=[],
256 help='Worker hosts where to start QPS workers.')
257
258args = argp.parse_args()
259
Craig Tillerd92d5c52016-04-04 13:49:29 -0700260languages = set(scenario_config.LANGUAGES[l]
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700261 for l in itertools.chain.from_iterable(
Craig Tillerd92d5c52016-04-04 13:49:29 -0700262 scenario_config.LANGUAGES.iterkeys() if x == 'all' else [x]
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700263 for x in args.language))
264
Jan Tattermuschb2758442016-03-28 09:32:20 -0700265# Put together set of remote hosts where to run and build
266remote_hosts = set()
267if args.remote_worker_host:
268 for host in args.remote_worker_host:
269 remote_hosts.add(host)
270if args.remote_driver_host:
271 remote_hosts.add(args.remote_driver_host)
272
273if remote_hosts:
274 archive_repo()
275 prepare_remote_hosts(remote_hosts)
276
277build_local = False
278if not args.remote_driver_host:
279 build_local = True
Craig Tiller7797e3f2016-04-01 07:41:05 -0700280build_on_remote_hosts(remote_hosts, languages=[str(l) for l in languages], build_local=build_local)
Jan Tattermuschb2758442016-03-28 09:32:20 -0700281
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700282qpsworker_jobs = start_qpsworkers(languages, args.remote_worker_host)
Jan Tattermuschb2758442016-03-28 09:32:20 -0700283
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700284# get list of worker addresses for each language.
285worker_addresses = dict([(str(language), []) for language in languages])
286for job in qpsworker_jobs:
287 worker_addresses[str(job.language)].append(job.host_and_port)
Jan Tattermuschb2758442016-03-28 09:32:20 -0700288
289try:
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700290 scenarios = create_scenarios(languages,
291 workers_by_lang=worker_addresses,
Jan Tattermuschb2758442016-03-28 09:32:20 -0700292 remote_host=args.remote_driver_host)
293 if not scenarios:
294 raise Exception('No scenarios to run')
295
296 jobset.message('START', 'Running scenarios.', do_newline=True)
297 num_failures, _ = jobset.run(
298 scenarios, newline_on_success=True, maxjobs=1)
299 if num_failures == 0:
300 jobset.message('SUCCESS',
301 'All scenarios finished successfully.',
302 do_newline=True)
303 else:
304 jobset.message('FAILED', 'Some of the scenarios failed.',
305 do_newline=True)
306 sys.exit(1)
307finally:
308 finish_qps_workers(qpsworker_jobs)