blob: b3729acd9f5673060c46b8bb31e022a11302768e [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
Craig Tiller0bda0b32016-03-03 12:51:53 -080036import json
Jan Tattermuschb2758442016-03-28 09:32:20 -070037import multiprocessing
38import os
Craig Tiller0bda0b32016-03-03 12:51:53 -080039import pipes
Jan Tattermusch38becc22016-04-14 08:00:35 -070040import re
Jan Tattermuschb2758442016-03-28 09:32:20 -070041import subprocess
42import sys
43import tempfile
44import time
Jan Tattermuschee9032c2016-04-14 08:35:51 -070045import traceback
Jan Tattermuschb2758442016-03-28 09:32:20 -070046import uuid
Craig Tillerd92d5c52016-04-04 13:49:29 -070047import performance.scenario_config as scenario_config
Jan Tattermuschb2758442016-03-28 09:32:20 -070048
49
50_ROOT = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), '../..'))
51os.chdir(_ROOT)
52
53
54_REMOTE_HOST_USERNAME = 'jenkins'
55
56
Jan Tattermuschb2758442016-03-28 09:32:20 -070057class QpsWorkerJob:
58 """Encapsulates a qps worker server job."""
59
Jan Tattermuschbb1a4532016-03-30 18:04:01 -070060 def __init__(self, spec, language, host_and_port):
Jan Tattermuschb2758442016-03-28 09:32:20 -070061 self._spec = spec
Jan Tattermuschbb1a4532016-03-30 18:04:01 -070062 self.language = language
Jan Tattermuschb2758442016-03-28 09:32:20 -070063 self.host_and_port = host_and_port
64 self._job = jobset.Job(spec, bin_hash=None, newline_on_success=True, travis=True, add_env={})
65
66 def is_running(self):
67 """Polls a job and returns True if given job is still running."""
68 return self._job.state(jobset.NoCache()) == jobset._RUNNING
69
70 def kill(self):
71 return self._job.kill()
72
73
Jan Tattermuschbb1a4532016-03-30 18:04:01 -070074def create_qpsworker_job(language, shortname=None,
75 port=10000, remote_host=None):
Jan Tattermuschb2758442016-03-28 09:32:20 -070076 # TODO: support more languages
Jan Tattermuschbb1a4532016-03-30 18:04:01 -070077 cmdline = language.worker_cmdline() + ['--driver_port=%s' % port]
Jan Tattermuschb2758442016-03-28 09:32:20 -070078 if remote_host:
79 user_at_host = '%s@%s' % (_REMOTE_HOST_USERNAME, remote_host)
Jan Tattermuschbb1a4532016-03-30 18:04:01 -070080 cmdline = ['ssh',
81 str(user_at_host),
82 'cd ~/performance_workspace/grpc/ && %s' % ' '.join(cmdline)]
Jan Tattermuschb2758442016-03-28 09:32:20 -070083 host_and_port='%s:%s' % (remote_host, port)
84 else:
85 host_and_port='localhost:%s' % port
86
Jan Tattermusch38becc22016-04-14 08:00:35 -070087 # TODO(jtattermusch): with some care, we can calculate the right timeout
88 # of a worker from the sum of warmup + benchmark times for all the scenarios
Jan Tattermuschb2758442016-03-28 09:32:20 -070089 jobspec = jobset.JobSpec(
Jan Tattermuschbb1a4532016-03-30 18:04:01 -070090 cmdline=cmdline,
91 shortname=shortname,
92 timeout_seconds=15*60)
93 return QpsWorkerJob(jobspec, language, host_and_port)
Jan Tattermuschb2758442016-03-28 09:32:20 -070094
95
Craig Tiller0bda0b32016-03-03 12:51:53 -080096def create_scenario_jobspec(scenario_json, workers, remote_host=None):
Jan Tattermuschb2758442016-03-28 09:32:20 -070097 """Runs one scenario using QPS driver."""
98 # setting QPS_WORKERS env variable here makes sure it works with SSH too.
Craig Tiller0bda0b32016-03-03 12:51:53 -080099 cmd = 'QPS_WORKERS="%s" bins/opt/qps_json_driver ' % ','.join(workers)
100 cmd += '--scenarios_json=%s' % pipes.quote(json.dumps({'scenarios': [scenario_json]}))
Jan Tattermuschb2758442016-03-28 09:32:20 -0700101 if remote_host:
102 user_at_host = '%s@%s' % (_REMOTE_HOST_USERNAME, remote_host)
103 cmd = 'ssh %s "cd ~/performance_workspace/grpc/ && %s"' % (user_at_host, cmd)
104
105 return jobset.JobSpec(
106 cmdline=[cmd],
Craig Tiller0bda0b32016-03-03 12:51:53 -0800107 shortname='qps_json_driver.%s' % scenario_json['name'],
108 timeout_seconds=3*60,
109 shell=True,
110 verbose_success=True)
111
112
113def create_quit_jobspec(workers, remote_host=None):
114 """Runs quit using QPS driver."""
115 # setting QPS_WORKERS env variable here makes sure it works with SSH too.
116 cmd = 'QPS_WORKERS="%s" bins/opt/qps_driver --quit' % ','.join(workers)
117 if remote_host:
118 user_at_host = '%s@%s' % (_REMOTE_HOST_USERNAME, remote_host)
119 cmd = 'ssh %s "cd ~/performance_workspace/grpc/ && %s"' % (user_at_host, cmd)
120
121 return jobset.JobSpec(
122 cmdline=[cmd],
123 shortname='qps_driver.quit',
Jan Tattermuschb2758442016-03-28 09:32:20 -0700124 timeout_seconds=3*60,
125 shell=True,
126 verbose_success=True)
127
128
129def archive_repo():
130 """Archives local version of repo including submodules."""
131 # TODO: also archive grpc-go and grpc-java repos
132 archive_job = jobset.JobSpec(
133 cmdline=['tar', '-cf', '../grpc.tar', '../grpc/'],
134 shortname='archive_repo',
135 timeout_seconds=3*60)
136
137 jobset.message('START', 'Archiving local repository.', do_newline=True)
138 num_failures, _ = jobset.run(
139 [archive_job], newline_on_success=True, maxjobs=1)
140 if num_failures == 0:
141 jobset.message('SUCCESS',
142 'Archive with local repository create successfully.',
143 do_newline=True)
144 else:
145 jobset.message('FAILED', 'Failed to archive local repository.',
146 do_newline=True)
147 sys.exit(1)
148
149
150def prepare_remote_hosts(hosts):
151 """Prepares remote hosts."""
152 prepare_jobs = []
153 for host in hosts:
154 user_at_host = '%s@%s' % (_REMOTE_HOST_USERNAME, host)
155 prepare_jobs.append(
156 jobset.JobSpec(
157 cmdline=['tools/run_tests/performance/remote_host_prepare.sh'],
158 shortname='remote_host_prepare.%s' % host,
159 environ = {'USER_AT_HOST': user_at_host},
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700160 timeout_seconds=5*60))
Jan Tattermuschb2758442016-03-28 09:32:20 -0700161 jobset.message('START', 'Preparing remote hosts.', do_newline=True)
162 num_failures, _ = jobset.run(
163 prepare_jobs, newline_on_success=True, maxjobs=10)
164 if num_failures == 0:
165 jobset.message('SUCCESS',
166 'Remote hosts ready to start build.',
167 do_newline=True)
168 else:
169 jobset.message('FAILED', 'Failed to prepare remote hosts.',
170 do_newline=True)
171 sys.exit(1)
172
173
Craig Tillerd92d5c52016-04-04 13:49:29 -0700174def build_on_remote_hosts(hosts, languages=scenario_config.LANGUAGES.keys(), build_local=False):
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700175 """Builds performance worker on remote hosts (and maybe also locally)."""
Jan Tattermuschb2758442016-03-28 09:32:20 -0700176 build_timeout = 15*60
177 build_jobs = []
178 for host in hosts:
179 user_at_host = '%s@%s' % (_REMOTE_HOST_USERNAME, host)
180 build_jobs.append(
181 jobset.JobSpec(
Craig Tiller7797e3f2016-04-01 07:41:05 -0700182 cmdline=['tools/run_tests/performance/remote_host_build.sh'] + languages,
Jan Tattermuschb2758442016-03-28 09:32:20 -0700183 shortname='remote_host_build.%s' % host,
184 environ = {'USER_AT_HOST': user_at_host, 'CONFIG': 'opt'},
185 timeout_seconds=build_timeout))
186 if build_local:
187 # Build locally as well
188 build_jobs.append(
189 jobset.JobSpec(
Craig Tiller7797e3f2016-04-01 07:41:05 -0700190 cmdline=['tools/run_tests/performance/build_performance.sh'] + languages,
Jan Tattermuschb2758442016-03-28 09:32:20 -0700191 shortname='local_build',
192 environ = {'CONFIG': 'opt'},
193 timeout_seconds=build_timeout))
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700194 jobset.message('START', 'Building.', do_newline=True)
Jan Tattermuschb2758442016-03-28 09:32:20 -0700195 num_failures, _ = jobset.run(
196 build_jobs, newline_on_success=True, maxjobs=10)
197 if num_failures == 0:
198 jobset.message('SUCCESS',
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700199 'Built successfully.',
Jan Tattermuschb2758442016-03-28 09:32:20 -0700200 do_newline=True)
201 else:
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700202 jobset.message('FAILED', 'Build failed.',
Jan Tattermuschb2758442016-03-28 09:32:20 -0700203 do_newline=True)
204 sys.exit(1)
205
206
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700207def start_qpsworkers(languages, worker_hosts):
Jan Tattermuschb2758442016-03-28 09:32:20 -0700208 """Starts QPS workers as background jobs."""
209 if not worker_hosts:
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700210 # run two workers locally (for each language)
Jan Tattermuschb2758442016-03-28 09:32:20 -0700211 workers=[(None, 10000), (None, 10010)]
212 elif len(worker_hosts) == 1:
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700213 # run two workers on the remote host (for each language)
Jan Tattermuschb2758442016-03-28 09:32:20 -0700214 workers=[(worker_hosts[0], 10000), (worker_hosts[0], 10010)]
215 else:
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700216 # run one worker per each remote host (for each language)
Jan Tattermuschb2758442016-03-28 09:32:20 -0700217 workers=[(worker_host, 10000) for worker_host in worker_hosts]
218
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700219 return [create_qpsworker_job(language,
220 shortname= 'qps_worker_%s_%s' % (language,
221 worker_idx),
222 port=worker[1] + language.worker_port_offset(),
Jan Tattermuschb2758442016-03-28 09:32:20 -0700223 remote_host=worker[0])
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700224 for language in languages
225 for worker_idx, worker in enumerate(workers)]
Jan Tattermuschb2758442016-03-28 09:32:20 -0700226
227
Jan Tattermusch38becc22016-04-14 08:00:35 -0700228def create_scenarios(languages, workers_by_lang, remote_host=None, regex='.*'):
Jan Tattermuschb2758442016-03-28 09:32:20 -0700229 """Create jobspecs for scenarios to run."""
230 scenarios = []
231 for language in languages:
Craig Tiller0bda0b32016-03-03 12:51:53 -0800232 for scenario_json in language.scenarios():
Jan Tattermusch38becc22016-04-14 08:00:35 -0700233 if re.search(args.regex, scenario_json['name']):
Jan Tattermuschee9032c2016-04-14 08:35:51 -0700234 workers = workers_by_lang[str(language)]
235 # 'SERVER_LANGUAGE' is an indicator for this script to pick
236 # a server in different language. It doesn't belong to the Scenario
237 # schema, so we also need to remove it.
238 custom_server_lang = scenario_json.pop('SERVER_LANGUAGE', None)
239 if custom_server_lang:
240 if not workers_by_lang.get(custom_server_lang, []):
241 print 'Warning: Skipping scenario %s as' % scenario_json['name']
242 print('SERVER_LANGUAGE is set to %s yet the language has '
243 'not been selected with -l' % custom_server_lang)
244 continue
245 for idx in range(0, scenario_json['num_servers']):
246 # replace first X workers by workers of a different language
247 workers[idx] = workers_by_lang[custom_server_lang][idx]
Jan Tattermusch38becc22016-04-14 08:00:35 -0700248 scenario = create_scenario_jobspec(scenario_json,
Jan Tattermuschee9032c2016-04-14 08:35:51 -0700249 workers,
Jan Tattermusch38becc22016-04-14 08:00:35 -0700250 remote_host=remote_host)
251 scenarios.append(scenario)
Jan Tattermuschb2758442016-03-28 09:32:20 -0700252
253 # the very last scenario requests shutting down the workers.
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700254 all_workers = [worker
255 for workers in workers_by_lang.values()
256 for worker in workers]
Craig Tiller0bda0b32016-03-03 12:51:53 -0800257 scenarios.append(create_quit_jobspec(all_workers, remote_host=remote_host))
Jan Tattermuschb2758442016-03-28 09:32:20 -0700258 return scenarios
259
260
261def finish_qps_workers(jobs):
262 """Waits for given jobs to finish and eventually kills them."""
263 retries = 0
264 while any(job.is_running() for job in jobs):
265 for job in qpsworker_jobs:
266 if job.is_running():
267 print 'QPS worker "%s" is still running.' % job.host_and_port
268 if retries > 10:
269 print 'Killing all QPS workers.'
270 for job in jobs:
271 job.kill()
272 retries += 1
273 time.sleep(3)
274 print 'All QPS workers finished.'
275
276
277argp = argparse.ArgumentParser(description='Run performance tests.')
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700278argp.add_argument('-l', '--language',
Craig Tillerd92d5c52016-04-04 13:49:29 -0700279 choices=['all'] + sorted(scenario_config.LANGUAGES.keys()),
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700280 nargs='+',
281 default=['all'],
282 help='Languages to benchmark.')
Jan Tattermuschb2758442016-03-28 09:32:20 -0700283argp.add_argument('--remote_driver_host',
284 default=None,
285 help='Run QPS driver on given host. By default, QPS driver is run locally.')
286argp.add_argument('--remote_worker_host',
287 nargs='+',
288 default=[],
289 help='Worker hosts where to start QPS workers.')
Jan Tattermusch38becc22016-04-14 08:00:35 -0700290argp.add_argument('-r', '--regex', default='.*', type=str,
291 help='Regex to select scenarios to run.')
Jan Tattermuschb2758442016-03-28 09:32:20 -0700292
293args = argp.parse_args()
294
Craig Tillerd92d5c52016-04-04 13:49:29 -0700295languages = set(scenario_config.LANGUAGES[l]
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700296 for l in itertools.chain.from_iterable(
Craig Tillerd92d5c52016-04-04 13:49:29 -0700297 scenario_config.LANGUAGES.iterkeys() if x == 'all' else [x]
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700298 for x in args.language))
299
Jan Tattermuschb2758442016-03-28 09:32:20 -0700300# Put together set of remote hosts where to run and build
301remote_hosts = set()
302if args.remote_worker_host:
303 for host in args.remote_worker_host:
304 remote_hosts.add(host)
305if args.remote_driver_host:
306 remote_hosts.add(args.remote_driver_host)
307
308if remote_hosts:
309 archive_repo()
310 prepare_remote_hosts(remote_hosts)
311
312build_local = False
313if not args.remote_driver_host:
314 build_local = True
Craig Tiller7797e3f2016-04-01 07:41:05 -0700315build_on_remote_hosts(remote_hosts, languages=[str(l) for l in languages], build_local=build_local)
Jan Tattermuschb2758442016-03-28 09:32:20 -0700316
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700317qpsworker_jobs = start_qpsworkers(languages, args.remote_worker_host)
Jan Tattermuschb2758442016-03-28 09:32:20 -0700318
Jan Tattermusch38becc22016-04-14 08:00:35 -0700319# TODO(jtattermusch): see https://github.com/grpc/grpc/issues/6174
320time.sleep(5)
321
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700322# get list of worker addresses for each language.
323worker_addresses = dict([(str(language), []) for language in languages])
324for job in qpsworker_jobs:
325 worker_addresses[str(job.language)].append(job.host_and_port)
Jan Tattermuschb2758442016-03-28 09:32:20 -0700326
327try:
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700328 scenarios = create_scenarios(languages,
329 workers_by_lang=worker_addresses,
Jan Tattermusch38becc22016-04-14 08:00:35 -0700330 remote_host=args.remote_driver_host,
331 regex=args.regex)
Jan Tattermuschb2758442016-03-28 09:32:20 -0700332 if not scenarios:
333 raise Exception('No scenarios to run')
334
335 jobset.message('START', 'Running scenarios.', do_newline=True)
336 num_failures, _ = jobset.run(
337 scenarios, newline_on_success=True, maxjobs=1)
338 if num_failures == 0:
339 jobset.message('SUCCESS',
340 'All scenarios finished successfully.',
341 do_newline=True)
342 else:
343 jobset.message('FAILED', 'Some of the scenarios failed.',
344 do_newline=True)
345 sys.exit(1)
Jan Tattermuschee9032c2016-04-14 08:35:51 -0700346except:
347 traceback.print_exc()
Jan Tattermuschb2758442016-03-28 09:32:20 -0700348finally:
349 finish_qps_workers(qpsworker_jobs)