Плагины анализа графа работали медленно — конкретно и измеримо. Не потому что алгоритмы были неправильными — потому что каждый запрос свойства посылал отдельный запрос к серверу графа. Один узел, один вопрос, одна пересылка туда-обратно. Умножить на каждый узел в графе, на каждую фазу вывода, на каждый плагин. На большой кодовой базе: миллионы запросов на один прогон анализа, каждый раз.
Спринт v0.2 открылся с одним замером: 6 минут 51 секунда. Цель — менее пяти минут. Кампания по ликвидации N+1 прошла через три плагина последовательно. Бригада установила паттерн на первом плагине и применила его ко всем трём: массовая предзагрузка релевантного подграфа одним Datalog-запросом, разрешение на JavaScript, запись результатов пакетными сбросами. Итоговая ведомость:
| Плагин | IPC-вызовов (до) | Запросов (после) | Коммит |
|---|---|---|---|
shape-verifier |
600 000 – 700 000 | 2 | 5ac5a302 |
method-call-resolver |
~600 000 | 4 | d3ce94e1 |
type-inference |
~1 270 000 | 7 | 4d8edc98 |
| Итого | ~2 500 000 | 13 | Ветка: perf/plugin-early-exit-gates |
Все три переработки заблокированы за GRAFEMA_DATALOG_PLUGINS. Устаревшие реализации сохранены в полном объёме. 681 тест проходит по всем вариантам. Бенчмарк — фактическое сравнение 6м51с с целевыми ≤5м — ожидает завершения CI со скомпилированными нативными бинарниками.
type-inference.mjs — последний и крупнейший. Первые две переработки установили паттерн; к моменту, когда бригада дошла до type-inference, подход был доказан. Изменился только масштаб. Этот плагин — 525 строк, отвечает за вычисление рёбер типов по всем нетипизированным переменным в графе. Исходный подход выполнял по одному или несколько IPC-вызовов на узел в каждой фазе вывода. На примерно 75 000 узлах в пяти фазах это накапливалось до ~1,27 миллиона IPC-туров — больше, чем в двух других плагинах вместе взятых.
Переработка заменяет цикл по узлам семью Datalog-запросами. Каждый запрос предзагружает полную таблицу разрешений; результат соединяется на JavaScript. Семь запросов по фазам:
needs_inference(N) заменяет ~350 000 IPC-вызововinfer_literal(N, TypeName)infer_constructor(N, ClassName)new ClassName() → INSTANCE_OF → ClassName.infer_import_ctor(N, ClassName)new X(), где X поступает через ребро IMPORT.infer_annotation(N, TypeName)call_needs_singleton(C, ClassName) · builtin_method(M, ClassName) заменяет ~125 000 IPC-вызововconsole.log, Math.floor и т.д.) на их канонический класс. Дедупликация по принципу first-wins по ключу ClassName::MethodName.param_typed(P, TypeName) заменяет ~180 000 IPC-вызововREFERENCE.Результаты Q2–Q4 объединяются по принципу first-wins в единое множество inferredVars. Обратная запись в граф пакетируется порциями по 500 рёбер — паттерн, установленный в первых двух переработках. Инспекция одобрила. Третья переработка была самой сложной. Она же дала наибольший результат.
Спринт v0.2 был не только о производительности. В очереди стояли три бага — каждый независимый, каждый с известной первопричиной, каждый ловушкой для следующего разработчика. Бригада закрыла все три параллельно с кампанией N+1.
В графическом интерфейсе было систематическое неверное отображение атрибуции, проявлявшееся только в файлах с несколькими функциями. Каждый CALL-узел внутри любой функции отображался как исходящий из первой функции в файле — не из той, которая его реально содержала. Файл с пятью функциями показывал все вызовы как исходящие из первой. Граф был структурно неверен в любом файле с более чем одной функцией — и таковым оставался некоторое время.
Эндпоинт /api/graph-stream не обходил цепочку родителей по рёбрам CONTAINS — он безусловно присваивал каждый невидимый узел первому видимому.
Исправление: примерно 33 строки Rust в build_graph_stream_body(). Строится parent_of: HashMap<u128, u128> из CONTAINS-рёбер (за исключением синтетических layout-pack рёбер), затем каждый невидимый узел обходит цепочку предков до нахождения корректного видимого контейнера. Если предок не найден — сохраняется исходное резервное поведение. Алгоритм уже был доказан в edges_stream() — это перенос, а не открытие. Влияние на производительность: около +2 секунд при загрузке graph-stream. Бригада Стахановцев оценила это как приемлемое.
grafema analyze --clear должна была остановить сервер и начать заново. Она этого не делала. Каждый последующий запуск после первого заканчивался ECONNREFUSED. Паттерн был стабильным: процесс завершался, PID-файл и socket-файл оставались, следующий вызов connect() находил их, делал вывод, что сервер уже запущен, и отказывался запускать новый. Он не был запущен. Файлы лгали.
shutdownServer() ждал завершения процесса, но не удалял PID-файл и socket-файл. Два вызова unlinkSync после _waitForPidExit — с try/catch для ENOENT — закрыли брешь. 21/21 тест проходит.
grafema doctor теперь обнаруживает устаревшие socket-файлы и удаляет их автоматически, возвращая статус warn с сообщением: «Устаревший сокет удалён. Запустите grafema analyze для перезапуска сервера». Ранее устаревший сокет приводил к отчёту об ошибке без пути к исправлению. Разведка данных отмечает: это тот же паттерн, что и в REG-1129. Сервер, который прежде оставлял ключ в замке, научился его забирать.
Каждый разработчик, когда-либо добавлявший новый плагин в Grafema, платил один и тот же налог: читал исходники оркестратора, чтобы обнаружить формат конфигурации. Документации не было. Примерного файла не было. Были исходники — и ты их читал. Налог был невелик. Он не был обязателен.
Добавление нового плагина в Grafema теперь не требует чтения исходников. Процесс установки сводится к двум командам:
cp _ai/orchestrator.config.example.yaml _ai/orchestrator.config sed -i "s|<GRAFEMA_ROOT>|$(pwd)|g" _ai/orchestrator.config
Полная процедура регистрации — в CONTRIBUTING.md. Аннотированный шаблон конфигурации — в _ai/orchestrator.config.example.yaml. Каждый будущий автор плагинов избегает того налога на чтение исходников, который платил каждый предыдущий.
Спринт v0.2 открылся с одним замером: 6 минут 51 секунда. Закрывается с четырьмя выполненными пунктами, 681 проходящим тестом и бенчмарком, ожидающим результатов CI. Разведка данных не прогнозирует результаты бенчмарка. Она считает подтверждённое: 2,5 миллиона IPC-вызовов устранены. Тринадцать запросов стоят на вахте.
Граф атрибутирован правильно там, где ошибался. Сервер останавливается там, где не останавливался. Регистрация плагинов задокументирована там, где не была. Замер придёт из CI.
Разведка данных завершена. Итог: 2 500 000 IPC-вызовов ликвидировано. 13 запросов несут вахту. Ожидаем бенчмарка.
GRAFEMA_DATALOG_PLUGINS=shape-verifier,method-call-resolver,type-inference grafema analyze
perf/plugin-early-exit-gates · PR #259 · CI запущенГлеб Всеволодович Сыскарёв подал этот отчёт в 23:45. К тому времени он уже начал считать следующую партию IPC-вызовов.