Производительность, тестирование и оптимизация
Tip
Когда нужно читать эту статью: если вас интересуют цифры пропускной способности и задержки событий, оценка производительности перед развертыванием, или если вы хотите сравнить производительность до и после обновления версии. Если вам это не нужно, можете пропустить эту статью, это не повлияет на использование других функций.
Тестирование производительности (2026-10-05, Python 3.13.14)
Один и тот же компьютер, два последовательных теста (предварительная прогревка + несколько циклов выборки с определением процентиля), NoneBot2 2.5.0 — контрольная среда в отдельном окружении.
Все числовые значения можно воспроизвести с помощью встроенного в репозиторий бенчмарка: python tests/performance/benchmark.py.
Распространение событий (ops/s, чем выше, тем лучше)
| Базовое тестирование | ErisPulse | NoneBot2 2.5.0 | Отношение |
|---|---|---|---|
| Задержка на одном процессоре с последовательной шиной | 9,189 | 3,021 | 3.0× |
| Пропускная способность шины с фантомным распространением (1000 событий/цикл) | 9,877 | 2,055 | 4.8× |
| Пропускная способность с 10 процессорами / событиями | 4,309 | 435 | 9.9× |
| Команда найдена (/команда) | 2,165 | 677 | 3.2× |
| Команда не найдена (некомандное сообщение) | 10,818 | 921 | 11.7× |
| Однопроцессорная шина сообщений (по умолчанию, полный путь) | 2,804 | 2,144 | 1.3× |
Другие подсистемы (после оптимизации)
| Базовое тестирование | ops/s | Базовое тестирование | Значение |
|---|---|---|---|
| Горячее чтение getConfig | 1,086,957 | ORM: создание одной записи | 407/s |
| KV aset (sqlite) | 1,659 | GET /health (ASGI) | 1,188/s |
| KV aget (sqlite) | 3,333 | module.call RPC | 148,905/s |
| Время往返 сообщений end-to-end | 298 | Смешанная нагрузка | 142/s |
Время запуска
| Фаза | ErisPulse | NoneBot2 |
|---|---|---|
| Пустая загрузка интерпретатора (контроль) | 59.5ms | — |
| Холодная импортная загрузка | 241.7ms | 249.6ms |
| sdk.init / инициализация NoneBot | 30.8ms | 162.3ms |
Самостоятельная проверка и сравнение с базовым тестом
# Быстрый режим (примерно 1/4 выборки, для быстрой проверки)
python tests/performance/benchmark.py --quick
# Полный базовый тест с сохранением результатов (8 компонентов: config/event/storage/send/router/module/overall/startup)
python tests/performance/benchmark.py --json baseline.json
# Сравнение с предыдущим базовым тестом (разница в пределах ±5% считается шумом)
python tests/performance/benchmark.py --compare baseline.json
# HTML-отчёт / указание бэкенда для хранения компонентов
python tests/performance/benchmark.py --html report.html
python tests/performance/benchmark.py --backend mysql
Примечание для CI: цифры в базовом тесте могут меняться в зависимости от машины. Порог ±5% в
--compareподходит только для сравнения на одной и той же машине (для проверки обновлений), но не подходит для сравнения между разными машинами.
Оптимизация
- Узкое место по всей цепочке сообщений — это запись сессий: по умолчанию включена запись transcript (с 2.9 версии — буферизация в памяти + фоновая пакетная запись на диск); при отсутствии потребностей в аудите можно отключить, пропускная способность по всей цепочке возрастёт примерно с 2,804 → 3,774 операций/с.
- Управление сборкой мусора (GC): группа настроек
proactive_gcвErisPulse.framework(interval/full_every/memory_growth_mb/idle_only) — при пике событий автоматически пропускает GC; на этапе инициализации уже встроенаgc.freeze(). - Хранилище: для локального одиночного экземпляра sqlite (WAL) достаточно; при необходимости совместного использования данных между несколькими экземплярами используйте mysql / postgres (см. Хранилище).
- Встроенные оптимизации критических участков кода (начиная с 2.9, не требует настройки): пакетная запись transcript, кэширование i18n, защита уровней логирования, предварительная компиляция матчера, объединённая синхронизация задержанных записей конфигурации.
- Максимальное количество состояний команд: таблица состояний охлаждения/ограничения/квоты ограничена 4096 записями (при превышении удаляются самые старые); при развертывании в больших группах настройка не требуется.
Диагностика памяти
При поиске проблемы «чем дольше работает, тем больше ест памяти» поставьте по одному снимку с меткой label до и после подозрительного участка кода — для одинаковых label дельта рассчитывается автоматически (по двум метрикам: RSS + tracemalloc):
from ErisPulse.runtime import log_snapshot, snapshot
class MyModule(BaseModule):
async def on_load(self, event):
snapshot("MyModule.load") # Собирает и возвращает dict (включая дельту RSS по label)
async def on_unload(self, event):
log_snapshot("MyModule.unload") # Сбор тем же методом и запись в лог уровня TRACE
snapshot(label)возвращает машиночитаемый dict, подходит для ассертов и отчётов;log_snapshot(label)сразу пишет в лог (уровень TRACE), подходит для временной диагностики- Кэш label ограничен по размеру и не «утекает» с ростом числа вызовов; в сочетании с метриками процесса
эндпоинта
/metricsможно вести долгосрочное наблюдение за трендами - Остаточный рост памяти после выгрузки модуля обычно указывает на отсутствие привязки владельца (см. записи
фоновых задач
bind_ownerв разделе система владения)
Связанные документы
- Наблюдение за метриками выполнения:
/health(идентификация экземпляра / количество событий) и/metrics(текст Prometheus,ErisPulse.server.metrics_enabledвключен) см. Руководство по развертыванию