diff --git a/.requirements/bench.txt b/.requirements/bench.txt index 30701ef..4649bc8 100644 --- a/.requirements/bench.txt +++ b/.requirements/bench.txt @@ -5,3 +5,4 @@ aiohttp==3.9.3 aiosonic==0.18.0 niquests==3.5.2 pycurl==7.45.3 +matplotlib==3.8.3 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 111ae6e..8fa30d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 To see unreleased changes, please see the [CHANGELOG on the main branch guide](https://github.com/gufolabs/gufo_http/blob/main/CHANGELOG.md). +## [Unreleased] + +### Added + +* Benchmark results and charts. + ## 0.1.1 - 2024-03-05 ### Added diff --git a/README.md b/README.md index d786184..44b7115 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ *Gufo HTTP* is a high-performance Python HTTP client library that handles both asynchronous and synchronous modes. It wraps famous [Reqwest][Reqwest] HTTP client, written in [Rust][Rust] language with [PyO3][PyO3] wrapper. +Our task is to reach maximal performance while maintaining clean and easy-to use API. The getting of single URL is a simple task: @@ -49,7 +50,22 @@ async with HttpClient(auth=BasicAuth("scott", "tiger")) as client: ## Performance +Gufo HTTP is proved to be one of the fastest Python HTTP client available +in the various scenarios. For example: +### Single HTTP/1.1 requests scenario + + + +### 100 Linear HTTP/1.1 requests scenario + + + +### 100 Parallel HTTP/1.1 requests scenario + + + +Refer to [benchmarks](benchmarks/README.md) for details. ## On Gufo Stack diff --git a/benchmarks/README.md b/benchmarks/README.md index 5f3444d..a9b8afb 100644 --- a/benchmarks/README.md +++ b/benchmarks/README.md @@ -73,7 +73,7 @@ Run tests: pytest benchmarks/test_single_x100_1k.py ``` -Results: +### Results (lower is better) ``` ================================================================= test session starts ================================================================= platform linux -- Python 3.11.2, pytest-7.4.3, pluggy-1.4.0 @@ -106,6 +106,8 @@ Legend: ================================================================= 10 passed in 9.12s ================================================================== ``` + + ## 100 Linear HTTP/1.1 Requests Perform set of 100 linear http requests to read 1kb text file using single client session @@ -120,7 +122,7 @@ Run tests: pytest benchmarks/test_linear_x100_1k.py ``` -Results: +### Results (lower is better) ``` ================================================================= test session starts ================================================================= platform linux -- Python 3.11.2, pytest-7.4.3, pluggy-1.4.0 @@ -153,6 +155,8 @@ Legend: ================================================================= 10 passed in 14.29s ================================================================= ``` + + ## 100 Parallel HTTP/1.1 Requests Perform 100 HTTP/1.1 requests to read 1kb text file with concurrency of 4 maintaininng @@ -169,7 +173,7 @@ Run tests: pytest benchmarks/test_p4_x100_1k.py ``` -Results: +### Results (lower is better) ``` ================================================================= test session starts ================================================================= platform linux -- Python 3.11.2, pytest-7.4.3, pluggy-1.4.0 @@ -201,6 +205,7 @@ Legend: OPS: Operations Per Second, computed as 1 / Mean ================================================================= 10 passed in 14.76s ================================================================= ``` + ## Feedback diff --git a/benchmarks/linear_x100_1k.png b/benchmarks/linear_x100_1k.png new file mode 120000 index 0000000..81889ef --- /dev/null +++ b/benchmarks/linear_x100_1k.png @@ -0,0 +1 @@ +../docs/linear_x100_1k.png \ No newline at end of file diff --git a/benchmarks/p4_x100_1k.png b/benchmarks/p4_x100_1k.png new file mode 120000 index 0000000..11412c7 --- /dev/null +++ b/benchmarks/p4_x100_1k.png @@ -0,0 +1 @@ +../docs/p4_x100_1k.png \ No newline at end of file diff --git a/benchmarks/single_x100_1k.png b/benchmarks/single_x100_1k.png new file mode 120000 index 0000000..8a6b2af --- /dev/null +++ b/benchmarks/single_x100_1k.png @@ -0,0 +1 @@ +../docs/single_x100_1k.png \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 58796e7..78daa2e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -13,6 +13,7 @@ hero: *Gufo HTTP* is a high-performance Python HTTP client library that handles both asynchronous and synchronous modes. It wraps famous [Reqwest][Reqwest] HTTP client, written in [Rust][Rust] language with [PyO3][PyO3] wrapper. +Our task is to reach maximal performance while maintaining clean and easy-to use API. The getting of single URL is a simple task: @@ -40,6 +41,25 @@ async with HttpClient(auth=BasicAuth("scott", "tiger")) as client: ... ``` +## Performance + +Gufo HTTP is proved to be one of the fastest Python HTTP client available +in the various scenarios. For example: + +### Single HTTP/1.1 requests scenario + + + +### 100 Linear HTTP/1.1 requests scenario + + + +### 100 Parallel HTTP/1.1 requests scenario + + + +Refer to [benchmarks](benchmarks.md) for details. + ## On Gufo Stack This product is a part of [Gufo Stack][Gufo Stack] - the collaborative effort diff --git a/docs/linear_x100_1k.png b/docs/linear_x100_1k.png new file mode 100644 index 0000000..7d45405 Binary files /dev/null and b/docs/linear_x100_1k.png differ diff --git a/docs/p4_x100_1k.png b/docs/p4_x100_1k.png new file mode 100644 index 0000000..3e3bdb1 Binary files /dev/null and b/docs/p4_x100_1k.png differ diff --git a/docs/single_x100_1k.png b/docs/single_x100_1k.png new file mode 100644 index 0000000..43ebdba Binary files /dev/null and b/docs/single_x100_1k.png differ diff --git a/tools/docs/update-bench-charts.py b/tools/docs/update-bench-charts.py new file mode 100755 index 0000000..310485a --- /dev/null +++ b/tools/docs/update-bench-charts.py @@ -0,0 +1,187 @@ +# --------------------------------------------------------------------- +# Gufo HTTP: Generate benchmark charts +# --------------------------------------------------------------------- +# Copyright (C) 2024, Gufo Labs +# See LICENSE.md for details +# --------------------------------------------------------------------- +"""Parse bechmark results and generate charts.""" + +# Python modules +import enum +import re +from dataclasses import dataclass +from typing import Iterable, List, Tuple + +# Third-party modules +import matplotlib.pyplot as plt +from matplotlib import ticker + +rx_name = re.compile(r"^Name \(time in (\S+)\)") + + +@dataclass +class Benchmark(object): + """ + Benchmark descriptor. + + Attributes: + path: Output chart path. + title: Chart title. + """ + + path: str + title: str + + +BENCHMARKS = [ + Benchmark( + title="Single HTTP/1.1 Requests (Median)", + path="docs/single_x100_1k.png", + ), + Benchmark( + title="100 Linear HTTP/1.1 Requests (Median)", + path="docs/linear_x100_1k.png", + ), + Benchmark( + title="100 Parallel HTTP/1.1 Requests (Median)", + path="docs/p4_x100_1k.png", + ), +] + +NAME_MAP = {"gufo_http": "Gufo HTTP", "pycurl": "PycURL"} + + +def normalize_name(s: str) -> str: + """ + Normalize test name. + + Args: + s: Test name. + + Returns: + Normalized name. + """ + if s.startswith("test_"): + s = s[5:] + mode = "" + if s.endswith("_sync"): + s = s[:-5] + mode = " (Sync)" + elif s.endswith("_async"): + s = s[:-6] + mode = " (Async)" + s = NAME_MAP.get(s, s) + return f"{s}{mode}" + + +def build_barchart( + bench: Benchmark, data: List[Tuple[str, float]], scale: str +) -> None: + """ + Build bar chart into SVG file. + + Args: + bench: Benchmark description. + data: List of (name, value). + scale: Time scale label. + """ + + def is_gufo_http(s: str) -> bool: + return "Gufo HTTP" in s + + # Extracting test names and measured values from the data + tests, values = zip(*data) + + # Creating the bar chart + plt.figure(figsize=(10, 6)) + plt.barh( + tests, + values, + color=[ + "#2c3e50" if is_gufo_http(test) else "#34495e" for test in tests + ], + ) + plt.xlabel(f"Time ({scale})") + plt.title(bench.title) + # Adding thousands separator to y-axis labels + plt.gca().xaxis.set_major_formatter(ticker.StrMethodFormatter("{x:,.0f}")) + # Adding text annotations for ratio between each bar and smallest one + min_value = min(values) + for test, value in zip(tests, values): + ratio = value / min_value + fontweight = "bold" if is_gufo_http(test) else "normal" + plt.text( + value, test, f" x{ratio:.2f}", va="center", fontweight=fontweight + ) + # Make y-axis labels bold for test names containing "gufo_http" + for tick_label in plt.gca().get_yticklabels(): + if is_gufo_http(tick_label.get_text()): + tick_label.set_weight("bold") + # Adjusting right padding to shift border to the right + plt.subplots_adjust(right=1.3) + # Saving the plot as an SVG file + print(f"Writing {bench.path}") + plt.savefig(bench.path, format="png", bbox_inches="tight") + plt.close() + + +class Mode(enum.Enum): + """ + Parser mode. + + Attributes: + WAITING: Waiting for table of results. + SKIP_LINE: Table detected, need to skip next line. + PARSING: Parsing table. + """ + + WAITING = 0 + SKIP_LINE = 1 + PARSING = 2 + + +def iter_results(path: str) -> Iterable[Tuple[str, List[Tuple[str, float]]]]: + """ + Read benchmarks docs and extract values for charts. + + Args: + path: README.md path + + Returns: + Yields tuple of (scale, data block) + """ + r = [] + mode = Mode.WAITING + scale = None + with open(path) as fp: + for line in fp: + ln = line.strip() + if mode == Mode.WAITING: + m = rx_name.search(ln) + if m: + scale = m.group(1) + mode = Mode.SKIP_LINE + elif mode == Mode.SKIP_LINE: + mode = Mode.PARSING + elif mode == Mode.PARSING: + if ln.startswith("---"): + mode = Mode.WAITING + yield scale, r + r = [] + else: + parts = ln.split() + name = normalize_name(parts[0]) + value = float(parts[9].replace(",", "")) + r.append((name, value)) + + +def main() -> None: + """Main function.""" + for bench, (scale, data) in zip( + BENCHMARKS, iter_results("benchmarks/README.md") + ): + build_barchart(bench, data, scale) + + +if __name__ == "__main__": + main()