Создаём DSL на C#: Пишем парсер

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

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

Создаю собственный 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 — не решение, а анестезия.

Если вы делаете RAG (Retrieval-Augmented Generation) на .NET, то рано или поздно упираетесь в вопрос: куда складывать эмбеддинги и как быстро искать по ним.
Существующие варианты делятся на два лагеря.
Внешние сервисы (Pinecone, Qdrant, Weaviate) — хороши, но требуют отдельной инфраструктуры. Сеть, авторизация, сериализация, мониторинг. Каждый запрос — это миллисекунды на HTTP. Плюс вы привязываетесь к конкретному облачному провайдеру или контейнеру.
Существующие .NET-решения — часто либо заброшены, либо имеют проблемы с производительностью (избыточные аллокации, медленный ANN, отсутствие гибридного поиска).
Но есть и третий путь: встраиваемая (embedded) векторная БД, которая работает прямо внутри вашего процесса. Никакой сети. Никакого внешнего сервиса. Только ваш код и процессор.

Так как в первой статье я сосредоточился на схемотехнике, в этой хотелось бы пройтись по разработке ПО и прошивки.
Напомню, я решил, что вместо прошивки мне подойдёт и нейрослоп - в конце концов это не серийное устройство, а поделка для себя. И тут, как обычно, в процессе работы пошли фейерверки.
ESP32-S3 имеет два USB выхода - обычный через микросхему CH340, которая конвертирует USB в UART, и второй USB OTG, который подключен к чипу напрямую. То есть чип может управлять тем, какое "устройство" он предоставит хостовой ОС! Более того, он может предоставлять несколько устройств одновременно. Это было как раз то, что надо: одним устройством будет USB HID клавиатура для "сырого" ввода в консоль, например, а вторым - USB RAW устройство, которое будет слушать софт бекенда - запускать приложения по ярлыкам, регулировать громкость, отправлять в устройство настройки. И всё это минуя тормознутый UART. Сказка. Но, как оказалось, для взрослых.
Казалось бы, полный интернет примеров того, как это настраивать. Даже в самом фреймворке esp-idf есть пример композитного устройства, значит, информация уж точно легкодоступная, бери да пользуйся. Я попросил ИИ добавить это в прошивку и... Оно не смогло. То есть вообще. Два дня и токенов примерно на 30 баксов - и я взял дело в свои руки, потому что ИИ выдумывал всё более и более изобретательные причины того, почему прошивка не компилируется, но вот выдумать компилирующуся прошивку не мог никак.
В этой серии туториалов мы реализуем простой Third Person Controller на базе MonoGame.
Серия рассчитана на читателей, уже знакомых с основами MonoGame и 3D-графики.

Я создал свой генератор случайных чисел, забудь про семена и формулы. Этот способ будет бесконечно генерировать случайные числа, не повторяясь...

Пятница, 23:47. PagerDuty: “Платёж AmEx, провайдер вернул 5xx три раза подряд, билеты не зарезервированы.” Открываю логи – действительно три ответа провайдера 5xx, ни одной успешной транзакции по нашей базе. Закрываю как временный сбой на стороне провайдера, пишу короткую сводку в дежурный чат и иду досматривать. Через 40 минут второй алерт – уже от ночной поддержки: клиент прислал скрин выписки, 3 списания подряд за одну бронь. У клиента рейс через 6 часов, ему нужна действующая бронь и подтверждение, что он завтра нормально улетит, а не тикет в поддержку.
Мы делали B2B-платформу для деловых поездок: бронь авиа, отели, трансфер, страховка, в финале – оплата корпоративной картой через платежный шлюз. С этой ночи началась история, которая закончилась переписыванием всего платёжного слоя нашего booking-сервиса. По дороге мы поймали 5 граблей.

Привет, Хабр! Меня зовут Михаил, в Циане я занимаюсь развитием культуры и developer experience. Архитектура у нас микросервисная, за каждый микросервис отвечает конкретная команда. В любой команде обычно есть микросервисы, которые помогают ей достигать собственных целей, и микросервисы, которые достались по наследству — поскольку архитектура наша не всегда развивалась достаточно последовательно. Бывает, что сервис приходит с каким-нибудь разработчиком из другой команды. Бывает, что сервис когда-то отдали команде, потому что больше некому было отдать.
Наш бэкенд написан на Python и C#. Иногда в одной команде используются микросервисы на обоих языкахмогут быть микросервисы и на Python, и на C#. Я считаю, что это нНе самый удобный расклад, я считаю: лучше все-таки иметь один стек в рамках одной команды. Если, например, в команде с питонистами и единственным шарпистом последний уходит в отпуск, то при поломке сервиса на C# остальной команде придется этого шарписта ждать. Либо срочно вызывать на подмогу другого шарписта.
Можно переписать все микросервисы на один язык. Довольно трудоемкая задача, если заниматься этим вручную. Разработчику нужно погрузиться в микросервис, максимально покрыть тестами бизнес-логику и аккуратно все переписать. Не забывая, что делать один в один нужно не всегда, поскольку архитектурные паттерны Python и C# различаются.
Для переписывания сервисов разумно привлечь на помощь LLM. Далее я расскажу, как за неделю своей частичной занятости я с помощью LLM переписал сервис, который потребовал бы для этого два месяца от живого специалиста.

В интеграционных проектах на DATAREON довольно быстро появляется одна и та же проблема: код бизнес-алгоритмов разрастается, а повторяющаяся логика начинает копироваться между процессами.
В DATAREON Platform эту проблему можно решить через программные модули — механизм для хранения и повторного использования собственных функций на C#.
В статье покажу как создавать такие функции и использовать в ваших процессах.