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