Производительность и качество
Архитектурные решения, влияющие на пропускную способность
- Одна сериализация на рассылку. Рассылка сериализуется один раз, и одни и те же байты записываются каждому получателю.
- Параллельный fanout. Локальным получателям данные записываются параллельно с одним общим дедлайном, поэтому медленный сокет прерывается, не задерживая остальных.
- Ограниченный объём работы на соединение. Фиксированное число параллельных запросов и ограниченная очередь не дают одному занятому клиенту исчерпать ресурсы сервера, а на heartbeat отвечается немедленно.
- Последовательная запись. У каждого сокета одновременно только один писатель; фреймы ответов и рассылок никогда не перемешиваются.
- Слоты доставки Redis. На каждом инстансе параллельно выполняется до 16 доставок Redis.
Бенчмарки
Микробенчмарки локального Release-прогона 5.0 на .NET 10.0.12, AMD Ryzen 7 3700X, Windows 11 (медиана пяти замеров, payload 364 байта). Транспорт — in-memory приёмник без сетевого I/O, поэтому эти числа показывают накладные расходы библиотеки, а не сквозную задержку.
| Сценарий | Получатели | Время на операцию | Память на операцию |
|---|---|---|---|
| Сериализация для каждого получателя | 100 | 0.266 ms | 39.2 KB |
| Однократная сериализация | 100 | 0.004 ms | 1.2 KB |
| In-memory fanout | 100 | 0.021 ms | 3.5 KB |
| Сериализация для каждого получателя | 1 000 | 2.901 ms | 385 KB |
| Однократная сериализация | 1 000 | 0.009 ms | 1.2 KB |
| In-memory fanout | 1 000 | 0.143 ms | 11.8 KB |
| Сериализация для каждого получателя | 10 000 | 6.040 ms | 3.84 MB |
| Однократная сериализация | 10 000 | 0.021 ms | 1.2 KB |
| In-memory fanout | 10 000 | 2.202 ms | 102 KB |
| Разбор 1 000 запросов | – | 4.438 ms | 984 KB |
- Сериализация для каждого получателя и однократная сериализация сравнивают наивную сериализацию под каждого клиента с общим payload в DarkWS.
- In-memory fanout — это настоящий
IBroadcaster.PublishAsyncчерез in-memory backplane каждому получателю: около 0.14 µs и 12 байт на получателя при 1 000 получателей. - Разбор 1 000 запросов разбирает конверты запросов, около 4 µs на запрос.
Запустите их сами на ненагруженной машине:
dotnet run --project benchmarks/DarkWS.Benchmarks -c Release
Результаты зависят от железа и среды выполнения; сравнивайте прогоны на одной и той же машине. Они не моделируют TLS, медленных клиентов и работу обработчиков.
Тесты
Каждое изменение прогоняет полный набор тестов на .NET 8, 9 и 10:
| Набор | Тесты |
|---|---|
Сервер (DarkWS) | 193 |
| .NET-клиент | 56 |
| Пакет для тестирования | 29 |
| Redis backplane (настоящий Redis в Docker) | 27 |
| Браузерный клиент (Vitest) | 161 |
Пороги покрытия проваливают сборку, если у любого пакета покрытие ниже 90% по строкам или 80% по ветвям. Текущее покрытие:
| Пакет | Строки | Ветви |
|---|---|---|
DarkWS | 97.6% | 94.3% |
DarkWS.Redis | 93.2% | 91.7% |
DarkWS.Client | 96.3% | 92.3% |
DarkWS.Client.DependencyInjection | 100% | 100% |
DarkWS.Testing | 100% | 100% |
darkws | 99.0% | 97.1% |
Помимо модульных тестов, проверка:
- запускает браузерный клиент, собранный из исходников, в Node.js против настоящего сервера DarkWS;
- тестирует Redis backplane с StackExchange.Redis 2.13.17 и 3.2.1;
- упаковывает каждый NuGet-пакет, проверяет его XML-документацию и символы, устанавливает в чистый проект-потребитель и запускает на всех целевых фреймворках;
- упаковывает npm-пакет из чистой копии, чтобы проверить его сборку;
- запускает регрессионный нагрузочный тест Redis, который проверяет незавершённую работу и удерживаемую память при зависшем получателе.
Стабильность API
Публичные API отслеживаются с помощью PublicApiAnalyzers: любое изменение публичной поверхности должно быть задекларировано и проверено, иначе сборка падает. Ломающие изменения выходят только в мажорных версиях, с инструкциями по миграции в changelog и в разделе Обновление.