Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions .github/workflows/codspeed.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
name: CodSpeed

on:
push:
branches:
- master
pull_request:
# `workflow_dispatch` allows CodSpeed to trigger backtest
# performance analysis in order to generate initial data.
workflow_dispatch:

permissions:
contents: read
id-token: write # for OpenID Connect authentication with CodSpeed

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

jobs:
benchmarks:
strategy:
fail-fast: false
matrix:
include:
- mode: instrumentation
runner: ubuntu-latest
- mode: walltime
runner: codspeed-macro
- mode: memory
runner: ubuntu-latest

name: Run benchmarks (${{ matrix.mode }})
runs-on: ${{ matrix.runner }}

steps:
- uses: actions/checkout@v6.0.3
with:
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.12'

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may want to have the report for different python versions, idk

wdyt @sobolevn?


- name: Install poetry
run: |
curl -sSL "https://install.python-poetry.org" | python

# Adding `poetry` to `$PATH`:
echo "$HOME/.local/bin" >> $GITHUB_PATH

- name: Install dependencies
run: |
poetry config virtualenvs.in-project true
poetry install --all-extras

- name: Run benchmarks
uses: CodSpeedHQ/action@v4.17.6
with:
mode: ${{ matrix.mode }}
run: poetry run pytest benchmarks/ --codspeed -p no:cov -o addopts=""
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

[![test](https://github.com/dry-python/returns/actions/workflows/test.yml/badge.svg?branch=master&event=push)](https://github.com/dry-python/returns/actions/workflows/test.yml)
[![codecov](https://codecov.io/gh/dry-python/returns/branch/master/graph/badge.svg)](https://codecov.io/gh/dry-python/returns)
[![CodSpeed](https://img.shields.io/endpoint?url=https://codspeed.io/badge.json)](https://app.codspeed.io/dry-python/returns?utm_source=badge)
[![Documentation Status](https://readthedocs.org/projects/returns/badge/?version=latest)](https://returns.readthedocs.io/en/latest/?badge=latest)
[![Python Version](https://img.shields.io/pypi/pyversions/returns.svg)](https://pypi.org/project/returns/)
[![conda](https://img.shields.io/conda/v/conda-forge/returns?label=conda)](https://anaconda.org/conda-forge/returns)
Expand Down
158 changes: 158 additions & 0 deletions benchmarks/test_benchmarks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
"""Performance benchmarks for the core ``returns`` containers.

These benchmarks exercise the hot paths of the most commonly used
containers (``Result``, ``Maybe``, ``IO``) together with the pipeline
and iterable helpers. They are measured by CodSpeed in CI.
"""

from returns.io import IO
from returns.iterables import Fold
from returns.maybe import Maybe, Nothing, Some
from returns.pipeline import flow
from returns.pointfree import bind, map_
from returns.result import Failure, Result, Success, safe


def _increment(value: int) -> int:
return value + 1


def _as_success(value: int) -> Result[int, str]:
return Success(value + 1)


def _as_some(value: int) -> Maybe[int]:
return Some(value + 1)


def test_result_map_chain(benchmark) -> None:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, test .do notation.

"""A long chain of ``.map`` calls over a ``Result``."""

def run() -> Result[int, str]:
container: Result[int, str] = Success(0)
for _ in range(100):
container = container.map(_increment)
return container

assert benchmark(run) == Success(100)


def test_result_bind_chain(benchmark) -> None:
"""A long chain of ``.bind`` calls over a ``Result``."""

def run() -> Result[int, str]:
container: Result[int, str] = Success(0)
for _ in range(100):
container = container.bind(_as_success)
return container

assert benchmark(run) == Success(100)


def test_result_do_notation(benchmark) -> None:
"""Compose ``Result`` values through ``.do`` notation."""

def run() -> Result[int, str]:
return Result.do(
first + second
for first in Success(1)
for second in Success(2)
)

assert benchmark(run) == Success(3)


def test_maybe_do_notation(benchmark) -> None:
"""Compose ``Maybe`` values through ``.do`` notation."""

def run() -> Maybe[int]:
return Maybe.do(
first + second
for first in Some(1)
for second in Some(2)
)

assert benchmark(run) == Some(3)


def test_result_failure_lash(benchmark) -> None:
"""Recover from a failure using ``.lash`` and ``.value_or``."""

def run() -> int:
container: Result[int, str] = Failure('boom')
return container.lash(lambda _: Success(42)).value_or(0)

assert benchmark(run) == 42


def test_safe_decorator(benchmark) -> None:
"""The ``@safe`` decorator wrapping a raising function."""

@safe
def _divide(numerator: int, denominator: int) -> float:
return numerator / denominator

def run() -> Result[float, Exception]:
return _divide(10, 0)

result = benchmark(run)
assert isinstance(result, Failure)


def test_maybe_map_chain(benchmark) -> None:
"""A long chain of ``.map`` calls over a ``Maybe``."""

def run() -> Maybe[int]:
container: Maybe[int] = Some(0)
for _ in range(100):
container = container.map(_increment)
return container

assert benchmark(run) == Some(100)


def test_maybe_bind_nothing(benchmark) -> None:
"""Short-circuiting a ``Maybe`` chain through ``Nothing``."""

def run() -> int:
container: Maybe[int] = Some(1)
container = container.bind(lambda _: Nothing)
return container.bind(_as_some).value_or(-1)

assert benchmark(run) == -1


def test_io_map_chain(benchmark) -> None:
"""A long chain of ``.map`` calls over an ``IO`` container."""

def run() -> IO[int]:
container = IO(0)
for _ in range(100):
container = container.map(_increment)
return container

assert benchmark(run) == IO(100)


def test_flow_pipeline(benchmark) -> None:
"""Compose containers through ``flow`` with point-free helpers."""

def run() -> Result[int, str]:
return flow(
Success(1),
map_(_increment),
bind(_as_success),
map_(_increment),
)

assert benchmark(run) == Success(4)


def test_fold_collect_results(benchmark) -> None:
"""Fold an iterable of ``Result`` values into a single container."""
items = [Success(index) for index in range(100)]

def run() -> Result[tuple[int, ...], str]:
return Fold.collect(items, Success(()))

assert benchmark(run) == Success(tuple(range(100)))
71 changes: 68 additions & 3 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ pytest-mypy-plugins = ">=3.1,<5.0"
pytest-subtests = ">=0.14,<0.16"
pytest-shard = "^0.1"
covdefaults = "^2.3"
pytest-codspeed = "^5.0"

[tool.poetry.group.docs]
optional = true
Expand Down Expand Up @@ -169,6 +170,9 @@ pydocstyle.convention = "google"

[tool.ruff.lint.per-file-ignores]
"*.pyi" = ["D103"]
"benchmarks/*.py" = [
"S101", # asserts
]
"returns/context/__init__.py" = ["F401", "PLC0414"]
"returns/contrib/mypy/*.py" = ["S101"]
"returns/contrib/mypy/_typeops/visitor.py" = ["S101"]
Expand Down
Loading