diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 4d77978d1e3..61ec5594616 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -240,6 +240,47 @@ jobs: with: token: ${{ secrets.CODECOV_TOKEN }} + benchmark: + name: Benchmark + needs: gen_llhttp + + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout project + uses: actions/checkout@v4 + with: + submodules: true + - name: Setup Python 3.12 + id: python-install + uses: actions/setup-python@v5 + with: + python-version: 3.12 + cache: pip + cache-dependency-path: requirements/*.txt + - name: Update pip, wheel, setuptools, build, twine + run: | + python -m pip install -U pip wheel setuptools build twine + - name: Install dependencies + run: | + python -m pip install -r requirements/test.in -c requirements/test.txt + - name: Restore llhttp generated files + uses: actions/download-artifact@v3 + with: + name: llhttp + path: vendor/llhttp/build/ + - name: Cythonize + run: | + make cythonize + - name: Install self + run: python -m pip install -e . + - name: Run benchmarks + uses: CodSpeedHQ/action@v3 + with: + token: ${{ secrets.CODSPEED_TOKEN }} + run: python -Im pytest --no-cov -vvvvv --codspeed + + check: # This job does nothing and is only used for the branch protection if: always() diff --git a/.mypy.ini b/.mypy.ini index 78001c36e8f..4d8e38d6952 100644 --- a/.mypy.ini +++ b/.mypy.ini @@ -36,3 +36,17 @@ ignore_missing_imports = True [mypy-gunicorn.*] ignore_missing_imports = True + +# Benchmark configuration is because pytest_codspeed is missing +# a py.typed file. Can be removed once the following PR is merged +# and released: +# https://github.com/CodSpeedHQ/pytest-codspeed/pull/53 +[mypy-test_benchmarks_client_request] +disable_error_code = + no-any-unimported, + misc + +[mypy-test_benchmarks_cookiejar] +disable_error_code = + no-any-unimported, + misc diff --git a/requirements/lint.in b/requirements/lint.in index 0d46809a083..2a8ce66330f 100644 --- a/requirements/lint.in +++ b/requirements/lint.in @@ -5,6 +5,7 @@ mypy; implementation_name == "cpython" pre-commit pytest pytest-mock +pytest_codspeed python-on-whales slotscheck trustme diff --git a/requirements/lint.txt b/requirements/lint.txt index 5f1b068cb1c..48c88933fab 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -18,6 +18,7 @@ cffi==1.17.0 # via # cryptography # pycares + # pytest-codspeed cfgv==3.3.1 # via pre-commit charset-normalizer==3.3.2 @@ -26,18 +27,24 @@ click==8.1.6 # via # slotscheck # typer +cryptography==43.0.3 + # via trustme distlib==0.3.7 # via virtualenv exceptiongroup==1.1.2 # via pytest filelock==3.12.2 - # via virtualenv + # via + # pytest-codspeed + # virtualenv freezegun==1.5.1 # via -r requirements/lint.in identify==2.5.26 # via pre-commit idna==3.7 - # via requests + # via + # requests + # trustme iniconfig==2.0.0 # via pytest markdown-it-py==3.0.0 @@ -69,9 +76,16 @@ pydantic-core==2.23.4 pygments==2.17.2 # via rich pytest==8.3.2 + # via + # -r requirements/lint.in + # pytest-codspeed + # pytest-mock +pytest-codspeed==2.2.1 # via -r requirements/lint.in pytest-mock==3.14.0 # via -r requirements/lint.in +python-dateutil==2.9.0.post0 + # via freezegun python-on-whales==0.72.0 # via -r requirements/lint.in pyyaml==6.0.1 @@ -82,6 +96,8 @@ rich==13.7.1 # via typer shellingham==1.5.4 # via typer +six==1.16.0 + # via python-dateutil slotscheck==0.19.0 # via -r requirements/lint.in tomli==2.0.1 diff --git a/requirements/test.in b/requirements/test.in index 686cd6dbf2e..801189ea72c 100644 --- a/requirements/test.in +++ b/requirements/test.in @@ -8,6 +8,7 @@ proxy.py >= 2.4.4rc4 pytest pytest-cov pytest-mock +pytest_codspeed python-on-whales re-assert setuptools-git diff --git a/requirements/test.txt b/requirements/test.txt index 8be1e493f2d..c03b33fb228 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -24,6 +24,7 @@ cffi==1.17.0 # via # cryptography # pycares + # pytest-codspeed charset-normalizer==3.2.0 # via requests click==8.1.6 @@ -38,6 +39,8 @@ cryptography==41.0.2 # via trustme exceptiongroup==1.1.2 # via pytest +filelock==3.16.1 + # via pytest-codspeed freezegun==1.5.1 # via -r requirements/test.in frozenlist==1.4.1 @@ -80,8 +83,11 @@ pydantic-core==2.23.4 pytest==8.3.2 # via # -r requirements/test.in + # pytest-codspeed # pytest-cov # pytest-mock +pytest-codspeed==2.2.1 + # via -r requirements/test.in pytest-cov==5.0.0 # via -r requirements/test.in pytest-mock==3.14.0 diff --git a/tests/test_benchmarks_client_request.py b/tests/test_benchmarks_client_request.py new file mode 100644 index 00000000000..63c77dfcdc8 --- /dev/null +++ b/tests/test_benchmarks_client_request.py @@ -0,0 +1,22 @@ +"""codspeed benchmarks for client requests.""" + +import asyncio +from http.cookies import Morsel + +from pytest_codspeed import BenchmarkFixture # type: ignore[import-untyped] +from yarl import URL + +from aiohttp.client_reqrep import ClientRequest + + +def test_client_request_update_cookies( + loop: asyncio.AbstractEventLoop, benchmark: BenchmarkFixture +) -> None: + req = ClientRequest("get", URL("http://python.org"), loop=loop) + morsel: "Morsel[str]" = Morsel() + morsel.set(key="string", val="Another string", coded_val="really") + morsel_cookie = {"str": morsel} + + @benchmark + def _run() -> None: + req.update_cookies(cookies=morsel_cookie) diff --git a/tests/test_benchmarks_cookiejar.py b/tests/test_benchmarks_cookiejar.py new file mode 100644 index 00000000000..508b49f68cb --- /dev/null +++ b/tests/test_benchmarks_cookiejar.py @@ -0,0 +1,26 @@ +"""codspeed benchmarks for cookies.""" + +from http.cookies import BaseCookie + +from pytest_codspeed import BenchmarkFixture # type: ignore[import-untyped] +from yarl import URL + +from aiohttp.cookiejar import CookieJar + + +async def test_load_cookies_into_temp_cookiejar(benchmark: BenchmarkFixture) -> None: + """Benchmark for creating a temp CookieJar and filtering by URL. + + This benchmark matches what the client request does when cookies + are passed to the request. + """ + all_cookies: BaseCookie[str] = BaseCookie() + url = URL("http://example.com") + cookies = {"cookie1": "value1", "cookie2": "value2"} + + @benchmark + def _run() -> None: + tmp_cookie_jar = CookieJar() + tmp_cookie_jar.update_cookies(cookies) + req_cookies = tmp_cookie_jar.filter_cookies(url) + all_cookies.load(req_cookies)