Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/pkg
Path: blob/main/external/curl/tests/http/scorecard.py
2066 views
1
#!/usr/bin/env python3
2
# -*- coding: utf-8 -*-
3
#***************************************************************************
4
# _ _ ____ _
5
# Project ___| | | | _ \| |
6
# / __| | | | |_) | |
7
# | (__| |_| | _ <| |___
8
# \___|\___/|_| \_\_____|
9
#
10
# Copyright (C) Daniel Stenberg, <[email protected]>, et al.
11
#
12
# This software is licensed as described in the file COPYING, which
13
# you should have received as part of this distribution. The terms
14
# are also available at https://curl.se/docs/copyright.html.
15
#
16
# You may opt to use, copy, modify, merge, publish, distribute and/or sell
17
# copies of the Software, and permit persons to whom the Software is
18
# furnished to do so, under the terms of the COPYING file.
19
#
20
# This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
21
# KIND, either express or implied.
22
#
23
# SPDX-License-Identifier: curl
24
#
25
###########################################################################
26
#
27
import argparse
28
import datetime
29
import json
30
import logging
31
import os
32
import re
33
import sys
34
from statistics import mean
35
from typing import Dict, Any, Optional, List
36
37
from testenv import Env, Httpd, CurlClient, Caddy, ExecResult, NghttpxQuic, RunProfile
38
39
log = logging.getLogger(__name__)
40
41
42
class ScoreCardError(Exception):
43
pass
44
45
46
class Card:
47
@classmethod
48
def fmt_ms(cls, tval):
49
return f'{int(tval*1000)} ms' if tval >= 0 else '--'
50
51
@classmethod
52
def fmt_size(cls, val):
53
if val >= (1024*1024*1024):
54
return f'{val / (1024*1024*1024):0.000f}GB'
55
elif val >= (1024 * 1024):
56
return f'{val / (1024*1024):0.000f}MB'
57
elif val >= 1024:
58
return f'{val / 1024:0.000f}KB'
59
else:
60
return f'{val:0.000f}B'
61
62
@classmethod
63
def fmt_mbs(cls, val):
64
return f'{val/(1024*1024):0.000f} MB/s' if val >= 0 else '--'
65
66
@classmethod
67
def fmt_reqs(cls, val):
68
return f'{val:0.000f} r/s' if val >= 0 else '--'
69
70
@classmethod
71
def mk_mbs_cell(cls, samples, profiles, errors):
72
val = mean(samples) if len(samples) else -1
73
cell = {
74
'val': val,
75
'sval': Card.fmt_mbs(val) if val >= 0 else '--',
76
}
77
if len(profiles):
78
cell['stats'] = RunProfile.AverageStats(profiles)
79
if len(errors):
80
cell['errors'] = errors
81
return cell
82
83
@classmethod
84
def mk_reqs_cell(cls, samples, profiles, errors):
85
val = mean(samples) if len(samples) else -1
86
cell = {
87
'val': val,
88
'sval': Card.fmt_reqs(val) if val >= 0 else '--',
89
}
90
if len(profiles):
91
cell['stats'] = RunProfile.AverageStats(profiles)
92
if len(errors):
93
cell['errors'] = errors
94
return cell
95
96
@classmethod
97
def parse_size(cls, s):
98
m = re.match(r'(\d+)(mb|kb|gb)?', s, re.IGNORECASE)
99
if m is None:
100
raise Exception(f'unrecognized size: {s}')
101
size = int(m.group(1))
102
if not m.group(2):
103
pass
104
elif m.group(2).lower() == 'kb':
105
size *= 1024
106
elif m.group(2).lower() == 'mb':
107
size *= 1024 * 1024
108
elif m.group(2).lower() == 'gb':
109
size *= 1024 * 1024 * 1024
110
return size
111
112
@classmethod
113
def print_score(cls, score):
114
print(f'Scorecard curl, protocol {score["meta"]["protocol"]} '
115
f'via {score["meta"]["implementation"]}/'
116
f'{score["meta"]["implementation_version"]}')
117
print(f'Date: {score["meta"]["date"]}')
118
if 'curl_V' in score["meta"]:
119
print(f'Version: {score["meta"]["curl_V"]}')
120
if 'curl_features' in score["meta"]:
121
print(f'Features: {score["meta"]["curl_features"]}')
122
print(f'Samples Size: {score["meta"]["samples"]}')
123
if 'handshakes' in score:
124
print(f'{"Handshakes":<24} {"ipv4":25} {"ipv6":28}')
125
print(f' {"Host":<17} {"Connect":>12} {"Handshake":>12} '
126
f'{"Connect":>12} {"Handshake":>12} {"Errors":<20}')
127
for key, val in score["handshakes"].items():
128
print(f' {key:<17} {Card.fmt_ms(val["ipv4-connect"]):>12} '
129
f'{Card.fmt_ms(val["ipv4-handshake"]):>12} '
130
f'{Card.fmt_ms(val["ipv6-connect"]):>12} '
131
f'{Card.fmt_ms(val["ipv6-handshake"]):>12} '
132
f'{"/".join(val["ipv4-errors"] + val["ipv6-errors"]):<20}'
133
)
134
for name in ['downloads', 'uploads', 'requests']:
135
if name in score:
136
Card.print_score_table(score[name])
137
138
@classmethod
139
def print_score_table(cls, score):
140
cols = score['cols']
141
rows = score['rows']
142
colw = []
143
statw = 13
144
errors = []
145
col_has_stats = []
146
for idx, col in enumerate(cols):
147
cellw = max([len(r[idx]["sval"]) for r in rows])
148
colw.append(max(cellw, len(col)))
149
col_has_stats.append(False)
150
for row in rows:
151
if 'stats' in row[idx]:
152
col_has_stats[idx] = True
153
break
154
if 'title' in score['meta']:
155
print(score['meta']['title'])
156
for idx, col in enumerate(cols):
157
if col_has_stats[idx]:
158
print(f' {col:>{colw[idx]}} {"[cpu/rss]":<{statw}}', end='')
159
else:
160
print(f' {col:>{colw[idx]}}', end='')
161
print('')
162
for row in rows:
163
for idx, cell in enumerate(row):
164
print(f' {cell["sval"]:>{colw[idx]}}', end='')
165
if col_has_stats[idx]:
166
if 'stats' in cell:
167
s = f'[{cell["stats"]["cpu"]:>.1f}%' \
168
f'/{Card.fmt_size(cell["stats"]["rss"])}]'
169
else:
170
s = ''
171
print(f' {s:<{statw}}', end='')
172
if 'errors' in cell:
173
errors.extend(cell['errors'])
174
print('')
175
if len(errors):
176
print(f'Errors: {errors}')
177
178
179
class ScoreRunner:
180
181
def __init__(self, env: Env,
182
protocol: str,
183
server_descr: str,
184
server_port: int,
185
verbose: int,
186
curl_verbose: int,
187
download_parallel: int = 0,
188
server_addr: Optional[str] = None):
189
self.verbose = verbose
190
self.env = env
191
self.protocol = protocol
192
self.server_descr = server_descr
193
self.server_addr = server_addr
194
self.server_port = server_port
195
self._silent_curl = not curl_verbose
196
self._download_parallel = download_parallel
197
198
def info(self, msg):
199
if self.verbose > 0:
200
sys.stderr.write(msg)
201
sys.stderr.flush()
202
203
def handshakes(self) -> Dict[str, Any]:
204
props = {}
205
sample_size = 5
206
self.info('TLS Handshake\n')
207
for authority in [
208
'curl.se', 'google.com', 'cloudflare.com', 'nghttp2.org'
209
]:
210
self.info(f' {authority}...')
211
props[authority] = {}
212
for ipv in ['ipv4', 'ipv6']:
213
self.info(f'{ipv}...')
214
c_samples = []
215
hs_samples = []
216
errors = []
217
for _ in range(sample_size):
218
curl = CurlClient(env=self.env, silent=self._silent_curl,
219
server_addr=self.server_addr)
220
args = [
221
'--http3-only' if self.protocol == 'h3' else '--http2',
222
f'--{ipv}', f'https://{authority}/'
223
]
224
r = curl.run_direct(args=args, with_stats=True)
225
if r.exit_code == 0 and len(r.stats) == 1:
226
c_samples.append(r.stats[0]['time_connect'])
227
hs_samples.append(r.stats[0]['time_appconnect'])
228
else:
229
errors.append(f'exit={r.exit_code}')
230
props[authority][f'{ipv}-connect'] = mean(c_samples) \
231
if len(c_samples) else -1
232
props[authority][f'{ipv}-handshake'] = mean(hs_samples) \
233
if len(hs_samples) else -1
234
props[authority][f'{ipv}-errors'] = errors
235
self.info('ok.\n')
236
return props
237
238
def _make_docs_file(self, docs_dir: str, fname: str, fsize: int):
239
fpath = os.path.join(docs_dir, fname)
240
data1k = 1024*'x'
241
flen = 0
242
with open(fpath, 'w') as fd:
243
while flen < fsize:
244
fd.write(data1k)
245
flen += len(data1k)
246
return fpath
247
248
def setup_resources(self, server_docs: str,
249
downloads: Optional[List[int]] = None):
250
if downloads is not None:
251
for fsize in downloads:
252
label = Card.fmt_size(fsize)
253
fname = f'score{label}.data'
254
self._make_docs_file(docs_dir=server_docs,
255
fname=fname, fsize=fsize)
256
self._make_docs_file(docs_dir=server_docs,
257
fname='reqs10.data', fsize=10*1024)
258
259
def _check_downloads(self, r: ExecResult, count: int):
260
error = ''
261
if r.exit_code != 0:
262
error += f'exit={r.exit_code} '
263
if r.exit_code != 0 or len(r.stats) != count:
264
error += f'stats={len(r.stats)}/{count} '
265
fails = [s for s in r.stats if s['response_code'] != 200]
266
if len(fails) > 0:
267
error += f'{len(fails)} failed'
268
return error if len(error) > 0 else None
269
270
def dl_single(self, url: str, nsamples: int = 1):
271
count = 1
272
samples = []
273
errors = []
274
profiles = []
275
self.info('single...')
276
for _ in range(nsamples):
277
curl = CurlClient(env=self.env, silent=self._silent_curl,
278
server_addr=self.server_addr)
279
r = curl.http_download(urls=[url], alpn_proto=self.protocol,
280
no_save=True, with_headers=False,
281
with_profile=True)
282
err = self._check_downloads(r, count)
283
if err:
284
errors.append(err)
285
else:
286
total_size = sum([s['size_download'] for s in r.stats])
287
samples.append(total_size / r.duration.total_seconds())
288
profiles.append(r.profile)
289
return Card.mk_mbs_cell(samples, profiles, errors)
290
291
def dl_serial(self, url: str, count: int, nsamples: int = 1):
292
samples = []
293
errors = []
294
profiles = []
295
url = f'{url}?[0-{count - 1}]'
296
self.info('serial...')
297
for _ in range(nsamples):
298
curl = CurlClient(env=self.env, silent=self._silent_curl,
299
server_addr=self.server_addr)
300
r = curl.http_download(urls=[url], alpn_proto=self.protocol,
301
no_save=True,
302
with_headers=False, with_profile=True)
303
err = self._check_downloads(r, count)
304
if err:
305
errors.append(err)
306
else:
307
total_size = sum([s['size_download'] for s in r.stats])
308
samples.append(total_size / r.duration.total_seconds())
309
profiles.append(r.profile)
310
return Card.mk_mbs_cell(samples, profiles, errors)
311
312
def dl_parallel(self, url: str, count: int, nsamples: int = 1):
313
samples = []
314
errors = []
315
profiles = []
316
max_parallel = self._download_parallel if self._download_parallel > 0 else count
317
url = f'{url}?[0-{count - 1}]'
318
self.info('parallel...')
319
for _ in range(nsamples):
320
curl = CurlClient(env=self.env, silent=self._silent_curl,
321
server_addr=self.server_addr)
322
r = curl.http_download(urls=[url], alpn_proto=self.protocol,
323
no_save=True,
324
with_headers=False,
325
with_profile=True,
326
extra_args=[
327
'--parallel',
328
'--parallel-max', str(max_parallel)
329
])
330
err = self._check_downloads(r, count)
331
if err:
332
errors.append(err)
333
else:
334
total_size = sum([s['size_download'] for s in r.stats])
335
samples.append(total_size / r.duration.total_seconds())
336
profiles.append(r.profile)
337
return Card.mk_mbs_cell(samples, profiles, errors)
338
339
def downloads(self, count: int, fsizes: List[int], meta: Dict[str, Any]) -> Dict[str, Any]:
340
nsamples = meta['samples']
341
max_parallel = self._download_parallel if self._download_parallel > 0 else count
342
cols = ['size', 'single']
343
if count > 1:
344
cols.append(f'serial({count})')
345
cols.append(f'parallel({count}x{max_parallel})')
346
rows = []
347
for fsize in fsizes:
348
row = [{
349
'val': fsize,
350
'sval': Card.fmt_size(fsize)
351
}]
352
self.info(f'{row[0]["sval"]} downloads...')
353
url = f'https://{self.env.domain1}:{self.server_port}/score{row[0]["sval"]}.data'
354
355
row.append(self.dl_single(url=url, nsamples=nsamples))
356
if count > 1:
357
row.append(self.dl_serial(url=url, count=count, nsamples=nsamples))
358
row.append(self.dl_parallel(url=url, count=count, nsamples=nsamples))
359
rows.append(row)
360
self.info('done.\n')
361
return {
362
'meta': {
363
'title': f'Downloads from {meta["server"]}',
364
'count': count,
365
'max-parallel': max_parallel,
366
},
367
'cols': cols,
368
'rows': rows,
369
}
370
371
def _check_uploads(self, r: ExecResult, count: int):
372
error = ''
373
if r.exit_code != 0:
374
error += f'exit={r.exit_code} '
375
if r.exit_code != 0 or len(r.stats) != count:
376
error += f'stats={len(r.stats)}/{count} '
377
fails = [s for s in r.stats if s['response_code'] != 200]
378
if len(fails) > 0:
379
error += f'{len(fails)} failed'
380
for f in fails:
381
error += f'[{f["response_code"]}]'
382
return error if len(error) > 0 else None
383
384
def ul_single(self, url: str, fpath: str, nsamples: int = 1):
385
samples = []
386
errors = []
387
profiles = []
388
self.info('single...')
389
for _ in range(nsamples):
390
curl = CurlClient(env=self.env, silent=self._silent_curl,
391
server_addr=self.server_addr)
392
r = curl.http_put(urls=[url], fdata=fpath, alpn_proto=self.protocol,
393
with_headers=False, with_profile=True)
394
err = self._check_uploads(r, 1)
395
if err:
396
errors.append(err)
397
else:
398
total_size = sum([s['size_upload'] for s in r.stats])
399
samples.append(total_size / r.duration.total_seconds())
400
profiles.append(r.profile)
401
return Card.mk_mbs_cell(samples, profiles, errors)
402
403
def ul_serial(self, url: str, fpath: str, count: int, nsamples: int = 1):
404
samples = []
405
errors = []
406
profiles = []
407
url = f'{url}?id=[0-{count - 1}]'
408
self.info('serial...')
409
for _ in range(nsamples):
410
curl = CurlClient(env=self.env, silent=self._silent_curl,
411
server_addr=self.server_addr)
412
r = curl.http_put(urls=[url], fdata=fpath, alpn_proto=self.protocol,
413
with_headers=False, with_profile=True)
414
err = self._check_uploads(r, count)
415
if err:
416
errors.append(err)
417
else:
418
total_size = sum([s['size_upload'] for s in r.stats])
419
samples.append(total_size / r.duration.total_seconds())
420
profiles.append(r.profile)
421
return Card.mk_mbs_cell(samples, profiles, errors)
422
423
def ul_parallel(self, url: str, fpath: str, count: int, nsamples: int = 1):
424
samples = []
425
errors = []
426
profiles = []
427
max_parallel = self._download_parallel if self._download_parallel > 0 else count
428
url = f'{url}?id=[0-{count - 1}]'
429
self.info('parallel...')
430
for _ in range(nsamples):
431
curl = CurlClient(env=self.env, silent=self._silent_curl,
432
server_addr=self.server_addr)
433
r = curl.http_put(urls=[url], fdata=fpath, alpn_proto=self.protocol,
434
with_headers=False, with_profile=True,
435
extra_args=[
436
'--parallel',
437
'--parallel-max', str(max_parallel)
438
])
439
err = self._check_uploads(r, count)
440
if err:
441
errors.append(err)
442
else:
443
total_size = sum([s['size_upload'] for s in r.stats])
444
samples.append(total_size / r.duration.total_seconds())
445
profiles.append(r.profile)
446
return Card.mk_mbs_cell(samples, profiles, errors)
447
448
def uploads(self, count: int, fsizes: List[int], meta: Dict[str, Any]) -> Dict[str, Any]:
449
nsamples = meta['samples']
450
max_parallel = self._download_parallel if self._download_parallel > 0 else count
451
url = f'https://{self.env.domain2}:{self.server_port}/curltest/put'
452
cols = ['size', 'single']
453
if count > 1:
454
cols.append(f'serial({count})')
455
cols.append(f'parallel({count}x{max_parallel})')
456
rows = []
457
for fsize in fsizes:
458
row = [{
459
'val': fsize,
460
'sval': Card.fmt_size(fsize)
461
}]
462
fname = f'upload{row[0]["sval"]}.data'
463
fpath = self._make_docs_file(docs_dir=self.env.gen_dir,
464
fname=fname, fsize=fsize)
465
466
self.info(f'{row[0]["sval"]} uploads...')
467
row.append(self.ul_single(url=url, fpath=fpath, nsamples=nsamples))
468
if count > 1:
469
row.append(self.ul_serial(url=url, fpath=fpath, count=count, nsamples=nsamples))
470
row.append(self.ul_parallel(url=url, fpath=fpath, count=count, nsamples=nsamples))
471
rows.append(row)
472
self.info('done.\n')
473
return {
474
'meta': {
475
'title': f'Uploads to {meta["server"]}',
476
'count': count,
477
'max-parallel': max_parallel,
478
},
479
'cols': cols,
480
'rows': rows,
481
}
482
483
def do_requests(self, url: str, count: int, max_parallel: int = 1, nsamples: int = 1):
484
samples = []
485
errors = []
486
profiles = []
487
url = f'{url}?[0-{count - 1}]'
488
extra_args = [
489
'-w', '%{response_code},\\n',
490
]
491
if max_parallel > 1:
492
extra_args.extend([
493
'--parallel', '--parallel-max', str(max_parallel)
494
])
495
self.info(f'{max_parallel}...')
496
for _ in range(nsamples):
497
curl = CurlClient(env=self.env, silent=self._silent_curl,
498
server_addr=self.server_addr)
499
r = curl.http_download(urls=[url], alpn_proto=self.protocol, no_save=True,
500
with_headers=False, with_profile=True,
501
with_stats=False, extra_args=extra_args)
502
if r.exit_code != 0:
503
errors.append(f'exit={r.exit_code}')
504
else:
505
samples.append(count / r.duration.total_seconds())
506
non_200s = 0
507
for line in r.stdout.splitlines():
508
if not line.startswith('200,'):
509
non_200s += 1
510
if non_200s > 0:
511
errors.append(f'responses != 200: {non_200s}')
512
profiles.append(r.profile)
513
return Card.mk_reqs_cell(samples, profiles, errors)
514
515
def requests(self, count: int, meta: Dict[str, Any]) -> Dict[str, Any]:
516
url = f'https://{self.env.domain1}:{self.server_port}/reqs10.data'
517
fsize = 10*1024
518
cols = ['size', 'total']
519
rows = []
520
cols.extend([f'{mp} max' for mp in [1, 6, 25, 50, 100, 300]])
521
row = [{
522
'val': fsize,
523
'sval': Card.fmt_size(fsize)
524
},{
525
'val': count,
526
'sval': f'{count}',
527
}]
528
self.info('requests, max parallel...')
529
row.extend([self.do_requests(url=url, count=count,
530
max_parallel=mp, nsamples=meta["samples"])
531
for mp in [1, 6, 25, 50, 100, 300]])
532
rows.append(row)
533
self.info('done.\n')
534
return {
535
'meta': {
536
'title': f'Requests in parallel to {meta["server"]}',
537
'count': count,
538
},
539
'cols': cols,
540
'rows': rows,
541
}
542
543
def score(self,
544
handshakes: bool = True,
545
downloads: Optional[List[int]] = None,
546
download_count: int = 50,
547
uploads: Optional[List[int]] = None,
548
upload_count: int = 50,
549
req_count=5000,
550
nsamples: int = 1,
551
requests: bool = True):
552
self.info(f"scoring {self.protocol} against {self.server_descr}\n")
553
554
score = {
555
'meta': {
556
'curl_version': self.env.curl_version(),
557
'curl_V': self.env.curl_fullname(),
558
'curl_features': self.env.curl_features_string(),
559
'os': self.env.curl_os(),
560
'server': self.server_descr,
561
'samples': nsamples,
562
'date': f'{datetime.datetime.now(tz=datetime.timezone.utc).isoformat()}',
563
}
564
}
565
if self.protocol == 'h3':
566
score['meta']['protocol'] = 'h3'
567
if not self.env.have_h3_curl():
568
raise ScoreCardError('curl does not support HTTP/3')
569
for lib in ['ngtcp2', 'quiche', 'msh3', 'nghttp3']:
570
if self.env.curl_uses_lib(lib):
571
score['meta']['implementation'] = lib
572
break
573
elif self.protocol == 'h2':
574
score['meta']['protocol'] = 'h2'
575
if not self.env.have_h2_curl():
576
raise ScoreCardError('curl does not support HTTP/2')
577
for lib in ['nghttp2']:
578
if self.env.curl_uses_lib(lib):
579
score['meta']['implementation'] = lib
580
break
581
elif self.protocol == 'h1' or self.protocol == 'http/1.1':
582
score['meta']['protocol'] = 'http/1.1'
583
score['meta']['implementation'] = 'native'
584
else:
585
raise ScoreCardError(f"unknown protocol: {self.protocol}")
586
587
if 'implementation' not in score['meta']:
588
raise ScoreCardError('did not recognized protocol lib')
589
score['meta']['implementation_version'] = Env.curl_lib_version(score['meta']['implementation'])
590
591
if handshakes:
592
score['handshakes'] = self.handshakes()
593
if downloads and len(downloads) > 0:
594
score['downloads'] = self.downloads(count=download_count,
595
fsizes=downloads,
596
meta=score['meta'])
597
if uploads and len(uploads) > 0:
598
score['uploads'] = self.uploads(count=upload_count,
599
fsizes=uploads,
600
meta=score['meta'])
601
if requests:
602
score['requests'] = self.requests(count=req_count, meta=score['meta'])
603
return score
604
605
606
def run_score(args, protocol):
607
if protocol not in ['http/1.1', 'h1', 'h2', 'h3']:
608
sys.stderr.write(f'ERROR: protocol "{protocol}" not known to scorecard\n')
609
sys.exit(1)
610
if protocol == 'h1':
611
protocol = 'http/1.1'
612
613
handshakes = True
614
downloads = [1024 * 1024, 10 * 1024 * 1024, 100 * 1024 * 1024]
615
if args.download_sizes is not None:
616
downloads = []
617
for x in args.download_sizes:
618
downloads.extend([Card.parse_size(s) for s in x.split(',')])
619
620
uploads = [1024 * 1024, 10 * 1024 * 1024, 100 * 1024 * 1024]
621
if args.upload_sizes is not None:
622
uploads = []
623
for x in args.upload_sizes:
624
uploads.extend([Card.parse_size(s) for s in x.split(',')])
625
626
requests = True
627
if args.downloads or args.uploads or args.requests or args.handshakes:
628
handshakes = args.handshakes
629
if not args.downloads:
630
downloads = None
631
if not args.uploads:
632
uploads = None
633
requests = args.requests
634
635
test_httpd = protocol != 'h3'
636
test_caddy = protocol == 'h3'
637
if args.caddy or args.httpd:
638
test_caddy = args.caddy
639
test_httpd = args.httpd
640
641
rv = 0
642
env = Env()
643
env.setup()
644
env.test_timeout = None
645
httpd = None
646
nghttpx = None
647
caddy = None
648
try:
649
cards = []
650
651
if args.remote:
652
m = re.match(r'^(.+):(\d+)$', args.remote)
653
if m is None:
654
raise ScoreCardError(f'unable to parse ip:port from --remote {args.remote}')
655
test_httpd = False
656
test_caddy = False
657
remote_addr = m.group(1)
658
remote_port = int(m.group(2))
659
card = ScoreRunner(env=env,
660
protocol=protocol,
661
server_descr=f'Server at {args.remote}',
662
server_addr=remote_addr,
663
server_port=remote_port,
664
verbose=args.verbose,
665
curl_verbose=args.curl_verbose,
666
download_parallel=args.download_parallel)
667
cards.append(card)
668
669
if test_httpd:
670
httpd = Httpd(env=env)
671
assert httpd.exists(), \
672
f'httpd not found: {env.httpd}'
673
httpd.clear_logs()
674
server_docs = httpd.docs_dir
675
assert httpd.initial_start()
676
if protocol == 'h3':
677
nghttpx = NghttpxQuic(env=env)
678
nghttpx.clear_logs()
679
assert nghttpx.initial_start()
680
server_descr = f'nghttpx: https:{env.h3_port} [backend httpd/{env.httpd_version()}]'
681
server_port = env.h3_port
682
else:
683
server_descr = f'httpd/{env.httpd_version()}'
684
server_port = env.https_port
685
card = ScoreRunner(env=env,
686
protocol=protocol,
687
server_descr=server_descr,
688
server_port=server_port,
689
verbose=args.verbose, curl_verbose=args.curl_verbose,
690
download_parallel=args.download_parallel)
691
card.setup_resources(server_docs, downloads)
692
cards.append(card)
693
694
if test_caddy and env.caddy:
695
backend = ''
696
if uploads and httpd is None:
697
backend = f' [backend httpd: {env.httpd_version()}]'
698
httpd = Httpd(env=env)
699
assert httpd.exists(), \
700
f'httpd not found: {env.httpd}'
701
httpd.clear_logs()
702
assert httpd.initial_start()
703
caddy = Caddy(env=env)
704
caddy.clear_logs()
705
assert caddy.initial_start()
706
server_descr = f'Caddy/{env.caddy_version()} {backend}'
707
server_port = caddy.port
708
server_docs = caddy.docs_dir
709
card = ScoreRunner(env=env,
710
protocol=protocol,
711
server_descr=server_descr,
712
server_port=server_port,
713
verbose=args.verbose, curl_verbose=args.curl_verbose,
714
download_parallel=args.download_parallel)
715
card.setup_resources(server_docs, downloads)
716
cards.append(card)
717
718
if args.start_only:
719
print('started servers:')
720
for card in cards:
721
print(f'{card.server_descr}')
722
sys.stderr.write('press [RETURN] to finish')
723
sys.stderr.flush()
724
sys.stdin.readline()
725
else:
726
for card in cards:
727
score = card.score(handshakes=handshakes,
728
downloads=downloads,
729
download_count=args.download_count,
730
uploads=uploads,
731
upload_count=args.upload_count,
732
req_count=args.request_count,
733
requests=requests,
734
nsamples=args.samples)
735
if args.json:
736
print(json.JSONEncoder(indent=2).encode(score))
737
else:
738
Card.print_score(score)
739
740
except ScoreCardError as ex:
741
sys.stderr.write(f"ERROR: {ex}\n")
742
rv = 1
743
except KeyboardInterrupt:
744
log.warning("aborted")
745
rv = 1
746
finally:
747
if caddy:
748
caddy.stop()
749
if nghttpx:
750
nghttpx.stop(wait_dead=False)
751
if httpd:
752
httpd.stop()
753
return rv
754
755
756
def print_file(filename):
757
if not os.path.exists(filename):
758
sys.stderr.write(f"ERROR: file does not exist {filename}\n")
759
return 1
760
with open(filename) as file:
761
data = json.load(file)
762
Card.print_score(data)
763
return 0
764
765
766
def main():
767
parser = argparse.ArgumentParser(prog='scorecard', description="""
768
Run a range of tests to give a scorecard for a HTTP protocol
769
'h3' or 'h2' implementation in curl.
770
""")
771
parser.add_argument("-v", "--verbose", action='count', default=1,
772
help="log more output on stderr")
773
parser.add_argument("-j", "--json", action='store_true',
774
default=False, help="print json instead of text")
775
parser.add_argument("-H", "--handshakes", action='store_true',
776
default=False, help="evaluate handshakes only")
777
parser.add_argument("-d", "--downloads", action='store_true',
778
default=False, help="evaluate downloads")
779
parser.add_argument("--download-sizes", action='append', type=str,
780
metavar='numberlist',
781
default=None, help="evaluate download size")
782
parser.add_argument("--download-count", action='store', type=int,
783
metavar='number',
784
default=50, help="perform that many downloads")
785
parser.add_argument("--samples", action='store', type=int, metavar='number',
786
default=1, help="how many sample runs to make")
787
parser.add_argument("--download-parallel", action='store', type=int,
788
metavar='number', default=0,
789
help="perform that many downloads in parallel (default all)")
790
parser.add_argument("-u", "--uploads", action='store_true',
791
default=False, help="evaluate uploads")
792
parser.add_argument("--upload-sizes", action='append', type=str,
793
metavar='numberlist',
794
default=None, help="evaluate upload size")
795
parser.add_argument("--upload-count", action='store', type=int,
796
metavar='number', default=50,
797
help="perform that many uploads")
798
parser.add_argument("-r", "--requests", action='store_true',
799
default=False, help="evaluate requests")
800
parser.add_argument("--request-count", action='store', type=int,
801
metavar='number',
802
default=5000, help="perform that many requests")
803
parser.add_argument("--httpd", action='store_true', default=False,
804
help="evaluate httpd server only")
805
parser.add_argument("--caddy", action='store_true', default=False,
806
help="evaluate caddy server only")
807
parser.add_argument("--curl-verbose", action='store_true',
808
default=False, help="run curl with `-v`")
809
parser.add_argument("--print", type=str, default=None, metavar='filename',
810
help="print the results from a JSON file")
811
parser.add_argument("protocol", default=None, nargs='?',
812
help="Name of protocol to score")
813
parser.add_argument("--start-only", action='store_true', default=False,
814
help="only start the servers")
815
parser.add_argument("--remote", action='store', type=str,
816
default=None, help="score against the remote server at <ip>:<port>")
817
args = parser.parse_args()
818
819
if args.verbose > 0:
820
console = logging.StreamHandler()
821
console.setLevel(logging.INFO)
822
console.setFormatter(logging.Formatter(logging.BASIC_FORMAT))
823
logging.getLogger('').addHandler(console)
824
825
if args.print:
826
rv = print_file(args.print)
827
elif not args.protocol:
828
parser.print_usage()
829
rv = 1
830
else:
831
rv = run_score(args, args.protocol)
832
833
sys.exit(rv)
834
835
836
if __name__ == "__main__":
837
main()
838
839