Skip to content

Commit be5bbc1

Browse files
authored
Add Codspeed performance benchmarks (#1831)
* Add Codspeed * Add create and get tests with bigger model * Use Python 3.12 * Skip benchmarks during regular tests
1 parent 8ac9b28 commit be5bbc1

File tree

9 files changed

+544
-231
lines changed

9 files changed

+544
-231
lines changed

.github/workflows/codspeed.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: CodSpeed
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
# `workflow_dispatch` allows CodSpeed to trigger backtest
9+
# performance analysis in order to generate initial data.
10+
workflow_dispatch:
11+
12+
jobs:
13+
benchmarks:
14+
name: Run benchmarks
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/checkout@v4
18+
- uses: actions/setup-python@v5
19+
with:
20+
# 3.12 is the minimum reqquired version for profiling enabled
21+
python-version: "3.12"
22+
23+
- name: Install and configure Poetry
24+
run: |
25+
pip install -U pip poetry
26+
poetry config virtualenvs.create false
27+
28+
- name: Install dependencies
29+
run: make build
30+
31+
- name: Run benchmarks
32+
uses: CodSpeedHQ/action@v3
33+
with:
34+
token: ${{ secrets.CODSPEED_TOKEN }}
35+
run: pytest tests/benchmarks --codspeed

poetry.lock

Lines changed: 251 additions & 224 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ coveralls = "*"
6464
pytest = "*"
6565
pytest-xdist = "*"
6666
pytest-cov = "*"
67+
pytest-codspeed = { version = "*", python = "^3.9" }
6768
# Pypi
6869
twine = "*"
6970
# Sample integration - Quart

tests/benchmarks/conftest.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
from __future__ import annotations
2+
3+
import asyncio
4+
from decimal import Decimal
5+
import random
6+
7+
import pytest
8+
9+
from tests.testmodels import BenchmarkFewFields, BenchmarkManyFields
10+
from tortoise.contrib.test import _restore_default, truncate_all_models
11+
12+
13+
@pytest.fixture(scope="function", autouse=True)
14+
def setup_database():
15+
_restore_default()
16+
yield
17+
asyncio.get_event_loop().run_until_complete(truncate_all_models())
18+
19+
20+
@pytest.fixture(scope="module", autouse=True)
21+
def skip_if_codspeed_not_enabled(request):
22+
if not request.config.getoption("--codspeed", default=None):
23+
pytest.skip("codspeed is not enabled")
24+
25+
26+
@pytest.fixture
27+
def few_fields_benchmark_dataset() -> list[BenchmarkFewFields]:
28+
async def _create() -> list[BenchmarkFewFields]:
29+
res = []
30+
for _ in range(100):
31+
level = random.randint(0, 100) # nosec
32+
res.append(await BenchmarkFewFields.create(level=level, text="test"))
33+
return res
34+
35+
return asyncio.get_event_loop().run_until_complete(_create())
36+
37+
38+
@pytest.fixture
39+
def many_fields_benchmark_dataset() -> list[BenchmarkManyFields]:
40+
async def _create() -> list[BenchmarkManyFields]:
41+
res = []
42+
for _ in range(100):
43+
res.append(
44+
await BenchmarkManyFields.create(
45+
level=random.randint(0, 100), # nosec
46+
text="test",
47+
col_float1=2.2,
48+
col_smallint1=2,
49+
col_int1=2000000,
50+
col_bigint1=99999999,
51+
col_char1="value1",
52+
col_text1="Moo,Foo,Baa,Waa,Moo,Foo,Baa,Waa,Moo,Foo,Baa,Waa",
53+
col_decimal1=Decimal("2.2"),
54+
col_json1={"a": 1, "b": "b", "c": [2], "d": {"e": 3}, "f": True},
55+
col_float2=0.2,
56+
col_smallint2=None,
57+
col_int2=22,
58+
col_bigint2=None,
59+
col_char2=None,
60+
col_text2=None,
61+
col_decimal2=None,
62+
col_json2=None,
63+
col_float3=2.2,
64+
col_smallint3=2,
65+
col_int3=2000000,
66+
col_bigint3=99999999,
67+
col_char3="value1",
68+
col_text3="Moo,Foo,Baa,Waa,Moo,Foo,Baa,Waa,Moo,Foo,Baa,Waa",
69+
col_decimal3=Decimal("2.2"),
70+
col_json3={"a": 1, "b": 2, "c": [2]},
71+
col_float4=0.00004,
72+
col_smallint4=None,
73+
col_int4=4,
74+
col_bigint4=99999999000000,
75+
col_char4="value4",
76+
col_text4="AAAAAAAA",
77+
col_decimal4=None,
78+
col_json4=None,
79+
)
80+
)
81+
return res
82+
83+
return asyncio.get_event_loop().run_until_complete(_create())

tests/benchmarks/test_create.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import asyncio
2+
from decimal import Decimal
3+
import random
4+
5+
from tests.testmodels import BenchmarkFewFields, BenchmarkManyFields
6+
7+
8+
def test_create_few_fields(benchmark):
9+
loop = asyncio.get_event_loop()
10+
11+
@benchmark
12+
def bench():
13+
async def _bench():
14+
level = random.randint(0, 100) # nosec
15+
await BenchmarkFewFields.create(level=level, text="test")
16+
17+
loop.run_until_complete(_bench())
18+
19+
20+
def test_create_many_fields(benchmark):
21+
loop = asyncio.get_event_loop()
22+
23+
@benchmark
24+
def bench():
25+
async def _bench():
26+
await BenchmarkManyFields.create(
27+
level=random.randint(0, 100), # nosec
28+
text="test",
29+
col_float1=2.2,
30+
col_smallint1=2,
31+
col_int1=2000000,
32+
col_bigint1=99999999,
33+
col_char1="value1",
34+
col_text1="Moo,Foo,Baa,Waa,Moo,Foo,Baa,Waa,Moo,Foo,Baa,Waa",
35+
col_decimal1=Decimal("2.2"),
36+
col_json1={"a": 1, "b": "b", "c": [2], "d": {"e": 3}, "f": True},
37+
col_float2=0.2,
38+
col_smallint2=None,
39+
col_int2=22,
40+
col_bigint2=None,
41+
col_char2=None,
42+
col_text2=None,
43+
col_decimal2=None,
44+
col_json2=None,
45+
col_float3=2.2,
46+
col_smallint3=2,
47+
col_int3=2000000,
48+
col_bigint3=99999999,
49+
col_char3="value1",
50+
col_text3="Moo,Foo,Baa,Waa,Moo,Foo,Baa,Waa,Moo,Foo,Baa,Waa",
51+
col_decimal3=Decimal("2.2"),
52+
col_json3={"a": 1, "b": 2, "c": [2]},
53+
col_float4=0.00004,
54+
col_smallint4=None,
55+
col_int4=4,
56+
col_bigint4=99999999000000,
57+
col_char4="value4",
58+
col_text4="AAAAAAAA",
59+
col_decimal4=None,
60+
col_json4=None,
61+
)
62+
63+
loop.run_until_complete(_bench())

tests/benchmarks/test_filter.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import asyncio
2+
import random
3+
4+
from tests.testmodels import BenchmarkFewFields
5+
6+
7+
def test_filter_few_fields(benchmark, few_fields_benchmark_dataset):
8+
loop = asyncio.get_event_loop()
9+
levels = list(set([o.level for o in few_fields_benchmark_dataset]))
10+
11+
@benchmark
12+
def bench():
13+
async def _bench():
14+
await BenchmarkFewFields.filter(level__in=random.sample(levels, 5)).limit(5)
15+
16+
loop.run_until_complete(_bench())

tests/benchmarks/test_get.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import asyncio
2+
import random
3+
4+
from tests.testmodels import BenchmarkFewFields, BenchmarkManyFields
5+
6+
7+
def test_get_few_fields(benchmark, few_fields_benchmark_dataset):
8+
loop = asyncio.get_event_loop()
9+
minid = min(o.id for o in few_fields_benchmark_dataset)
10+
maxid = max(o.id for o in few_fields_benchmark_dataset)
11+
12+
@benchmark
13+
def bench():
14+
async def _bench():
15+
randid = random.randint(minid, maxid) # nosec
16+
await BenchmarkFewFields.get(id=randid)
17+
18+
loop.run_until_complete(_bench())
19+
20+
21+
def test_get_many_fields(benchmark, many_fields_benchmark_dataset):
22+
loop = asyncio.get_event_loop()
23+
minid = min(o.id for o in many_fields_benchmark_dataset)
24+
maxid = max(o.id for o in many_fields_benchmark_dataset)
25+
26+
@benchmark
27+
def bench():
28+
async def _bench():
29+
randid = random.randint(minid, maxid) # nosec
30+
await BenchmarkManyFields.get(id=randid)
31+
32+
loop.run_until_complete(_bench())

tests/testmodels.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -998,3 +998,55 @@ class CallableDefault(Model):
998998
id = fields.IntField(primary_key=True)
999999
callable_default = fields.CharField(max_length=32, default=callable_default)
10001000
async_default = fields.CharField(max_length=32, default=async_callable_default)
1001+
1002+
1003+
class BenchmarkFewFields(Model):
1004+
timestamp = fields.DatetimeField(auto_now_add=True)
1005+
level = fields.SmallIntField(index=True)
1006+
text = fields.CharField(max_length=255)
1007+
1008+
1009+
class BenchmarkManyFields(Model):
1010+
timestamp = fields.DatetimeField(auto_now_add=True)
1011+
level = fields.SmallIntField(index=True)
1012+
text = fields.CharField(max_length=255)
1013+
1014+
col_float1 = fields.FloatField(default=2.2)
1015+
col_smallint1 = fields.SmallIntField(default=2)
1016+
col_int1 = fields.IntField(default=2000000)
1017+
col_bigint1 = fields.BigIntField(default=99999999)
1018+
col_char1 = fields.CharField(max_length=255, default="value1")
1019+
col_text1 = fields.TextField(default="Moo,Foo,Baa,Waa,Moo,Foo,Baa,Waa,Moo,Foo,Baa,Waa")
1020+
col_decimal1 = fields.DecimalField(12, 8, default=Decimal("2.2"))
1021+
col_json1 = fields.JSONField[dict](
1022+
default={"a": 1, "b": "b", "c": [2], "d": {"e": 3}, "f": True}
1023+
)
1024+
1025+
col_float2 = fields.FloatField(null=True)
1026+
col_smallint2 = fields.SmallIntField(null=True)
1027+
col_int2 = fields.IntField(null=True)
1028+
col_bigint2 = fields.BigIntField(null=True)
1029+
col_char2 = fields.CharField(max_length=255, null=True)
1030+
col_text2 = fields.TextField(null=True)
1031+
col_decimal2 = fields.DecimalField(12, 8, null=True)
1032+
col_json2 = fields.JSONField[dict](null=True)
1033+
1034+
col_float3 = fields.FloatField(default=2.2)
1035+
col_smallint3 = fields.SmallIntField(default=2)
1036+
col_int3 = fields.IntField(default=2000000)
1037+
col_bigint3 = fields.BigIntField(default=99999999)
1038+
col_char3 = fields.CharField(max_length=255, default="value1")
1039+
col_text3 = fields.TextField(default="Moo,Foo,Baa,Waa,Moo,Foo,Baa,Waa,Moo,Foo,Baa,Waa")
1040+
col_decimal3 = fields.DecimalField(12, 8, default=Decimal("2.2"))
1041+
col_json3 = fields.JSONField[dict](
1042+
default={"a": 1, "b": "b", "c": [2], "d": {"e": 3}, "f": True}
1043+
)
1044+
1045+
col_float4 = fields.FloatField(null=True)
1046+
col_smallint4 = fields.SmallIntField(null=True)
1047+
col_int4 = fields.IntField(null=True)
1048+
col_bigint4 = fields.BigIntField(null=True)
1049+
col_char4 = fields.CharField(max_length=255, null=True)
1050+
col_text4 = fields.TextField(null=True)
1051+
col_decimal4 = fields.DecimalField(12, 8, null=True)
1052+
col_json4 = fields.JSONField[dict](null=True)

tortoise/contrib/test/__init__.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,16 @@ def _restore_default() -> None:
100100
Tortoise._inited = True
101101

102102

103+
async def truncate_all_models() -> None:
104+
# TODO: This is a naive implementation: Will fail to clear M2M and non-cascade foreign keys
105+
for app in Tortoise.apps.values():
106+
for model in app.values():
107+
quote_char = model._meta.db.query_class._builder().QUOTE_CHAR
108+
await model._meta.db.execute_script(
109+
f"DELETE FROM {quote_char}{model._meta.db_table}{quote_char}" # nosec
110+
)
111+
112+
103113
def initializer(
104114
modules: Iterable[Union[str, ModuleType]],
105115
db_url: Optional[str] = None,
@@ -287,13 +297,7 @@ async def _setUpDB(self) -> None:
287297

288298
async def _tearDownDB(self) -> None:
289299
_restore_default()
290-
# TODO: This is a naive implementation: Will fail to clear M2M and non-cascade foreign keys
291-
for app in Tortoise.apps.values():
292-
for model in app.values():
293-
quote_char = model._meta.db.query_class._builder().QUOTE_CHAR
294-
await model._meta.db.execute_script(
295-
f"DELETE FROM {quote_char}{model._meta.db_table}{quote_char}" # nosec
296-
)
300+
await truncate_all_models()
297301
await super()._tearDownDB()
298302

299303

0 commit comments

Comments
 (0)