Ого, если спотифай играет на ПК, то переключать трэки и ставить их на паузу можно с мака
Только что починил очень интересный баг.
Для начала, расскажу, что же это за часть кода такая. Вкратце - если пользователь не указал определенные заголовки, они автоматически сами подставятся в ответ. Это одна из задач движка рендеринга, и это основной пункт расходов производительности.
Как это работало раньше: у нас была хэшмапа со значениями типа
По части производительности - это в целом было терпимо. Но что не было терпимо, так это то, что с каждым новым заголовком она деградировала. Потому я пришёл к новому решению.
Заголовки по-умолчанию отныне так же, как и обычные заголовки ответа, представляют из себя слайс строк. Проходя по обычным заголовкам, проверяем на вхождение в дефолтные, если есть - затираем ключ (устанавливаем значение в
А теперь - к сути проблемы. Тесты у меня начали быть нестабильными - их результат являлся недетерминированным. Проще говоря, рандомным - проходило 70/30.
Как я уже сказал, заголовки представляют из себя слайс строк в формате
Через примерно час я смотрю на функцию, которая переводит передаваемую извне хэшмапу с заголовками по-умолчанию в формат слайса строк. И тут меня осеняет. Смотрю в соседний файлик, где лежала функция, проверяющая ключ на вхождение в слайс заголовков - тот же баг с
А результаты, кстати, нестабильные были потому, что при итерации по хэшмапе, значения подаются рандомно. А почему не 50/50 - потому что в тесте только часть заголовков по-умолчанию перезаписывались пользовательскими. Соответственно, порядок, в котором хэшмапа выдаст значения при итерации, влияет и на успешное прохождение теста (попадёт ли дефолтный заголовок в первую, или вторую половину слайса)
Для начала, расскажу, что же это за часть кода такая. Вкратце - если пользователь не указал определенные заголовки, они автоматически сами подставятся в ответ. Это одна из задач движка рендеринга, и это основной пункт расходов производительности.
Как это работало раньше: у нас была хэшмапа со значениями типа
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 - потому что в тесте только часть заголовков по-умолчанию перезаписывались пользовательскими. Соответственно, порядок, в котором хэшмапа выдаст значения при итерации, влияет и на успешное прохождение теста (попадёт ли дефолтный заголовок в первую, или вторую половину слайса)
Если я добавил новую настройку с ограничением, по семверу это бампит минорную версию, или патч?
Пришлось переименовать имя модуля (как же в го всё с этим сложно). Аж офигел, насколько долго тестики проходят - всё же хорошо, что
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 нацепить.
Всем чаю!
- Я научился в оптимизации, даже вынес для себя некоторые основные приёмы. Кратко - сначала упрощай алгоритм до невозможности, потом приступай к низкоуровенщине
- Решил достаточно много дизайнерских проблем. В данном случае - "а как удобнее сделать ту или иную фичу". Не то, чтобы я действительно понимал, почему то или иное решение удобнее в проде, но внешне выглядит в принципе симпатично
- Подтвердил для себя теорию, что любая задача кажется тривиальной, если известно решение. Довольно забавно с высока вспоминать времена, когда я ещё не мог на питоне асинхронный фреймворк написать :)
- Научился чуть лучше работать с крупными проектами. На данный момент, фреймворк насчитывает почти 100 файлов и 8.2к строк кода (примерно треть - тесты), что лично для меня - много, поскольку раньше обычно дальше 2-2.5к строк кода проекты не заходили. Фан факт: последний раз, когда мне приходилось рефакторить проект практически с нуля, был на отметке в 3.5к строк кода. Тогда архитектура и сам код обвалились под собственным весом, преодолев критическую массу, когда абсолютно вся работа сводилась к починке багов. Когда починил - образовались новые. Заштопывать заплатки, рвущиеся сразу же после наложения, в общем
- Научился в github actions. Оказалось, что это даже не такая уж и сложная штука - хоть и было сложновато, пока я не додумался почитать документацию :)
- Стал больше уделять внимания описанию коммитов. Около двух лет метался из крайностей в крайности, пока наконец не пришёл к некоторому универсальному стилю. TL;DR - прислушался к названиям полей для ввода в GUI-клиентах гита
- Даже логотип сверстал! Правда, документации как не было, так и нет :) Однако я уже приступил к гайду (который я ни в коем случае не копирую из фибера). Надеюсь, скоро наконец сбудется, и я смогу полноценно гитбук на кастомный домен github pages нацепить.
Всем чаю!
❤2
А, ну и самым знаковым моментом за всё существование проекта - я по праву считаю переход с модели разделения пользовательского и серверного пространства, на одно общее пространство. Я, кажется, об этом ещё не писал, но вот краткое описание оного:
Разделение пользовательского и серверного пространств подразумевает две горутины. В первой происходит вся внутренняя кухня, начиная с чтения из сокета, заканчивая http-специфичными штуками. Во второй - ждём сигнала из оповестительного канала. При получении, вызываем обработчик, при завершении - извещаем об этом ядро. И на этом строится вся синхронизация, от которой зависит половина кодовой базы. Это, помимо постоянных дедлоков при изменении логики работы данного механизма (причина нескольких переписываний чуть ли не с нуля), было очень удобным для имплементации потоковой обработки тела. Но была одна проблема...
...и заключалась эта проблема в чрезмерном количестве синхронизаций, основанных на межканальном взаимодействии. А происходили они действительно часто, особенно при работе с телом, и избежать их нельзя было никак (даже если с телом никто не взаимодействует). Это было медленно. Это создавало слишком частые переключения. Это приводило к повышению задержек. Это, в конце концов, приводило к нестабильным результатам бенчмарков - погрешность была больно уж велика.
Новая же модель подразумевает, что http-парсер будет парсить до тела. Само же тело он не трогает. Как только спарсились заголовки - вызывается обработчик, и уже внутри обработчика при потребности происходит чтение из сокета, и собственно обработка тела. Сам сокет был обёрнут в некий объект, который умел хранить "ненужные" данные, которые будут возвращены при следующем чтении - чтобы вычитав заголовки с кусочком тела, всё не сломалось к чертям. Это и позволило безболезненно разделить оба процесса парсинга друг от друга.
В результате, бенчмаркать стало немного труднее, однако это не есть проблема. Сами же бенчмарки стали более обширными, покрывая также tcp-сервер (однако не совсем точным образом; палки в колёса вставляют таймауты на чтение). Не смотря на это, прирост составил около трёхкратную разницу в случае с GET-запросом на 10 заголовков (3000ns -> 860ns).
Собственно, суть данного поста в донесении следующих мыслей:
- горутины сами по себе не дорогие; дорого их синхронизировать
- здесь сработал принцип упрощения алгоритма: несмотря на когнитивное усложнение архитектуры, отныне из алгоритма исключена целая куча синхронизаций. Это привело не только к ускорению кода, но и к тому, что работать с ним стало проще - меньше потенциальных мест, изменения в которых могут привести к отстреленной ноге
Разделение пользовательского и серверного пространств подразумевает две горутины. В первой происходит вся внутренняя кухня, начиная с чтения из сокета, заканчивая http-специфичными штуками. Во второй - ждём сигнала из оповестительного канала. При получении, вызываем обработчик, при завершении - извещаем об этом ядро. И на этом строится вся синхронизация, от которой зависит половина кодовой базы. Это, помимо постоянных дедлоков при изменении логики работы данного механизма (причина нескольких переписываний чуть ли не с нуля), было очень удобным для имплементации потоковой обработки тела. Но была одна проблема...
...и заключалась эта проблема в чрезмерном количестве синхронизаций, основанных на межканальном взаимодействии. А происходили они действительно часто, особенно при работе с телом, и избежать их нельзя было никак (даже если с телом никто не взаимодействует). Это было медленно. Это создавало слишком частые переключения. Это приводило к повышению задержек. Это, в конце концов, приводило к нестабильным результатам бенчмарков - погрешность была больно уж велика.
Новая же модель подразумевает, что http-парсер будет парсить до тела. Само же тело он не трогает. Как только спарсились заголовки - вызывается обработчик, и уже внутри обработчика при потребности происходит чтение из сокета, и собственно обработка тела. Сам сокет был обёрнут в некий объект, который умел хранить "ненужные" данные, которые будут возвращены при следующем чтении - чтобы вычитав заголовки с кусочком тела, всё не сломалось к чертям. Это и позволило безболезненно разделить оба процесса парсинга друг от друга.
В результате, бенчмаркать стало немного труднее, однако это не есть проблема. Сами же бенчмарки стали более обширными, покрывая также tcp-сервер (однако не совсем точным образом; палки в колёса вставляют таймауты на чтение). Не смотря на это, прирост составил около трёхкратную разницу в случае с GET-запросом на 10 заголовков (3000ns -> 860ns).
Собственно, суть данного поста в донесении следующих мыслей:
- горутины сами по себе не дорогие; дорого их синхронизировать
- здесь сработал принцип упрощения алгоритма: несмотря на когнитивное усложнение архитектуры, отныне из алгоритма исключена целая куча синхронизаций. Это привело не только к ускорению кода, но и к тому, что работать с ним стало проще - меньше потенциальных мест, изменения в которых могут привести к отстреленной ноге
Что делать
Загадка от Жака Фреско: угадайте, где здесь происходит аллокация на 24 байта. На ответ даётся 30 секунд
Ответ на загадку: аллокацию вызывает передача метода
Сейчас буду пытаться разузнать причину этого. Даже интересно стало, что же оно там такого аллоцирует на 24 байта🤨
client.WriteСейчас буду пытаться разузнать причину этого. Даже интересно стало, что же оно там такого аллоцирует на 24 байта🤨
В общем, я так и не понял, почему так, но штуку с горем пополам пофиксил. Главное - не забыл оставить гневное послание core-мейнтейнерам в комментарии