diff --git a/app/api/notifications.py b/app/api/notifications.py index 7927b255..8b14eafb 100644 --- a/app/api/notifications.py +++ b/app/api/notifications.py @@ -452,17 +452,16 @@ async def update_preferences( ) try: + # Pre-fetch existing preferences for this user to avoid N+1 queries + existing_prefs = ( + db.query(UserNotificationPreference).filter(UserNotificationPreference.owner_id == owner_id).all() + ) + + # Build a fast lookup dictionary keyed by (event_type, channel_type, target_id) + prefs_dict = {(pref.event_type, pref.channel_type, pref.target_id): pref for pref in existing_prefs} + for item in body.preferences: - existing = ( - db.query(UserNotificationPreference) - .filter( - UserNotificationPreference.owner_id == owner_id, - UserNotificationPreference.event_type == item.event_type, - UserNotificationPreference.channel_type == item.channel_type, - UserNotificationPreference.target_id == item.target_id, - ) - .first() - ) + existing = prefs_dict.get((item.event_type, item.channel_type, item.target_id)) if existing: existing.is_enabled = item.is_enabled else: diff --git a/benchmark_notifications.py b/benchmark_notifications.py new file mode 100644 index 00000000..dec8b535 --- /dev/null +++ b/benchmark_notifications.py @@ -0,0 +1,50 @@ +import json +import time +import pytest +from app.database import get_db +from app.models import UserNotificationTarget, UserNotificationPreference +from app.main import app +from tests.test_notifications_api import _make_client, _OWNER, _cleanup +import statistics + +def run_benchmark(notif_engine, notif_session, client, items_count, iterations=5): + # Setup + target = UserNotificationTarget( + owner_id=_OWNER, + channel_type="webhook", + name="My Webhook", + config=json.dumps({"url": "https://x.com"}), + ) + notif_session.add(target) + notif_session.commit() + notif_session.refresh(target) + + # Generate big payload + preferences = [] + for i in range(items_count): + preferences.append({ + "event_type": f"event.type.{i}", + "channel_type": "webhook", + "is_enabled": True, + "target_id": target.id, + }) + + payload = {"preferences": preferences} + + # Warm up + client.put("/api/user-notifications/preferences", json=payload) + + times = [] + for _ in range(iterations): + # Alter the values a bit so it's a real update + for p in payload["preferences"]: + p["is_enabled"] = not p["is_enabled"] + + start = time.time() + resp = client.put("/api/user-notifications/preferences", json=payload) + end = time.time() + + assert resp.status_code == 200 + times.append(end - start) + + return statistics.mean(times) diff --git a/tests/test_notifications_api.py b/tests/test_notifications_api.py index 32bf94f4..98c56348 100644 --- a/tests/test_notifications_api.py +++ b/tests/test_notifications_api.py @@ -830,3 +830,54 @@ class TestUserNotificationService: result = _send_email_notification({"smtp_host": "smtp.example.com"}, "Title", "Body") assert result is False + +class TestBenchmark: + @pytest.mark.unit + def test_update_preferences_benchmark(self, notif_engine, notif_session): + import time + import statistics + from app.main import app + + target = UserNotificationTarget( + owner_id=_OWNER, + channel_type="webhook", + name="My Webhook", + config=json.dumps({"url": "https://x.com"}), + ) + notif_session.add(target) + notif_session.commit() + notif_session.refresh(target) + + client = _make_client(notif_engine, _OWNER) + try: + items_count = 100 + preferences = [] + for i in range(items_count): + preferences.append({ + "event_type": f"event.type.{i}", + "channel_type": "webhook", + "is_enabled": True, + "target_id": target.id, + }) + + payload = {"preferences": preferences} + + # Warm up + client.put("/api/user-notifications/preferences", json=payload) + + times = [] + for _ in range(5): + # Alter the values a bit so it's a real update + for p in payload["preferences"]: + p["is_enabled"] = not p["is_enabled"] + + start = time.time() + resp = client.put("/api/user-notifications/preferences", json=payload) + end = time.time() + + assert resp.status_code == 200 + times.append(end - start) + + print(f"\nAverage time: {statistics.mean(times):.4f}s") + finally: + _cleanup(app)