Add benchmarks
This commit is contained in:
0
tests/benchmark/__init__.py
Normal file
0
tests/benchmark/__init__.py
Normal file
11
tests/benchmark/benchmark_settings.py
Normal file
11
tests/benchmark/benchmark_settings.py
Normal file
@@ -0,0 +1,11 @@
|
||||
TRANSACTION_GENERATOR_EXCEPTION_LIMIT = 15
|
||||
"""
|
||||
The random transaction generator uses a set seed to generate transactions.
|
||||
However, not all transactions are valid in all contexts. We catch illegal
|
||||
generated transactions with a try/except, and retry until we generate a valid
|
||||
one. However, if we exceed this limit, something is likely wrong with the generator
|
||||
instead, due to the unlikely high number of exceptions.
|
||||
"""
|
||||
|
||||
BENCHMARK_ITERATIONS = 5
|
||||
BENCHMARK_ROUNDS = 3
|
||||
299
tests/benchmark/helpers.py
Normal file
299
tests/benchmark/helpers.py
Normal file
@@ -0,0 +1,299 @@
|
||||
import random
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from dibbler.models import Product, Transaction, TransactionType, User
|
||||
from dibbler.queries import joint_buy_product
|
||||
from tests.benchmark.benchmark_settings import TRANSACTION_GENERATOR_EXCEPTION_LIMIT
|
||||
|
||||
|
||||
def insert_users_and_products(
|
||||
sql_session: Session, user_count: int = 10, product_count: int = 10
|
||||
) -> tuple[list[User], list[Product]]:
|
||||
users = []
|
||||
for i in range(user_count):
|
||||
user = User(f"User{i + 1}")
|
||||
sql_session.add(user)
|
||||
users.append(user)
|
||||
sql_session.commit()
|
||||
|
||||
products = []
|
||||
for i in range(product_count):
|
||||
barcode = str(1000000000000 + i)
|
||||
product = Product(barcode, f"Product{i + 1}")
|
||||
sql_session.add(product)
|
||||
products.append(product)
|
||||
sql_session.commit()
|
||||
|
||||
return users, products
|
||||
|
||||
|
||||
def generate_random_transactions(
|
||||
sql_session: Session,
|
||||
n: int,
|
||||
seed: int = 42,
|
||||
transaction_type_filter: list[TransactionType] | None = None,
|
||||
distribution: dict[TransactionType, float] | None = None,
|
||||
) -> list[Transaction]:
|
||||
random.seed(seed)
|
||||
|
||||
if transaction_type_filter is None:
|
||||
transaction_type_filter = list(TransactionType)
|
||||
|
||||
if TransactionType.JOINT_BUY_PRODUCT in transaction_type_filter:
|
||||
transaction_type_filter.remove(TransactionType.JOINT_BUY_PRODUCT)
|
||||
|
||||
# TODO: implement me
|
||||
if TransactionType.THROW_PRODUCT in transaction_type_filter:
|
||||
transaction_type_filter.remove(TransactionType.THROW_PRODUCT)
|
||||
|
||||
if distribution is None:
|
||||
distribution = {t: 1 / len(transaction_type_filter) for t in transaction_type_filter}
|
||||
transaction_types = list(distribution.keys())
|
||||
weights = list(distribution.values())
|
||||
transactions: list[Transaction] = []
|
||||
last_time = datetime(2023, 1, 1, 0, 0, 0)
|
||||
for _ in range(n):
|
||||
transaction_type = random.choices(transaction_types, weights=weights, k=1)[0]
|
||||
generator = RANDOM_GENERATORS[transaction_type]
|
||||
transaction_or_transactions = generator(sql_session, last_time)
|
||||
if isinstance(transaction_or_transactions, list):
|
||||
transactions.extend(transaction_or_transactions)
|
||||
last_time = max(t.time for t in transaction_or_transactions)
|
||||
else:
|
||||
transactions.append(transaction_or_transactions)
|
||||
last_time = transaction_or_transactions.time
|
||||
return transactions
|
||||
|
||||
|
||||
def random_add_product_transaction(sql_session: Session, last_time: datetime) -> Transaction:
|
||||
i = 0
|
||||
while True:
|
||||
i += 1
|
||||
user = random.choice(sql_session.query(User).all())
|
||||
product = random.choice(sql_session.query(Product).all())
|
||||
product_count = random.randint(1, 10)
|
||||
product_price = random.randint(15, 45)
|
||||
amount = product_count * product_price + random.randint(-7, 0)
|
||||
new_datetime = last_time + timedelta(minutes=random.randint(1, 60))
|
||||
try:
|
||||
transaction = Transaction.add_product(
|
||||
amount,
|
||||
user.id,
|
||||
product.id,
|
||||
product_price,
|
||||
product_count,
|
||||
time=new_datetime,
|
||||
)
|
||||
except Exception:
|
||||
if i > TRANSACTION_GENERATOR_EXCEPTION_LIMIT:
|
||||
raise RuntimeError(
|
||||
"Too many failed attempts to create a valid transaction, consider changing the seed"
|
||||
)
|
||||
continue
|
||||
return transaction
|
||||
|
||||
|
||||
def random_adjust_balance_transaction(sql_session: Session, last_time: datetime) -> Transaction:
|
||||
i = 0
|
||||
while True:
|
||||
i += 1
|
||||
user = random.choice(sql_session.query(User).all())
|
||||
amount = random.randint(-50, 100)
|
||||
new_datetime = last_time + timedelta(minutes=random.randint(1, 60))
|
||||
try:
|
||||
transaction = Transaction.adjust_balance(
|
||||
amount,
|
||||
user.id,
|
||||
time=new_datetime,
|
||||
)
|
||||
except Exception:
|
||||
if i > TRANSACTION_GENERATOR_EXCEPTION_LIMIT:
|
||||
raise RuntimeError(
|
||||
"Too many failed attempts to create a valid transaction, consider changing the seed"
|
||||
)
|
||||
continue
|
||||
return transaction
|
||||
|
||||
|
||||
def random_adjust_interest_transaction(sql_session: Session, last_time: datetime) -> Transaction:
|
||||
i = 0
|
||||
while True:
|
||||
i += 1
|
||||
user = random.choice(sql_session.query(User).all())
|
||||
amount = random.randint(-5, 5)
|
||||
new_datetime = last_time + timedelta(minutes=random.randint(1, 60))
|
||||
try:
|
||||
transaction = Transaction.adjust_interest(
|
||||
amount,
|
||||
user.id,
|
||||
time=new_datetime,
|
||||
)
|
||||
except Exception:
|
||||
if i > TRANSACTION_GENERATOR_EXCEPTION_LIMIT:
|
||||
raise RuntimeError(
|
||||
"Too many failed attempts to create a valid transaction, consider changing the seed"
|
||||
)
|
||||
continue
|
||||
return transaction
|
||||
|
||||
|
||||
def random_adjust_penalty_transaction(sql_session: Session, last_time: datetime) -> Transaction:
|
||||
i = 0
|
||||
while True:
|
||||
i += 1
|
||||
user = random.choice(sql_session.query(User).all())
|
||||
penalty_multiplier_percent = random.randint(100, 200)
|
||||
penalty_threshold = random.randint( -150, -50)
|
||||
new_datetime = last_time + timedelta(minutes=random.randint(1, 60))
|
||||
try:
|
||||
transaction = Transaction.adjust_penalty(
|
||||
penalty_multiplier_percent,
|
||||
penalty_threshold,
|
||||
user.id,
|
||||
time=new_datetime,
|
||||
)
|
||||
except Exception:
|
||||
if i > TRANSACTION_GENERATOR_EXCEPTION_LIMIT:
|
||||
raise RuntimeError(
|
||||
"Too many failed attempts to create a valid transaction, consider changing the seed"
|
||||
)
|
||||
continue
|
||||
return transaction
|
||||
|
||||
|
||||
def random_adjust_stock_transaction(sql_session: Session, last_time: datetime) -> Transaction:
|
||||
i = 0
|
||||
while True:
|
||||
i += 1
|
||||
user = random.choice(sql_session.query(User).all())
|
||||
product = random.choice(sql_session.query(Product).all())
|
||||
stock_change = random.randint(-5, 6)
|
||||
new_datetime = last_time + timedelta(minutes=random.randint(1, 60))
|
||||
try:
|
||||
transaction = Transaction.adjust_stock(
|
||||
user_id=user.id,
|
||||
product_id=product.id,
|
||||
product_count=stock_change,
|
||||
time=new_datetime,
|
||||
)
|
||||
except Exception:
|
||||
if i > TRANSACTION_GENERATOR_EXCEPTION_LIMIT:
|
||||
raise RuntimeError(
|
||||
"Too many failed attempts to create a valid transaction, consider changing the seed"
|
||||
)
|
||||
continue
|
||||
return transaction
|
||||
|
||||
|
||||
def random_buy_product_transaction(sql_session: Session, last_time: datetime) -> Transaction:
|
||||
i = 0
|
||||
while True:
|
||||
i += 1
|
||||
user = random.choice(sql_session.query(User).all())
|
||||
product = random.choice(sql_session.query(Product).all())
|
||||
product_count = random.randint(1, 5)
|
||||
new_datetime = last_time + timedelta(minutes=random.randint(1, 60))
|
||||
try:
|
||||
transaction = Transaction.buy_product(
|
||||
user_id=user.id,
|
||||
product_id=product.id,
|
||||
product_count=product_count,
|
||||
time=new_datetime,
|
||||
)
|
||||
except Exception:
|
||||
if i > TRANSACTION_GENERATOR_EXCEPTION_LIMIT:
|
||||
raise RuntimeError(
|
||||
"Too many failed attempts to create a valid transaction, consider changing the seed"
|
||||
)
|
||||
continue
|
||||
return transaction
|
||||
|
||||
|
||||
def random_joint_transaction(sql_session: Session, last_time: datetime) -> list[Transaction]:
|
||||
i = 0
|
||||
while True:
|
||||
i += 1
|
||||
user_count = random.randint(2, 4)
|
||||
users = random.sample(sql_session.query(User).all(), k=user_count)
|
||||
product = random.choice(sql_session.query(Product).all())
|
||||
product_count = random.randint(1, 5)
|
||||
new_datetime = last_time + timedelta(minutes=random.randint(1, 60))
|
||||
|
||||
try:
|
||||
transactions = joint_buy_product(
|
||||
sql_session,
|
||||
product=product,
|
||||
product_count=product_count,
|
||||
instigator=users[0],
|
||||
users=users,
|
||||
time=new_datetime,
|
||||
)
|
||||
except Exception:
|
||||
if i > TRANSACTION_GENERATOR_EXCEPTION_LIMIT:
|
||||
raise RuntimeError(
|
||||
"Too many failed attempts to create a valid transaction, consider changing the seed"
|
||||
)
|
||||
continue
|
||||
return transactions
|
||||
|
||||
|
||||
def random_transfer_transaction(sql_session: Session, last_time: datetime) -> Transaction:
|
||||
i = 0
|
||||
while True:
|
||||
i += 1
|
||||
sender, receiver = random.sample(sql_session.query(User).all(), k=2)
|
||||
amount = random.randint(1, 50)
|
||||
new_datetime = last_time + timedelta(minutes=random.randint(1, 60))
|
||||
try:
|
||||
transaction = Transaction.transfer(
|
||||
amount,
|
||||
sender.id,
|
||||
receiver.id,
|
||||
time=new_datetime,
|
||||
)
|
||||
except Exception:
|
||||
if i > TRANSACTION_GENERATOR_EXCEPTION_LIMIT:
|
||||
raise RuntimeError(
|
||||
"Too many failed attempts to create a valid transaction, consider changing the seed"
|
||||
)
|
||||
continue
|
||||
return transaction
|
||||
|
||||
|
||||
def random_throw_product_transaction(sql_session: Session, last_time: datetime) -> Transaction:
|
||||
i = 0
|
||||
while True:
|
||||
i += 1
|
||||
user = random.choice(sql_session.query(User).all())
|
||||
product = random.choice(sql_session.query(Product).all())
|
||||
product_count = random.randint(1, 5)
|
||||
new_datetime = last_time + timedelta(minutes=random.randint(1, 60))
|
||||
try:
|
||||
transaction = Transaction.throw_product(
|
||||
user_id=user.id,
|
||||
product_id=product.id,
|
||||
product_count=product_count,
|
||||
time=new_datetime,
|
||||
)
|
||||
except Exception:
|
||||
if i > TRANSACTION_GENERATOR_EXCEPTION_LIMIT:
|
||||
raise RuntimeError(
|
||||
"Too many failed attempts to create a valid transaction, consider changing the seed"
|
||||
)
|
||||
continue
|
||||
return transaction
|
||||
|
||||
|
||||
RANDOM_GENERATORS = {
|
||||
TransactionType.ADD_PRODUCT: random_add_product_transaction,
|
||||
TransactionType.ADJUST_BALANCE: random_adjust_balance_transaction,
|
||||
TransactionType.ADJUST_INTEREST: random_adjust_interest_transaction,
|
||||
TransactionType.ADJUST_PENALTY: random_adjust_penalty_transaction,
|
||||
TransactionType.ADJUST_STOCK: random_adjust_stock_transaction,
|
||||
TransactionType.BUY_PRODUCT: random_buy_product_transaction,
|
||||
TransactionType.JOINT: random_joint_transaction,
|
||||
TransactionType.TRANSFER: random_transfer_transaction,
|
||||
TransactionType.THROW_PRODUCT: random_throw_product_transaction,
|
||||
}
|
||||
47
tests/benchmark/test_benchmark_product_owners.py
Normal file
47
tests/benchmark/test_benchmark_product_owners.py
Normal file
@@ -0,0 +1,47 @@
|
||||
import pytest
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from dibbler.models import Product, TransactionType
|
||||
from dibbler.queries import product_owners
|
||||
from tests.benchmark.benchmark_settings import BENCHMARK_ITERATIONS, BENCHMARK_ROUNDS
|
||||
from tests.benchmark.helpers import generate_random_transactions, insert_users_and_products
|
||||
|
||||
|
||||
@pytest.mark.benchmark(group='product_owners')
|
||||
@pytest.mark.parametrize(
|
||||
"transaction_count",
|
||||
[
|
||||
100,
|
||||
500,
|
||||
1000,
|
||||
2000,
|
||||
5000,
|
||||
10000,
|
||||
],
|
||||
)
|
||||
def test_benchmark_product_owners(benchmark, sql_session: Session, transaction_count: int):
|
||||
_users, products = insert_users_and_products(sql_session)
|
||||
|
||||
generate_random_transactions(
|
||||
sql_session,
|
||||
transaction_count,
|
||||
transaction_type_filter=[
|
||||
TransactionType.ADD_PRODUCT,
|
||||
TransactionType.ADJUST_STOCK,
|
||||
TransactionType.BUY_PRODUCT,
|
||||
TransactionType.JOINT,
|
||||
TransactionType.THROW_PRODUCT,
|
||||
],
|
||||
)
|
||||
|
||||
benchmark.pedantic(
|
||||
query_all_product_owners,
|
||||
args=(sql_session, products),
|
||||
iterations=BENCHMARK_ITERATIONS,
|
||||
rounds=BENCHMARK_ROUNDS,
|
||||
)
|
||||
|
||||
|
||||
def query_all_product_owners(sql_session: Session, products: list[Product]) -> None:
|
||||
for product in products:
|
||||
product_owners(sql_session, product)
|
||||
44
tests/benchmark/test_benchmark_product_price.py
Normal file
44
tests/benchmark/test_benchmark_product_price.py
Normal file
@@ -0,0 +1,44 @@
|
||||
import pytest
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from dibbler.models import Product, TransactionType
|
||||
from dibbler.queries import product_price
|
||||
from tests.benchmark.benchmark_settings import BENCHMARK_ITERATIONS, BENCHMARK_ROUNDS
|
||||
from tests.benchmark.helpers import generate_random_transactions, insert_users_and_products
|
||||
|
||||
|
||||
@pytest.mark.benchmark(group='product_price')
|
||||
@pytest.mark.parametrize("transaction_count", [
|
||||
100,
|
||||
500,
|
||||
1000,
|
||||
2000,
|
||||
5000,
|
||||
10000,
|
||||
])
|
||||
def test_benchmark_product_price(benchmark, sql_session: Session, transaction_count):
|
||||
_users, products = insert_users_and_products(sql_session)
|
||||
|
||||
generate_random_transactions(
|
||||
sql_session,
|
||||
transaction_count,
|
||||
transaction_type_filter=[
|
||||
TransactionType.ADD_PRODUCT,
|
||||
TransactionType.ADJUST_STOCK,
|
||||
TransactionType.BUY_PRODUCT,
|
||||
TransactionType.JOINT,
|
||||
TransactionType.THROW_PRODUCT,
|
||||
],
|
||||
)
|
||||
|
||||
benchmark.pedantic(
|
||||
query_all_products_price,
|
||||
args=(sql_session, products),
|
||||
iterations=BENCHMARK_ITERATIONS,
|
||||
rounds=BENCHMARK_ROUNDS,
|
||||
)
|
||||
|
||||
|
||||
def query_all_products_price(sql_session: Session, products: list[Product]) -> None:
|
||||
for product in products:
|
||||
product_price(sql_session, product)
|
||||
47
tests/benchmark/test_benchmark_product_stock.py
Normal file
47
tests/benchmark/test_benchmark_product_stock.py
Normal file
@@ -0,0 +1,47 @@
|
||||
import pytest
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from dibbler.models import Product, TransactionType
|
||||
from dibbler.queries import product_stock
|
||||
from tests.benchmark.benchmark_settings import BENCHMARK_ITERATIONS, BENCHMARK_ROUNDS
|
||||
from tests.benchmark.helpers import generate_random_transactions, insert_users_and_products
|
||||
|
||||
|
||||
@pytest.mark.benchmark(group='product_stock')
|
||||
@pytest.mark.parametrize(
|
||||
"transaction_count",
|
||||
[
|
||||
100,
|
||||
500,
|
||||
1000,
|
||||
2000,
|
||||
5000,
|
||||
10000,
|
||||
],
|
||||
)
|
||||
def test_benchmark_product_stock(benchmark, sql_session: Session, transaction_count: int):
|
||||
_users, products = insert_users_and_products(sql_session)
|
||||
|
||||
generate_random_transactions(
|
||||
sql_session,
|
||||
transaction_count,
|
||||
transaction_type_filter=[
|
||||
TransactionType.ADD_PRODUCT,
|
||||
TransactionType.ADJUST_STOCK,
|
||||
TransactionType.BUY_PRODUCT,
|
||||
TransactionType.JOINT,
|
||||
TransactionType.THROW_PRODUCT,
|
||||
],
|
||||
)
|
||||
|
||||
benchmark.pedantic(
|
||||
query_all_products_stock,
|
||||
args=(sql_session, products),
|
||||
iterations=BENCHMARK_ITERATIONS,
|
||||
rounds=BENCHMARK_ROUNDS,
|
||||
)
|
||||
|
||||
|
||||
def query_all_products_stock(sql_session: Session, products: list[Product]) -> None:
|
||||
for product in products:
|
||||
product_stock(sql_session, product)
|
||||
47
tests/benchmark/test_benchmark_transaction_log.py
Normal file
47
tests/benchmark/test_benchmark_transaction_log.py
Normal file
@@ -0,0 +1,47 @@
|
||||
import pytest
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from dibbler.models import Product, User
|
||||
from dibbler.queries import transaction_log
|
||||
from tests.benchmark.benchmark_settings import BENCHMARK_ITERATIONS, BENCHMARK_ROUNDS
|
||||
from tests.benchmark.helpers import generate_random_transactions, insert_users_and_products
|
||||
|
||||
|
||||
@pytest.mark.benchmark(group='transaction_log')
|
||||
@pytest.mark.parametrize(
|
||||
"transaction_count",
|
||||
[
|
||||
100,
|
||||
500,
|
||||
1000,
|
||||
2000,
|
||||
5000,
|
||||
10000,
|
||||
],
|
||||
)
|
||||
def test_benchmark_transaction_log(benchmark, sql_session: Session, transaction_count: int):
|
||||
users, products = insert_users_and_products(sql_session)
|
||||
|
||||
generate_random_transactions(
|
||||
sql_session,
|
||||
transaction_count,
|
||||
)
|
||||
|
||||
benchmark.pedantic(
|
||||
query_transaction_log,
|
||||
args=(
|
||||
sql_session,
|
||||
products,
|
||||
users,
|
||||
),
|
||||
iterations=BENCHMARK_ITERATIONS,
|
||||
rounds=BENCHMARK_ROUNDS,
|
||||
)
|
||||
|
||||
|
||||
def query_transaction_log(sql_session: Session, products: list[Product], users: list[User]) -> None:
|
||||
for user in users:
|
||||
transaction_log(sql_session, user=user)
|
||||
|
||||
for product in products:
|
||||
transaction_log(sql_session, product=product)
|
||||
39
tests/benchmark/test_benchmark_user_balance.py
Normal file
39
tests/benchmark/test_benchmark_user_balance.py
Normal file
@@ -0,0 +1,39 @@
|
||||
import pytest
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from dibbler.models import User
|
||||
from dibbler.queries import user_balance
|
||||
from tests.benchmark.benchmark_settings import BENCHMARK_ITERATIONS, BENCHMARK_ROUNDS
|
||||
from tests.benchmark.helpers import generate_random_transactions, insert_users_and_products
|
||||
|
||||
|
||||
@pytest.mark.benchmark(group='user_balance')
|
||||
@pytest.mark.parametrize(
|
||||
"transaction_count",
|
||||
[
|
||||
100,
|
||||
500,
|
||||
1000,
|
||||
1500,
|
||||
2000,
|
||||
],
|
||||
)
|
||||
def test_benchmark_user_balance(benchmark, sql_session: Session, transaction_count: int):
|
||||
users, _products = insert_users_and_products(sql_session)
|
||||
|
||||
generate_random_transactions(
|
||||
sql_session,
|
||||
transaction_count,
|
||||
)
|
||||
|
||||
benchmark.pedantic(
|
||||
query_all_users_balance,
|
||||
args=(sql_session, users),
|
||||
iterations=BENCHMARK_ITERATIONS,
|
||||
rounds=BENCHMARK_ROUNDS,
|
||||
)
|
||||
|
||||
|
||||
def query_all_users_balance(sql_session: Session, users: list[User]) -> None:
|
||||
for user in users:
|
||||
user_balance(sql_session, user)
|
||||
Reference in New Issue
Block a user