TGViewer
Channel Public Channel
Сергей Мелюков

Сергей Мелюков

@smelukov_dev

Про разработку, AI, open source, etc...
Ведет @smelukov
Subscribers
646
Photos
13
Videos
3
Links
73
Recent Posts 20 shown
Post #131 493
Подумал тут про покрытие тестами кода, который написала модель

Есть отдельный интересный кейс, в который легко попасть даже если вроде бы делаешь все правильно
Например:

- попросили модель реализовать фичу
- посмотрели глазами, вроде работает
- попросили модель покрыть это тестами
- получили зеленые тесты
- пошли рефакторить

Казалось бы, всё хорошо. Код есть, тесты есть, можно жить. Да, но нет 🙂

Когда мы просим модель написать тесты на уже существующий код, она воспринимает этот код как источник истины
Но модель не знает, как этот код должен работать по задумке. Она видит только то, как он работает сейчас
А значит, если в коде уже есть странное поведение или баг на edge-case, модель может спокойно написать тест именно на это поведение
И дальше получается неприятная штука: ты начинаешь рефакторить код, меняешь реализацию, тесты падают, а потом выясняется, что ты не сломал поведение, а наоборот починил существующую проблему. Просто тесты уже успели зафиксировать ее как ожидаемую

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

Важно: тесты, которые фиксируют текущее поведение, сами по себе не плохие
В легаси это иногда именно то, что нужно. Мы можем специально написать characterization tests, чтобы понять, что система сейчас делает, и не разнести ее случайно во время переписывания
Проблема начинается тогда, когда мы не разделяем два разных режима:

- зафиксировать текущее поведение (вместе со всеми багами)
- проверить что все работает как задумано

Для модели это легко смешивается в одно. Для нас - нет
Поэтому сейчас я стараюсь делать чуть иначе: перед тем как просить модель писать тесты, я сначала прошу ее изучить код и описать, как она его понимает

Потом прошу отдельно выписать спорные места:

- где поведение неочевидное
- где могут быть edge-cases
- где код выглядит так, будто он работает случайно
- где модель не уверена, что именно нужно проверять

И только после этого прошу задать вопросы, ответы на которые повлияют на тесты
В результате появляется важная синхронизация: я вижу, какое поведение модель собирается считать правильным
Если я с этим не согласен, значит либо модель не поняла код, либо код сейчас действительно делает не то, что я ожидал
Иногда я еще прошу сложить это понимание в md-файл, чтобы не потерять контекст при суммаризации. Особенно если задача не на 15 минут и будет несколько итераций

В итоговый промпт почти всегда добавляю что-то вроде:
Тесты должны проверять ожидаемое поведение, а не просто фиксировать текущую реализацию. Если текущее поведение выглядит спорным - сначала задай вопрос


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

Мораль: LLM нормально помогает писать тесты, но просьба "просто покрой это тестами" для существующего кода может привести к тому, что вы закрепите не ожидаемое поведение, а текущее, а текущее поведение - это не всегда то, что вам нужно
  • 👍 16
  • ❤ 3
  • 🤔 1
Post #130 730
Кстати, если вы когда-нибудь озадачивались подсчетом ресурсов для развертывания LLM локально или на сервере, то у меня для вас есть калькулятор https://smelukov.github.io/WeightRoom/
Позволяет понять "влезет или не влезет моделька на мое устройство и какой TPS я получу" или сравнить разные хостинги по цене и TPS (token per second) чтобы выбрать наиболее подходящий

Вот пример расчета для Google Gemma 4 31B с 4-битной квантизацией на Macbook Pro M1 Max (да, 11 tps не так уж и быстро, но работает, проверено)

Можно даже вот такие красивые картиночки и бейджики генерить 😁
  • 👍 3
  • 🔥 3
Post #129 650
В чем произошла заруба:
Среди прочего, я хочу понимать какие MCP были запущены и сколько они работали.
Как это реализовала модель: добавила методы подсчета и отправки счетчиков сессии прямо в класс, который реализует ACPSession, а метрики ask tool шли вообще без привязки к сессии
Я задал совершенно справедливые вопросы, мол, "что за фигня и как это планируется масштабировать?"
Дальше я получил примерно такой ответ (упрощаю):
Ну, смотри - у нас метрики привязаны к сессии, поэтому вот тебе метод broadcastCounters у сессии, а ask tools... ну так мы же запускаем ask MCP вне сессии, а значит не знаем о том, внутри какой сессии задаем вопросы пользователю. А то, что ты просил делать масштабируемый код... ну, код в основном вроде расширяемый, просто есть некоторые ограничения, которые мы не можем обойти, либо их обход обойдется слишком дорого

Смотрите, что произошло: код по сути рабочий, но были нарушены базовые требования + развивая такой код мы получили бы неподдерживаемую и слаборасширяемую архитектуру, то есть качество кода подверглось бы деградации

Я попросил модель унести все что касается счетчиков в отдельную сущность SessionStats и создавать объект при создание ACP сессии:


const stats = new ACPSessionStats();
const session new ACPSession();
registerSession(session, stats);


Статы подписываются на события сессии, но так же их можно модифицировать и извне, если это потребуется.
А за счет подписки на события конкретной сессии мы получаем привязку к сессии для любых вызовов любых инструментов и код отправки метрик из ask tools можно убрать.
Метрики при этом можно собрать так:


const metrics = renderMetrics(stats);


Мораль: вне зависимости от того, какую модель вы используете и какие требования описывали, всегда проверяйте на адекватность то, что выдала модель, иначе попадете в ситуацию, когда код вроде рабочий, но не на долго
Был ли у вас опыт реализации ACP-клиентов и для каких целей?
Agent Client Protocol Introduction - Agent Client Protocol Get started with the Agent Client Protocol.
  • 👍 4
  • ❤ 3
Post #128 602
Подумал: почему бы не писать о том, что делаю прямо сейчас и с чем сталкиваюсь 🙂

Я придерживаюсь мнения, что вайбкодинг без контроля - это зло и при каждом удобном случае пытаюсь это донести
Если не следить за тем, что делает модель (даже самая дорогая и топовая), то через пару десятков итераций (может и раньше) вы столкнетесь с тем, что код невозможно будет либо поддерживать, либо масштабировать (либо и то и другое)
При проектировании чего-то нового, я выбираю одну из двух стратегий:
• описать архитектуру и попросить модель ее критически поревьюить
• описать базовые требования, которым точно нужно соответствовать и попросить модель предложить вариант реализации
Выбор зависит в большей степени от проекта (важность, production-use и тп) и в меньшей степени от настроения
Делаю я тут значит ACP-клиент для opencode (ACP - это протокол, который позволяет организовать взаимодействие с агентами и встраивать их в разные места. Например, именно через ACP вы можете использовать любимые coding agent (open code, claude, codex, pi и т.п.) в любимых IDE), проверяю очередную итерацию, которую предложила мне модель и тут мы с ней зарубились...

Немного о самой идее и предыстории:
Мне нужно завернуть взаимодействие пользователя с агентами и скиллами через свой CLI-инструмент
Это дает больше гибкости, чем если бы пользователь свободно вызывал агенты и скиллы напрямую в opencode или claudecode
Ну и мне важен one-shot сценарий с моими обвязками (запустил, выполнилось и завершилось)
Такой подход требует разных интересных решений, например, надо решить как агент будет задавать вопросы пользователю.
Мой CLI - это ACP-клиент к opencode, но opencode acp запускается в отдельном процессе, внутренности которого мы не контролируем. Так например, задача "задать вопросы пользователю" превращается в задачу о том, как усидеть на двух стульях, т.к. нужно получить контроль за stdout (чтобы выводить вопрос/анкету через тот же Inquirer.js) и stdin (чтобы получать данные), но при этом модель должна воспринимать это как MCP-инструмент. Более того, в CLI есть спиннер, которым нужно управлять в зависимости от того, что сейчас происходит

У ACP есть драфт на тему поддержки такой штуки из коробки - Elicitation: Structured User Input который в свою очередь ссылается на другой драфт из спецификации MCP - Elicitation. Если коротко, то elicitation позволяет нативно запрашивать у пользователя данные (задавать вопросы, заполнять формы и тп)
Поддержки elicitation в opencode пока нет, но есть в обертке для claude. Мне хочется поддерживать и то и то, поэтому я делаю даунгрейд до решения, которое работало бы везде, а именно: MCP запускается в том же процессе что и CLI и информация о запущенном MCP передается в сессию opencode (обычно opencode сам запускает все зарегистрированные MCP и обслуживает их жизненный цикл)

Если бы opencode поддерживал elicitation, то MCP который задает вопросы можно было бы зарегистрировать как обычный MCP внутри opencode
Этот MCP отправлял бы специальный elicitation request и opencode проксировал бы его в моего ACP-клиента (CLI), а там уже я решаю как вывести анкету, что сделать со спиннером и т.д., но, увы, issue пока открыт.
Agent Client Protocol Introduction - Agent Client Protocol Get started with the Agent Client Protocol.
  • 👍 1
Post #127 808
Сергей Мелюков Что вам сейчас интересно?
ИИ победил!
Готовлю кое-что интересное.
Интересно, что Ведьмак и Статоскоп поделили степень интереса 😄
  • 👍 6
  • 🔥 4
  • 😢 1
  • 👌 1
  • 🤣 1
Post #123 3.26K
Привет, VK! 💬👋☺️
С удовольствием встретил здесь бывших коллег и старых знакомых ☺️
Буду трудиться в роли ведущего эксперта/архитектора над новой архитектурой фронтенда https://cloud.vk.com/
Впереди действительно много работы и один большооооой вызов 🚀
  • 🔥 108
  • 👍 75
  • 💩 33
  • 😢 30
  • 👎 5
  • ❤ 2
  • 😁 1
Post #122 3.19K
Я покидаю Яндекс 😀👋☺️
Это были хорошие 4 года: чему-то научился сам, чему-то научил других - было круто.
С благодарностью к коллегам за совместную работу ❤️

Сейчас у меня неделя отдыха, а после начинаю неистово работать над другим проектом, в другой компании 🤫
В результате недельного отдыха планирую значительно продвинуться с новой архитектурой статоскопа. За эти выходные набросал прототип, чтобы проверить накопившиеся идеи, полет отличный. Новая архитектура позволила избавиться от того, от чего давно хотел избавиться, упростить то, что давно хотел упростить, добавить фичи, которые давно хотел добавить… в общем, должно быть огонь!
  • ❤ 52
  • 👍 12
  • 💔 5
Post #121 2.95K
Ну и, думаю, последний пост на тему css-парсинга в этом цикле )
Возможно, у вас в голове крутится что-то вроде:
"Сергей, в предыдущем посте ты плевался от работы с исходником через токены, а в генераторе сам этим грешишь!".
Все так, и в случае, когда нужно просто добавить пробелов - это ок. Более того, css-tree делает намного более детальную токенизацию, нежели postcss-value-parser
Но! Если все таки хочется заморочиться с контекстом и не вставлять пробелы только перед именем шрифта и только в свойстве font, то так тоже можно.
Решение делится на несколько этапов:
- найти все декларации у которых имя свойства равно font
- найти в них ноды со значением типа family-name
- вставить пробелы в генераторе только для этих нод

Этап 1: собираем все декларация типа font:

Для этого воспользуемся волкером и обойдем AST:


const {walk} = require('css-tree');

walk(compressedAST, {
enter(node) {
if (node.type === 'Declaration' && node.property === 'font') {
// нашли!
}
}
});


Этап 2: ищем в этих декларация ноды со значением типа family-name

Все не так просто. Так, например, здесь:


.foo {
color: red;
animation-name: blue;
}


только один цвет - red, а blue, хоть и валидное имя цвета, но используется как название анимации.
AST не знает какой тип значения (цвет, размер, имя шрифта и тп) хранится в ноде. Для этого, нам нужно подняться на уровень лексического разбора и найти нужные нам лексемы. Для этого у css-tree есть лексер. Вот его мы и используем чтобы в декларациях типа font найти все имена шрифтов:


const {walk, lexer} = require('css-tree');

const allFamilyNameNodes = new WeakSet();

walk(compressedAST, {
enter(node) {
if (node.type === 'Declaration' && node.property === 'font') {
const familyNames = lexer.findAllFragments(node, 'Type', 'family-name');

for (const item of familyNames) {
for (const node of item.nodes) {
targetNodes.add(node);
}
}
}
}
});


Теперь в allFamilyNameNodes хранятся все ноды, которые именно по смыслу содержат имя шрифта.

Этап 3: вставляем пробелы только перед собранными нодами

Здесь берем за основу уже знакомый нам код декоратора и чуть-чуть меняем его так, чтобы он срабатывал только для нод, которые мы собрали


const css = generate(compressedAST, {
decorator(handlers) {
return {
...handlers,
node(node) {
this.currentNode = node;
handlers.node(node);
},
tokenBefore(prev, current, value) {
if (
prev !== tokenTypes.WhiteSpace &&
current === tokenTypes.String &&
allFamilyNameNodes.has(this.currentNode)
) {
this.emit(' ');
return tokenTypes.WhiteSpace;
}
return handlers.tokenBefore(prev, current, value);
}
};
}
});


Всё.

Да, здесь можно было сразу найти все family-name, не обходя декларация типа font:

const familyNames = lexer.findAllFragments(compressedAST, 'Type', 'family-name');


Но в таком случае мы бы нашли вообще все family-name и в других свойствах. Тем не менее, вполне рабочий вариант, нечто среднее между первым и вторым, но мне захотелось показать более комплексный пример, да и такие вот комплексные штуки как раз используеются в разного рода плагинах к IDE, например.
  • 👍 6
  • ❤ 2
Post #119 1.42K
Мой ПР, из поста выше, пока не посмотрели, а проблему решать как-то надо.
Начал перебирать варианты:
- заменить минификатор
- поменять csso и cssnano местами
- отключить у csso все оптимизации кроме вырезания неиспользуемых классов

Первый вариант слишком рискованный
Второй выглядит как так себе, потому что cssnano вышит в конфиг сборки, а csso просто используется в плагине, на этапе optimizeChunkAssets
Третий вариант тоже не подходит, потому что дело, как оказалось, не в csso, а в css-tree. Это css-tree убирает пробел:

const {parse, generate} = require("css-tree");

const ast = parse('.foo {font: 1em "Arial"}');
const source = generate(ast);

console.log(source); // .foo{font:1em"Arial"}


Делает это css-tree совершенно законно и генерит валидный CSS. У css-tree нет задачи восстановить CSS из AST в первозданном виде.
Но проблему, все такие надо как-то решать, например добавить пробелы перед строками везде, где их нет. Звучит как костыль, но пока мой ПР в cssnano не вмержили - ок.

Оказалось, что генератор в css-tree можно кастомизировать. Вот так можно вставить пробелы вообще перед всеми токенами:


const {parse, generate} = require("css-tree");
const {tokenTypes} = require("css-tree/tokenizer");

const ast = parse('.foo {font: 1em "Arial"}');
const source = generate(ast, {
decorator(handlers) {
return {
...handlers,
tokenBefore() {
this.emit(' ');
return tokenTypes.WhiteSpace;
}
}
}
});
console.log(source); // . foo { font : 1em "Arial" }


Хендлер tokenBefore используется для расстановки пробелов между токенами во время генерации из AST, потому что само AST не содержит информации о пробелах (whitespaces). Например, если парсить border: 1px solid red в AST и потом обратно в CSS, то именно tokenBefore даст понять генератору, что 1px, solid и red должны быть разделены пробелами.

Предыдущий результат - это, конечно, не то, что нам нужно, поэтому немного перепишем:


const {parse, generate} = require("css-tree");
const {tokenTypes} = require("css-tree/tokenizer");

const ast = parse('.foo {font: 1em "Arial"}');
const source = generate(ast, {
decorator(handlers) {
return {
...handlers,
tokenBefore(prev, current, value) {
if (prev !== tokenTypes.WhiteSpace && current === tokenTypes.String) {
this.emit(' ');
return tokenTypes.WhiteSpace;
}

return handlers.tokenBefore(prev, current, value);
}
}
}
});
console.log(source); // .foo{font:1em "Arial"}


Здесь мы добавляем пробелы только перед строковыми токенами и только в тех случаях, когда перед ними еще нет пробела.

Но мы же работаем не с css-tree напрямую, а с csso. Как здесь быть?
Очень просто. Если раньше мы делали так:


const CSSO = require('csso');

const compressedCSS = CSSO.minify(input.source, {
sourceMap: false,
restructure: false,
usage: {
classes: usedClasses
}
});


То теперь будет так:


const {parse, compress, generate} = require('csso/syntax');

const ast = parse(source);
const compressedAST = compress(ast, {
restructure: false,
usage: {
classes: usedClasses
}
}).ast;
const compressedCSS = generate(compressedAST, {
sourceMap: false,
decorator(handlers) {
// ...
}
});


Вот и всё, так я обхожу баг в cssnano
  • 👍 18
Post #118 1.15K
Привет! Давненько тут ничего не было. Исправляюсь. Пока просто небольшая заметка, но чуть позже расскажу и покажу кое-что интересное ☺️
Сейчас я занимаюсь вопросом тришейкинга CSS в бандле Яндекс Маркета: пытаюсь определить CSS-классы, которые точно не используются и вырезать весь CSS, связанный с этими классами. Задачка оказалась чуть сложнее, чем казалось изначально, но все движется к завершению, о чем и расскажу позже.
Чтобы вырезать CSS неиспользуемых классов я беру CSSO, передаю ему CSS нашего бандла и список всех классов, которые точно используются, а CSSO отдает CSS без "мертвых" (неиспользуемых) стилей. Как такие стили могли попасть в бандл - другой вопрос, расскажу позже. Эта заметка о другом.

Исторически, для минификации CSS, мы используем cssnano.
Но теперь, в пайплайн обработки CSS добавился еще и CSSO. Казалось бы, все должно быть хорошо, но не тут-то было.
В нашем CSS есть вполне безобидная конструкция:
body {
font: .8em 'YS Text', Arial, Helvetica, sans-serif;
}


Если обработать ее отдельно через разные минификаторы, то получатся вполне валидные конструкции:
cssnano: body{font:.8em YS Text,Arial,Helvetica,sans-serif}
CSSO: body{font:.8em"YS Text",Arial,Helvetica,sans-serif}

А если обработать ее сначала через CSSO, а потом через cssnano, то получаем невалидный css:
body{font:.8em"YS Text"Arial,Helvetica,sans-serif}

Проблема вот здесь: .8em"YS Text"Arial
cssnano не вставил запятую перед Arial и получилась невалидное значение.
То есть вот такой вариант .8em "YS Text" cssnano минифицирует нормально, а вот такой .8em"YS Text" уже нет, хотя это валидное значение.
Пошел смотреть исходники cssnano, порадовало, что все разделено на пакеты. Это, по сути, набор плагинов для postcss.
Корень проблемы оказался вот здесь, в пакете postcss-minify-font-values. Тут определяется токен, на котором заканчивается описание внешнего вида (текст, размре и т.д.) и начинается описание font-family, и работает это по такой логике: "мамой клянусь - через 2 токена будет описание font-family" 😄
Например, для значения .8em "YS Text",Arial будет вот так:
- { type: 'word', value: '.8em' } <- здесь заканчивается описание внешнего вида и через 2 токена отсюда будет font-family
- { type: "space", value: " " } <- раз
- { type: 'string', quote: '"', value: 'YS Text' } <- два, а вот и font-family
- { type: 'div', value: ',', before: '', after: ' ' }
- { type: 'word', value: 'Arial' }

Но перед cssnano стоит CSSO и превращает эту конструкцию в .8em"YS Text",Arial и это абсолютно валидное значение, но логика той части cssnano, которая отвечает за работу со значениями font-свойств перестает работать:
- { type: 'word', value: '.8em' } <- через 2 токена должен быть font-family
- { type: 'string', quote: '"', value: 'YS Text' } <- раз
- { type: 'div', value: ',', before: '', after: ' ' } <- два... ой 😳
- { type: 'word', value: 'Arial' }

В результате, cssnano просто игнорирует этот токен и на выходе получаем .8em"YS Text"Arial

Недолго думая, занес в cssnano ПР с фиксом.
Там просто учтен кейс, в котором перед font-family можен не быть пробела и конструкция .8em"YS Text",Arial превратится в совершенно валидную .8em YS Text,Arial 🎉

Но проблема, на самом деле, глубже. Заключается она в том, что работа с CSS в cssnano и плагинах postcss (поверх которого работает cssnano) происходит на уровне токенов, а не на уровне детального AST, вот и получается, что разбор значений происходит не по спеке, а на "честном слове". Таким способом проблематично учесть всю сложность синтаксиса CSS (а он действительно сложный!). Вот пример еще одного issue из cssnano на тему странностей парсинга и трансформации.
css-tree (поверх которого работает CSSO) разбирает CSS по спеке и строит детальное AST.
Здесь можно посмотреть внутренности разбора любого значения.

У Романа Дворнова (автора css-tree и мейнтейнера CSSO) есть целый доклад на эту тему.

P.S.: возможно, в экосистеме postcss есть плагин, который генерит детальный AST по спеке, но в данном случае, postcss-minify-font-values , а точнее postcss-value-parser, при помощи которого парсятся значения, этого не делает
GitHub GitHub - css/csso: CSS minifier with structural optimizations CSS minifier with structural optimizations. Contribute to css/csso development by creating an account on GitHub.
  • 🔥 24
  • ❤ 7
  • 👍 4
Post #117 1.74K
🎙️ Поболтали с Сергеем Бережным про разное: про то, как я пришел в профессию, про то, как я программирую. Если понравится формат, то на канале есть и другие видео ;)

https://youtu.be/QHqXE-bo67c
YouTube Как ты кодишь? Сергей Мелюков, Statoscope, техлид фронтенд платформы Яндекс Маркета https://t.me/smelukov_dev https://twitter.com/smelukov ✦ Statoscope: https://statoscope.tech/ ✦ Jora: https://github.com/discoveryjs/jora ✦ DiscoveryJS: https://github.com/discoveryjs/discovery ✦ DiscoveryJS-плагин для chromium-based браузеров: https://…
  • 👍 12
  • 🔥 3
  • ❤ 1
  • 🤡 1
  • 👨‍💻 1
Post #116 1.88K
Теперь о том, как можно использовать revelation у себя, в качестве jest-резолвера:

rev-resolver.js:
const Revelation = require('revelation-resolver').default;
const rev = new Revelation(options);
module.exports = (request, options) => rev.resolve(options.basedir, request);

jest.config.js:
module.exports = {
resolver: './path/to/rev-resolver'
};

И всё.

Поделитесь пожалуйста своими циферками, если вдруг будете пробовать Rev у себя, очень интересно. Возможно, по сравнению с дефолтным jest-резолвером, такой разницы и не будет. Просто у нас нет большого количества чистых jest-тестов, чтобы провести корректный замер. А может, на вашем примере я пойму что еще можно улучшить в резолвере, чтобы он стал лучше и быстрее и так мы нанесем сокрушительную и непоправимую пользу другим 🚀

PS: нет, использовать Rev в качестве резолвера для webpack не получится, т.к. множество плагинов и внутрянка webpack завязаны на родной резолвер 😉
  • 👍 8
Post #115 1.69K
А помните мой доклад про Testament - нашу внутреннюю разработку в Яндекс Маркете для интеграционных тестов на базе jest и IPC?

Так вот, все это время одной из самых бесячих проблем является скорость выполнения пака тестов (всего 7к тестов: 4к - десктоп и 3к - тач/мобильные устройства).
Мы много чего перекопали во внутренностях jest, многое о нем узнали, попробовали разные методики ускорения, от простых, до самых "упоротых" вроде форкнуть трансформер и научить его шарить кеш в CI.
Пока не будут рассказывать обо всех методиках, т.к. мы их сами еще только обкатываем, расскажу всего об одном, но очень эффективном. Сначала немного предыстории.

По результатам наших исследований, самыми медленными частями jest (не берем в расчет код внутри spec-файлов) являются: изоляция, трансформер, резолвер и сборщик кавереджа.
Вы можете возразить, мол, трансформер - это же про babel, какой смысл причислять его к внутренностям jest?
Да, но нет 🙂
У jest есть отдельный трансформер-фасад, а вот бэкендом может быть уже что угодно (babel, ts, swc, etc...). И вот этот фасад рулит кешем и всякими разными другими штуками и делает это не очень оптимально и это особенно заметно на больших паках тестов.

Резолвер - отдельная история. Подробнее смотрите в вышеупомянутом докладе. А если коротко, то jest-резолвер нам не подходит, т.к. мы собираем статику вебпаком и используем различные настройки webapack-резолвера. Поведение некоторых из них просто невозможно воспроизвести в jest-резолвере. Поэтому мы начали использовать родной резолвер вебпака.
Это замедлило нам тесты, потому что у webpack очень медленный резолвер и он активно работает с ФС, несмотря на встроенный кеш, а еще, он генерит очень большой callstack, но об этом позже. Мы решили выйти из положения при помощи memfs сэмулировав ФС в памяти и scanfs для сканирования реальной ФС и помещения структуры ФС в memfs. Таким образом, мы сделали нечто похожее на haste-map, который в jest-резолвере делает +- то же самое, и отыграли скорость, потерянную от переезда на enhanced-resolve.
Время шло, кодовая база росла, количество тестов росло, время прогона пака увеличивалось
Отдельно отмечу, что профайлинг такой конфигурации - занятие специфическое, потому что enhanced-resolve медленный и генерит оооооооочень глубокий стектрейс. Дамп прогона одного spec-файла часто достигал более 500мб и v8 просто отказывался мне его отдавать, потому что максимальная длина строки в v8 - 512мб. Да и дамп таких размеров - то еще удовольствие. А добавьте туда еще глубокую рутину внутри memfs, который тоже так себе написан и получите чудовищных размером cpu-профиль.

В какой-то момент я подумал, что "хватит это терпеть" и решил набросать собственный резолвер, который умел бы больше, чем jest-резолвер, но не был бы таким перегруженным и медленным, как enhanced-resolve. Так появился Revelation - быстрый резолвер модулей для node.js с поддержкой свойства mainFiles и возможностью модифицировать package.json налету. Резолвер обмазан всевозможными кешами, в том числе кешированием ближайшей node_modules (или другой директории с модулями, в зависимостями от опции). А со всеми этими приседаниями с кешем, memfs оказался уже не нужен, потому что revalation < enhanced-resolve + memfs > enhanced-resolve.

В итоге, мы получили следующее ускорение прогона тестов (основано на графиках нашего CI с 4к тестов в десктопе и 3к тестов в таче):
90 перцентиль:
- время уменьшилось на 55% в десктопе
- время уменьшилось на 40% в таче

95 перцентиль:
- время уменьшилось на 60% в десктопе
- время уменьшилось на 55% в таче

В качестве бонуса, получили возможность снимать cpu-профайлы и локально отлаживать большие тесты, потому что профиль похудел с более чем 500мб до 52мб (в 10 раз!).
Очень хороший профит как по скорости, так и по DX.
Из того, что еще хотелось бы реализовать - это поддержка свойств exports и imports в package.json.
Telegram Сергей Мелюков Привет! А вот и видео доклада. С тех пор мы придумали как ускорить jest, написав для нашей инсталляции кастомный рантайм, который делает меньше проверок на попытке зарекваирить файл. Казалось бы, проверки простейшие, но на тысячах рекваеров это сильно заметно.…
  • 🔥 21
  • ❤ 2
  • 👍 1
Post #114 1.8K
Идем дальше. На скриншоте изображен итоговый пайплайн работы со статами.
Чтобы получить HTML-отчет, статы проходят несколько стадий:
- нормализация (Да, статоскоп умеет нормализовывать статы. До нормализации, статы весят 1.5гб, а после - 250мб. Подробнее описывал здесь)
- сжатие с binary JSON
- сжатие с gzip
- пилим gzip-стрим на чанки (зачем это делать подбробно описывал тут)
- энкодинг чанков в base64 (чтобы можно было хранить чанки с бинарными данными в HTML)
- запись в файл

Всё, отчет готов.
Таков путь сырых статов в 1.5гб до HTML-отчета в 5мб (и это несмотря на то, что base64 дает оверхед в ~33% от размера декодируемых данных).

Когда мы открываем такой отчет в браузере, все происходит в отбратном порядке:

- декодируем чанки из base64
- разжимаем из gzip
- разжимаем binary JSON
- денормализуем статы
- отправляем статы в UI

Итого, самая медленная часть статоскопа - это денормализация статов и их подготовка к использованию в UI. А все потому, что webpack генерит статы, не особо заворачиваясь с форматом.

И тут мы плавно переходим к Statoscope 6, над которым я работаю уже довольно давно, с переменной активностью. Вот его основные фичи:

- собственный формат статов
- расширяемость как у vscode 😇

Зачем: чтобы любой человек мог написать плагин (если его еще нет), в котором реализован UI для его любимого сборщика. Сейчас же статоскоп сильно завязан на webpack и мне это не нравится.

Будет классно, если напишете какая у вас разница в размерах статов получилась, очень интересно.

PS: Я тут заглянул в OpenCollective статоскопа и выяснилось, что там уже 413$ накапало. Это конечно не webpack с миллионным бюджетом, но все равно приятно ☺️
  • 🔥 39
  • ❤ 1
Post #113 5.15K
Я, наконец, опубликовал statoscope 5.25 🎉
И знаете, что? Я очень рад. Хотя бы потому, что теперь statoscope-отчет со сравнением двух (master vs PR) клиентских сборок Яндекс Маркета весит не 494мб, а 11мб.
Нет, вам не кажется - статы пожались почти в 45 раз эффективнее 😇
Получаем экономию квоты на железо и времени пользователя, который ждет пока отчет загрузится.
Это стало возможным благодаря сжатию статов в Binary JSON.

Если очень коротко, то значения из объекта компактно записываются в виде байтов, поверх применяются всякие оптимизации типа дедупликации значений и компактного представления строк и массивов. В результате получается сплошной поток типов и данных. Никаких скобочек, кавычек и форматирования, а данные записаны компактно. Это все будет иметь заметный эффект на большом объеме данных (например, от 10мб).

Возможно, вы задаетесь вопросом: "А почему нельзя обойтись просто gzip'ом?". Binary JSON + gzip жмет в 2 раза лучше, чем просто gzip. А все потому, что сжимая JSON gzip'ом - мы сжимаем "что-то там чем-то там", а сжимая JSON инструментом, который заточен под JSON (знаем формат и специфику данных) - мы сжимаем именно JSON и делаем это эффективно. Да, многое зависит от структуры исходных данных, но я смотрю на реальные данные в виде статов Яндекс Маркета и вижу огромный профит.

На скриншоте можно посмотреть сравнение json-ext с другими решениями, которые умеют в binary JSON. Сравнение проводится на примере нормализованных статов клиентской сборки Яндекс Маркета. Как видите, идея binary JSON не нова и эксперименты в этой области продолжаются, но судя по всему, остальные остановились на достигнутом и json-ext пока впереди 🤓
Думаю @gorshochekvarit еще сам расскажет подробнее про это. Хотя я и сам чуть-чуть приложил руку к энкодеру/декодеру, но после этого Роман накоммитил туда много всяких интересных оптимизаций (например, хранить массив объектов колонками, как в колончатых БД, что делает их более компактными).
  • 🔥 33
  • 👍 5
  • ❤ 1
Post #112 1.7K
А еще, я завершаю в Statoscope работу над фичей, которая еще больше сжимает html-отчет.
Сейчас отчет о сборке Я.Маркета весит ~250mb. А вчера я радостно вкрячил фичу, которая уменьшает размер отчета до 8mb!
А вообще, сырые статы Маркета занимают ~1.3gb
Только вдумайтесь, с 1.3gb до 8mb. Компрессия Statoscope = х162 (конечно, зависит от данных, но все же)
Конечно, тут не обошлось без решений безмерно уважаемого мной Романа Дворнова.
Как только всё оттестирую и допилю кое-что, обязательно расскажу. Будет какое-то количество computer science 🧪
Telegram Горшочек варит Про фронтенд и около, над чем работаю, разборы, мысли разные Пишу для истории и тех, кому интересно как получается то, что у меня получается // Рома Дворнов (@rdvornov)
  • 👍 23
  • 🔥 8
Post #111 1.65K
Сергей Мелюков HolyJS 2022 - Jest.pdf
Привет! А вот и видео доклада.
С тех пор мы придумали как ускорить jest, написав для нашей инсталляции кастомный рантайм, который делает меньше проверок на попытке зарекваирить файл.
Казалось бы, проверки простейшие, но на тысячах рекваеров это сильно заметно.
Еще мы прикрутили inline-require plugin для бабеля и научились собирать кавередж.
Казалось бы, а какие проблемы с кавереджем? Включил опцию, запустил тесты - получил html отчёт, профит.
Да, но нет :)
Testament ведь создаёт еще один jest-рантайм в дочернем процессе, а значит надо как-то собирать и мержить кавередж с обоих рантаймов в единый отчёт (никому ведь не хочется смотреть 2 разных отчета: по клиентскому и серверному кавереджу, правда?)
Пишите что из этого больше всего интересно, расскажу. Либо расскажу обо всем постепенно ;)
YouTube Сергей Мелюков — Жесть для Jest: Round 2. Fight! Подробнее о конференции HolyJS: https://jrg.su/EM4wwV — — Несколько лет назад Сергей выступал с докладом про Jest речь шла о том, как он устроен и о том, как спикер препарировал его для построения платформы для компонентного тестирования. Сейчас Сергей…
  • 👍 13
  • 🔥 8
Older posts →

About this channel

How can I read @smelukov_dev without a Telegram account?
TGViewer shows the public web preview Telegram publishes for Сергей Мелюков: recent posts, photos, videos and the subscriber count, with no app, login or account.
How many subscribers does Сергей Мелюков have?
Сергей Мелюков (@smelukov_dev) has 646 subscribers on Telegram, refreshed roughly every 30 minutes.
Does Сергей Мелюков know I viewed it here?
No. Public channel previews carry no viewer identity, and TGViewer has no accounts or tracking of what you look up.
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →