blob: cf6b3929366746749044b3e7c7cc99e1bb79cbe2 [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
43
44
45_ROOT = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), '../..'))
46os.chdir(_ROOT)
47
48
49_REMOTE_HOST_USERNAME = 'jenkins'
50
51
52class CXXLanguage:
53
54 def __init__(self):
55 self.safename = 'cxx'
56
Jan Tattermuschbb1a4532016-03-30 18:04:01 -070057 def worker_cmdline(self):
58 return ['bins/opt/qps_worker']
59
60 def worker_port_offset(self):
61 return 0
Jan Tattermuschd7be7892016-03-30 16:08:16 -070062
Jan Tattermuschb2758442016-03-28 09:32:20 -070063 def scenarios(self):
64 # TODO(jtattermusch): add more scenarios
65 return {
66 # Scenario 1: generic async streaming ping-pong (contentionless latency)
67 'cpp_async_generic_streaming_ping_pong': [
68 '--rpc_type=STREAMING',
69 '--client_type=ASYNC_CLIENT',
70 '--server_type=ASYNC_GENERIC_SERVER',
71 '--outstanding_rpcs_per_channel=1',
72 '--client_channels=1',
73 '--bbuf_req_size=0',
74 '--bbuf_resp_size=0',
75 '--async_client_threads=1',
76 '--async_server_threads=1',
77 '--secure_test=true',
78 '--num_servers=1',
79 '--num_clients=1',
80 '--server_core_limit=0',
81 '--client_core_limit=0'],
82 # Scenario 5: Sync unary ping-pong with protobufs
83 'cpp_sync_unary_ping_pong_protobuf': [
84 '--rpc_type=UNARY',
85 '--client_type=SYNC_CLIENT',
86 '--server_type=SYNC_SERVER',
87 '--outstanding_rpcs_per_channel=1',
88 '--client_channels=1',
89 '--simple_req_size=0',
90 '--simple_resp_size=0',
91 '--secure_test=true',
92 '--num_servers=1',
93 '--num_clients=1',
94 '--server_core_limit=0',
95 '--client_core_limit=0']}
96
97 def __str__(self):
98 return 'c++'
99
100
101class CSharpLanguage:
102
103 def __init__(self):
104 self.safename = str(self)
105
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700106 def worker_cmdline(self):
107 return ['tools/run_tests/performance/run_worker_csharp.sh']
108
109 def worker_port_offset(self):
110 return 100
111
112 def scenarios(self):
113 # TODO(jtattermusch): add more scenarios
114 return {
115 # Scenario 1: generic async streaming ping-pong (contentionless latency)
116 'csharp_async_generic_streaming_ping_pong': [
117 '--rpc_type=STREAMING',
118 '--client_type=ASYNC_CLIENT',
119 '--server_type=ASYNC_GENERIC_SERVER',
120 '--outstanding_rpcs_per_channel=1',
121 '--client_channels=1',
122 '--bbuf_req_size=0',
123 '--bbuf_resp_size=0',
124 '--async_client_threads=1',
125 '--async_server_threads=1',
126 '--secure_test=true',
127 '--num_servers=1',
128 '--num_clients=1',
129 '--server_core_limit=0',
130 '--client_core_limit=0']}
Jan Tattermuschd7be7892016-03-30 16:08:16 -0700131
Jan Tattermuschb2758442016-03-28 09:32:20 -0700132 def __str__(self):
133 return 'csharp'
134
135
136class NodeLanguage:
137
138 def __init__(self):
139 pass
140 self.safename = str(self)
141
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700142 def worker_cmdline(self):
143 return ['tools/run_tests/performance/run_worker_node.sh']
144
145 def worker_port_offset(self):
146 return 200
147
148 def scenarios(self):
149 # TODO(jtattermusch): add more scenarios
150 return {
151 'node_sync_unary_ping_pong_protobuf': [
152 '--rpc_type=UNARY',
153 '--client_type=ASYNC_CLIENT',
154 '--server_type=ASYNC_SERVER',
155 '--outstanding_rpcs_per_channel=1',
156 '--client_channels=1',
157 '--simple_req_size=0',
158 '--simple_resp_size=0',
159 '--secure_test=false',
160 '--num_servers=1',
161 '--num_clients=1',
162 '--server_core_limit=0',
163 '--client_core_limit=0']}
Jan Tattermuschd7be7892016-03-30 16:08:16 -0700164
Jan Tattermuschb2758442016-03-28 09:32:20 -0700165 def __str__(self):
166 return 'node'
167
168
169_LANGUAGES = {
170 'c++' : CXXLanguage(),
171 'csharp' : CSharpLanguage(),
172 'node' : NodeLanguage(),
173}
174
175
176class QpsWorkerJob:
177 """Encapsulates a qps worker server job."""
178
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700179 def __init__(self, spec, language, host_and_port):
Jan Tattermuschb2758442016-03-28 09:32:20 -0700180 self._spec = spec
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700181 self.language = language
Jan Tattermuschb2758442016-03-28 09:32:20 -0700182 self.host_and_port = host_and_port
183 self._job = jobset.Job(spec, bin_hash=None, newline_on_success=True, travis=True, add_env={})
184
185 def is_running(self):
186 """Polls a job and returns True if given job is still running."""
187 return self._job.state(jobset.NoCache()) == jobset._RUNNING
188
189 def kill(self):
190 return self._job.kill()
191
192
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700193def create_qpsworker_job(language, shortname=None,
194 port=10000, remote_host=None):
Jan Tattermuschb2758442016-03-28 09:32:20 -0700195 # TODO: support more languages
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700196 cmdline = language.worker_cmdline() + ['--driver_port=%s' % port]
Jan Tattermuschb2758442016-03-28 09:32:20 -0700197 if remote_host:
198 user_at_host = '%s@%s' % (_REMOTE_HOST_USERNAME, remote_host)
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700199 cmdline = ['ssh',
200 str(user_at_host),
201 'cd ~/performance_workspace/grpc/ && %s' % ' '.join(cmdline)]
Jan Tattermuschb2758442016-03-28 09:32:20 -0700202 host_and_port='%s:%s' % (remote_host, port)
203 else:
204 host_and_port='localhost:%s' % port
205
206 jobspec = jobset.JobSpec(
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700207 cmdline=cmdline,
208 shortname=shortname,
209 timeout_seconds=15*60)
210 return QpsWorkerJob(jobspec, language, host_and_port)
Jan Tattermuschb2758442016-03-28 09:32:20 -0700211
212
213def create_scenario_jobspec(scenario_name, driver_args, workers, remote_host=None):
214 """Runs one scenario using QPS driver."""
215 # setting QPS_WORKERS env variable here makes sure it works with SSH too.
216 cmd = 'QPS_WORKERS="%s" bins/opt/qps_driver ' % ','.join(workers)
217 cmd += ' '.join(driver_args)
218 if remote_host:
219 user_at_host = '%s@%s' % (_REMOTE_HOST_USERNAME, remote_host)
220 cmd = 'ssh %s "cd ~/performance_workspace/grpc/ && %s"' % (user_at_host, cmd)
221
222 return jobset.JobSpec(
223 cmdline=[cmd],
224 shortname='qps_driver.%s' % scenario_name,
225 timeout_seconds=3*60,
226 shell=True,
227 verbose_success=True)
228
229
230def archive_repo():
231 """Archives local version of repo including submodules."""
232 # TODO: also archive grpc-go and grpc-java repos
233 archive_job = jobset.JobSpec(
234 cmdline=['tar', '-cf', '../grpc.tar', '../grpc/'],
235 shortname='archive_repo',
236 timeout_seconds=3*60)
237
238 jobset.message('START', 'Archiving local repository.', do_newline=True)
239 num_failures, _ = jobset.run(
240 [archive_job], newline_on_success=True, maxjobs=1)
241 if num_failures == 0:
242 jobset.message('SUCCESS',
243 'Archive with local repository create successfully.',
244 do_newline=True)
245 else:
246 jobset.message('FAILED', 'Failed to archive local repository.',
247 do_newline=True)
248 sys.exit(1)
249
250
251def prepare_remote_hosts(hosts):
252 """Prepares remote hosts."""
253 prepare_jobs = []
254 for host in hosts:
255 user_at_host = '%s@%s' % (_REMOTE_HOST_USERNAME, host)
256 prepare_jobs.append(
257 jobset.JobSpec(
258 cmdline=['tools/run_tests/performance/remote_host_prepare.sh'],
259 shortname='remote_host_prepare.%s' % host,
260 environ = {'USER_AT_HOST': user_at_host},
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700261 timeout_seconds=5*60))
Jan Tattermuschb2758442016-03-28 09:32:20 -0700262 jobset.message('START', 'Preparing remote hosts.', do_newline=True)
263 num_failures, _ = jobset.run(
264 prepare_jobs, newline_on_success=True, maxjobs=10)
265 if num_failures == 0:
266 jobset.message('SUCCESS',
267 'Remote hosts ready to start build.',
268 do_newline=True)
269 else:
270 jobset.message('FAILED', 'Failed to prepare remote hosts.',
271 do_newline=True)
272 sys.exit(1)
273
274
275def build_on_remote_hosts(hosts, build_local=False):
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700276 """Builds performance worker on remote hosts (and maybe also locally)."""
Jan Tattermuschb2758442016-03-28 09:32:20 -0700277 build_timeout = 15*60
278 build_jobs = []
279 for host in hosts:
280 user_at_host = '%s@%s' % (_REMOTE_HOST_USERNAME, host)
281 build_jobs.append(
282 jobset.JobSpec(
283 cmdline=['tools/run_tests/performance/remote_host_build.sh'],
284 shortname='remote_host_build.%s' % host,
285 environ = {'USER_AT_HOST': user_at_host, 'CONFIG': 'opt'},
286 timeout_seconds=build_timeout))
287 if build_local:
288 # Build locally as well
289 build_jobs.append(
290 jobset.JobSpec(
291 cmdline=['tools/run_tests/performance/build_performance.sh'],
292 shortname='local_build',
293 environ = {'CONFIG': 'opt'},
294 timeout_seconds=build_timeout))
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700295 jobset.message('START', 'Building.', do_newline=True)
Jan Tattermuschb2758442016-03-28 09:32:20 -0700296 num_failures, _ = jobset.run(
297 build_jobs, newline_on_success=True, maxjobs=10)
298 if num_failures == 0:
299 jobset.message('SUCCESS',
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700300 'Built successfully.',
Jan Tattermuschb2758442016-03-28 09:32:20 -0700301 do_newline=True)
302 else:
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700303 jobset.message('FAILED', 'Build failed.',
Jan Tattermuschb2758442016-03-28 09:32:20 -0700304 do_newline=True)
305 sys.exit(1)
306
307
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700308def start_qpsworkers(languages, worker_hosts):
Jan Tattermuschb2758442016-03-28 09:32:20 -0700309 """Starts QPS workers as background jobs."""
310 if not worker_hosts:
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700311 # run two workers locally (for each language)
Jan Tattermuschb2758442016-03-28 09:32:20 -0700312 workers=[(None, 10000), (None, 10010)]
313 elif len(worker_hosts) == 1:
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700314 # run two workers on the remote host (for each language)
Jan Tattermuschb2758442016-03-28 09:32:20 -0700315 workers=[(worker_hosts[0], 10000), (worker_hosts[0], 10010)]
316 else:
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700317 # run one worker per each remote host (for each language)
Jan Tattermuschb2758442016-03-28 09:32:20 -0700318 workers=[(worker_host, 10000) for worker_host in worker_hosts]
319
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700320 return [create_qpsworker_job(language,
321 shortname= 'qps_worker_%s_%s' % (language,
322 worker_idx),
323 port=worker[1] + language.worker_port_offset(),
Jan Tattermuschb2758442016-03-28 09:32:20 -0700324 remote_host=worker[0])
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700325 for language in languages
326 for worker_idx, worker in enumerate(workers)]
Jan Tattermuschb2758442016-03-28 09:32:20 -0700327
328
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700329def create_scenarios(languages, workers_by_lang, remote_host=None):
Jan Tattermuschb2758442016-03-28 09:32:20 -0700330 """Create jobspecs for scenarios to run."""
331 scenarios = []
332 for language in languages:
333 for scenario_name, driver_args in language.scenarios().iteritems():
334 scenario = create_scenario_jobspec(scenario_name,
335 driver_args,
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700336 workers_by_lang[str(language)],
Jan Tattermuschb2758442016-03-28 09:32:20 -0700337 remote_host=remote_host)
338 scenarios.append(scenario)
339
340 # the very last scenario requests shutting down the workers.
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700341 all_workers = [worker
342 for workers in workers_by_lang.values()
343 for worker in workers]
Jan Tattermuschb2758442016-03-28 09:32:20 -0700344 scenarios.append(create_scenario_jobspec('quit_workers',
345 ['--quit=true'],
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700346 all_workers,
Jan Tattermuschb2758442016-03-28 09:32:20 -0700347 remote_host=remote_host))
348 return scenarios
349
350
351def finish_qps_workers(jobs):
352 """Waits for given jobs to finish and eventually kills them."""
353 retries = 0
354 while any(job.is_running() for job in jobs):
355 for job in qpsworker_jobs:
356 if job.is_running():
357 print 'QPS worker "%s" is still running.' % job.host_and_port
358 if retries > 10:
359 print 'Killing all QPS workers.'
360 for job in jobs:
361 job.kill()
362 retries += 1
363 time.sleep(3)
364 print 'All QPS workers finished.'
365
366
367argp = argparse.ArgumentParser(description='Run performance tests.')
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700368argp.add_argument('-l', '--language',
369 choices=['all'] + sorted(_LANGUAGES.keys()),
370 nargs='+',
371 default=['all'],
372 help='Languages to benchmark.')
Jan Tattermuschb2758442016-03-28 09:32:20 -0700373argp.add_argument('--remote_driver_host',
374 default=None,
375 help='Run QPS driver on given host. By default, QPS driver is run locally.')
376argp.add_argument('--remote_worker_host',
377 nargs='+',
378 default=[],
379 help='Worker hosts where to start QPS workers.')
380
381args = argp.parse_args()
382
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700383languages = set(_LANGUAGES[l]
384 for l in itertools.chain.from_iterable(
385 _LANGUAGES.iterkeys() if x == 'all' else [x]
386 for x in args.language))
387
Jan Tattermuschb2758442016-03-28 09:32:20 -0700388# Put together set of remote hosts where to run and build
389remote_hosts = set()
390if args.remote_worker_host:
391 for host in args.remote_worker_host:
392 remote_hosts.add(host)
393if args.remote_driver_host:
394 remote_hosts.add(args.remote_driver_host)
395
396if remote_hosts:
397 archive_repo()
398 prepare_remote_hosts(remote_hosts)
399
400build_local = False
401if not args.remote_driver_host:
402 build_local = True
403build_on_remote_hosts(remote_hosts, build_local=build_local)
404
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700405qpsworker_jobs = start_qpsworkers(languages, args.remote_worker_host)
Jan Tattermuschb2758442016-03-28 09:32:20 -0700406
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700407# get list of worker addresses for each language.
408worker_addresses = dict([(str(language), []) for language in languages])
409for job in qpsworker_jobs:
410 worker_addresses[str(job.language)].append(job.host_and_port)
Jan Tattermuschb2758442016-03-28 09:32:20 -0700411
412try:
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700413 scenarios = create_scenarios(languages,
414 workers_by_lang=worker_addresses,
Jan Tattermuschb2758442016-03-28 09:32:20 -0700415 remote_host=args.remote_driver_host)
416 if not scenarios:
417 raise Exception('No scenarios to run')
418
419 jobset.message('START', 'Running scenarios.', do_newline=True)
420 num_failures, _ = jobset.run(
421 scenarios, newline_on_success=True, maxjobs=1)
422 if num_failures == 0:
423 jobset.message('SUCCESS',
424 'All scenarios finished successfully.',
425 do_newline=True)
426 else:
427 jobset.message('FAILED', 'Some of the scenarios failed.',
428 do_newline=True)
429 sys.exit(1)
430finally:
431 finish_qps_workers(qpsworker_jobs)