123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859 |
- #!/usr/bin/env python3
- # -*- coding: utf-8 -*-
- #***************************************************************************
- # _ _ ____ _
- # Project ___| | | | _ \| |
- # / __| | | | |_) | |
- # | (__| |_| | _ <| |___
- # \___|\___/|_| \_\_____|
- #
- # Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
- #
- # This software is licensed as described in the file COPYING, which
- # you should have received as part of this distribution. The terms
- # are also available at https://curl.se/docs/copyright.html.
- #
- # You may opt to use, copy, modify, merge, publish, distribute and/or sell
- # copies of the Software, and permit persons to whom the Software is
- # furnished to do so, under the terms of the COPYING file.
- #
- # This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
- # KIND, either express or implied.
- #
- # SPDX-License-Identifier: curl
- #
- ###########################################################################
- #
- import argparse
- import json
- import logging
- import os
- import re
- import sys
- from statistics import mean
- from typing import Dict, Any, Optional, List
- from testenv import Env, Httpd, CurlClient, Caddy, ExecResult, NghttpxQuic, RunProfile
- log = logging.getLogger(__name__)
- class ScoreCardError(Exception):
- pass
- class ScoreCard:
- def __init__(self, env: Env,
- protocol: str,
- server_descr: str,
- server_port: int,
- verbose: int,
- curl_verbose: int,
- download_parallel: int = 0,
- server_addr: Optional[str] = None):
- self.verbose = verbose
- self.env = env
- self.protocol = protocol
- self.server_descr = server_descr
- self.server_addr = server_addr
- self.server_port = server_port
- self._silent_curl = not curl_verbose
- self._download_parallel = download_parallel
- def info(self, msg):
- if self.verbose > 0:
- sys.stderr.write(msg)
- sys.stderr.flush()
- def handshakes(self) -> Dict[str, Any]:
- props = {}
- sample_size = 5
- self.info('TLS Handshake\n')
- for authority in [
- 'curl.se', 'google.com', 'cloudflare.com', 'nghttp2.org'
- ]:
- self.info(f' {authority}...')
- props[authority] = {}
- for ipv in ['ipv4', 'ipv6']:
- self.info(f'{ipv}...')
- c_samples = []
- hs_samples = []
- errors = []
- for _ in range(sample_size):
- curl = CurlClient(env=self.env, silent=self._silent_curl,
- server_addr=self.server_addr)
- args = [
- '--http3-only' if self.protocol == 'h3' else '--http2',
- f'--{ipv}', f'https://{authority}/'
- ]
- r = curl.run_direct(args=args, with_stats=True)
- if r.exit_code == 0 and len(r.stats) == 1:
- c_samples.append(r.stats[0]['time_connect'])
- hs_samples.append(r.stats[0]['time_appconnect'])
- else:
- errors.append(f'exit={r.exit_code}')
- props[authority][f'{ipv}-connect'] = mean(c_samples) \
- if len(c_samples) else -1
- props[authority][f'{ipv}-handshake'] = mean(hs_samples) \
- if len(hs_samples) else -1
- props[authority][f'{ipv}-errors'] = errors
- self.info('ok.\n')
- return props
- def _make_docs_file(self, docs_dir: str, fname: str, fsize: int):
- fpath = os.path.join(docs_dir, fname)
- data1k = 1024*'x'
- flen = 0
- with open(fpath, 'w') as fd:
- while flen < fsize:
- fd.write(data1k)
- flen += len(data1k)
- return fpath
- def setup_resources(self, server_docs: str,
- downloads: Optional[List[int]] = None):
- for fsize in downloads:
- label = self.fmt_size(fsize)
- fname = f'score{label}.data'
- self._make_docs_file(docs_dir=server_docs,
- fname=fname, fsize=fsize)
- self._make_docs_file(docs_dir=server_docs,
- fname='reqs10.data', fsize=10*1024)
- def _check_downloads(self, r: ExecResult, count: int):
- error = ''
- if r.exit_code != 0:
- error += f'exit={r.exit_code} '
- if r.exit_code != 0 or len(r.stats) != count:
- error += f'stats={len(r.stats)}/{count} '
- fails = [s for s in r.stats if s['response_code'] != 200]
- if len(fails) > 0:
- error += f'{len(fails)} failed'
- return error if len(error) > 0 else None
- def transfer_single(self, url: str, count: int):
- sample_size = count
- count = 1
- samples = []
- errors = []
- profiles = []
- self.info('single...')
- for _ in range(sample_size):
- curl = CurlClient(env=self.env, silent=self._silent_curl,
- server_addr=self.server_addr)
- r = curl.http_download(urls=[url], alpn_proto=self.protocol,
- no_save=True, with_headers=False,
- with_profile=True)
- err = self._check_downloads(r, count)
- if err:
- errors.append(err)
- else:
- total_size = sum([s['size_download'] for s in r.stats])
- samples.append(total_size / r.duration.total_seconds())
- profiles.append(r.profile)
- return {
- 'count': count,
- 'samples': sample_size,
- 'max-parallel': 1,
- 'speed': mean(samples) if len(samples) else -1,
- 'errors': errors,
- 'stats': RunProfile.AverageStats(profiles),
- }
- def transfer_serial(self, url: str, count: int):
- sample_size = 1
- samples = []
- errors = []
- profiles = []
- url = f'{url}?[0-{count - 1}]'
- self.info('serial...')
- for _ in range(sample_size):
- curl = CurlClient(env=self.env, silent=self._silent_curl,
- server_addr=self.server_addr)
- r = curl.http_download(urls=[url], alpn_proto=self.protocol,
- no_save=True,
- with_headers=False, with_profile=True)
- err = self._check_downloads(r, count)
- if err:
- errors.append(err)
- else:
- total_size = sum([s['size_download'] for s in r.stats])
- samples.append(total_size / r.duration.total_seconds())
- profiles.append(r.profile)
- return {
- 'count': count,
- 'samples': sample_size,
- 'max-parallel': 1,
- 'speed': mean(samples) if len(samples) else -1,
- 'errors': errors,
- 'stats': RunProfile.AverageStats(profiles),
- }
- def transfer_parallel(self, url: str, count: int):
- sample_size = 1
- samples = []
- errors = []
- profiles = []
- max_parallel = self._download_parallel if self._download_parallel > 0 else count
- url = f'{url}?[0-{count - 1}]'
- self.info('parallel...')
- for _ in range(sample_size):
- curl = CurlClient(env=self.env, silent=self._silent_curl,
- server_addr=self.server_addr)
- r = curl.http_download(urls=[url], alpn_proto=self.protocol,
- no_save=True,
- with_headers=False,
- with_profile=True,
- extra_args=[
- '--parallel',
- '--parallel-max', str(max_parallel)
- ])
- err = self._check_downloads(r, count)
- if err:
- errors.append(err)
- else:
- total_size = sum([s['size_download'] for s in r.stats])
- samples.append(total_size / r.duration.total_seconds())
- profiles.append(r.profile)
- return {
- 'count': count,
- 'samples': sample_size,
- 'max-parallel': max_parallel,
- 'speed': mean(samples) if len(samples) else -1,
- 'errors': errors,
- 'stats': RunProfile.AverageStats(profiles),
- }
- def download_url(self, label: str, url: str, count: int):
- self.info(f' {count}x{label}: ')
- props = {
- 'single': self.transfer_single(url=url, count=10),
- }
- if count > 1:
- props['serial'] = self.transfer_serial(url=url, count=count)
- props['parallel'] = self.transfer_parallel(url=url, count=count)
- self.info('ok.\n')
- return props
- def downloads(self, count: int, fsizes: List[int]) -> Dict[str, Any]:
- scores = {}
- for fsize in fsizes:
- label = self.fmt_size(fsize)
- fname = f'score{label}.data'
- url = f'https://{self.env.domain1}:{self.server_port}/{fname}'
- scores[label] = self.download_url(label=label, url=url, count=count)
- return scores
- def _check_uploads(self, r: ExecResult, count: int):
- error = ''
- if r.exit_code != 0:
- error += f'exit={r.exit_code} '
- if r.exit_code != 0 or len(r.stats) != count:
- error += f'stats={len(r.stats)}/{count} '
- fails = [s for s in r.stats if s['response_code'] != 200]
- if len(fails) > 0:
- error += f'{len(fails)} failed'
- for f in fails:
- error += f'[{f["response_code"]}]'
- return error if len(error) > 0 else None
- def upload_single(self, url: str, fpath: str, count: int):
- sample_size = count
- count = 1
- samples = []
- errors = []
- profiles = []
- self.info('single...')
- for _ in range(sample_size):
- curl = CurlClient(env=self.env, silent=self._silent_curl,
- server_addr=self.server_addr)
- r = curl.http_put(urls=[url], fdata=fpath, alpn_proto=self.protocol,
- with_headers=False, with_profile=True)
- err = self._check_uploads(r, count)
- if err:
- errors.append(err)
- else:
- total_size = sum([s['size_upload'] for s in r.stats])
- samples.append(total_size / r.duration.total_seconds())
- profiles.append(r.profile)
- return {
- 'count': count,
- 'samples': sample_size,
- 'max-parallel': 1,
- 'speed': mean(samples) if len(samples) else -1,
- 'errors': errors,
- 'stats': RunProfile.AverageStats(profiles) if len(profiles) else {},
- }
- def upload_serial(self, url: str, fpath: str, count: int):
- sample_size = 1
- samples = []
- errors = []
- profiles = []
- url = f'{url}?id=[0-{count - 1}]'
- self.info('serial...')
- for _ in range(sample_size):
- curl = CurlClient(env=self.env, silent=self._silent_curl,
- server_addr=self.server_addr)
- r = curl.http_put(urls=[url], fdata=fpath, alpn_proto=self.protocol,
- with_headers=False, with_profile=True)
- err = self._check_uploads(r, count)
- if err:
- errors.append(err)
- else:
- total_size = sum([s['size_upload'] for s in r.stats])
- samples.append(total_size / r.duration.total_seconds())
- profiles.append(r.profile)
- return {
- 'count': count,
- 'samples': sample_size,
- 'max-parallel': 1,
- 'speed': mean(samples) if len(samples) else -1,
- 'errors': errors,
- 'stats': RunProfile.AverageStats(profiles) if len(profiles) else {},
- }
- def upload_parallel(self, url: str, fpath: str, count: int):
- sample_size = 1
- samples = []
- errors = []
- profiles = []
- max_parallel = count
- url = f'{url}?id=[0-{count - 1}]'
- self.info('parallel...')
- for _ in range(sample_size):
- curl = CurlClient(env=self.env, silent=self._silent_curl,
- server_addr=self.server_addr)
- r = curl.http_put(urls=[url], fdata=fpath, alpn_proto=self.protocol,
- with_headers=False, with_profile=True,
- extra_args=[
- '--parallel',
- '--parallel-max', str(max_parallel)
- ])
- err = self._check_uploads(r, count)
- if err:
- errors.append(err)
- else:
- total_size = sum([s['size_upload'] for s in r.stats])
- samples.append(total_size / r.duration.total_seconds())
- profiles.append(r.profile)
- return {
- 'count': count,
- 'samples': sample_size,
- 'max-parallel': max_parallel,
- 'speed': mean(samples) if len(samples) else -1,
- 'errors': errors,
- 'stats': RunProfile.AverageStats(profiles) if len(profiles) else {},
- }
- def upload_url(self, label: str, url: str, fpath: str, count: int):
- self.info(f' {count}x{label}: ')
- props = {
- 'single': self.upload_single(url=url, fpath=fpath, count=10),
- }
- if count > 1:
- props['serial'] = self.upload_serial(url=url, fpath=fpath, count=count)
- props['parallel'] = self.upload_parallel(url=url, fpath=fpath, count=count)
- self.info('ok.\n')
- return props
- def uploads(self, count: int, fsizes: List[int]) -> Dict[str, Any]:
- scores = {}
- url = f'https://{self.env.domain2}:{self.server_port}/curltest/put'
- fpaths = {}
- for fsize in fsizes:
- label = self.fmt_size(fsize)
- fname = f'upload{label}.data'
- fpaths[label] = self._make_docs_file(docs_dir=self.env.gen_dir,
- fname=fname, fsize=fsize)
- for label, fpath in fpaths.items():
- scores[label] = self.upload_url(label=label, url=url, fpath=fpath,
- count=count)
- return scores
- def do_requests(self, url: str, count: int, max_parallel: int = 1):
- sample_size = 1
- samples = []
- errors = []
- profiles = []
- url = f'{url}?[0-{count - 1}]'
- extra_args = [
- '-w', '%{response_code},\\n',
- ]
- if max_parallel > 1:
- extra_args.extend([
- '--parallel', '--parallel-max', str(max_parallel)
- ])
- self.info(f'{max_parallel}...')
- for _ in range(sample_size):
- curl = CurlClient(env=self.env, silent=self._silent_curl,
- server_addr=self.server_addr)
- r = curl.http_download(urls=[url], alpn_proto=self.protocol, no_save=True,
- with_headers=False, with_profile=True,
- with_stats=False, extra_args=extra_args)
- if r.exit_code != 0:
- errors.append(f'exit={r.exit_code}')
- else:
- samples.append(count / r.duration.total_seconds())
- non_200s = 0
- for line in r.stdout.splitlines():
- if not line.startswith('200,'):
- non_200s += 1
- if non_200s > 0:
- errors.append(f'responses != 200: {non_200s}')
- profiles.append(r.profile)
- return {
- 'count': count,
- 'samples': sample_size,
- 'speed': mean(samples) if len(samples) else -1,
- 'errors': errors,
- 'stats': RunProfile.AverageStats(profiles),
- }
- def requests_url(self, url: str, count: int):
- self.info(f' {url}: ')
- props = {}
- # 300 is max in curl, see tool_main.h
- for m in [1, 6, 25, 50, 100, 300]:
- props[str(m)] = self.do_requests(url=url, count=count, max_parallel=m)
- self.info('ok.\n')
- return props
- def requests(self, req_count) -> Dict[str, Any]:
- url = f'https://{self.env.domain1}:{self.server_port}/reqs10.data'
- return {
- 'count': req_count,
- '10KB': self.requests_url(url=url, count=req_count),
- }
- def score(self,
- handshakes: bool = True,
- downloads: Optional[List[int]] = None,
- download_count: int = 50,
- uploads: Optional[List[int]] = None,
- upload_count: int = 50,
- req_count=5000,
- requests: bool = True):
- self.info(f"scoring {self.protocol} against {self.server_descr}\n")
- p = {}
- if self.protocol == 'h3':
- p['name'] = 'h3'
- if not self.env.have_h3_curl():
- raise ScoreCardError('curl does not support HTTP/3')
- for lib in ['ngtcp2', 'quiche', 'msh3', 'nghttp3']:
- if self.env.curl_uses_lib(lib):
- p['implementation'] = lib
- break
- elif self.protocol == 'h2':
- p['name'] = 'h2'
- if not self.env.have_h2_curl():
- raise ScoreCardError('curl does not support HTTP/2')
- for lib in ['nghttp2']:
- if self.env.curl_uses_lib(lib):
- p['implementation'] = lib
- break
- elif self.protocol == 'h1' or self.protocol == 'http/1.1':
- proto = 'http/1.1'
- p['name'] = proto
- p['implementation'] = 'native'
- else:
- raise ScoreCardError(f"unknown protocol: {self.protocol}")
- if 'implementation' not in p:
- raise ScoreCardError(f'did not recognized {p} lib')
- p['version'] = Env.curl_lib_version(p['implementation'])
- score = {
- 'curl': self.env.curl_fullname(),
- 'os': self.env.curl_os(),
- 'protocol': p,
- 'server': self.server_descr,
- }
- if handshakes:
- score['handshakes'] = self.handshakes()
- if downloads and len(downloads) > 0:
- score['downloads'] = self.downloads(count=download_count,
- fsizes=downloads)
- if uploads and len(uploads) > 0:
- score['uploads'] = self.uploads(count=upload_count,
- fsizes=uploads)
- if requests:
- score['requests'] = self.requests(req_count=req_count)
- self.info("\n")
- return score
- def fmt_ms(self, tval):
- return f'{int(tval*1000)} ms' if tval >= 0 else '--'
- def fmt_size(self, val):
- if val >= (1024*1024*1024):
- return f'{val / (1024*1024*1024):0.000f}GB'
- elif val >= (1024 * 1024):
- return f'{val / (1024*1024):0.000f}MB'
- elif val >= 1024:
- return f'{val / 1024:0.000f}KB'
- else:
- return f'{val:0.000f}B'
- def fmt_mbs(self, val):
- return f'{val/(1024*1024):0.000f} MB/s' if val >= 0 else '--'
- def fmt_reqs(self, val):
- return f'{val:0.000f} r/s' if val >= 0 else '--'
- def print_score(self, score):
- print(f'{score["protocol"]["name"].upper()} in {score["curl"]}')
- if 'handshakes' in score:
- print(f'{"Handshakes":<24} {"ipv4":25} {"ipv6":28}')
- print(f' {"Host":<17} {"Connect":>12} {"Handshake":>12} '
- f'{"Connect":>12} {"Handshake":>12} {"Errors":<20}')
- for key, val in score["handshakes"].items():
- print(f' {key:<17} {self.fmt_ms(val["ipv4-connect"]):>12} '
- f'{self.fmt_ms(val["ipv4-handshake"]):>12} '
- f'{self.fmt_ms(val["ipv6-connect"]):>12} '
- f'{self.fmt_ms(val["ipv6-handshake"]):>12} '
- f'{"/".join(val["ipv4-errors"] + val["ipv6-errors"]):<20}'
- )
- if 'downloads' in score:
- # get the key names of all sizes and measurements made
- sizes = []
- measures = []
- m_names = {}
- mcol_width = 12
- mcol_sw = 17
- for sskey, ssval in score['downloads'].items():
- if isinstance(ssval, str):
- continue
- if sskey not in sizes:
- sizes.append(sskey)
- for mkey, mval in score['downloads'][sskey].items():
- if mkey not in measures:
- measures.append(mkey)
- m_names[mkey] = f'{mkey}({mval["count"]}x{mval["max-parallel"]})'
- print(f'Downloads from {score["server"]}')
- print(f' {"Size":>8}', end='')
- for m in measures:
- print(f' {m_names[m]:>{mcol_width}} {"[cpu/rss]":<{mcol_sw}}', end='')
- print(f' {"Errors":^20}')
- for size in score['downloads']:
- size_score = score['downloads'][size]
- print(f' {size:>8}', end='')
- errors = []
- for val in size_score.values():
- if 'errors' in val:
- errors.extend(val['errors'])
- for m in measures:
- if m in size_score:
- print(f' {self.fmt_mbs(size_score[m]["speed"]):>{mcol_width}}', end='')
- s = f'[{size_score[m]["stats"]["cpu"]:>.1f}%'\
- f'/{self.fmt_size(size_score[m]["stats"]["rss"])}]'
- print(f' {s:<{mcol_sw}}', end='')
- else:
- print(' '*mcol_width, end='')
- if len(errors):
- print(f' {"/".join(errors):<20}')
- else:
- print(f' {"-":^20}')
- if 'uploads' in score:
- # get the key names of all sizes and measurements made
- sizes = []
- measures = []
- m_names = {}
- mcol_width = 12
- mcol_sw = 17
- for sskey, ssval in score['uploads'].items():
- if isinstance(ssval, str):
- continue
- if sskey not in sizes:
- sizes.append(sskey)
- for mkey, mval in ssval.items():
- if mkey not in measures:
- measures.append(mkey)
- m_names[mkey] = f'{mkey}({mval["count"]}x{mval["max-parallel"]})'
- print(f'Uploads to {score["server"]}')
- print(f' {"Size":>8}', end='')
- for m in measures:
- print(f' {m_names[m]:>{mcol_width}} {"[cpu/rss]":<{mcol_sw}}', end='')
- print(f' {"Errors":^20}')
- for size in sizes:
- size_score = score['uploads'][size]
- print(f' {size:>8}', end='')
- errors = []
- for val in size_score.values():
- if 'errors' in val:
- errors.extend(val['errors'])
- for m in measures:
- if m in size_score:
- print(f' {self.fmt_mbs(size_score[m]["speed"]):>{mcol_width}}', end='')
- stats = size_score[m]["stats"]
- if 'cpu' in stats:
- s = f'[{stats["cpu"]:>.1f}%/{self.fmt_size(stats["rss"])}]'
- else:
- s = '[???/???]'
- print(f' {s:<{mcol_sw}}', end='')
- else:
- print(' '*mcol_width, end='')
- if len(errors):
- print(f' {"/".join(errors):<20}')
- else:
- print(f' {"-":^20}')
- if 'requests' in score:
- sizes = []
- measures = []
- m_names = {}
- mcol_width = 9
- mcol_sw = 13
- for sskey, ssval in score['requests'].items():
- if isinstance(ssval, (str, int)):
- continue
- if sskey not in sizes:
- sizes.append(sskey)
- for mkey in score['requests'][sskey]:
- if mkey not in measures:
- measures.append(mkey)
- m_names[mkey] = f'{mkey}'
- print('Requests (max parallel) to {score["server"]}')
- print(f' {"Size":>6} {"Reqs":>6}', end='')
- for m in measures:
- print(f' {m_names[m]:>{mcol_width}} {"[cpu/rss]":<{mcol_sw}}', end='')
- print(f' {"Errors":^10}')
- for size in sizes:
- size_score = score['requests'][size]
- count = score['requests']['count']
- print(f' {size:>6} {count:>6}', end='')
- errors = []
- for val in size_score.values():
- if 'errors' in val:
- errors.extend(val['errors'])
- for m in measures:
- if m in size_score:
- print(f' {self.fmt_reqs(size_score[m]["speed"]):>{mcol_width}}', end='')
- s = f'[{size_score[m]["stats"]["cpu"]:>.1f}%'\
- f'/{self.fmt_size(size_score[m]["stats"]["rss"])}]'
- print(f' {s:<{mcol_sw}}', end='')
- else:
- print(' '*mcol_width, end='')
- if len(errors):
- print(f' {"/".join(errors):<10}')
- else:
- print(f' {"-":^10}')
- def parse_size(s):
- m = re.match(r'(\d+)(mb|kb|gb)?', s, re.IGNORECASE)
- if m is None:
- raise Exception(f'unrecognized size: {s}')
- size = int(m.group(1))
- if not m.group(2):
- pass
- elif m.group(2).lower() == 'kb':
- size *= 1024
- elif m.group(2).lower() == 'mb':
- size *= 1024 * 1024
- elif m.group(2).lower() == 'gb':
- size *= 1024 * 1024 * 1024
- return size
- def main():
- parser = argparse.ArgumentParser(prog='scorecard', description="""
- Run a range of tests to give a scorecard for a HTTP protocol
- 'h3' or 'h2' implementation in curl.
- """)
- parser.add_argument("-v", "--verbose", action='count', default=1,
- help="log more output on stderr")
- parser.add_argument("-j", "--json", action='store_true',
- default=False, help="print json instead of text")
- parser.add_argument("-H", "--handshakes", action='store_true',
- default=False, help="evaluate handshakes only")
- parser.add_argument("-d", "--downloads", action='store_true',
- default=False, help="evaluate downloads")
- parser.add_argument("--download", action='append', type=str,
- default=None, help="evaluate download size")
- parser.add_argument("--download-count", action='store', type=int,
- default=50, help="perform that many downloads")
- parser.add_argument("--download-parallel", action='store', type=int,
- default=0, help="perform that many downloads in parallel (default all)")
- parser.add_argument("-u", "--uploads", action='store_true',
- default=False, help="evaluate uploads")
- parser.add_argument("--upload", action='append', type=str,
- default=None, help="evaluate upload size")
- parser.add_argument("--upload-count", action='store', type=int,
- default=50, help="perform that many uploads")
- parser.add_argument("-r", "--requests", action='store_true',
- default=False, help="evaluate requests")
- parser.add_argument("--request-count", action='store', type=int,
- default=5000, help="perform that many requests")
- parser.add_argument("--httpd", action='store_true', default=False,
- help="evaluate httpd server only")
- parser.add_argument("--caddy", action='store_true', default=False,
- help="evaluate caddy server only")
- parser.add_argument("--curl-verbose", action='store_true',
- default=False, help="run curl with `-v`")
- parser.add_argument("protocol", default='h2', nargs='?',
- help="Name of protocol to score")
- parser.add_argument("--start-only", action='store_true', default=False,
- help="only start the servers")
- parser.add_argument("--remote", action='store', type=str,
- default=None, help="score against the remote server at <ip>:<port>")
- args = parser.parse_args()
- if args.verbose > 0:
- console = logging.StreamHandler()
- console.setLevel(logging.INFO)
- console.setFormatter(logging.Formatter(logging.BASIC_FORMAT))
- logging.getLogger('').addHandler(console)
- protocol = args.protocol
- handshakes = True
- downloads = [1024 * 1024, 10 * 1024 * 1024, 100 * 1024 * 1024]
- if args.download is not None:
- downloads = []
- for x in args.download:
- downloads.extend([parse_size(s) for s in x.split(',')])
- uploads = [1024 * 1024, 10 * 1024 * 1024, 100 * 1024 * 1024]
- if args.upload is not None:
- uploads = []
- for x in args.upload:
- uploads.extend([parse_size(s) for s in x.split(',')])
- requests = True
- if args.downloads or args.uploads or args.requests or args.handshakes:
- handshakes = args.handshakes
- if not args.downloads:
- downloads = None
- if not args.uploads:
- uploads = None
- requests = args.requests
- test_httpd = protocol != 'h3'
- test_caddy = True
- if args.caddy or args.httpd:
- test_caddy = args.caddy
- test_httpd = args.httpd
- rv = 0
- env = Env()
- env.setup()
- env.test_timeout = None
- httpd = None
- nghttpx = None
- caddy = None
- try:
- cards = []
- if args.remote:
- m = re.match(r'^(.+):(\d+)$', args.remote)
- if m is None:
- raise ScoreCardError(f'unable to parse ip:port from --remote {args.remote}')
- test_httpd = False
- test_caddy = False
- remote_addr = m.group(1)
- remote_port = int(m.group(2))
- card = ScoreCard(env=env,
- protocol=protocol,
- server_descr=f'Server at {args.remote}',
- server_addr=remote_addr,
- server_port=remote_port,
- verbose=args.verbose, curl_verbose=args.curl_verbose,
- download_parallel=args.download_parallel)
- cards.append(card)
- if test_httpd:
- httpd = Httpd(env=env)
- assert httpd.exists(), \
- f'httpd not found: {env.httpd}'
- httpd.clear_logs()
- server_docs = httpd.docs_dir
- assert httpd.start()
- if protocol == 'h3':
- nghttpx = NghttpxQuic(env=env)
- nghttpx.clear_logs()
- assert nghttpx.start()
- server_descr = f'nghttpx: https:{env.h3_port} [backend httpd: {env.httpd_version()}, https:{env.https_port}]'
- server_port = env.h3_port
- else:
- server_descr = f'httpd: {env.httpd_version()}, http:{env.http_port} https:{env.https_port}'
- server_port = env.https_port
- card = ScoreCard(env=env,
- protocol=protocol,
- server_descr=server_descr,
- server_port=server_port,
- verbose=args.verbose, curl_verbose=args.curl_verbose,
- download_parallel=args.download_parallel)
- card.setup_resources(server_docs, downloads)
- cards.append(card)
- if test_caddy and env.caddy:
- backend = ''
- if uploads and httpd is None:
- backend = f' [backend httpd: {env.httpd_version()}, http:{env.http_port} https:{env.https_port}]'
- httpd = Httpd(env=env)
- assert httpd.exists(), \
- f'httpd not found: {env.httpd}'
- httpd.clear_logs()
- assert httpd.start()
- caddy = Caddy(env=env)
- caddy.clear_logs()
- assert caddy.start()
- server_descr = f'Caddy: {env.caddy_version()}, http:{env.caddy_http_port} https:{env.caddy_https_port}{backend}'
- server_port = caddy.port
- server_docs = caddy.docs_dir
- card = ScoreCard(env=env,
- protocol=protocol,
- server_descr=server_descr,
- server_port=server_port,
- verbose=args.verbose, curl_verbose=args.curl_verbose,
- download_parallel=args.download_parallel)
- card.setup_resources(server_docs, downloads)
- cards.append(card)
- if args.start_only:
- print('started servers:')
- for card in cards:
- print(f'{card.server_descr}')
- sys.stderr.write('press [RETURN] to finish')
- sys.stderr.flush()
- sys.stdin.readline()
- else:
- for card in cards:
- score = card.score(handshakes=handshakes,
- downloads=downloads,
- download_count=args.download_count,
- uploads=uploads,
- upload_count=args.upload_count,
- req_count=args.request_count,
- requests=requests)
- if args.json:
- print(json.JSONEncoder(indent=2).encode(score))
- else:
- card.print_score(score)
- except ScoreCardError as ex:
- sys.stderr.write(f"ERROR: {ex}\n")
- rv = 1
- except KeyboardInterrupt:
- log.warning("aborted")
- rv = 1
- finally:
- if caddy:
- caddy.stop()
- if nghttpx:
- nghttpx.stop(wait_dead=False)
- if httpd:
- httpd.stop()
- sys.exit(rv)
- if __name__ == "__main__":
- main()
|