[perf][mcp] getPage — доминирующая нагрузка агентов (812 вызовов/2ч, p95 840 мс): конверсия PM→Markdown без кэша + последовательный fetch subpages #479
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Суть
Первые же сутки per-tool метрик (#402) назвали остаток «всё равно тормозит» после мержа перф-пакета. Что стало быстрым:
updatePageMarkdownp95 182 мс,deletePage96 мс (кэш сессий #400/#435 работает), трэшинга нет (33 doc-load за 2 ч), event loop чистый (max 2.97 с против 30 до пакета). Что осталось:getPage— доминирующая операция агентского цикла (агент перечитывает страницы постоянно: контекст, верификация после правок, сканирование). 812 × ~0.5–0.8 с — это и есть субъективное «агенты медленные», и заодно фоновая CPU-нагрузка на общий event loop.Диагноз (по коду
packages/mcp/src/client.tsgetPage)convertProseMirrorToMarkdown(resultData.content, …)— полный обход ProseMirror-дерева (на больших страницах ~84 КБ+ JSON) при каждом чтении, кэша нет (единственный кэш в клиенте — collab-токен из #435). CPU тратится на том же event loop, что HTTP/collab.Решение
1. Content-addressed кэш конверсии
LRU-кэш в MCP-хосте: ключ
(pageId, page.updatedAt, optionsHash)→ готовый Markdown.updatedAtуже приходит с контентом — инвалидация бесплатная и точная (страница изменилась → новый ключ).optionsHashобязателен: конверсия параметризована (dropResolvedCommentAnchorsиз #328 — getPage и export зовут с разными опциями). Размер LRU скромный (~50 записей / ограничение по суммарным байтам) — агенты работают с малым рабочим набором страниц.Ожидание: p95 повторных чтений — до стоимости HTTP-fetch (~50–100 мс), CPU конверсии уходит из горячего цикла.
2. Параллелить subpages
Promise.all([pageFetch, subpagesFetch])вместо последовательных await — минус целый round-trip с каждого вызова. (Вариант глубже — сделать subpages опциональными параметром инструмента — менять контракт инструмента не предлагаю, паралеллизации достаточно.)3. createPage 2.35 с — только понаблюдать
9 вызовов, включает инициализацию collab-сессии нового дока — вероятно легитимно; после пп. 1–2 посмотреть на чистом профиле, отдельно не чинить.
Крайние случаи
updatedAtобновляется дебаунс-стором — окно, когда контент уже новый, аupdatedAtстарый? Нет: getPage читает контент иupdatedAtиз ОДНОГО ответа/pages/info— они консистентны между собой; кэш максимум отдаст тот же снапшот, что вернул бы сервер. Рассинхрон невозможен по построению.mcp_getpage_cache_hits_total/misses_total(2 counter'а, bounded) — иначе эффект кэша будет невидим и хит-рейт не проверить.Тесты / проверка
(pageId, updatedAt, options), miss после сменыupdatedAtи смены опций; вытеснение LRU по байтам.mcp_tool_duration_seconds{tool="getPage"}p95 < 200 мс при живом агентском цикле; hit-rate > 50% по новым счётчикам; алерт/mcpмолчит на read-нагрузке.Вне скоупа
/pages/info(ETag/304 из роадмапа) — отдельная тема, этот кэш живёт в MCP-клиенте и не зависит от неё.План работ
Promise.allдля subpages.packages/mcp/build/*.mcp_tool_duration_seconds{tool="getPage"}до/после.agent_vscode referenced this issue2026-07-11 05:09:03 +03:00