Skip to content
Merged
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
7 changes: 4 additions & 3 deletions .github/workflows/perf.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
name: Benchmark

on:
push:
paths:
- 'perf/**'
# push:
# paths:
# - '.github/**'
# - 'perf/**'
workflow_dispatch:

jobs:
Expand Down
123 changes: 123 additions & 0 deletions .github/workflows/perfhistory.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
####################################################################################
# Runs benchmarks for BlackSheep source code at various points of the commit history,
# on Ubuntu and Windows for a single version of Python.
#
# This workflow supports both manual and automatic triggers.
# If triggered manually, it is possible to select commits hashes or tags to checkout.
#
# The minimum supported BlackSheep version by the benchmarks is v2.0.1!
####################################################################################
name: HistoryBenchmark

on:
# push:
# paths:
# - '.github/**'
# - 'perf/**'
workflow_dispatch:
inputs:
commits:
description: "List of commits or tags to benchmark (space-separated)"
required: true
default: "v2.0.1 v2.2.0 v2.3.0 current"
memory:
description: "Include memory benchmark (Y|N). Time consuming."
required: true
default: "N"

env:
DEFAULT_MEMORY: "N"
DEFAULT_COMMITS: "v2.0.1 v2.2.0 v2.3.0 current"

jobs:
perf-tests:
strategy:
fail-fast: false
matrix:
python-version: ["3.13"]
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}

steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: false

- name: Use Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
pip install -r requirements.txt
cd perf
pip install -r req.txt

- name: Run benchmark
shell: bash
env:
MEMORY: ${{ github.event.inputs.memory || env.DEFAULT_MEMORY }}
COMMITS: ${{ github.event.inputs.commits || env.DEFAULT_COMMITS }}
run: |

echo "Running benchmarks for commits: $COMMITS"
export PYTHONPATH="."

if [ $MEMORY == "Y" ]; then
echo "➔ Including memory benchmarks 🟢"
python perf/historyrun.py --commits $COMMITS --times 3 --memory
else
echo "➔ Excluding memory benchmarks 🔴"
python perf/historyrun.py --commits $COMMITS --times 3 --no-memory
fi

- name: Upload results
uses: actions/upload-artifact@v4
with:
name: benchmark-results-${{ matrix.os }}-${{ matrix.python-version }}
path: benchmark_results

genreport:
runs-on: ubuntu-latest
needs: [perf-tests]
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: false

- name: Download a distribution artifact
uses: actions/download-artifact@v4
with:
pattern: benchmark-results-*
merge-multiple: true
path: benchmark_results

- name: Use Python 3.13
uses: actions/setup-python@v5
with:
python-version: '3.13'

- name: Install dependencies
run: |
cd perf
pip install -r req.txt

- name: Generate report
shell: bash
run: |
ls -R benchmark_results
chmod -R 755 benchmark_results

export PYTHONPATH="."
python perf/genreport.py
python perf/genreport.py --output windows-results.xlsx --platform Windows
python perf/genreport.py --output linux-results.xlsx --platform Linux

- name: Upload reports
uses: actions/upload-artifact@v4
with:
name: benchmark-reports
path: "**/*.xlsx" # Upload all .xlsx files
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,4 @@ venv*
benchmark_results/
*.xlsx
.~lock*
.cython-hash
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.3.1] - 2025-??-?? :horse_racing:

- Fix [#559](https://github.com/Neoteroi/BlackSheep/issues/559), which is a
performance regression introduced in `2.3.0`. Remove support for Pydantic v1
`validate_arguments` decorator (added in 2.3.0). Pydantic's v2
`validate_call` supports async and does not require specific code.

## [2.3.0] - 2025-05-10 :sun_behind_small_cloud:

> [!IMPORTANT]
Expand Down
2 changes: 1 addition & 1 deletion blacksheep/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"""

__author__ = "Roberto Prevato <roberto.prevato@gmail.com>"
__version__ = "2.3.0"
__version__ = "2.3.1"

from .contents import Content as Content
from .contents import FormContent as FormContent
Expand Down
6 changes: 1 addition & 5 deletions blacksheep/server/normalization.py
Original file line number Diff line number Diff line change
Expand Up @@ -677,11 +677,7 @@ def _get_async_wrapper_for_output(
) -> Callable[[Request], Awaitable[Response]]:
@wraps(method)
async def handler(request: Request) -> Response:
response = await method(request)
# Handle consequences of an async decorator
if inspect.isawaitable(response):
response = await response
return ensure_response(response)
return ensure_response(await method(request))

return handler

Expand Down
27 changes: 26 additions & 1 deletion perf/genreport.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import glob
import json
import os
import subprocess

import matplotlib.pyplot as plt
import pandas as pd
Expand Down Expand Up @@ -42,6 +43,21 @@ def load_results(
return results


def _try_get_git_tag(commit_hash):
"""
If a tag is associated with the given commit hash, returns it, as it is more
readable than the hash. Otherwise it returns the same hash.
"""
try:
return subprocess.check_output(
["git", "describe", "--tags", "--exact-match", commit_hash],
universal_newlines=True,
stderr=subprocess.DEVNULL, # Suppress error output
).strip()
except subprocess.CalledProcessError:
return commit_hash


def create_comparison_table(results):
"""Create a pandas DataFrame for comparison"""
rows = []
Expand All @@ -51,7 +67,7 @@ def create_comparison_table(results):

row = {
"timestamp": result.get("timestamp", "unknown"),
"commit": commit,
"commit": _try_get_git_tag(commit),
"date": date,
"python_version": result.get("system_info", {}).get(
"python_version", "unknown"
Expand Down Expand Up @@ -167,6 +183,11 @@ def _set_conditional_formatting(df, worksheet, max_row):
def _add_ms_chart(workbook, df, worksheet, max_row):
time_cols = [col for col in df.columns if col.endswith("_avg_ms")]

if not time_cols:
# This should never happen, unless in the future support for running
# only memory benchmarks is added.
return

chart = workbook.add_chart({"type": "line"}) # type: ignore

for col in time_cols:
Expand Down Expand Up @@ -202,6 +223,10 @@ def _add_ms_chart(workbook, df, worksheet, max_row):
def _add_mem_chart(workbook, df, worksheet, max_row):
mem_cols = [col for col in df.columns if col.endswith("_peak_mb")]

if not mem_cols:
# This happens if memory benchmarks are disabled (--no-memory)
return

chart = workbook.add_chart({"type": "line"}) # type: ignore

for col in mem_cols:
Expand Down
Loading
Loading