Что делать
120 subscribers
209 photos
3 videos
4 files
133 links
Не смешно
Download Telegram
Продолжая тему предыдущего поста, я хочу рассказать, как я пришёл к тому, чтобы переписать парсер под безусловные переходы и получить до 2х буста производительности

Для начала, стоит рассказать, как у меня этот самый парсер вообще устроен. Кратко - это обычный for-range по слайсу байт, и вложенный большой свитч по состояниям парсера.

Занимаясь предельной оптимизацией парсера (в который у меня всё по итогу и упирается), я додумался, что вместо того, чтобы при каждой итерации делать свитч по состоянию, я могу воспользоваться тем, что состояние обычно редко меняется (проход по пути запроса, ключу/значению заголовка, etc.), а значит, свитч в теле цикла у меня лишний. А свитч-то не маленький, на 32 состояния.

Подумал, и решил вынести независимые циклы по слайсу байт в кейсы свитча по состояниям. И тут я столкнулся с проблемой: я в парсере сохраняю негласную конвенцию о написании кода - практически везде, где нужно сравнение байта с 1 и более вариантов, я пишу свитч. А как известно, break в свитче прерывает этот самый свитч. А мне надо прерывать также цикл. Идея использовать if-else цепочку меня категорически отталкивает.

И тут я вспомнил о безусловных переходах! Панацея, подумал Штирлиц. И начал вставлять лейблы в парсер. Как оказалось позже, это вам не шахматы, тут думать надо. Решил проблему кардинально: убрал полностью главный цикл, свитч по состояниям вынес в начало функции для того, чтобы исходя из последнего состояния, сделать переход в нужную часть кода. Для "одноразовых" участков (участков, которые сразу же меняют состояние дальше), правда, пришлось немного покопипастить, но в целом терпимо. Запустил бенчмарки - парсер стал от 14% до 300% быстрее. Великолепно, подумал Миллер
Встретился с выбором: а реализовывать ли https и ограничение на максимальное количество подключений с одного ip-адреса. С одной стороны, веб-фреймворк этим заниматься не должен, а должен реверс-прокси. С другой - такой подход теряет в самостоятельности. То есть, если условно какому-нибудь проекту понадобится сервер для вебхуков (привет фреймворкам для телеграм-ботов), никто тащить в поставке нжинкс не будет. Заставлять пользователя его ручками настраивать - и подавно.

А что об этом думаете вы?
У меня во фреймворке постепенно начал вырисовываться пакет functools. Да, именно с функциональными штучками - с дженериками, такая штука очень даже может существовать, как оказалось, даже в го. И этим очень удобно пользоваться! Во всяких мелких деталях, например, "применить на каждый элемент слайса функцию". Или, например, скопировать слайс, как на пикриле🤭
Ого, к полю с типом types.DefaultHeaders (newtype от []string) можно спокойно присваивать результат функции с типом []string
Ну и чего оно мне вот этот вот текст некрасивый рисует. Причём исключительно под виндой
Ого, если спотифай играет на ПК, то переключать трэки и ставить их на паузу можно с мака
Только что починил очень интересный баг.

Для начала, расскажу, что же это за часть кода такая. Вкратце - если пользователь не указал определенные заголовки, они автоматически сами подставятся в ответ. Это одна из задач движка рендеринга, и это основной пункт расходов производительности.

Как это работало раньше: у нас была хэшмапа со значениями типа struct { Seen bool, Value string }. Когда мы проходились по заголовкам ответа, мы проверяли на вхождение в эту мапу, и если есть совпадение, ставили флаг Seen в true. Дальше проходились по всем значениям мапы с Seen = false и рендерили.
По части производительности - это в целом было терпимо. Но что не было терпимо, так это то, что с каждым новым заголовком она деградировала. Потому я пришёл к новому решению.

Заголовки по-умолчанию отныне так же, как и обычные заголовки ответа, представляют из себя слайс строк. Проходя по обычным заголовкам, проверяем на вхождение в дефолтные, если есть - затираем ключ (устанавливаем значение в ""). Это решение показало себя очень хорошо - дало очень неплохой буст в производительности. Да, теперь и проверка на вхождение линейная, однако обычно этот слайс настолько маленький, что найти значение в нём быстрее, чем с хэшмапой.

А теперь - к сути проблемы. Тесты у меня начали быть нестабильными - их результат являлся недетерминированным. Проще говоря, рандомным - проходило 70/30.
Как я уже сказал, заголовки представляют из себя слайс строк в формате [Key value Key value]. Соответственно, итерирование по заголовкам должно быть представлено в виде:

for i := 0; i < len(headers); i += 2 {
...
}


у меня же, в свою очередь, условие было записано, как len(headers)/2. Этот баг затрагивал также обычные заголовки (вторая половина не рендерилась). Однако не рендерилась константно - тут же результаты нестабильные, хотя хэшмап никаких не использую, рандомизации поведения никакого быть не должно.

Через примерно час я смотрю на функцию, которая переводит передаваемую извне хэшмапу с заголовками по-умолчанию в формат слайса строк. И тут меня осеняет. Смотрю в соседний файлик, где лежала функция, проверяющая ключ на вхождение в слайс заголовков - тот же баг с len(headers)/2. Занавес.

А результаты, кстати, нестабильные были потому, что при итерации по хэшмапе, значения подаются рандомно. А почему не 50/50 - потому что в тесте только часть заголовков по-умолчанию перезаписывались пользовательскими. Соответственно, порядок, в котором хэшмапа выдаст значения при итерации, влияет и на успешное прохождение теста (попадёт ли дефолтный заголовок в первую, или вторую половину слайса)
Если я добавил новую настройку с ограничением, по семверу это бампит минорную версию, или патч?
Вот блин
😁1
Вот блин
😁1
Что делать
Вот блин
ААААА, оно импортировало net/http.Request вместо нужного. Чйорт
👍1
Пришлось переименовать имя модуля (как же в го всё с этим сложно). Аж офигел, насколько долго тестики проходят - всё же хорошо, что go test кэширует результаты
👍1
Больше, чем отсутствие актуальных версий в репозиториях, я ненавижу версионирование и работу с ним тулчейна го.
Что делать
Больше, чем отсутствие актуальных версий в репозиториях, я ненавижу версионирование и работу с ним тулчейна го.
Это просто ужас. Почему-то с v2.3.1-alpha оно ну никак, а с v 2.3.1-beta зафурыкало. Надеюсь, больше никогда не придется работать с этой фигнёй
Я заебался. Когда в Германии уже сделают премиум?
Думаю, все знают, что можно после запуска тестов получить профайл покрытия кода. Но я лично только сейчас узнал, что из коробки есть go tool cover. Он в том числе может открывать страничку, где подсвечен код, который покрыт тестами, а который - не покрыт

Если что, go test -coverprofile=c.out ./... && go tool cover -html=c.out
👍2
Кстати, сегодня индиге исполнился ровно год - первый коммит в репозиторий был совершён 3 марта 2022 года
Итак, а что же поменялось?

- Я научился в оптимизации, даже вынес для себя некоторые основные приёмы. Кратко - сначала упрощай алгоритм до невозможности, потом приступай к низкоуровенщине

- Решил достаточно много дизайнерских проблем. В данном случае - "а как удобнее сделать ту или иную фичу". Не то, чтобы я действительно понимал, почему то или иное решение удобнее в проде, но внешне выглядит в принципе симпатично

- Подтвердил для себя теорию, что любая задача кажется тривиальной, если известно решение. Довольно забавно с высока вспоминать времена, когда я ещё не мог на питоне асинхронный фреймворк написать :)

- Научился чуть лучше работать с крупными проектами. На данный момент, фреймворк насчитывает почти 100 файлов и 8.2к строк кода (примерно треть - тесты), что лично для меня - много, поскольку раньше обычно дальше 2-2.5к строк кода проекты не заходили. Фан факт: последний раз, когда мне приходилось рефакторить проект практически с нуля, был на отметке в 3.5к строк кода. Тогда архитектура и сам код обвалились под собственным весом, преодолев критическую массу, когда абсолютно вся работа сводилась к починке багов. Когда починил - образовались новые. Заштопывать заплатки, рвущиеся сразу же после наложения, в общем

- Научился в github actions. Оказалось, что это даже не такая уж и сложная штука - хоть и было сложновато, пока я не додумался почитать документацию :)

- Стал больше уделять внимания описанию коммитов. Около двух лет метался из крайностей в крайности, пока наконец не пришёл к некоторому универсальному стилю. TL;DR - прислушался к названиям полей для ввода в GUI-клиентах гита

- Даже логотип сверстал! Правда, документации как не было, так и нет :) Однако я уже приступил к гайду (который я ни в коем случае не копирую из фибера). Надеюсь, скоро наконец сбудется, и я смогу полноценно гитбук на кастомный домен github pages нацепить.

Всем чаю!
2
А, ну и самым знаковым моментом за всё существование проекта - я по праву считаю переход с модели разделения пользовательского и серверного пространства, на одно общее пространство. Я, кажется, об этом ещё не писал, но вот краткое описание оного:

Разделение пользовательского и серверного пространств подразумевает две горутины. В первой происходит вся внутренняя кухня, начиная с чтения из сокета, заканчивая http-специфичными штуками. Во второй - ждём сигнала из оповестительного канала. При получении, вызываем обработчик, при завершении - извещаем об этом ядро. И на этом строится вся синхронизация, от которой зависит половина кодовой базы. Это, помимо постоянных дедлоков при изменении логики работы данного механизма (причина нескольких переписываний чуть ли не с нуля), было очень удобным для имплементации потоковой обработки тела. Но была одна проблема...

...и заключалась эта проблема в чрезмерном количестве синхронизаций, основанных на межканальном взаимодействии. А происходили они действительно часто, особенно при работе с телом, и избежать их нельзя было никак (даже если с телом никто не взаимодействует). Это было медленно. Это создавало слишком частые переключения. Это приводило к повышению задержек. Это, в конце концов, приводило к нестабильным результатам бенчмарков - погрешность была больно уж велика.

Новая же модель подразумевает, что http-парсер будет парсить до тела. Само же тело он не трогает. Как только спарсились заголовки - вызывается обработчик, и уже внутри обработчика при потребности происходит чтение из сокета, и собственно обработка тела. Сам сокет был обёрнут в некий объект, который умел хранить "ненужные" данные, которые будут возвращены при следующем чтении - чтобы вычитав заголовки с кусочком тела, всё не сломалось к чертям. Это и позволило безболезненно разделить оба процесса парсинга друг от друга.

В результате, бенчмаркать стало немного труднее, однако это не есть проблема. Сами же бенчмарки стали более обширными, покрывая также tcp-сервер (однако не совсем точным образом; палки в колёса вставляют таймауты на чтение). Не смотря на это, прирост составил около трёхкратную разницу в случае с GET-запросом на 10 заголовков (3000ns -> 860ns).

Собственно, суть данного поста в донесении следующих мыслей:
- горутины сами по себе не дорогие; дорого их синхронизировать
- здесь сработал принцип упрощения алгоритма: несмотря на когнитивное усложнение архитектуры, отныне из алгоритма исключена целая куча синхронизаций. Это привело не только к ускорению кода, но и к тому, что работать с ним стало проще - меньше потенциальных мест, изменения в которых могут привести к отстреленной ноге
Finally. Рантайм-директива, чтобы в некоторых случаях можно было избежать runtime.KeepAlive()
Ух ты. Красиво получилось