├── .gitignore ├── Dockerfile ├── README.md ├── ab2json.py ├── bitmath.py ├── docker-compose.yml ├── gen_report.py ├── gen_test_data.py ├── monitor-app.sh ├── nginx-docker.conf ├── pgbouncer-userlist.txt ├── requirements.txt ├── run-docker.sh ├── schema.sql ├── src ├── app_aio.py ├── app_aioflask.py ├── app_bottle.py ├── app_falcon.py ├── app_fastapi.py ├── app_flask.py ├── app_quart.py ├── app_sanic.py ├── app_starlette.py ├── app_tornado.py ├── async_db.py ├── meinheld_psycopg2.py ├── serve-aiohttp.sh ├── serve-daphne-starlette.sh ├── serve-gunicorn-bottle.sh ├── serve-gunicorn-falcon.sh ├── serve-gunicorn-flask.sh ├── serve-gunicorn-gevent-bottle.sh ├── serve-gunicorn-gevent-falcon.sh ├── serve-gunicorn-gevent-flask.sh ├── serve-gunicorn-meinheld-bottle.sh ├── serve-gunicorn-meinheld-falcon.sh ├── serve-gunicorn-meinheld-flask.sh ├── serve-hypercorn-quart.sh ├── serve-sanic-own.sh ├── serve-tornado.sh ├── serve-uvicorn-aioflask.sh ├── serve-uvicorn-fastapi.sh ├── serve-uvicorn-quart.sh ├── serve-uvicorn-sanic.sh ├── serve-uvicorn-starlette.sh ├── serve-uwsgi-bottle-own-proto.sh ├── serve-uwsgi-bottle.sh ├── serve-uwsgi-falcon.sh ├── serve-uwsgi-flask.sh └── sync_db.py └── tests.txt /.gitignore: -------------------------------------------------------------------------------- 1 | .mypy_cache/ 2 | __pycache__/ 3 | test.csv 4 | Vagrantfile 5 | .vagrant 6 | *.log 7 | venv 8 | data.csv 9 | runs/* 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8 2 | WORKDIR /app 3 | COPY requirements.txt /app/ 4 | RUN pip install -r requirements.txt 5 | COPY src/*.py /app/ 6 | COPY src/*.sh /app/ 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python webserver performance comparison 2 | 3 | Heavily based on [Cal Paterson's benchmark](https://github.com/calpaterson/python-web-perf), with some bugs fixed and some 4 | new web servers and web frameworks added. 5 | 6 | A description of this benchmark and some results are published in my article 7 | [Ignore All Web Performance Benchmarks, Including This One](https://blog.miguelgrinberg.com/post/ignore-all-web-performance-benchmarks-including-this-one). 8 | 9 | ## Benchmark code 10 | 11 | You can find the implementations of this benchmark for the different web 12 | frameworks, as well as the startup commands for all web servers, in the `src` 13 | directory. 14 | 15 | ## Running the benchmark 16 | 17 | The `run-docker.sh` command creates containers for nginx, pgbouncer and 18 | postgres and sets them up for the test. Then it builds a local image with all 19 | the application code and server startup scripts. It finally runs each of the 20 | tests in random order by starting a web server container and a load generator 21 | container. To run one or more specific tests you can pass the test name(s) as 22 | arguments. The list of available tests is in the file `tests.txt`. 23 | 24 | Look at the top of `run-docker.sh` for the list of environment variables that 25 | configure the test environment. 26 | -------------------------------------------------------------------------------- /ab2json.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import json 3 | import re 4 | import sys 5 | 6 | 7 | def parse_ab_output(output): 8 | results = {} 9 | for line in output.split('\n'): 10 | if line.startswith('This is ApacheBench'): 11 | results['version'] = \ 12 | re.match(r'.*Version ([0-9\.]+)', line).group(1) 13 | else: 14 | m = re.match(r'(.*):\s+(.*)', line) 15 | if m: 16 | key = m.group(1).lower().replace(' ', '_') 17 | values = m.group(2).split() 18 | if key in ['connect', 'processing', 'waiting', 'total']: 19 | if 'connection_times' not in results: 20 | results['connection_times'] = {} 21 | results['connection_times'][key] = { 22 | 'min': float(values[0]), 23 | 'mean': float(values[1]), 24 | 'sd': float(values[2]), 25 | 'median': float(values[3]), 26 | 'max': float(values[4]), 27 | } 28 | else: 29 | results[key] = float(re.match(r'[0-9\.]+', values[0]).group(0)) \ 30 | if values[0][0].isnumeric() else values[0] 31 | else: 32 | m = re.match(r'\s*([0-9]+%)\s+([0-9]+)', line) 33 | if m: 34 | key = m.group(1) 35 | value = int(m.group(2)) 36 | if 'percentages' not in results: 37 | results['percentages'] = {} 38 | results['percentages'][key] = value 39 | return results 40 | 41 | 42 | print(json.dumps(parse_ab_output(sys.stdin.read()), indent=4)) 43 | -------------------------------------------------------------------------------- /bitmath.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # The MIT License (MIT) 3 | # 4 | # Copyright © 2014-2016 Tim Bielawa 5 | # See GitHub Contributors Graph for more information 6 | # 7 | # Permission is hereby granted, free of charge, to any person 8 | # obtaining a copy of this software and associated documentation files 9 | # (the "Software"), to deal in the Software without restriction, 10 | # including without limitation the rights to use, copy, modify, merge, 11 | # publish, distribute, sub-license, and/or sell copies of the Software, 12 | # and to permit persons to whom the Software is furnished to do so, 13 | # subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 22 | # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 23 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 24 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | # SOFTWARE. 26 | # pylint: disable=bad-continuation,missing-docstring,invalid-name,line-too-long 27 | 28 | """Reference material: 29 | The bitmath homepage is located at: 30 | * http://bitmath.readthedocs.io/en/latest/ 31 | 32 | Prefixes for binary multiples: 33 | http://physics.nist.gov/cuu/Units/binary.html 34 | 35 | decimal and binary prefixes: 36 | man 7 units (from the Linux Documentation Project 'man-pages' package) 37 | 38 | 39 | BEFORE YOU GET HASTY WITH EXCLUDING CODE FROM COVERAGE: If you 40 | absolutely need to skip code coverage because of a strange Python 2.x 41 | vs 3.x thing, use the fancy environment substitution stuff from the 42 | .coverage RC file. In review: 43 | 44 | * If you *NEED* to skip a statement because of Python 2.x issues add the following:: 45 | 46 | # pragma: PY2X no cover 47 | 48 | * If you *NEED* to skip a statement because of Python 3.x issues add the following:: 49 | 50 | # pragma: PY3X no cover 51 | 52 | In this configuration, statements which are skipped in 2.x are still 53 | covered in 3.x, and the reverse holds true for tests skipped in 3.x. 54 | """ 55 | 56 | from __future__ import print_function 57 | 58 | import argparse 59 | import contextlib 60 | import fnmatch 61 | import math 62 | import numbers 63 | import os 64 | import os.path 65 | import platform 66 | import sys 67 | 68 | # For device capacity reading in query_device_capacity(). Only supported 69 | # on posix systems for now. Will be addressed in issue #52 on GitHub. 70 | if os.name == 'posix': 71 | import stat 72 | import fcntl 73 | import struct 74 | 75 | 76 | __all__ = ['Bit', 'Byte', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 77 | 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB', 'Kib', 78 | 'Mib', 'Gib', 'Tib', 'Pib', 'Eib', 'kb', 'Mb', 'Gb', 'Tb', 79 | 'Pb', 'Eb', 'Zb', 'Yb', 'getsize', 'listdir', 'format', 80 | 'format_string', 'format_plural', 'parse_string', 'parse_string_unsafe', 81 | 'ALL_UNIT_TYPES', 'NIST', 'NIST_PREFIXES', 'NIST_STEPS', 82 | 'SI', 'SI_PREFIXES', 'SI_STEPS'] 83 | 84 | # Python 3.x compat 85 | if sys.version > '3': 86 | long = int # pragma: PY2X no cover 87 | unicode = str # pragma: PY2X no cover 88 | 89 | #: A list of all the valid prefix unit types. Mostly for reference, 90 | #: also used by the CLI tool as valid types 91 | ALL_UNIT_TYPES = ['Bit', 'Byte', 'kb', 'kB', 'Mb', 'MB', 'Gb', 'GB', 'Tb', 92 | 'TB', 'Pb', 'PB', 'Eb', 'EB', 'Zb', 'ZB', 'Yb', 93 | 'YB', 'Kib', 'KiB', 'Mib', 'MiB', 'Gib', 'GiB', 94 | 'Tib', 'TiB', 'Pib', 'PiB', 'Eib', 'EiB'] 95 | 96 | # ##################################################################### 97 | # Set up our module variables/constants 98 | 99 | ################################### 100 | # Internal: 101 | 102 | # Console repr(), ex: MiB(13.37), or kB(42.0) 103 | _FORMAT_REPR = '{unit_singular}({value})' 104 | 105 | # ################################## 106 | # Exposed: 107 | 108 | #: Constants for referring to NIST prefix system 109 | NIST = int(2) 110 | 111 | #: Constants for referring to SI prefix system 112 | SI = int(10) 113 | 114 | # ################################## 115 | 116 | #: All of the SI prefixes 117 | SI_PREFIXES = ['k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'] 118 | 119 | #: Byte values represented by each SI prefix unit 120 | SI_STEPS = { 121 | 'Bit': 1 / 8.0, 122 | 'Byte': 1, 123 | 'k': 1000, 124 | 'M': 1000000, 125 | 'G': 1000000000, 126 | 'T': 1000000000000, 127 | 'P': 1000000000000000, 128 | 'E': 1000000000000000000, 129 | 'Z': 1000000000000000000000, 130 | 'Y': 1000000000000000000000000 131 | } 132 | 133 | 134 | #: All of the NIST prefixes 135 | NIST_PREFIXES = ['Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei'] 136 | 137 | #: Byte values represented by each NIST prefix unit 138 | NIST_STEPS = { 139 | 'Bit': 1 / 8.0, 140 | 'Byte': 1, 141 | 'Ki': 1024, 142 | 'Mi': 1048576, 143 | 'Gi': 1073741824, 144 | 'Ti': 1099511627776, 145 | 'Pi': 1125899906842624, 146 | 'Ei': 1152921504606846976 147 | } 148 | 149 | #: String representation, ex: ``13.37 MiB``, or ``42.0 kB`` 150 | format_string = "{value} {unit}" 151 | 152 | #: Pluralization behavior 153 | format_plural = False 154 | 155 | 156 | def os_name(): 157 | # makes unittesting platform specific code easier 158 | return os.name 159 | 160 | 161 | def capitalize_first(s): 162 | """Capitalize ONLY the first letter of the input `s` 163 | 164 | * returns a copy of input `s` with the first letter capitalized 165 | """ 166 | pfx = s[0].upper() 167 | _s = s[1:] 168 | return pfx + _s 169 | 170 | 171 | ###################################################################### 172 | # Base class for everything else 173 | class Bitmath(object): 174 | """The base class for all the other prefix classes""" 175 | 176 | # All the allowed input types 177 | valid_types = (int, float, long) 178 | 179 | def __init__(self, value=0, bytes=None, bits=None): 180 | """Instantiate with `value` by the unit, in plain bytes, or 181 | bits. Don't supply more than one keyword. 182 | 183 | default behavior: initialize with value of 0 184 | only setting value: assert bytes is None and bits is None 185 | only setting bytes: assert value == 0 and bits is None 186 | only setting bits: assert value == 0 and bytes is None 187 | """ 188 | _raise = False 189 | if (value == 0) and (bytes is None) and (bits is None): 190 | pass 191 | # Setting by bytes 192 | elif bytes is not None: 193 | if (value == 0) and (bits is None): 194 | pass 195 | else: 196 | _raise = True 197 | # setting by bits 198 | elif bits is not None: 199 | if (value == 0) and (bytes is None): 200 | pass 201 | else: 202 | _raise = True 203 | 204 | if _raise: 205 | raise ValueError("Only one parameter of: value, bytes, or bits is allowed") 206 | 207 | self._do_setup() 208 | if bytes: 209 | # We were provided with the fundamental base unit, no need 210 | # to normalize 211 | self._byte_value = bytes 212 | self._bit_value = bytes * 8.0 213 | elif bits: 214 | # We were *ALMOST* given the fundamental base 215 | # unit. Translate it into the fundamental unit then 216 | # normalize. 217 | self._byte_value = bits / 8.0 218 | self._bit_value = bits 219 | else: 220 | # We were given a value representative of this *prefix 221 | # unit*. We need to normalize it into the number of bytes 222 | # it represents. 223 | self._norm(value) 224 | 225 | # We have the fundamental unit figured out. Set the 'pretty' unit 226 | self._set_prefix_value() 227 | 228 | def _set_prefix_value(self): 229 | self.prefix_value = self._to_prefix_value(self._byte_value) 230 | 231 | def _to_prefix_value(self, value): 232 | """Return the number of bits/bytes as they would look like if we 233 | converted *to* this unit""" 234 | return value / float(self._unit_value) 235 | 236 | def _setup(self): 237 | raise NotImplementedError("The base 'bitmath.Bitmath' class can not be used directly") 238 | 239 | def _do_setup(self): 240 | """Setup basic parameters for this class. 241 | 242 | `base` is the numeric base which when raised to `power` is equivalent 243 | to 1 unit of the corresponding prefix. I.e., base=2, power=10 244 | represents 2^10, which is the NIST Binary Prefix for 1 Kibibyte. 245 | 246 | Likewise, for the SI prefix classes `base` will be 10, and the `power` 247 | for the Kilobyte is 3. 248 | """ 249 | (self._base, self._power, self._name_singular, self._name_plural) = self._setup() 250 | self._unit_value = self._base ** self._power 251 | 252 | def _norm(self, value): 253 | """Normalize the input value into the fundamental unit for this prefix 254 | type. 255 | 256 | :param number value: The input value to be normalized 257 | :raises ValueError: if the input value is not a type of real number 258 | """ 259 | if isinstance(value, self.valid_types): 260 | self._byte_value = value * self._unit_value 261 | self._bit_value = self._byte_value * 8.0 262 | else: 263 | raise ValueError("Initialization value '%s' is of an invalid type: %s. " 264 | "Must be one of %s" % ( 265 | value, 266 | type(value), 267 | ", ".join(str(x) for x in self.valid_types))) 268 | 269 | ################################################################## 270 | # Properties 271 | 272 | #: The mathematical base of an instance 273 | base = property(lambda s: s._base) 274 | 275 | binary = property(lambda s: bin(int(s.bits))) 276 | """The binary representation of an instance in binary 1s and 0s. Note 277 | that for very large numbers this will mean a lot of 1s and 0s. For 278 | example, GiB(100) would be represented as:: 279 | 280 | 0b1100100000000000000000000000000000000000 281 | 282 | That leading ``0b`` is normal. That's how Python represents binary. 283 | 284 | """ 285 | 286 | #: Alias for :attr:`binary` 287 | bin = property(lambda s: s.binary) 288 | 289 | #: The number of bits in an instance 290 | bits = property(lambda s: s._bit_value) 291 | 292 | #: The number of bytes in an instance 293 | bytes = property(lambda s: s._byte_value) 294 | 295 | #: The mathematical power of an instance 296 | power = property(lambda s: s._power) 297 | 298 | @property 299 | def system(self): 300 | """The system of units used to measure an instance""" 301 | if self._base == 2: 302 | return "NIST" 303 | elif self._base == 10: 304 | return "SI" 305 | else: 306 | # I don't expect to ever encounter this logic branch, but 307 | # hey, it's better to have extra test coverage than 308 | # insufficient test coverage. 309 | raise ValueError("Instances mathematical base is an unsupported value: %s" % ( 310 | str(self._base))) 311 | 312 | @property 313 | def unit(self): 314 | """The string that is this instances prefix unit name in agreement 315 | with this instance value (singular or plural). Following the 316 | convention that only 1 is singular. This will always be the singular 317 | form when :attr:`bitmath.format_plural` is ``False`` (default value). 318 | 319 | For example: 320 | 321 | >>> KiB(1).unit == 'KiB' 322 | >>> Byte(0).unit == 'Bytes' 323 | >>> Byte(1).unit == 'Byte' 324 | >>> Byte(1.1).unit == 'Bytes' 325 | >>> Gb(2).unit == 'Gbs' 326 | 327 | """ 328 | global format_plural 329 | 330 | if self.prefix_value == 1: 331 | # If it's a '1', return it singular, no matter what 332 | return self._name_singular 333 | elif format_plural: 334 | # Pluralization requested 335 | return self._name_plural 336 | else: 337 | # Pluralization NOT requested, and the value is not 1 338 | return self._name_singular 339 | 340 | @property 341 | def unit_plural(self): 342 | """The string that is an instances prefix unit name in the plural 343 | form. 344 | 345 | For example: 346 | 347 | >>> KiB(1).unit_plural == 'KiB' 348 | >>> Byte(1024).unit_plural == 'Bytes' 349 | >>> Gb(1).unit_plural == 'Gb' 350 | 351 | """ 352 | return self._name_plural 353 | 354 | @property 355 | def unit_singular(self): 356 | """The string that is an instances prefix unit name in the singular 357 | form. 358 | 359 | For example: 360 | 361 | >>> KiB(1).unit_singular == 'KiB' 362 | >>> Byte(1024).unit == 'B' 363 | >>> Gb(1).unit_singular == 'Gb' 364 | """ 365 | return self._name_singular 366 | 367 | #: The "prefix" value of an instance 368 | value = property(lambda s: s.prefix_value) 369 | 370 | @classmethod 371 | def from_other(cls, item): 372 | """Factory function to return instances of `item` converted into a new 373 | instance of ``cls``. Because this is a class method, it may be called 374 | from any bitmath class object without the need to explicitly 375 | instantiate the class ahead of time. 376 | 377 | *Implicit Parameter:* 378 | 379 | * ``cls`` A bitmath class, implicitly set to the class of the 380 | instance object it is called on 381 | 382 | *User Supplied Parameter:* 383 | 384 | * ``item`` A :class:`bitmath.Bitmath` subclass instance 385 | 386 | *Example:* 387 | 388 | >>> import bitmath 389 | >>> kib = bitmath.KiB.from_other(bitmath.MiB(1)) 390 | >>> print kib 391 | KiB(1024.0) 392 | 393 | """ 394 | if isinstance(item, Bitmath): 395 | return cls(bits=item.bits) 396 | else: 397 | raise ValueError("The provided items must be a valid bitmath class: %s" % 398 | str(item.__class__)) 399 | 400 | ###################################################################### 401 | # The following implement the Python datamodel customization methods 402 | # 403 | # Reference: http://docs.python.org/2.7/reference/datamodel.html#basic-customization 404 | 405 | def __repr__(self): 406 | """Representation of this object as you would expect to see in an 407 | interpreter""" 408 | global _FORMAT_REPR 409 | return self.format(_FORMAT_REPR) 410 | 411 | def __str__(self): 412 | """String representation of this object""" 413 | global format_string 414 | return self.format(format_string) 415 | 416 | def format(self, fmt): 417 | """Return a representation of this instance formatted with user 418 | supplied syntax""" 419 | _fmt_params = { 420 | 'base': self.base, 421 | 'bin': self.bin, 422 | 'binary': self.binary, 423 | 'bits': self.bits, 424 | 'bytes': self.bytes, 425 | 'power': self.power, 426 | 'system': self.system, 427 | 'unit': self.unit, 428 | 'unit_plural': self.unit_plural, 429 | 'unit_singular': self.unit_singular, 430 | 'value': self.value 431 | } 432 | 433 | return fmt.format(**_fmt_params) 434 | 435 | ################################################################## 436 | # Guess the best human-readable prefix unit for representation 437 | ################################################################## 438 | 439 | def best_prefix(self, system=None): 440 | """Optional parameter, `system`, allows you to prefer NIST or SI in 441 | the results. By default, the current system is used (Bit/Byte default 442 | to NIST). 443 | 444 | Logic discussion/notes: 445 | 446 | Base-case, does it need converting? 447 | 448 | If the instance is less than one Byte, return the instance as a Bit 449 | instance. 450 | 451 | Else, begin by recording the unit system the instance is defined 452 | by. This determines which steps (NIST_STEPS/SI_STEPS) we iterate over. 453 | 454 | If the instance is not already a ``Byte`` instance, convert it to one. 455 | 456 | NIST units step up by powers of 1024, SI units step up by powers of 457 | 1000. 458 | 459 | Take integer value of the log(base=STEP_POWER) of the instance's byte 460 | value. E.g.: 461 | 462 | >>> int(math.log(Gb(100).bytes, 1000)) 463 | 3 464 | 465 | This will return a value >= 0. The following determines the 'best 466 | prefix unit' for representation: 467 | 468 | * result == 0, best represented as a Byte 469 | * result >= len(SYSTEM_STEPS), best represented as an Exbi/Exabyte 470 | * 0 < result < len(SYSTEM_STEPS), best represented as SYSTEM_PREFIXES[result-1] 471 | 472 | """ 473 | 474 | # Use absolute value so we don't return Bit's for *everything* 475 | # less than Byte(1). From github issue #55 476 | if abs(self) < Byte(1): 477 | return Bit.from_other(self) 478 | else: 479 | if type(self) is Byte: # pylint: disable=unidiomatic-typecheck 480 | _inst = self 481 | else: 482 | _inst = Byte.from_other(self) 483 | 484 | # Which table to consult? Was a preferred system provided? 485 | if system is None: 486 | # No preference. Use existing system 487 | if self.system == 'NIST': 488 | _STEPS = NIST_PREFIXES 489 | _BASE = 1024 490 | elif self.system == 'SI': 491 | _STEPS = SI_PREFIXES 492 | _BASE = 1000 493 | # Anything else would have raised by now 494 | else: 495 | # Preferred system provided. 496 | if system == NIST: 497 | _STEPS = NIST_PREFIXES 498 | _BASE = 1024 499 | elif system == SI: 500 | _STEPS = SI_PREFIXES 501 | _BASE = 1000 502 | else: 503 | raise ValueError("Invalid value given for 'system' parameter." 504 | " Must be one of NIST or SI") 505 | 506 | # Index of the string of the best prefix in the STEPS list 507 | _index = int(math.log(abs(_inst.bytes), _BASE)) 508 | 509 | # Recall that the log() function returns >= 0. This doesn't 510 | # map to the STEPS list 1:1. That is to say, 0 is handled with 511 | # special care. So if the _index is 1, we actually want item 0 512 | # in the list. 513 | 514 | if _index == 0: 515 | # Already a Byte() type, so return it. 516 | return _inst 517 | elif _index >= len(_STEPS): 518 | # This is a really big number. Use the biggest prefix we've got 519 | _best_prefix = _STEPS[-1] 520 | elif 0 < _index < len(_STEPS): 521 | # There is an appropriate prefix unit to represent this 522 | _best_prefix = _STEPS[_index - 1] 523 | 524 | _conversion_method = getattr( 525 | self, 526 | 'to_%sB' % _best_prefix) 527 | 528 | return _conversion_method() 529 | 530 | ################################################################## 531 | 532 | def to_Bit(self): 533 | return Bit(self._bit_value) 534 | 535 | def to_Byte(self): 536 | return Byte(self._byte_value / float(NIST_STEPS['Byte'])) 537 | 538 | # Properties 539 | Bit = property(lambda s: s.to_Bit()) 540 | Byte = property(lambda s: s.to_Byte()) 541 | 542 | ################################################################## 543 | 544 | def to_KiB(self): 545 | return KiB(bits=self._bit_value) 546 | 547 | def to_Kib(self): 548 | return Kib(bits=self._bit_value) 549 | 550 | def to_kB(self): 551 | return kB(bits=self._bit_value) 552 | 553 | def to_kb(self): 554 | return kb(bits=self._bit_value) 555 | 556 | # Properties 557 | KiB = property(lambda s: s.to_KiB()) 558 | Kib = property(lambda s: s.to_Kib()) 559 | kB = property(lambda s: s.to_kB()) 560 | kb = property(lambda s: s.to_kb()) 561 | 562 | ################################################################## 563 | 564 | def to_MiB(self): 565 | return MiB(bits=self._bit_value) 566 | 567 | def to_Mib(self): 568 | return Mib(bits=self._bit_value) 569 | 570 | def to_MB(self): 571 | return MB(bits=self._bit_value) 572 | 573 | def to_Mb(self): 574 | return Mb(bits=self._bit_value) 575 | 576 | # Properties 577 | MiB = property(lambda s: s.to_MiB()) 578 | Mib = property(lambda s: s.to_Mib()) 579 | MB = property(lambda s: s.to_MB()) 580 | Mb = property(lambda s: s.to_Mb()) 581 | 582 | ################################################################## 583 | 584 | def to_GiB(self): 585 | return GiB(bits=self._bit_value) 586 | 587 | def to_Gib(self): 588 | return Gib(bits=self._bit_value) 589 | 590 | def to_GB(self): 591 | return GB(bits=self._bit_value) 592 | 593 | def to_Gb(self): 594 | return Gb(bits=self._bit_value) 595 | 596 | # Properties 597 | GiB = property(lambda s: s.to_GiB()) 598 | Gib = property(lambda s: s.to_Gib()) 599 | GB = property(lambda s: s.to_GB()) 600 | Gb = property(lambda s: s.to_Gb()) 601 | 602 | ################################################################## 603 | 604 | def to_TiB(self): 605 | return TiB(bits=self._bit_value) 606 | 607 | def to_Tib(self): 608 | return Tib(bits=self._bit_value) 609 | 610 | def to_TB(self): 611 | return TB(bits=self._bit_value) 612 | 613 | def to_Tb(self): 614 | return Tb(bits=self._bit_value) 615 | 616 | # Properties 617 | TiB = property(lambda s: s.to_TiB()) 618 | Tib = property(lambda s: s.to_Tib()) 619 | TB = property(lambda s: s.to_TB()) 620 | Tb = property(lambda s: s.to_Tb()) 621 | 622 | ################################################################## 623 | 624 | def to_PiB(self): 625 | return PiB(bits=self._bit_value) 626 | 627 | def to_Pib(self): 628 | return Pib(bits=self._bit_value) 629 | 630 | def to_PB(self): 631 | return PB(bits=self._bit_value) 632 | 633 | def to_Pb(self): 634 | return Pb(bits=self._bit_value) 635 | 636 | # Properties 637 | PiB = property(lambda s: s.to_PiB()) 638 | Pib = property(lambda s: s.to_Pib()) 639 | PB = property(lambda s: s.to_PB()) 640 | Pb = property(lambda s: s.to_Pb()) 641 | 642 | ################################################################## 643 | 644 | def to_EiB(self): 645 | return EiB(bits=self._bit_value) 646 | 647 | def to_Eib(self): 648 | return Eib(bits=self._bit_value) 649 | 650 | def to_EB(self): 651 | return EB(bits=self._bit_value) 652 | 653 | def to_Eb(self): 654 | return Eb(bits=self._bit_value) 655 | 656 | # Properties 657 | EiB = property(lambda s: s.to_EiB()) 658 | Eib = property(lambda s: s.to_Eib()) 659 | EB = property(lambda s: s.to_EB()) 660 | Eb = property(lambda s: s.to_Eb()) 661 | 662 | ################################################################## 663 | # The SI units go beyond the NIST units. They also have the Zetta 664 | # and Yotta prefixes. 665 | 666 | def to_ZB(self): 667 | return ZB(bits=self._bit_value) 668 | 669 | def to_Zb(self): 670 | return Zb(bits=self._bit_value) 671 | 672 | # Properties 673 | ZB = property(lambda s: s.to_ZB()) 674 | Zb = property(lambda s: s.to_Zb()) 675 | 676 | ################################################################## 677 | 678 | def to_YB(self): 679 | return YB(bits=self._bit_value) 680 | 681 | def to_Yb(self): 682 | return Yb(bits=self._bit_value) 683 | 684 | #: A new object representing this instance as a Yottabyte 685 | YB = property(lambda s: s.to_YB()) 686 | Yb = property(lambda s: s.to_Yb()) 687 | 688 | ################################################################## 689 | # Rich comparison operations 690 | ################################################################## 691 | 692 | def __lt__(self, other): 693 | if isinstance(other, numbers.Number): 694 | return self.prefix_value < other 695 | else: 696 | return self._byte_value < other.bytes 697 | 698 | def __le__(self, other): 699 | if isinstance(other, numbers.Number): 700 | return self.prefix_value <= other 701 | else: 702 | return self._byte_value <= other.bytes 703 | 704 | def __eq__(self, other): 705 | if isinstance(other, numbers.Number): 706 | return self.prefix_value == other 707 | else: 708 | return self._byte_value == other.bytes 709 | 710 | def __ne__(self, other): 711 | if isinstance(other, numbers.Number): 712 | return self.prefix_value != other 713 | else: 714 | return self._byte_value != other.bytes 715 | 716 | def __gt__(self, other): 717 | if isinstance(other, numbers.Number): 718 | return self.prefix_value > other 719 | else: 720 | return self._byte_value > other.bytes 721 | 722 | def __ge__(self, other): 723 | if isinstance(other, numbers.Number): 724 | return self.prefix_value >= other 725 | else: 726 | return self._byte_value >= other.bytes 727 | 728 | ################################################################## 729 | # Basic math operations 730 | ################################################################## 731 | 732 | # Reference: http://docs.python.org/2.7/reference/datamodel.html#emulating-numeric-types 733 | 734 | """These methods are called to implement the binary arithmetic 735 | operations (+, -, *, //, %, divmod(), pow(), **, <<, >>, &, ^, |). For 736 | instance, to evaluate the expression x + y, where x is an instance of 737 | a class that has an __add__() method, x.__add__(y) is called. The 738 | __divmod__() method should be the equivalent to using __floordiv__() 739 | and __mod__(); it should not be related to __truediv__() (described 740 | below). Note that __pow__() should be defined to accept an optional 741 | third argument if the ternary version of the built-in pow() function 742 | is to be supported.object.__complex__(self) 743 | """ 744 | 745 | def __add__(self, other): 746 | """Supported operations with result types: 747 | 748 | - bm + bm = bm 749 | - bm + num = num 750 | - num + bm = num (see radd) 751 | """ 752 | if isinstance(other, numbers.Number): 753 | # bm + num 754 | return other + self.value 755 | else: 756 | # bm + bm 757 | total_bytes = self._byte_value + other.bytes 758 | return (type(self))(bytes=total_bytes) 759 | 760 | def __sub__(self, other): 761 | """Subtraction: Supported operations with result types: 762 | 763 | - bm - bm = bm 764 | - bm - num = num 765 | - num - bm = num (see rsub) 766 | """ 767 | if isinstance(other, numbers.Number): 768 | # bm - num 769 | return self.value - other 770 | else: 771 | # bm - bm 772 | total_bytes = self._byte_value - other.bytes 773 | return (type(self))(bytes=total_bytes) 774 | 775 | def __mul__(self, other): 776 | """Multiplication: Supported operations with result types: 777 | 778 | - bm1 * bm2 = bm1 779 | - bm * num = bm 780 | - num * bm = num (see rmul) 781 | """ 782 | if isinstance(other, numbers.Number): 783 | # bm * num 784 | result = self._byte_value * other 785 | return (type(self))(bytes=result) 786 | else: 787 | # bm1 * bm2 788 | _other = other.value * other.base ** other.power 789 | _self = self.prefix_value * self._base ** self._power 790 | return (type(self))(bytes=_other * _self) 791 | 792 | """The division operator (/) is implemented by these methods. The 793 | __truediv__() method is used when __future__.division is in effect, 794 | otherwise __div__() is used. If only one of these two methods is 795 | defined, the object will not support division in the alternate 796 | context; TypeError will be raised instead.""" 797 | 798 | def __div__(self, other): 799 | """Division: Supported operations with result types: 800 | 801 | - bm1 / bm2 = num 802 | - bm / num = bm 803 | - num / bm = num (see rdiv) 804 | """ 805 | if isinstance(other, numbers.Number): 806 | # bm / num 807 | result = self._byte_value / other 808 | return (type(self))(bytes=result) 809 | else: 810 | # bm1 / bm2 811 | return self._byte_value / float(other.bytes) 812 | 813 | def __truediv__(self, other): 814 | # num / bm 815 | return self.__div__(other) 816 | 817 | # def __floordiv__(self, other): 818 | # return NotImplemented 819 | 820 | # def __mod__(self, other): 821 | # return NotImplemented 822 | 823 | # def __divmod__(self, other): 824 | # return NotImplemented 825 | 826 | # def __pow__(self, other, modulo=None): 827 | # return NotImplemented 828 | 829 | ################################################################## 830 | 831 | """These methods are called to implement the binary arithmetic 832 | operations (+, -, *, /, %, divmod(), pow(), **, <<, >>, &, ^, |) with 833 | reflected (swapped) operands. These functions are only called if the 834 | left operand does not support the corresponding operation and the 835 | operands are of different types. [2] For instance, to evaluate the 836 | expression x - y, where y is an instance of a class that has an 837 | __rsub__() method, y.__rsub__(x) is called if x.__sub__(y) returns 838 | NotImplemented. 839 | 840 | These are the add/sub/mul/div methods for syntax where a number type 841 | is given for the LTYPE and a bitmath object is given for the 842 | RTYPE. E.g., 3 * MiB(3), or 10 / GB(42) 843 | """ 844 | 845 | def __radd__(self, other): 846 | # num + bm = num 847 | return other + self.value 848 | 849 | def __rsub__(self, other): 850 | # num - bm = num 851 | return other - self.value 852 | 853 | def __rmul__(self, other): 854 | # num * bm = bm 855 | return self * other 856 | 857 | def __rdiv__(self, other): 858 | # num / bm = num 859 | return other / float(self.value) 860 | 861 | def __rtruediv__(self, other): 862 | # num / bm = num 863 | return other / float(self.value) 864 | 865 | """Called to implement the built-in functions complex(), int(), 866 | long(), and float(). Should return a value of the appropriate type. 867 | 868 | If one of those methods does not support the operation with the 869 | supplied arguments, it should return NotImplemented. 870 | 871 | For bitmath purposes, these methods return the int/long/float 872 | equivalent of the this instances prefix Unix value. That is to say: 873 | 874 | - int(KiB(3.336)) would return 3 875 | - long(KiB(3.336)) would return 3L 876 | - float(KiB(3.336)) would return 3.336 877 | """ 878 | 879 | def __int__(self): 880 | """Return this instances prefix unit as an integer""" 881 | return int(self.prefix_value) 882 | 883 | def __long__(self): 884 | """Return this instances prefix unit as a long integer""" 885 | return long(self.prefix_value) # pragma: PY3X no cover 886 | 887 | def __float__(self): 888 | """Return this instances prefix unit as a floating point number""" 889 | return float(self.prefix_value) 890 | 891 | ################################################################## 892 | # Bitwise operations 893 | ################################################################## 894 | 895 | def __lshift__(self, other): 896 | """Left shift, ex: 100 << 2 897 | 898 | A left shift by n bits is equivalent to multiplication by pow(2, 899 | n). A long integer is returned if the result exceeds the range of 900 | plain integers.""" 901 | shifted = int(self.bits) << other 902 | return type(self)(bits=shifted) 903 | 904 | def __rshift__(self, other): 905 | """Right shift, ex: 100 >> 2 906 | 907 | A right shift by n bits is equivalent to division by pow(2, n).""" 908 | shifted = int(self.bits) >> other 909 | return type(self)(bits=shifted) 910 | 911 | def __and__(self, other): 912 | """"Bitwise and, ex: 100 & 2 913 | 914 | bitwise and". Each bit of the output is 1 if the corresponding bit 915 | of x AND of y is 1, otherwise it's 0.""" 916 | andd = int(self.bits) & other 917 | return type(self)(bits=andd) 918 | 919 | def __xor__(self, other): 920 | """Bitwise xor, ex: 100 ^ 2 921 | 922 | Does a "bitwise exclusive or". Each bit of the output is the same 923 | as the corresponding bit in x if that bit in y is 0, and it's the 924 | complement of the bit in x if that bit in y is 1.""" 925 | xord = int(self.bits) ^ other 926 | return type(self)(bits=xord) 927 | 928 | def __or__(self, other): 929 | """Bitwise or, ex: 100 | 2 930 | 931 | Does a "bitwise or". Each bit of the output is 0 if the corresponding 932 | bit of x AND of y is 0, otherwise it's 1.""" 933 | ord = int(self.bits) | other 934 | return type(self)(bits=ord) 935 | 936 | ################################################################## 937 | 938 | def __neg__(self): 939 | """The negative version of this instance""" 940 | return (type(self))(-abs(self.prefix_value)) 941 | 942 | def __pos__(self): 943 | return (type(self))(abs(self.prefix_value)) 944 | 945 | def __abs__(self): 946 | return (type(self))(abs(self.prefix_value)) 947 | 948 | # def __invert__(self): 949 | # """Called to implement the unary arithmetic operations (-, +, abs() 950 | # and ~).""" 951 | # return NotImplemented 952 | 953 | 954 | ###################################################################### 955 | # First, the bytes... 956 | 957 | class Byte(Bitmath): 958 | """Byte based types fundamentally operate on self._bit_value""" 959 | def _setup(self): 960 | return (2, 0, 'Byte', 'Bytes') 961 | 962 | ###################################################################### 963 | # NIST Prefixes for Byte based types 964 | 965 | 966 | class KiB(Byte): 967 | def _setup(self): 968 | return (2, 10, 'KiB', 'KiBs') 969 | 970 | 971 | Kio = KiB 972 | 973 | 974 | class MiB(Byte): 975 | def _setup(self): 976 | return (2, 20, 'MiB', 'MiBs') 977 | 978 | 979 | Mio = MiB 980 | 981 | 982 | class GiB(Byte): 983 | def _setup(self): 984 | return (2, 30, 'GiB', 'GiBs') 985 | 986 | 987 | Gio = GiB 988 | 989 | 990 | class TiB(Byte): 991 | def _setup(self): 992 | return (2, 40, 'TiB', 'TiBs') 993 | 994 | 995 | Tio = TiB 996 | 997 | 998 | class PiB(Byte): 999 | def _setup(self): 1000 | return (2, 50, 'PiB', 'PiBs') 1001 | 1002 | 1003 | Pio = PiB 1004 | 1005 | 1006 | class EiB(Byte): 1007 | def _setup(self): 1008 | return (2, 60, 'EiB', 'EiBs') 1009 | 1010 | 1011 | Eio = EiB 1012 | 1013 | 1014 | ###################################################################### 1015 | # SI Prefixes for Byte based types 1016 | class kB(Byte): 1017 | def _setup(self): 1018 | return (10, 3, 'kB', 'kBs') 1019 | 1020 | 1021 | ko = kB 1022 | 1023 | 1024 | class MB(Byte): 1025 | def _setup(self): 1026 | return (10, 6, 'MB', 'MBs') 1027 | 1028 | 1029 | Mo = MB 1030 | 1031 | 1032 | class GB(Byte): 1033 | def _setup(self): 1034 | return (10, 9, 'GB', 'GBs') 1035 | 1036 | 1037 | Go = GB 1038 | 1039 | 1040 | class TB(Byte): 1041 | def _setup(self): 1042 | return (10, 12, 'TB', 'TBs') 1043 | 1044 | 1045 | To = TB 1046 | 1047 | 1048 | class PB(Byte): 1049 | def _setup(self): 1050 | return (10, 15, 'PB', 'PBs') 1051 | 1052 | 1053 | Po = PB 1054 | 1055 | 1056 | class EB(Byte): 1057 | def _setup(self): 1058 | return (10, 18, 'EB', 'EBs') 1059 | 1060 | 1061 | Eo = EB 1062 | 1063 | 1064 | class ZB(Byte): 1065 | def _setup(self): 1066 | return (10, 21, 'ZB', 'ZBs') 1067 | 1068 | 1069 | Zo = ZB 1070 | 1071 | 1072 | class YB(Byte): 1073 | def _setup(self): 1074 | return (10, 24, 'YB', 'YBs') 1075 | 1076 | 1077 | Yo = YB 1078 | 1079 | 1080 | ###################################################################### 1081 | # And now the bit types 1082 | class Bit(Bitmath): 1083 | """Bit based types fundamentally operate on self._bit_value""" 1084 | 1085 | def _set_prefix_value(self): 1086 | self.prefix_value = self._to_prefix_value(self._bit_value) 1087 | 1088 | def _setup(self): 1089 | return (2, 0, 'Bit', 'Bits') 1090 | 1091 | def _norm(self, value): 1092 | """Normalize the input value into the fundamental unit for this prefix 1093 | type""" 1094 | self._bit_value = value * self._unit_value 1095 | self._byte_value = self._bit_value / 8.0 1096 | 1097 | 1098 | ###################################################################### 1099 | # NIST Prefixes for Bit based types 1100 | class Kib(Bit): 1101 | def _setup(self): 1102 | return (2, 10, 'Kib', 'Kibs') 1103 | 1104 | 1105 | class Mib(Bit): 1106 | def _setup(self): 1107 | return (2, 20, 'Mib', 'Mibs') 1108 | 1109 | 1110 | class Gib(Bit): 1111 | def _setup(self): 1112 | return (2, 30, 'Gib', 'Gibs') 1113 | 1114 | 1115 | class Tib(Bit): 1116 | def _setup(self): 1117 | return (2, 40, 'Tib', 'Tibs') 1118 | 1119 | 1120 | class Pib(Bit): 1121 | def _setup(self): 1122 | return (2, 50, 'Pib', 'Pibs') 1123 | 1124 | 1125 | class Eib(Bit): 1126 | def _setup(self): 1127 | return (2, 60, 'Eib', 'Eibs') 1128 | 1129 | 1130 | ###################################################################### 1131 | # SI Prefixes for Bit based types 1132 | class kb(Bit): 1133 | def _setup(self): 1134 | return (10, 3, 'kb', 'kbs') 1135 | 1136 | 1137 | class Mb(Bit): 1138 | def _setup(self): 1139 | return (10, 6, 'Mb', 'Mbs') 1140 | 1141 | 1142 | class Gb(Bit): 1143 | def _setup(self): 1144 | return (10, 9, 'Gb', 'Gbs') 1145 | 1146 | 1147 | class Tb(Bit): 1148 | def _setup(self): 1149 | return (10, 12, 'Tb', 'Tbs') 1150 | 1151 | 1152 | class Pb(Bit): 1153 | def _setup(self): 1154 | return (10, 15, 'Pb', 'Pbs') 1155 | 1156 | 1157 | class Eb(Bit): 1158 | def _setup(self): 1159 | return (10, 18, 'Eb', 'Ebs') 1160 | 1161 | 1162 | class Zb(Bit): 1163 | def _setup(self): 1164 | return (10, 21, 'Zb', 'Zbs') 1165 | 1166 | 1167 | class Yb(Bit): 1168 | def _setup(self): 1169 | return (10, 24, 'Yb', 'Ybs') 1170 | 1171 | 1172 | ###################################################################### 1173 | # Utility functions 1174 | def best_prefix(bytes, system=NIST): 1175 | """Return a bitmath instance representing the best human-readable 1176 | representation of the number of bytes given by ``bytes``. In addition 1177 | to a numeric type, the ``bytes`` parameter may also be a bitmath type. 1178 | 1179 | Optionally select a preferred unit system by specifying the ``system`` 1180 | keyword. Choices for ``system`` are ``bitmath.NIST`` (default) and 1181 | ``bitmath.SI``. 1182 | 1183 | Basically a shortcut for: 1184 | 1185 | >>> import bitmath 1186 | >>> b = bitmath.Byte(12345) 1187 | >>> best = b.best_prefix() 1188 | 1189 | Or: 1190 | 1191 | >>> import bitmath 1192 | >>> best = (bitmath.KiB(12345) * 4201).best_prefix() 1193 | """ 1194 | if isinstance(bytes, Bitmath): 1195 | value = bytes.bytes 1196 | else: 1197 | value = bytes 1198 | return Byte(value).best_prefix(system=system) 1199 | 1200 | 1201 | def query_device_capacity(device_fd): 1202 | """Create bitmath instances of the capacity of a system block device 1203 | 1204 | Make one or more ioctl request to query the capacity of a block 1205 | device. Perform any processing required to compute the final capacity 1206 | value. Return the device capacity in bytes as a :class:`bitmath.Byte` 1207 | instance. 1208 | 1209 | Thanks to the following resources for help figuring this out Linux/Mac 1210 | ioctl's for querying block device sizes: 1211 | 1212 | * http://stackoverflow.com/a/12925285/263969 1213 | * http://stackoverflow.com/a/9764508/263969 1214 | 1215 | :param file device_fd: A ``file`` object of the device to query the 1216 | capacity of (as in ``get_device_capacity(open("/dev/sda"))``). 1217 | 1218 | :return: a bitmath :class:`bitmath.Byte` instance equivalent to the 1219 | capacity of the target device in bytes. 1220 | """ 1221 | if os_name() != 'posix': 1222 | raise NotImplementedError("'bitmath.query_device_capacity' is not supported on this platform: %s" % os_name()) 1223 | 1224 | s = os.stat(device_fd.name).st_mode 1225 | if not stat.S_ISBLK(s): 1226 | raise ValueError("The file descriptor provided is not of a device type") 1227 | 1228 | # The keys of the ``ioctl_map`` dictionary correlate to possible 1229 | # values from the ``platform.system`` function. 1230 | ioctl_map = { 1231 | # ioctls for the "Linux" platform 1232 | "Linux": { 1233 | "request_params": [ 1234 | # A list of parameters to calculate the block size. 1235 | # 1236 | # ( PARAM_NAME , FORMAT_CHAR , REQUEST_CODE ) 1237 | ("BLKGETSIZE64", "L", 0x80081272) 1238 | # Per , the BLKGETSIZE64 request returns a 1239 | # 'u64' sized value. This is an unsigned 64 bit 1240 | # integer C type. This means to correctly "buffer" the 1241 | # result we need 64 bits, or 8 bytes, of memory. 1242 | # 1243 | # The struct module documentation include a reference 1244 | # chart relating formatting characters to native C 1245 | # Types. In this case, using the "native size", the 1246 | # table tells us: 1247 | # 1248 | # * Character 'L' - Unsigned Long C Type (u64) - Loads into a Python int type 1249 | # 1250 | # Confirm this character is right by running (on Linux): 1251 | # 1252 | # >>> import struct 1253 | # >>> print 8 == struct.calcsize('L') 1254 | # 1255 | # The result should be true as long as your kernel 1256 | # headers define BLKGETSIZE64 as a u64 type (please 1257 | # file a bug report at 1258 | # https://github.com/tbielawa/bitmath/issues/new if 1259 | # this does *not* work for you) 1260 | ], 1261 | # func is how the final result is decided. Because the 1262 | # Linux BLKGETSIZE64 call returns the block device 1263 | # capacity in bytes as an integer value, no extra 1264 | # calculations are required. Simply return the value of 1265 | # BLKGETSIZE64. 1266 | "func": lambda x: x["BLKGETSIZE64"] 1267 | }, 1268 | # ioctls for the "Darwin" (Mac OS X) platform 1269 | "Darwin": { 1270 | "request_params": [ 1271 | # A list of parameters to calculate the block size. 1272 | # 1273 | # ( PARAM_NAME , FORMAT_CHAR , REQUEST_CODE ) 1274 | ("DKIOCGETBLOCKCOUNT", "L", 0x40086419), 1275 | # Per : get media's block count - uint64_t 1276 | # 1277 | # As in the BLKGETSIZE64 example, an unsigned 64 bit 1278 | # integer will use the 'L' formatting character 1279 | ("DKIOCGETBLOCKSIZE", "I", 0x40046418) 1280 | # Per : get media's block size - uint32_t 1281 | # 1282 | # This request returns an unsigned 32 bit integer, or 1283 | # in other words: just a normal integer (or 'int' c 1284 | # type). That should require 4 bytes of space for 1285 | # buffering. According to the struct modules 1286 | # 'Formatting Characters' chart: 1287 | # 1288 | # * Character 'I' - Unsigned Int C Type (uint32_t) - Loads into a Python int type 1289 | ], 1290 | # OS X doesn't have a direct equivalent to the Linux 1291 | # BLKGETSIZE64 request. Instead, we must request how many 1292 | # blocks (or "sectors") are on the disk, and the size (in 1293 | # bytes) of each block. Finally, multiply the two together 1294 | # to obtain capacity: 1295 | # 1296 | # n Block * y Byte 1297 | # capacity (bytes) = ------- 1298 | # 1 Block 1299 | "func": lambda x: x["DKIOCGETBLOCKCOUNT"] * x["DKIOCGETBLOCKSIZE"] 1300 | # This expression simply accepts a dictionary ``x`` as a 1301 | # parameter, and then returns the result of multiplying 1302 | # the two named dictionary items together. In this case, 1303 | # that means multiplying ``DKIOCGETBLOCKCOUNT``, the total 1304 | # number of blocks, by ``DKIOCGETBLOCKSIZE``, the size of 1305 | # each block in bytes. 1306 | } 1307 | } 1308 | 1309 | platform_params = ioctl_map[platform.system()] 1310 | results = {} 1311 | 1312 | for req_name, fmt, request_code in platform_params['request_params']: 1313 | # Read the systems native size (in bytes) of this format type. 1314 | buffer_size = struct.calcsize(fmt) 1315 | # Construct a buffer to store the ioctl result in 1316 | buffer = ' ' * buffer_size 1317 | 1318 | # This code has been ran on only a few test systems. If it's 1319 | # appropriate, maybe in the future we'll add try/except 1320 | # conditions for some possible errors. Really only for cases 1321 | # where it would add value to override the default exception 1322 | # message string. 1323 | buffer = fcntl.ioctl(device_fd.fileno(), request_code, buffer) 1324 | 1325 | # Unpack the raw result from the ioctl call into a familiar 1326 | # python data type according to the ``fmt`` rules. 1327 | result = struct.unpack(fmt, buffer)[0] 1328 | # Add the new result to our collection 1329 | results[req_name] = result 1330 | 1331 | return Byte(platform_params['func'](results)) 1332 | 1333 | 1334 | def getsize(path, bestprefix=True, system=NIST): 1335 | """Return a bitmath instance in the best human-readable representation 1336 | of the file size at `path`. Optionally, provide a preferred unit 1337 | system by setting `system` to either `bitmath.NIST` (default) or 1338 | `bitmath.SI`. 1339 | 1340 | Optionally, set ``bestprefix`` to ``False`` to get ``bitmath.Byte`` 1341 | instances back. 1342 | """ 1343 | _path = os.path.realpath(path) 1344 | size_bytes = os.path.getsize(_path) 1345 | if bestprefix: 1346 | return Byte(size_bytes).best_prefix(system=system) 1347 | else: 1348 | return Byte(size_bytes) 1349 | 1350 | 1351 | def listdir(search_base, followlinks=False, filter='*', 1352 | relpath=False, bestprefix=False, system=NIST): 1353 | """This is a generator which recurses the directory tree 1354 | `search_base`, yielding 2-tuples of: 1355 | 1356 | * The absolute/relative path to a discovered file 1357 | * A bitmath instance representing the "apparent size" of the file. 1358 | 1359 | - `search_base` - The directory to begin walking down. 1360 | - `followlinks` - Whether or not to follow symbolic links to directories 1361 | - `filter` - A glob (see :py:mod:`fnmatch`) to filter results with 1362 | (default: ``*``, everything) 1363 | - `relpath` - ``True`` to return the relative path from `pwd` or 1364 | ``False`` (default) to return the fully qualified path 1365 | - ``bestprefix`` - set to ``False`` to get ``bitmath.Byte`` 1366 | instances back instead. 1367 | - `system` - Provide a preferred unit system by setting `system` 1368 | to either ``bitmath.NIST`` (default) or ``bitmath.SI``. 1369 | 1370 | .. note:: This function does NOT return tuples for directory entities. 1371 | 1372 | .. note:: Symlinks to **files** are followed automatically 1373 | 1374 | """ 1375 | for root, dirs, files in os.walk(search_base, followlinks=followlinks): 1376 | for name in fnmatch.filter(files, filter): 1377 | _path = os.path.join(root, name) 1378 | if relpath: 1379 | # RELATIVE path 1380 | _return_path = os.path.relpath(_path, '.') 1381 | else: 1382 | # REAL path 1383 | _return_path = os.path.realpath(_path) 1384 | 1385 | if followlinks: 1386 | yield (_return_path, getsize(_path, bestprefix=bestprefix, system=system)) 1387 | else: 1388 | if os.path.isdir(_path) or os.path.islink(_path): 1389 | pass 1390 | else: 1391 | yield (_return_path, getsize(_path, bestprefix=bestprefix, system=system)) 1392 | 1393 | 1394 | def parse_string(s): 1395 | """Parse a string with units and try to make a bitmath object out of 1396 | it. 1397 | 1398 | String inputs may include whitespace characters between the value and 1399 | the unit. 1400 | """ 1401 | # Strings only please 1402 | if not isinstance(s, (str, unicode)): 1403 | raise ValueError("parse_string only accepts string inputs but a %s was given" % 1404 | type(s)) 1405 | 1406 | # get the index of the first alphabetic character 1407 | try: 1408 | index = list([i.isalpha() for i in s]).index(True) 1409 | except ValueError: 1410 | # If there's no alphabetic characters we won't be able to .index(True) 1411 | raise ValueError("No unit detected, can not parse string '%s' into a bitmath object" % s) 1412 | 1413 | # split the string into the value and the unit 1414 | val, unit = s[:index], s[index:] 1415 | 1416 | # see if the unit exists as a type in our namespace 1417 | 1418 | if unit == "b": 1419 | unit_class = Bit 1420 | elif unit == "B": 1421 | unit_class = Byte 1422 | else: 1423 | if not (hasattr(sys.modules[__name__], unit) and isinstance(getattr(sys.modules[__name__], unit), type)): 1424 | raise ValueError("The unit %s is not a valid bitmath unit" % unit) 1425 | unit_class = globals()[unit] 1426 | 1427 | try: 1428 | val = float(val) 1429 | except ValueError: 1430 | raise 1431 | try: 1432 | return unit_class(val) 1433 | except: # pragma: no cover 1434 | raise ValueError("Can't parse string %s into a bitmath object" % s) 1435 | 1436 | 1437 | def parse_string_unsafe(s, system=SI): 1438 | """Attempt to parse a string with ambiguous units and try to make a 1439 | bitmath object out of it. 1440 | 1441 | This may produce inaccurate results if parsing shell output. For 1442 | example `ls` may say a 2730 Byte file is '2.7K'. 2730 Bytes == 2.73 kB 1443 | ~= 2.666 KiB. See the documentation for all of the important details. 1444 | 1445 | Note the following caveats: 1446 | 1447 | * All inputs are assumed to be byte-based (as opposed to bit based) 1448 | 1449 | * Numerical inputs (those without any units) are assumed to be a 1450 | number of bytes 1451 | 1452 | * Inputs with single letter units (k, M, G, etc) are assumed to be SI 1453 | units (base-10). Set the `system` parameter to `bitmath.NIST` to 1454 | change this behavior. 1455 | 1456 | * Inputs with an `i` character following the leading letter (Ki, Mi, 1457 | Gi) are assumed to be NIST units (base 2) 1458 | 1459 | * Capitalization does not matter 1460 | 1461 | """ 1462 | if not isinstance(s, (str, unicode)) and \ 1463 | not isinstance(s, numbers.Number): 1464 | raise ValueError("parse_string_unsafe only accepts string/number inputs but a %s was given" % 1465 | type(s)) 1466 | 1467 | ###################################################################### 1468 | # Is the input simple to parse? Just a number, or a number 1469 | # masquerading as a string perhaps? 1470 | 1471 | # Test case: raw number input (easy!) 1472 | if isinstance(s, numbers.Number): 1473 | # It's just a number. Assume bytes 1474 | return Byte(s) 1475 | 1476 | # Test case: a number pretending to be a string 1477 | if isinstance(s, (str, unicode)): 1478 | try: 1479 | # Can we turn it directly into a number? 1480 | return Byte(float(s)) 1481 | except ValueError: 1482 | # Nope, this is not a plain number 1483 | pass 1484 | 1485 | ###################################################################### 1486 | # At this point: 1487 | # - the input is also not just a number wrapped in a string 1488 | # - nor is is just a plain number type 1489 | # 1490 | # We need to do some more digging around now to figure out exactly 1491 | # what we were given and possibly normalize the input into a 1492 | # format we can recognize. 1493 | 1494 | # First we'll separate the number and the unit. 1495 | # 1496 | # Get the index of the first alphabetic character 1497 | try: 1498 | index = list([i.isalpha() for i in s]).index(True) 1499 | except ValueError: # pragma: no cover 1500 | # If there's no alphabetic characters we won't be able to .index(True) 1501 | raise ValueError("No unit detected, can not parse string '%s' into a bitmath object" % s) 1502 | 1503 | # Split the string into the value and the unit 1504 | val, unit = s[:index], s[index:] 1505 | 1506 | # Don't trust anything. We'll make sure the correct 'b' is in place. 1507 | unit = unit.rstrip('Bb') 1508 | unit += 'B' 1509 | 1510 | # At this point we can expect `unit` to be either: 1511 | # 1512 | # - 2 Characters (for SI, ex: kB or GB) 1513 | # - 3 Caracters (so NIST, ex: KiB, or GiB) 1514 | # 1515 | # A unit with any other number of chars is not a valid unit 1516 | 1517 | # SI 1518 | if len(unit) == 2: 1519 | # Has NIST parsing been requested? 1520 | if system == NIST: 1521 | # NIST units requested. Ensure the unit begins with a 1522 | # capital letter and is followed by an 'i' character. 1523 | unit = capitalize_first(unit) 1524 | # Insert an 'i' char after the first letter 1525 | _unit = list(unit) 1526 | _unit.insert(1, 'i') 1527 | # Collapse the list back into a 3 letter string 1528 | unit = ''.join(_unit) 1529 | unit_class = globals()[unit] 1530 | else: 1531 | # Default parsing (SI format) 1532 | # 1533 | # Edge-case checking: SI 'thousand' is a lower-case K 1534 | if unit.startswith('K'): 1535 | unit = unit.replace('K', 'k') 1536 | elif not unit.startswith('k'): 1537 | # Otherwise, ensure the first char is capitalized 1538 | unit = capitalize_first(unit) 1539 | 1540 | # This is an SI-type unit 1541 | if unit[0] in SI_PREFIXES: 1542 | unit_class = globals()[unit] 1543 | # NIST 1544 | elif len(unit) == 3: 1545 | unit = capitalize_first(unit) 1546 | 1547 | # This is a NIST-type unit 1548 | if unit[:2] in NIST_PREFIXES: 1549 | unit_class = globals()[unit] 1550 | else: 1551 | # This is not a unit we recognize 1552 | raise ValueError("The unit %s is not a valid bitmath unit" % unit) 1553 | 1554 | try: 1555 | unit_class 1556 | except UnboundLocalError: 1557 | raise ValueError("The unit %s is not a valid bitmath unit" % unit) 1558 | 1559 | return unit_class(float(val)) 1560 | 1561 | 1562 | ###################################################################### 1563 | # Contxt Managers 1564 | @contextlib.contextmanager 1565 | def format(fmt_str=None, plural=False, bestprefix=False): 1566 | """Context manager for printing bitmath instances. 1567 | 1568 | ``fmt_str`` - a formatting mini-language compat formatting string. See 1569 | the @properties (above) for a list of available items. 1570 | 1571 | ``plural`` - True enables printing instances with 's's if they're 1572 | plural. False (default) prints them as singular (no trailing 's'). 1573 | 1574 | ``bestprefix`` - True enables printing instances in their best 1575 | human-readable representation. False, the default, prints instances 1576 | using their current prefix unit. 1577 | """ 1578 | if 'bitmath' not in globals(): 1579 | import bitmath 1580 | 1581 | if plural: 1582 | orig_fmt_plural = bitmath.format_plural 1583 | bitmath.format_plural = True 1584 | 1585 | if fmt_str: 1586 | orig_fmt_str = bitmath.format_string 1587 | bitmath.format_string = fmt_str 1588 | 1589 | yield 1590 | 1591 | if plural: 1592 | bitmath.format_plural = orig_fmt_plural 1593 | 1594 | if fmt_str: 1595 | bitmath.format_string = orig_fmt_str 1596 | 1597 | 1598 | def cli_script_main(cli_args): 1599 | """ 1600 | A command line interface to basic bitmath operations. 1601 | """ 1602 | choices = ALL_UNIT_TYPES 1603 | 1604 | parser = argparse.ArgumentParser( 1605 | description='Converts from one type of size to another.') 1606 | parser.add_argument('--from-stdin', default=False, action='store_true', 1607 | help='Reads number from stdin rather than the cli') 1608 | parser.add_argument( 1609 | '-f', '--from', choices=choices, nargs=1, 1610 | type=str, dest='fromunit', default=['Byte'], 1611 | help='Input type you are converting from. Defaultes to Byte.') 1612 | parser.add_argument( 1613 | '-t', '--to', choices=choices, required=False, nargs=1, type=str, 1614 | help=('Input type you are converting to. ' 1615 | 'Attempts to detect best result if omitted.'), dest='tounit') 1616 | parser.add_argument( 1617 | 'size', nargs='*', type=float, 1618 | help='The number to convert.') 1619 | 1620 | args = parser.parse_args(cli_args) 1621 | 1622 | # Not sure how to cover this with tests, or if the functionality 1623 | # will remain in this form long enough for it to make writing a 1624 | # test worth the effort. 1625 | if args.from_stdin: # pragma: no cover 1626 | args.size = [float(sys.stdin.readline()[:-1])] 1627 | 1628 | results = [] 1629 | 1630 | for size in args.size: 1631 | instance = getattr(__import__( 1632 | 'bitmath', fromlist=['True']), args.fromunit[0])(size) 1633 | 1634 | # If we have a unit provided then use it 1635 | if args.tounit: 1636 | result = getattr(instance, args.tounit[0]) 1637 | # Otherwise use the best_prefix call 1638 | else: 1639 | result = instance.best_prefix() 1640 | 1641 | results.append(result) 1642 | 1643 | return results 1644 | 1645 | 1646 | def cli_script(): # pragma: no cover 1647 | # Wrapper around cli_script_main so we can unittest the command 1648 | # line functionality 1649 | for result in cli_script_main(sys.argv[1:]): 1650 | print(result) 1651 | 1652 | 1653 | if __name__ == '__main__': 1654 | cli_script() 1655 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | 3 | services: 4 | 5 | db: 6 | container_name: perf-db 7 | image: postgres:alpine 8 | restart: always 9 | volumes: 10 | - ./data.csv:/tmp/data.csv 11 | environment: 12 | POSTGRES_USER: test 13 | POSTGRES_PASSWORD: test 14 | POSTGRES_DB: test 15 | 16 | dbpool: 17 | container_name: perf-dbpool 18 | image: edoburu/pgbouncer 19 | restart: always 20 | volumes: 21 | - ./pgbouncer-userlist.txt:/etc/pgbouncer/userlist.txt 22 | environment: 23 | POOL_MODE: session 24 | DB_HOST: perf-db 25 | DB_USER: test 26 | DB_PASSWORD: test 27 | DB_NAME: test 28 | MAX_CLIENT_CONN: 2000 29 | DEFAULT_POOL_SIZE: ${NUM_DB_SESSIONS} 30 | 31 | server: 32 | container_name: perf-server 33 | image: nginx:stable-alpine 34 | restart: always 35 | volumes: 36 | - ./nginx-docker.conf:/etc/nginx/nginx.conf 37 | ports: 38 | - 8000:8000 39 | -------------------------------------------------------------------------------- /gen_report.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from glob import glob 3 | import json 4 | import bitmath 5 | 6 | 7 | def read_requirements(): 8 | reqs = {} 9 | with open('runs/requirements.txt') as f: 10 | for line in f.readlines(): 11 | pkg, version = line.strip().split('==') 12 | reqs[pkg.lower()] = (pkg, version) 13 | return reqs 14 | 15 | 16 | def gen_report(reqs): 17 | report = [] 18 | for test in glob('runs/*.json'): 19 | deps = [dep for dep in test[5:-5].split('-') if dep in reqs] 20 | workers = int(test.split('-x')[1].split('.')[0]) 21 | if len(deps) == 1: 22 | deps = [deps[0], deps[0]] 23 | try: 24 | with open(test) as f: 25 | results = json.loads(f.read()) 26 | except: 27 | continue 28 | with open(test.replace('.json', '.db')) as f: 29 | db_connections = int(f.read().strip()) 30 | with open(test.replace('.json', '.perf')) as f: 31 | f.readline() 32 | cpu = 0 33 | ram = 0 34 | net_in = 0 35 | net_out = 0 36 | pids = 0 37 | for line in f.readlines(): 38 | c, r, n, b, p = line.strip().split(',') 39 | c = float(c.split('%')[0]) 40 | r = float(str(bitmath.parse_string(r.split(' / ')[0]).to_MB()).split()[0]) 41 | n = [float(str(bitmath.parse_string(nn).to_MB()).split()[0]) 42 | for nn in n.split(' / ')] 43 | p = int(p) 44 | cpu = max(cpu, c) 45 | ram = max(ram, r) 46 | net_in = max(net_in, n[0]) 47 | net_out = max(net_out, n[1]) 48 | pids = max(pids, p) 49 | report.append([ 50 | f'{reqs[deps[-1]][0]} {reqs[deps[-1]][1]}', 51 | ' with '.join([f'{reqs[dep][0]} {reqs[dep][1]}' 52 | for dep in deps[:-1]]), 53 | workers, 54 | results['requests_per_second'], 55 | results['percentages']['50%'], 56 | results['percentages']['99%'], 57 | db_connections, 58 | int(results['complete_requests']), 59 | int(results.get('non-2xx_responses', 0)), 60 | cpu, 61 | ram, 62 | net_in, 63 | net_out, 64 | pids, 65 | ]) 66 | report = sorted(report, key=lambda r: r[3], reverse=True) 67 | report.insert(0, [ 68 | 'framework', 69 | 'server', 70 | 'workers', 71 | 'reqs/sec', 72 | 'P50', 73 | 'P99', 74 | 'db_sessions', 75 | 'total_reqs', 76 | 'failed_reqs', 77 | 'cpu_%', 78 | 'ram_mb', 79 | 'net_in_mb', 80 | 'net_out_mb', 81 | 'pids', 82 | ]) 83 | return report 84 | 85 | 86 | report = gen_report(read_requirements()) 87 | for line in report: 88 | print(','.join([v.__str__() for v in line])) 89 | -------------------------------------------------------------------------------- /gen_test_data.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import csv 3 | import sys 4 | import random 5 | import string 6 | 7 | # static seed to generate the same data all the time 8 | rng = random.Random(0) 9 | 10 | csv_writer = csv.writer(sys.stdout) 11 | csv_writer.writerow(["a", "b"]) 12 | 13 | for i in range(1_000_000): 14 | short_random_string = "".join( 15 | rng.choice(string.ascii_letters) for _ in range(20)) 16 | csv_writer.writerow([i, short_random_string]) 17 | -------------------------------------------------------------------------------- /monitor-app.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo CPUPerc,MemUsage,NetIO,BlockIO,PIDs >runs/$1.perf 3 | while true; do 4 | docker stats --no-stream --format "{{.CPUPerc}},{{.MemUsage}},{{.NetIO}},{{.BlockIO}},{{.PIDs}}" perf-app >>runs/$1.perf 5 | sleep 3 6 | done 7 | -------------------------------------------------------------------------------- /nginx-docker.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes auto; 3 | 4 | error_log /var/log/nginx/error.log warn; 5 | pid /var/run/nginx.pid; 6 | 7 | 8 | events { 9 | worker_connections 20000; 10 | } 11 | 12 | 13 | http { 14 | include /etc/nginx/mime.types; 15 | default_type application/octet-stream; 16 | 17 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 18 | '$status $body_bytes_sent "$http_referer" ' 19 | '"$http_user_agent" "$http_x_forwarded_for"'; 20 | 21 | access_log /var/log/nginx/access.log main; 22 | 23 | sendfile on; 24 | #tcp_nopush on; 25 | 26 | keepalive_timeout 65; 27 | 28 | #gzip on; 29 | 30 | server { 31 | listen 8000; 32 | server_name _; 33 | location / { 34 | proxy_pass http://127.0.0.1:8001; 35 | } 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /pgbouncer-userlist.txt: -------------------------------------------------------------------------------- 1 | "test" "md505a671c66aefea124cc08b76ea6d30bb" 2 | "postgres" "md53175bce1d3201d16594cebf9d7eb3f9d" 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp[speedups] 2 | flask 3 | gunicorn 4 | starlette 5 | uvicorn 6 | uwsgi 7 | aiopg 8 | psycopg2-binary 9 | gevent 10 | psycogreen 11 | meinheld 12 | falcon 13 | sanic 14 | bottle 15 | daphne 16 | Quart 17 | fastapi 18 | tornado 19 | aioflask 20 | requests 21 | psutil 22 | -------------------------------------------------------------------------------- /run-docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | [[ -z "$NUM_CLIENTS" ]] && NUM_CLIENTS=100 4 | [[ -z "$NUM_CONNECTIONS" ]] && NUM_CONNECTIONS=50000 5 | [[ -z "$NUM_WORKERS_SYNC" ]] && NUM_WORKERS_SYNC=19 6 | [[ -z "$NUM_WORKERS_ASYNC" ]] && NUM_WORKERS_ASYNC=6 7 | [[ -z "$NUM_DB_SESSIONS" ]] && NUM_DB_SESSIONS=100 8 | [[ -z "$DB_SLEEP" ]] && DB_SLEEP=0.02 9 | 10 | ALL_TESTS=$(cat tests.txt | shuf) 11 | 12 | if [[ "$@" != "" ]]; then 13 | ALL_TESTS="$@" 14 | fi 15 | 16 | ALREADY_UP= 17 | if [[ "$(docker-compose ps -q)" != "" ]]; then 18 | ALREADY_UP=1 19 | fi 20 | 21 | if [[ "$ALREADY_UP" == "" ]]; then 22 | if [[ ! -f data.csv ]]; then 23 | ./gen_test_data.py 24 | fi 25 | NUM_DB_SESSIONS=$NUM_DB_SESSIONS docker-compose up -d 26 | sleep 2 27 | docker-compose run --rm -e PGPASSWORD=test dbpool psql -h perf-dbpool -U test < schema.sql 28 | docker-compose run --rm -e PGPASSWORD=test dbpool psql -h perf-dbpool -U test -c "COPY test FROM '/tmp/data.csv' DELIMITER ',' CSV HEADER;" 29 | fi 30 | 31 | docker build -t perf-app . 32 | mkdir -p runs 33 | rm -f runs/* 34 | docker run --rm perf-app pip freeze > runs/requirements.txt 35 | 36 | for test in $ALL_TESTS; do 37 | if [[ -z "$NUM_WORKERS" ]]; then 38 | PWPWORKERS=$NUM_WORKERS_ASYNC 39 | if [[ "$test" == *gunicorn* || "$test" == *uwsgi* ]]; then 40 | if [[ "$test" != *meinheld* && "$test" != *gevent* ]]; then 41 | PWPWORKERS=$NUM_WORKERS_SYNC 42 | fi 43 | else 44 | PWPWORKERS=$NUM_WORKERS_ASYNC 45 | fi 46 | else 47 | PWPWORKERS=$NUM_WORKERS 48 | fi 49 | echo Running $test x$PWPWORKERS 50 | docker run --rm -d --name perf-app --network container:perf-server -e PWPWORKERS=$PWPWORKERS -e DB_SLEEP=$DB_SLEEP perf-app ./serve-$test.sh 51 | sleep 2 52 | ./monitor-app.sh $test-x$PWPWORKERS & 53 | MONITOR_PID=$! 54 | $(docker run --rm --name perf-test --network container:perf-server jordi/ab -c$NUM_CLIENTS -n$NUM_CONNECTIONS http://localhost:8000/test | python ab2json.py > runs/$test-x$PWPWORKERS.json) 55 | kill $MONITOR_PID 56 | docker rm -f perf-app 57 | DB_CONN=$(docker-compose run --rm -e PGPASSWORD=postgres db psql -h perf-dbpool -U postgres -P pager=off -c "show servers;" pgbouncer | wc -l) 58 | DB_CONN=$((DB_CONN-4)) 59 | docker-compose run --rm -e PGPASSWORD=postgres db psql -h perf-dbpool -U postgres -c "kill test;" pgbouncer 60 | docker-compose run --rm -e PGPASSWORD=postgres db psql -h perf-dbpool -U postgres -c "resume test;" pgbouncer 61 | echo $DB_CONN > runs/$test-x$PWPWORKERS.db 62 | done 63 | 64 | if [[ "$ALREADY_UP" == "" ]]; then 65 | docker-compose down -v 66 | fi 67 | 68 | ./gen_report.py 69 | -------------------------------------------------------------------------------- /schema.sql: -------------------------------------------------------------------------------- 1 | create table test (a int primary key, b text); 2 | -------------------------------------------------------------------------------- /src/app_aio.py: -------------------------------------------------------------------------------- 1 | import json 2 | from aiohttp import web 3 | import aiopg 4 | 5 | from async_db import get_row 6 | 7 | async def handle(request): 8 | a, b = await get_row() 9 | return web.Response(text=json.dumps({"a": str(a).zfill(10), "b": b})) 10 | 11 | 12 | app = web.Application() 13 | app.add_routes([web.get('/test', handle)]) 14 | -------------------------------------------------------------------------------- /src/app_aioflask.py: -------------------------------------------------------------------------------- 1 | from aioflask import Flask 2 | from async_db import get_row 3 | 4 | app = Flask("python-web-perf") 5 | 6 | 7 | @app.route("/test") 8 | async def test(): 9 | a, b = await get_row() 10 | return {"a": str(a).zfill(10), "b": b} 11 | -------------------------------------------------------------------------------- /src/app_bottle.py: -------------------------------------------------------------------------------- 1 | import bottle 2 | import json 3 | 4 | from sync_db import get_row 5 | 6 | app = bottle.Bottle() 7 | 8 | @app.route("/test") 9 | def test(): 10 | a, b = get_row() 11 | return json.dumps({"a": str(a).zfill(10), "b": b}) 12 | -------------------------------------------------------------------------------- /src/app_falcon.py: -------------------------------------------------------------------------------- 1 | import falcon 2 | import json 3 | 4 | from sync_db import get_row 5 | 6 | 7 | class ThingResource: 8 | def on_get(self, req, resp): 9 | a, b = get_row() 10 | resp.body = json.dumps({"a": str(a).zfill(10), "b": b}) 11 | 12 | app = falcon.API() 13 | 14 | things = ThingResource() 15 | 16 | app.add_route("/test", things) 17 | -------------------------------------------------------------------------------- /src/app_fastapi.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from async_db import get_row 3 | 4 | app = FastAPI() 5 | 6 | 7 | @app.get('/test') 8 | async def handle(): 9 | a, b = await get_row() 10 | return {'a': str(a).zfill(10), 'b': b} 11 | -------------------------------------------------------------------------------- /src/app_flask.py: -------------------------------------------------------------------------------- 1 | import flask 2 | import json 3 | from sync_db import get_row 4 | 5 | app = flask.Flask("python-web-perf") 6 | 7 | 8 | @app.route("/test") 9 | def test(): 10 | a, b = get_row() 11 | return json.dumps({"a": str(a).zfill(10), "b": b}) 12 | -------------------------------------------------------------------------------- /src/app_quart.py: -------------------------------------------------------------------------------- 1 | from os import environ 2 | 3 | from quart import Quart, jsonify 4 | from async_db import get_row 5 | 6 | app = Quart("python-web-perf") 7 | 8 | 9 | @app.route("/test") 10 | async def test(): 11 | a, b = await get_row() 12 | return jsonify({"a": str(a).zfill(10), "b": b}) 13 | -------------------------------------------------------------------------------- /src/app_sanic.py: -------------------------------------------------------------------------------- 1 | from os import environ 2 | 3 | from sanic import Sanic 4 | from sanic.response import json 5 | 6 | from async_db import get_row 7 | 8 | app = Sanic("python-web-perf") 9 | 10 | 11 | @app.route("/test") 12 | async def test(request): 13 | a, b = await get_row() 14 | return json({"a": str(a).zfill(10), "b": b}) 15 | 16 | 17 | if __name__ == "__main__": 18 | app.run(host="127.0.0.1", port=8001, workers=int(environ["PWPWORKERS"])) 19 | -------------------------------------------------------------------------------- /src/app_starlette.py: -------------------------------------------------------------------------------- 1 | from starlette.applications import Starlette 2 | from starlette.responses import JSONResponse 3 | from starlette.routing import Route 4 | 5 | from async_db import get_row 6 | 7 | 8 | async def homepage(request): 9 | a, b = await get_row() 10 | return JSONResponse({"a": str(a).zfill(10), "b": b}) 11 | 12 | 13 | routes = [ 14 | Route("/test", endpoint=homepage) 15 | ] 16 | 17 | 18 | app = Starlette(routes=routes) 19 | -------------------------------------------------------------------------------- /src/app_tornado.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | import os 4 | import uvloop 5 | import tornado.ioloop 6 | import tornado.web 7 | from async_db import get_row 8 | 9 | 10 | class Handler(tornado.web.RequestHandler): 11 | async def get(self): 12 | a, b = await get_row() 13 | self.write(json.dumps({"a": str(a).zfill(10), "b": b})) 14 | 15 | 16 | def make_app(): 17 | return tornado.web.Application([ 18 | (r"/test", Handler), 19 | ]) 20 | 21 | 22 | if __name__ == "__main__": 23 | asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) 24 | app = make_app() 25 | server = tornado.httpserver.HTTPServer(app) 26 | server.bind(8001) 27 | server.start(int(os.environ.get('PWPWORKERS', '1'))) 28 | tornado.ioloop.IOLoop.current().start() 29 | -------------------------------------------------------------------------------- /src/async_db.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | import aiopg 4 | 5 | max_n = 1000_000 - 1 6 | delay = float(os.getenv('DB_SLEEP', '0')) 7 | 8 | 9 | async def get_row(): 10 | async with aiopg.connect(dbname='test', user='test', password='test', port=5432, host='perf-dbpool') as conn: 11 | async with conn.cursor() as cursor: 12 | index = random.randint(1, max_n) 13 | await cursor.execute("select pg_sleep(%s); select a, b from test where a = %s", (delay, index)) 14 | ((a, b),) = await cursor.fetchall() 15 | return a, b 16 | -------------------------------------------------------------------------------- /src/meinheld_psycopg2.py: -------------------------------------------------------------------------------- 1 | import psycopg2 2 | from psycopg2 import extensions 3 | 4 | from meinheld import trampoline 5 | 6 | def patch_psycopg(): 7 | """Configure Psycopg to be used with eventlet in non-blocking way.""" 8 | if not hasattr(extensions, 'set_wait_callback'): 9 | raise ImportError( 10 | "support for coroutines not available in this Psycopg version (%s)" 11 | % psycopg2.__version__) 12 | 13 | extensions.set_wait_callback(wait_callback) 14 | 15 | def wait_callback(conn, timeout=-1): 16 | """A wait callback useful to allow eventlet to work with Psycopg.""" 17 | while 1: 18 | state = conn.poll() 19 | if state == extensions.POLL_OK: 20 | break 21 | elif state == extensions.POLL_READ: 22 | trampoline(conn.fileno(), read=True) 23 | elif state == extensions.POLL_WRITE: 24 | trampoline(conn.fileno(), write=True) 25 | else: 26 | raise psycopg2.OperationalError( 27 | "Bad result from poll: %r" % state) 28 | -------------------------------------------------------------------------------- /src/serve-aiohttp.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | gunicorn app_aio:app -w $PWPWORKERS --bind localhost:8001 --worker-class aiohttp.GunicornUVLoopWebWorker 4 | -------------------------------------------------------------------------------- /src/serve-daphne-starlette.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | daphne -b 0.0.0.0 -p 8001 app_starlette:app 4 | -------------------------------------------------------------------------------- /src/serve-gunicorn-bottle.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | gunicorn --bind :8001 -w $PWPWORKERS app_bottle:app 4 | -------------------------------------------------------------------------------- /src/serve-gunicorn-falcon.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | gunicorn --bind :8001 -w $PWPWORKERS app_falcon:app 4 | -------------------------------------------------------------------------------- /src/serve-gunicorn-flask.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | gunicorn -w $PWPWORKERS --bind :8001 app_flask:app 4 | -------------------------------------------------------------------------------- /src/serve-gunicorn-gevent-bottle.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export USE_GEVENT=1 4 | gunicorn --bind :8001 -w $PWPWORKERS app_bottle:app --worker-class gunicorn.workers.ggevent.GeventWorker 5 | -------------------------------------------------------------------------------- /src/serve-gunicorn-gevent-falcon.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export USE_GEVENT=1 4 | gunicorn --bind :8001 -w $PWPWORKERS app_falcon:app --worker-class gunicorn.workers.ggevent.GeventWorker 5 | -------------------------------------------------------------------------------- /src/serve-gunicorn-gevent-flask.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export USE_GEVENT=1 4 | gunicorn --bind :8001 -w $PWPWORKERS app_flask:app --worker-class gunicorn.workers.ggevent.GeventWorker 5 | -------------------------------------------------------------------------------- /src/serve-gunicorn-meinheld-bottle.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export USE_MEINHELD=1 4 | gunicorn --bind :8001 -w $PWPWORKERS app_bottle:app --worker-class "egg:meinheld#gunicorn_worker" 5 | -------------------------------------------------------------------------------- /src/serve-gunicorn-meinheld-falcon.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export USE_MEINHELD=1 4 | gunicorn --bind :8001 -w $PWPWORKERS app_falcon:app --worker-class "egg:meinheld#gunicorn_worker" 5 | -------------------------------------------------------------------------------- /src/serve-gunicorn-meinheld-flask.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export USE_MEINHELD=1 4 | gunicorn --bind :8001 -w $PWPWORKERS app_flask:app --worker-class "egg:meinheld#gunicorn_worker" 5 | -------------------------------------------------------------------------------- /src/serve-hypercorn-quart.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | hypercorn -b :8001 -w $PWPWORKERS app_quart:app 4 | -------------------------------------------------------------------------------- /src/serve-sanic-own.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | python3 app_sanic.py 4 | -------------------------------------------------------------------------------- /src/serve-tornado.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | python3 app_tornado.py 4 | -------------------------------------------------------------------------------- /src/serve-uvicorn-aioflask.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | uvicorn --port 8001 --workers $PWPWORKERS app_aioflask:app 4 | -------------------------------------------------------------------------------- /src/serve-uvicorn-fastapi.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | uvicorn --port 8001 --workers $PWPWORKERS app_fastapi:app 4 | -------------------------------------------------------------------------------- /src/serve-uvicorn-quart.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | uvicorn --port 8001 --workers $PWPWORKERS app_quart:app 4 | -------------------------------------------------------------------------------- /src/serve-uvicorn-sanic.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | uvicorn --port 8001 --workers $PWPWORKERS app_sanic:app 4 | -------------------------------------------------------------------------------- /src/serve-uvicorn-starlette.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | uvicorn --port 8001 --workers $PWPWORKERS app_starlette:app 4 | -------------------------------------------------------------------------------- /src/serve-uwsgi-bottle-own-proto.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | uwsgi --uwsgi-socket :8001 -w app_bottle:app --processes $PWPWORKERS 4 | -------------------------------------------------------------------------------- /src/serve-uwsgi-bottle.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | uwsgi --http-socket :8001 -w app_bottle:app --processes $PWPWORKERS 4 | -------------------------------------------------------------------------------- /src/serve-uwsgi-falcon.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | uwsgi --http-socket :8001 -w app_falcon:app --processes $PWPWORKERS 4 | -------------------------------------------------------------------------------- /src/serve-uwsgi-flask.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | uwsgi --http-socket :8001 -w app_flask:app --processes $PWPWORKERS 4 | -------------------------------------------------------------------------------- /src/sync_db.py: -------------------------------------------------------------------------------- 1 | import os 2 | import psycopg2 3 | import psycopg2.pool 4 | import random 5 | 6 | if os.getenv('USE_MEINHELD'): 7 | from meinheld_psycopg2 import patch_psycopg 8 | patch_psycopg() 9 | elif os.getenv('USE_GEVENT'): 10 | from psycogreen.gevent import patch_psycopg 11 | patch_psycopg() 12 | 13 | max_n = 1000_000 - 1 14 | delay = float(os.getenv('DB_SLEEP', '0')) 15 | 16 | def get_row(): 17 | conn = psycopg2.connect(dbname='test', user='test', password='test', 18 | port=5432, host='perf-dbpool') 19 | cursor = conn.cursor() 20 | index = random.randint(1, max_n) 21 | cursor.execute("select pg_sleep(%s); select a, b from test where a = %s;", (delay, index)) 22 | ((a, b),) = cursor.fetchall() 23 | cursor.close() 24 | conn.commit() 25 | conn.close() 26 | return a, b 27 | -------------------------------------------------------------------------------- /tests.txt: -------------------------------------------------------------------------------- 1 | gunicorn-bottle 2 | gunicorn-falcon 3 | gunicorn-flask 4 | gunicorn-gevent-bottle 5 | gunicorn-gevent-falcon 6 | gunicorn-gevent-flask 7 | gunicorn-meinheld-bottle 8 | gunicorn-meinheld-falcon 9 | gunicorn-meinheld-flask 10 | uwsgi-bottle 11 | uwsgi-falcon 12 | uwsgi-flask 13 | aiohttp 14 | hypercorn-quart 15 | sanic-own 16 | tornado 17 | uvicorn-aioflask 18 | uvicorn-fastapi 19 | uvicorn-quart 20 | uvicorn-sanic 21 | uvicorn-starlette 22 | --------------------------------------------------------------------------------