# لودتستِ دستیار سه اسکریپت، برای اینکه «چند دانش‌آموزِ هم‌زمان» هر بار با یک روش سنجیده شود. خروجیِ هر اجرا یک JSON است با زمانِ هر پرسش و هر درخواستِ صفحه‌ی عادی؛ جمع‌بندی در خودِ خروجیِ ترمینال چاپ می‌شود. | فایل | کار | |---|---| | `mkusers.py` | کاربرانِ آزمایشی (شماره‌ی جعلیِ `0900…`، بی‌پیامک، بی‌سیگنال) با توکن و سه سوالِ متفاوت از بانکِ سوال برای هر کدام | | `loadtest.py` | N نفر هم‌زمان می‌پرسند (استریم، دقیقاً مثلِ فرانت) و B نفرِ دیگر در خانه/آزمون/خلاصه/سهمیه می‌چرخند | | `fake_upstream.py` | جایگزینِ GapGPT و Qdrant برای آزمایشگاه: embedding، جوابِ استریم با زمانِ لاگ‌نرمال (پیش‌فرض: میانه‌ی ۵٫۲ و صدکِ ۹۰ِ ۱۳ ثانیه، از جواب‌های واقعیِ سرور) و جست‌وجوی Qdrant | ## آزمایشگاه (بی‌هزینه، روی لپ‌تاپ) روی یک کپی از پروژه اجرا کنید تا دیتابیسِ اصلیِ لوکال پر نشود. Qdrant هم شبیه‌سازی می‌شود، چون از لپ‌تاپ پهنای باندِ بالارفتن (هر بردار ~۳۰ کیلوبایت) جست‌وجوها را سریالی نشان می‌دهد؛ از خودِ سرور ۲۰ جست‌وجوی هم‌زمان ۰٫۲۶ ثانیه است. ```bash python tools/loadtest/fake_upstream.py & # :8901 (FAKE_SIGMA=0 یعنی همیشه ۵٫۲ ثانیه) export DJANGO_DEBUG=0 CACHE_URL= NUM_PROXIES=0 AI_DAILY_QUOTA_FREE=1000 HEARTS_ENFORCED=0 RAG_FORCE_MOCK=0 \ DJANGO_ALLOWED_HOSTS=127.0.0.1 OPENAI_BASE_URL=http://127.0.0.1:8901/v1 OPENAI_API_KEY=x \ QDRANT_URL=http://127.0.0.1:8901 QDRANT_API_KEY=x LT_N=150 python manage.py shell -c "exec(open('tools/loadtest/mkusers.py').read())" | grep ^LTJSON | cut -c7- > users.json gunicorn --bind 127.0.0.1:8800 config.wsgi:application & # همان gunicorn.conf.py ِ سرور python tools/loadtest/loadtest.py http://127.0.0.1:8800 users.json 50 10 0 out.json --writes ``` پیش از هر اجرا gunicorn را از نو بالا بیاورید (کشِ جوابِ هر worker خالی شود) و مطمئن شوید پروسه‌ی قبلی واقعاً رفته است؛ وگرنه سوال‌های تکراری از کش در ۰٫۱ ثانیه جواب می‌گیرند و عدد دروغ می‌گوید. ## سرورِ اصلی (هزینه‌ی واقعی: ~۰٫۱۸ سنت برای هر جواب) ۱. بکاپ از دیتابیس (`docs/operations.md`). ۲. کاربرانِ آزمایشی را روی سرور با `mkusers.py` بسازید (`LT_PREFIX` یک پیشوندِ جعلی که روی سرور نیست) و خروجی را فقط در یک فایلِ 600 نگه دارید. ۳. `loadtest.py` را با `https://mathapi.tadrisino2.ir` و بدونِ `--writes` اجرا کنید. ۴. زمانِ داخلِ GapGPT برای هر پیام در `ai_tutor.Message.latency_ms` است؛ اختلافِ آن با زمانِ کلاینت یعنی کارِ سرورِ ما و صفِ انتظار برای نخِ آزاد. ۵. `manage.py delete_users --ids … --yes` و پاک کردنِ فایلِ توکن‌ها. **روی سرورِ مشترک، throttleهای پروداکشن دست نمی‌خورند.** اگر سقفِ نرخ مانع است، یک gunicornِ موقت روی سوکتِ جدا در `~/mathapp-v2/.lt/` با `READ_THROTTLE_RATE` بالا و `DB_POOL_MAX_SIZE=4 --workers 2` بالا بیاورید (تا با ۳۲ اتصالِ پروداکشن از ۵۷ نگذرد)، کوتاه اجرا کنید و آخرش `.lt` را پاک کنید. در اسکریپت روی آن gunicornِ پس‌زمینه `wait` نزنید: تا ابد منتظر می‌ماند. ## بازیکن‌ها (نه دستیار): `mkplayers.py`، `play.py`، `agg.py` هر کاربرِ مجازی خانه ← آزمونِ یک جزیره ← ۴ جواب ← درس ← خلاصه را می‌چرخد، با مکثِ تصادفیِ ۱ تا THINK_MAX ثانیه. کاربرها اشتراکِ آزمایشیِ یک‌روزه می‌گیرند (بی دیوارِ پرداخت و بی کمبودِ قلب). روی سرور، مستقیم روی سوکتِ gunicorn: ```bash cd ~/mathapp-v2 && mkdir -m 700 .lt # اول بکاپِ دیتابیس LT_N=1000 LT_PREFIX=0900770 venv/bin/python manage.py shell -c "exec(open('tools/loadtest/mkplayers.py').read())" \ | grep ^LTJSON | cut -c7- > .lt/users.json for p in $(seq 0 9); do venv/bin/python tools/loadtest/play.py /run/gunicorn/mathapp.sock .lt/users.json .lt/out-$p.json \ "300:40,600:40,1000:60" 5 40 $p 10 & done; wait venv/bin/python tools/loadtest/agg.py ".lt/out-*.json" "300:40,600:40,1000:60" ``` **مولد را چندپروسه‌ای اجرا کنید (آخرین دو آرگومان).** یک پروسه‌ی httpx با ۶۰۰ اتصال خودش گلوگاه شد: کلاینت ۲۱ ثانیه دید درحالی‌که لاگِ اپ برای همان درخواست‌ها ۰٫۲ تا ۰٫۵ ثانیه می‌گفت (۲۰۲۶-۱۰-۱۳). همیشه `duration_ms`ِ `logs/app.log` (به وقتِ UTC) را با عددِ کلاینت مقایسه کنید. پاک کردن: `delete_users` از راهِ پایتون روی `phone__startswith=LT_PREFIX` (فهرستِ id از `manage.py shell` با خطِ هشدار قاطی می‌شود)، کشِ رتبه‌بندی، و `rm -rf .lt`.