blob: 51ed35f7608998a8c74ea90bebc3b6cb9a606c0d [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 Tattermuschb2758442016-03-28 09:32:20 -070040import subprocess
41import sys
42import tempfile
43import time
44import uuid
Craig Tillerd92d5c52016-04-04 13:49:29 -070045import performance.scenario_config as scenario_config
Jan Tattermuschb2758442016-03-28 09:32:20 -070046
47
48_ROOT = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), '../..'))
49os.chdir(_ROOT)
50
51
52_REMOTE_HOST_USERNAME = 'jenkins'
53
54
Jan Tattermuschb2758442016-03-28 09:32:20 -070055class QpsWorkerJob:
56 """Encapsulates a qps worker server job."""
57
Jan Tattermuschbb1a4532016-03-30 18:04:01 -070058 def __init__(self, spec, language, host_and_port):
Jan Tattermuschb2758442016-03-28 09:32:20 -070059 self._spec = spec
Jan Tattermuschbb1a4532016-03-30 18:04:01 -070060 self.language = language
Jan Tattermuschb2758442016-03-28 09:32:20 -070061 self.host_and_port = host_and_port
62 self._job = jobset.Job(spec, bin_hash=None, newline_on_success=True, travis=True, add_env={})
63
64 def is_running(self):
65 """Polls a job and returns True if given job is still running."""
66 return self._job.state(jobset.NoCache()) == jobset._RUNNING
67
68 def kill(self):
69 return self._job.kill()
70
71
Jan Tattermuschbb1a4532016-03-30 18:04:01 -070072def create_qpsworker_job(language, shortname=None,
73 port=10000, remote_host=None):
Jan Tattermuschb2758442016-03-28 09:32:20 -070074 # TODO: support more languages
Jan Tattermuschbb1a4532016-03-30 18:04:01 -070075 cmdline = language.worker_cmdline() + ['--driver_port=%s' % port]
Jan Tattermuschb2758442016-03-28 09:32:20 -070076 if remote_host:
77 user_at_host = '%s@%s' % (_REMOTE_HOST_USERNAME, remote_host)
Jan Tattermuschbb1a4532016-03-30 18:04:01 -070078 cmdline = ['ssh',
79 str(user_at_host),
80 'cd ~/performance_workspace/grpc/ && %s' % ' '.join(cmdline)]
Jan Tattermuschb2758442016-03-28 09:32:20 -070081 host_and_port='%s:%s' % (remote_host, port)
82 else:
83 host_and_port='localhost:%s' % port
84
85 jobspec = jobset.JobSpec(
Jan Tattermuschbb1a4532016-03-30 18:04:01 -070086 cmdline=cmdline,
87 shortname=shortname,
88 timeout_seconds=15*60)
89 return QpsWorkerJob(jobspec, language, host_and_port)
Jan Tattermuschb2758442016-03-28 09:32:20 -070090
91
Craig Tiller0bda0b32016-03-03 12:51:53 -080092def create_scenario_jobspec(scenario_json, workers, remote_host=None):
Jan Tattermuschb2758442016-03-28 09:32:20 -070093 """Runs one scenario using QPS driver."""
94 # setting QPS_WORKERS env variable here makes sure it works with SSH too.
Craig Tiller0bda0b32016-03-03 12:51:53 -080095 cmd = 'QPS_WORKERS="%s" bins/opt/qps_json_driver ' % ','.join(workers)
96 cmd += '--scenarios_json=%s' % pipes.quote(json.dumps({'scenarios': [scenario_json]}))
Jan Tattermuschb2758442016-03-28 09:32:20 -070097 if remote_host:
98 user_at_host = '%s@%s' % (_REMOTE_HOST_USERNAME, remote_host)
99 cmd = 'ssh %s "cd ~/performance_workspace/grpc/ && %s"' % (user_at_host, cmd)
100
101 return jobset.JobSpec(
102 cmdline=[cmd],
Craig Tiller0bda0b32016-03-03 12:51:53 -0800103 shortname='qps_json_driver.%s' % scenario_json['name'],
104 timeout_seconds=3*60,
105 shell=True,
106 verbose_success=True)
107
108
109def create_quit_jobspec(workers, remote_host=None):
110 """Runs quit using QPS driver."""
111 # setting QPS_WORKERS env variable here makes sure it works with SSH too.
112 cmd = 'QPS_WORKERS="%s" bins/opt/qps_driver --quit' % ','.join(workers)
113 if remote_host:
114 user_at_host = '%s@%s' % (_REMOTE_HOST_USERNAME, remote_host)
115 cmd = 'ssh %s "cd ~/performance_workspace/grpc/ && %s"' % (user_at_host, cmd)
116
117 return jobset.JobSpec(
118 cmdline=[cmd],
119 shortname='qps_driver.quit',
Jan Tattermuschb2758442016-03-28 09:32:20 -0700120 timeout_seconds=3*60,
121 shell=True,
122 verbose_success=True)
123
124
125def archive_repo():
126 """Archives local version of repo including submodules."""
127 # TODO: also archive grpc-go and grpc-java repos
128 archive_job = jobset.JobSpec(
129 cmdline=['tar', '-cf', '../grpc.tar', '../grpc/'],
130 shortname='archive_repo',
131 timeout_seconds=3*60)
132
133 jobset.message('START', 'Archiving local repository.', do_newline=True)
134 num_failures, _ = jobset.run(
135 [archive_job], newline_on_success=True, maxjobs=1)
136 if num_failures == 0:
137 jobset.message('SUCCESS',
138 'Archive with local repository create successfully.',
139 do_newline=True)
140 else:
141 jobset.message('FAILED', 'Failed to archive local repository.',
142 do_newline=True)
143 sys.exit(1)
144
145
146def prepare_remote_hosts(hosts):
147 """Prepares remote hosts."""
148 prepare_jobs = []
149 for host in hosts:
150 user_at_host = '%s@%s' % (_REMOTE_HOST_USERNAME, host)
151 prepare_jobs.append(
152 jobset.JobSpec(
153 cmdline=['tools/run_tests/performance/remote_host_prepare.sh'],
154 shortname='remote_host_prepare.%s' % host,
155 environ = {'USER_AT_HOST': user_at_host},
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700156 timeout_seconds=5*60))
Jan Tattermuschb2758442016-03-28 09:32:20 -0700157 jobset.message('START', 'Preparing remote hosts.', do_newline=True)
158 num_failures, _ = jobset.run(
159 prepare_jobs, newline_on_success=True, maxjobs=10)
160 if num_failures == 0:
161 jobset.message('SUCCESS',
162 'Remote hosts ready to start build.',
163 do_newline=True)
164 else:
165 jobset.message('FAILED', 'Failed to prepare remote hosts.',
166 do_newline=True)
167 sys.exit(1)
168
169
Craig Tillerd92d5c52016-04-04 13:49:29 -0700170def build_on_remote_hosts(hosts, languages=scenario_config.LANGUAGES.keys(), build_local=False):
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700171 """Builds performance worker on remote hosts (and maybe also locally)."""
Jan Tattermuschb2758442016-03-28 09:32:20 -0700172 build_timeout = 15*60
173 build_jobs = []
174 for host in hosts:
175 user_at_host = '%s@%s' % (_REMOTE_HOST_USERNAME, host)
176 build_jobs.append(
177 jobset.JobSpec(
Craig Tiller7797e3f2016-04-01 07:41:05 -0700178 cmdline=['tools/run_tests/performance/remote_host_build.sh'] + languages,
Jan Tattermuschb2758442016-03-28 09:32:20 -0700179 shortname='remote_host_build.%s' % host,
180 environ = {'USER_AT_HOST': user_at_host, 'CONFIG': 'opt'},
181 timeout_seconds=build_timeout))
182 if build_local:
183 # Build locally as well
184 build_jobs.append(
185 jobset.JobSpec(
Craig Tiller7797e3f2016-04-01 07:41:05 -0700186 cmdline=['tools/run_tests/performance/build_performance.sh'] + languages,
Jan Tattermuschb2758442016-03-28 09:32:20 -0700187 shortname='local_build',
188 environ = {'CONFIG': 'opt'},
189 timeout_seconds=build_timeout))
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700190 jobset.message('START', 'Building.', do_newline=True)
Jan Tattermuschb2758442016-03-28 09:32:20 -0700191 num_failures, _ = jobset.run(
192 build_jobs, newline_on_success=True, maxjobs=10)
193 if num_failures == 0:
194 jobset.message('SUCCESS',
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700195 'Built successfully.',
Jan Tattermuschb2758442016-03-28 09:32:20 -0700196 do_newline=True)
197 else:
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700198 jobset.message('FAILED', 'Build failed.',
Jan Tattermuschb2758442016-03-28 09:32:20 -0700199 do_newline=True)
200 sys.exit(1)
201
202
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700203def start_qpsworkers(languages, worker_hosts):
Jan Tattermuschb2758442016-03-28 09:32:20 -0700204 """Starts QPS workers as background jobs."""
205 if not worker_hosts:
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700206 # run two workers locally (for each language)
Jan Tattermuschb2758442016-03-28 09:32:20 -0700207 workers=[(None, 10000), (None, 10010)]
208 elif len(worker_hosts) == 1:
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700209 # run two workers on the remote host (for each language)
Jan Tattermuschb2758442016-03-28 09:32:20 -0700210 workers=[(worker_hosts[0], 10000), (worker_hosts[0], 10010)]
211 else:
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700212 # run one worker per each remote host (for each language)
Jan Tattermuschb2758442016-03-28 09:32:20 -0700213 workers=[(worker_host, 10000) for worker_host in worker_hosts]
214
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700215 return [create_qpsworker_job(language,
216 shortname= 'qps_worker_%s_%s' % (language,
217 worker_idx),
218 port=worker[1] + language.worker_port_offset(),
Jan Tattermuschb2758442016-03-28 09:32:20 -0700219 remote_host=worker[0])
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700220 for language in languages
221 for worker_idx, worker in enumerate(workers)]
Jan Tattermuschb2758442016-03-28 09:32:20 -0700222
223
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700224def create_scenarios(languages, workers_by_lang, remote_host=None):
Jan Tattermuschb2758442016-03-28 09:32:20 -0700225 """Create jobspecs for scenarios to run."""
226 scenarios = []
227 for language in languages:
Craig Tiller0bda0b32016-03-03 12:51:53 -0800228 for scenario_json in language.scenarios():
229 scenario = create_scenario_jobspec(scenario_json,
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700230 workers_by_lang[str(language)],
Jan Tattermuschb2758442016-03-28 09:32:20 -0700231 remote_host=remote_host)
232 scenarios.append(scenario)
233
234 # the very last scenario requests shutting down the workers.
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700235 all_workers = [worker
236 for workers in workers_by_lang.values()
237 for worker in workers]
Craig Tiller0bda0b32016-03-03 12:51:53 -0800238 scenarios.append(create_quit_jobspec(all_workers, remote_host=remote_host))
Jan Tattermuschb2758442016-03-28 09:32:20 -0700239 return scenarios
240
241
242def finish_qps_workers(jobs):
243 """Waits for given jobs to finish and eventually kills them."""
244 retries = 0
245 while any(job.is_running() for job in jobs):
246 for job in qpsworker_jobs:
247 if job.is_running():
248 print 'QPS worker "%s" is still running.' % job.host_and_port
249 if retries > 10:
250 print 'Killing all QPS workers.'
251 for job in jobs:
252 job.kill()
253 retries += 1
254 time.sleep(3)
255 print 'All QPS workers finished.'
256
257
258argp = argparse.ArgumentParser(description='Run performance tests.')
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700259argp.add_argument('-l', '--language',
Craig Tillerd92d5c52016-04-04 13:49:29 -0700260 choices=['all'] + sorted(scenario_config.LANGUAGES.keys()),
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700261 nargs='+',
262 default=['all'],
263 help='Languages to benchmark.')
Jan Tattermuschb2758442016-03-28 09:32:20 -0700264argp.add_argument('--remote_driver_host',
265 default=None,
266 help='Run QPS driver on given host. By default, QPS driver is run locally.')
267argp.add_argument('--remote_worker_host',
268 nargs='+',
269 default=[],
270 help='Worker hosts where to start QPS workers.')
271
272args = argp.parse_args()
273
Craig Tillerd92d5c52016-04-04 13:49:29 -0700274languages = set(scenario_config.LANGUAGES[l]
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700275 for l in itertools.chain.from_iterable(
Craig Tillerd92d5c52016-04-04 13:49:29 -0700276 scenario_config.LANGUAGES.iterkeys() if x == 'all' else [x]
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700277 for x in args.language))
278
Jan Tattermuschb2758442016-03-28 09:32:20 -0700279# Put together set of remote hosts where to run and build
280remote_hosts = set()
281if args.remote_worker_host:
282 for host in args.remote_worker_host:
283 remote_hosts.add(host)
284if args.remote_driver_host:
285 remote_hosts.add(args.remote_driver_host)
286
287if remote_hosts:
288 archive_repo()
289 prepare_remote_hosts(remote_hosts)
290
291build_local = False
292if not args.remote_driver_host:
293 build_local = True
Craig Tiller7797e3f2016-04-01 07:41:05 -0700294build_on_remote_hosts(remote_hosts, languages=[str(l) for l in languages], build_local=build_local)
Jan Tattermuschb2758442016-03-28 09:32:20 -0700295
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700296qpsworker_jobs = start_qpsworkers(languages, args.remote_worker_host)
Jan Tattermuschb2758442016-03-28 09:32:20 -0700297
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700298# get list of worker addresses for each language.
299worker_addresses = dict([(str(language), []) for language in languages])
300for job in qpsworker_jobs:
301 worker_addresses[str(job.language)].append(job.host_and_port)
Jan Tattermuschb2758442016-03-28 09:32:20 -0700302
303try:
Jan Tattermuschbb1a4532016-03-30 18:04:01 -0700304 scenarios = create_scenarios(languages,
305 workers_by_lang=worker_addresses,
Jan Tattermuschb2758442016-03-28 09:32:20 -0700306 remote_host=args.remote_driver_host)
307 if not scenarios:
308 raise Exception('No scenarios to run')
309
310 jobset.message('START', 'Running scenarios.', do_newline=True)
311 num_failures, _ = jobset.run(
312 scenarios, newline_on_success=True, maxjobs=1)
313 if num_failures == 0:
314 jobset.message('SUCCESS',
315 'All scenarios finished successfully.',
316 do_newline=True)
317 else:
318 jobset.message('FAILED', 'Some of the scenarios failed.',
319 do_newline=True)
320 sys.exit(1)
321finally:
322 finish_qps_workers(qpsworker_jobs)