Почему OSDev никогда не превратят в конструктор

Всем доброго времени суток. В этой статье разжуем некоторые проблемы OSDev'а в том виде, в котором его хотят видеть люди.
Обсудим «конструкторы ОС» и все его плюсы и недостатки.

Объектно-ориентированный язык программирования

Всем доброго времени суток. В этой статье разжуем некоторые проблемы OSDev'а в том виде, в котором его хотят видеть люди.
Обсудим «конструкторы ОС» и все его плюсы и недостатки.

Чуть больше полугода назад состоялся релиз .NET 10. Остались ли дефекты в исходном коде проекта, спустя месяцы после релиза? Постараемся сегодня найти ответ на этот вопрос.

С вами снова Илья Вязников, инженер сопровождения SOFROS. Продолжаю делиться практическими приёмами, которые упрощают жизнь при эксплуатации и доработке интеграций.
Не так давно я рассказывал, как вынести повторяющуюся логику в C# функции, но в любом серьёзном проекте со временем накапливаются не только функции, но и куча параметров: адреса сервисов, таймауты и т.д. Сначала их прописывают прямо в коде, но очень быстро это превращается в проблему.
Сегодня расскажу про глобальные константы — один из полезных инструментов платформы для централизованного хранения таких параметров.
Продолжаем серию про redb.Route — вводная и разбор четырёх in-memory каналов уже вышли. Сегодня не статья серии, а релизная заметка: в 3.0.1 три конкретных изменения в DSL, каждое с боевым примером из демо.
До 3.0.1 глубоко вложенные scope-ы требовали закрывать себя в строго обратном порядке — утомительно и легко ошибиться. Три вещи изменились.
FFI, P/Invoke, EmbeddedResource, DllImportResolver и кроссплатформенная доставка без ручного копирования .dll, .so и .dylib.
Когда .NET-коду нужно вызвать Rust-библиотеку, первый прототип обычно заводится быстро:
Rust собирается как cdylib;
функции экспортируются через extern "C";
C# вызывает их через DllImport;
результат возвращается через указатель.

Мы убрали одну блокировку, чтобы апрувы перестали тормозить. Через несколько недель из‑за этого клиент пробил квартальный бюджет — а наша система этого даже не заметила.
Полгода после MVP, первые крупные клиенты. B2B travel SaaS, конец 2016-го. Компании начали подключать не по 15–20 человек, а по 80–100.
Один из новых клиентов оказался кратно крупнее остальных — финансовый департамент почти на сотню человек с фиксированным квартальным бюджетом на командировки порядка нескольких сотен тысяч рублей. К середине квартала большая часть бюджета уже потрачена, остаток — заметно меньше половины. Два руководителя — в разных городах, в разных браузерах — одновременно открывают форму апрува командировок. Оба видят один и тот же остаток. Один одобряет крупную поездку, другой почти в то же время — ещё одну, сопоставимую по сумме; каждая по отдельности в остаток вписывалась. Оба получают подтверждение. Вместе две поездки пробили лимит — перерасход, которого ни один из руководителей в одиночку не допускал.
Обнаружили через 3–4 часа — когда финансовый менеджер клиента открыл квартальную сводку и позвонил нам.

Прошлая статья была обзорной — что такое redb.Route, зачем нам понадобился свой Apache Camel под .NET, как выглядит боевой маршрут. Если не читали, коротко: это fluent C# DSL для интеграции — 22 коннектора (~30 URI-схем, если считать https/wss/es-варианты), ~30 паттернов EIP нативно через 41 процессор, 8 in-process компонентов, компилируемый expression-движок. Сегодня заходим внутрь. Не список фич, а рабочий разбор.
Серия будет длинной, поэтому сразу скажу, что и в каком порядке:

Создаю собственный DSL на C#: рассказываю о том, как написать парсер, и как его оптимизировать. Так же покажу как работает обещанный DFA.

У вас не 5 микросервисов — у вас десятки. Бэкенд, который рос три года: монолит, расколотый на куски, GPS-фид от автопарка, мобильное приложение водителя, веб-кабинет диспетчера, интеграции с SAP / 1С / регуляторами / маркетплейсами, отдельный SMTP-воркер, отдельный PDF-генератор, отдельный шедулер ночных пересчётов. Между ними — Kafka (несколько кластеров, по топику на домен), RabbitMQ (RPC + pub/sub + DLQ), Redis (кэш, last-known-state, pub/sub-каналы), пара HTTP-эндпоинтов наружу, SFTP с поставщиком, SQL-polling outbox-таблицы старого монолита, MQTT с трекеров, IBM MQ для одного древнего банковского контура, SignalR-хабы для real-time-дашбордов. На каждом стыке — свой ретрай, свой DLQ (или нет DLQ), своя сериализация, свои метрики (или нет метрик), своя бойлерплейт-обвязка из консьюмеров и try/catch.
Каждый из этих стыков живёт своей жизнью в Program.cs соответствующего сервиса. Каждый — это hand-rolled цикл:

Каждый разработчик на .NET сталкивался с этим. Всё работает быстро, но иногда случается внезапный фриз. Игра проседает с 60 до 30 FPS на секунду. Сервис отвечает на запрос 100 мс вместо обычных 10. UI дёргается.
Виновник — Garbage Collector.
Когда GC решает собрать мусор, он останавливает все потоки приложения (Stop-The-World). Для игр и real-time сервисов это катастрофа.
Стандартные коллекции .NET создают мусор везде:

В real-time играх и серверах часто возникает задача: из N объектов нужно выбрать K лучших, чтобы обновить их в этом кадре.

Каждый игровой инженер сталкивался с этим. У вас есть 16.67 мс на кадр (60 FPS). В сцене 10 000+ объектов: враги, частицы, тени, декали, постыффекты. Нужно решить: на что потратить бюджет, чтобы игрок видел самое важное в максимальном качестве, а FPS не проседал?
Типичные подходы и их недостатки
1. Дистанционный LOD (расстояние до камеры)

Типизированное хранилище для .NET поверх Postgres и MSSQL. C#-класс как схема — без миграций, без Include, с полным LINQ. Работает в проде.

Практически в каждом высоконагруженном .NET-проекте рано или поздно появляется один и тот же паттерн:
Есть коллекция данных.
Для каждого элемента нужно выполнить дорогую операцию.
Например:
вычислить хэш;

Не так давно, я писал, о том как автоматизировать повторную обработку сообщений из архива в DATAREON Platform.
Было несколько вопросов и уточнений, поэтому хочу дополнительно разобрать код C# для повторной обработки.
Для ЛЛ: Полный алгоритм с комментариями в конце поста

Рассказываю, как написал кроссплатформенную .NET-библиотеку для глобального перехвата клавиатуры и мыши. Под капотом: WH_KEYBOARD_LL на Windows, CGEventTap на macOS и polling через XQueryKeymap на Linux. Один интерфейс, три реализации, ноль внешних зависимостей.

Представьте, что у вас есть многослойный пайплайн обработки данных.
Ширина слоя — 5000 узлов. Количество слоёв — 60. Общее число узлов — 300 000.
Каждую секунду приходит 10 новых событий (изменений на входе). Наивный подход — пересчитать всё с нуля — будет перебирать все 300 000 узлов на каждое обновление. При 10 обновлениях в секунду это 3 млн вычислений узлов в секунду. А если ширина слоя 100 000 и слоёв 100? Получаем 10 млн узлов на пересчёт. Компьютер не справляется.

Финансовые системы предъявляют жёсткие требования к производительности.
Риск-департамент запрашивает переоценку портфеля из 200 000 опционов. Маржинальная система требует пересчитать все позиции клиентов после сильного движения рынка. Алгоритмический трейдер хочет оценить Greeks для тысяч потенциальных сделок за миллисекунды.
Стандартные подходы на .NET дают сбой по трём причинам.
Причина 1: Объектная модель
Каждый опцион становится отдельным объектом в куче. Виртуальные методы, ссылки, разрозненное расположение в памяти. Для 200 000 объектов — миллионы байтов, GC-паузы на сборку, промахи кэша процессора.
Причина 2: Позлементные вычисления
Вызов функции ценообразования в цикле — плохо. Процессор не может векторизовать код, потому что не видит всю картину целиком. SIMD-инструкции простаивают.
Причина 3: Аллокации в горячем пути
Каждый вызов new double[100000] для хранения промежуточных результатов — это давление на GC. В 24/7 сервисе такие аллокации накапливаются и вызывают непредсказуемые паузы.
Требования к решению

Допустим, вы делаете симуляцию города. Или RTS. Или RPG с открытым миром. И у вас в сцене одновременно находится 5, 10, а то и 20 тысяч живых существ. У каждого свои цели, приоритеты, эмоции, социальные связи.
Ваша архитектура AI начинает трещать по швам.
Классический подход — дать каждому NPC компонент с методом Update() — перестаёт работать где-то после 500–1000 объектов. Дальше начинаются проблемы:
TL;DR. Один «безобидный» foo.GetAsync().Result в middleware способен превратить ASP.NET Core, державший 50k RPS с p99 = 40 мс, в сервис с 12k RPS и p99 = 4 с — при CPU 8 %.
Виноват не сам blocking call, а hill-climbing — фидбэк-луп в ThreadPool, в недрах которого живёт дискретное преобразование Фурье. Разбираем по исходникам CoreCLR, почему это вообще возможно, воспроизводим эффект на ~80 строках кода и разбираемся, почему SetMinThreads — не решение, а анестезия.