Что делать – Telegram
Что делать
94 subscribers
207 photos
3 videos
4 files
126 links
Не смешно
Download Telegram
2 месяца спустя, я наконец выкатил новый релиз. Пощипал баги, заделал кодеки (теперь и сжимать, и разжимать!), кучу всяких внутренних штук переделал, чтобы было красиво. Ну и тестами немного докрыл. Работы осталась куча, 0.17.3 ещё месяц-второй ждать, но пока выглядит работоспособно и вполне себе ебабельно.

Буду рад какому-либо фидбеку. Если по внутрянке - то ещё лучше.

А вот по доке не надо. Я и сам вижу, какая она ущербная:( Лучше правда не получается. Простите.

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

https://github.com/indigo-web/indigo
🎉6🔥1
Забавная ситуация произошла как-то, кстати. Когда я релизнул 0.17.0, решил сразу обновить pavlo.ooo, сократить параметр name до просто n (с телефона чтобы удобнее набирать было; добавить скрытый текстовый бокс мне религия не позволяет), и добавить вариант вообще без параметра это сделать - через динамический роутинг. Это оказалась проблема - внезапно я начал ловить ошибку синтаксиса темплейта. Ошибку, которой быть не должно - я даже в тестах так и не смог репродуцировать (правда, я и не сильно-то старался, в общем).

Вернулся в окно с индигой. Посмотрел я на тот парсер темплейтов. Если не ошибаюсь, это говно было написано году в 22-23, и парсило примерно следующее: /user/{id}/post/{postId}. На тот момент я почему-то решил сделать для этого парсер на машине состояний (через годика два я начну лениться и хуярить на strings.IndexByte), чтобы лучше валидировать, ошибочки покрасивше показывать, по красоте, в общем. Тестами вроде как даже покрыл, работать должно было норм.

Кстати, по поводу работать. С деревьями мне всегда было тяжко, и в первые разы моего освоения подвида префиксных (о них я, кстати, писал здесь) я не придумал ничего лучшего, чем разбить путь на сегменты - сплит по слэшам - и их как атом использовать. То есть, там каждый узел имел массив из наследников - у каждого наследника свой value, который показывал его сегмент. И вот так вот проходись по слайсу и сравнивай каждую строку. Херово, но худо-бедно в концепцию ещё вписывается, ок. Кстати, не мапа потому, что... А я не знаю, почему. С мапой было бы проще. И, в отличии от заголовков (которые я тоже в слайсе храню!), там те мапы не нужно было бы очищать, что и представляет из себя главную статью расходов при их реюзе. В общем, неопытность.

Сел и переписал всё с нуля. Теперь вайлдкарды, которые раньше в {...} заключены были, поменял, и сделал /user/:id/post/:postId. Это очень правильное решение, на самом деле, потому что теперь, вместо отдельного выделения вайлдкарда собственной парой открывающего и закрывающего символов, у нас остаётся только один - это двоеточие. Неявной закрывающей "кавычкой" представляется слэш, отделяющий текущий сегмент от следующего - а это важно, потому что он служит опорной точкой при матчинге. Иначе приходилось бы изъёбываться с поиском подстроки какими-то более умными алгоритмами, чем мой излюбленный strings.IndexByte. Ну, а ещё, мне теперь нужно меньше валидировать - потому что между окончанием вайлдкарда и закрывающим слэшем больше не может ничего стоять:)

Я, конечно, не помню, поддерживалось ли раньше что-то вроде /user{id}, или вайлдкард был обязан растягиваться на целый сегмент (скорее всего), но в новом дереве я и это добавил. Стало красиво и круто.

Но, собственно, а что же в новом дереве поменялось-то?
Я выкинул эту хуйню с сегментами. Теперь, когда я вставляю строку - прохожусь по всем наследникам ноды (корневая нода и кличется деревом, представляя из себя входной узел - но фактически является совершенно обыкновенной нодой; можно взять любую ноду из любой части дерева, и она будет точно так же функционировать, как полноценное дерево - пускай и подрезанное). Если вижу, что ключ имеет общий префикс со значением наследника - нахожу как раз этот самый общий префикс, и если его длина равна длине значения наследника - просто обрезаю ключ и сую его дальше. В противном случае, конечно, приходится делать немного противные вещи с тем, чтобы обрезать ещё и значение наследника, и заменить его новым узлом со значением, равным как раз общему префиксу.

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

Таким лёгким манёвром мы сделали так, чтобы можно было определять неконфликтующие роуты /user/:id и /user/me. Правда, не совсем понимаю, почему кто-то умудряется это в фичи приписывать - штука так-то дефолтная.

Ещё я потом между делом добавил greedy wildcards, в стандартном mux подглядел, и заменил ими костыльные path catcher'ы - аж от души отлегло.

Но вот незадача: тут-то уж точно хэшмапы не вкорячишь. Ну, то есть, можно было бы, но это было бы крайне неудобно из-за того, что при инсерте я довольно часто "ломаю" строки напополам и подменяю ноды. Приходится держать наследников всё так же в массиве. А это проблема - вполне себе может быть кейс, когда сервис насчитывает в себе 500-700 роутов. А с деревом штука такая, что чем меньше ключи похожи, тем ширше его пидорасит. Очень неприятно линейно перебирать пару сотен строк, знаете ли. Даже учитывая, что строки сами по себе не сильно-то на сравнение приятные:)

Поленившись ещё месяца 2, наконец допёр до нормальных бенчмарков (не в слепую же проблему решать?). Ситуацию спасает всеми излюбленный бинарный поиск. Удивительно просто оказалось сделать вставку упорядоченной - один цикл while key < predecessor.value и slices.Insert (кстати, невероятно полезный пакет). Немного сильнее попотеть пришлось над поиском - slices.BinarySearchFunc сгладил worst-case, но на более лайтовых кейсах всё-таки оверхеда прибавил. Нагло спиздил Вырезал с мясом Заинлайнил себе slices.BinarySearch, и получил очень приятные числа практически по всем бенчам. В сумме, где-то в 2-4 раза шустрее стало - особенно прирост был, очевидно, с "широкими" деревьями - где у одного узла очень много наследников (и, соответственно, каждого перебрать надо). Со 128 штуками, вместо 128 линейных сравнений, мы теперь делаем только 7 - чистая вин-вин ситуация. На удивление, "глубокие" деревья со 128 узлами вглубь тоже прирост показали.

Кстати, в slices.BinarySearch переполнения избежали более прикольным способом, чем в джабе - int(uint(a+b) >> 1). Что значит, если переполнение и случится - старший бит один хуй падёт пред побитовым сдвигом вправо:)
Ещё оказалось, что OPTIONS * (он же server-wide OPTIONS), который должен возвращать список всех поддерживаемых сервером методов, имеет свою причуду. Ему нужно вернуть не просто список всех поддерживаемых методов со всех эндпоинтов, а только их юнион! Только те методы, которые унивеврсально поддерживаются каждым эндпоинтом. При том OPTIONS и TRACE идут безусловно (ты так-то обязан поддерживать TRACE, есличо).

От себя ещё докинул безусловно добавлять HEAD, если в юнион входит GET. У меня такая штука, что если прилетел HEAD запрос, но эндпоинт его не поддерживает - запрос автоматически сунется в хендлер GET. Сериализатор потом один хуй сам тело ответа отсечёт.
Что делать
(ты так-то обязан поддерживать TRACE, есличо).
Ладно, чаще лучше не поддерживать. А то косвенно виноватым в проёбе кукисов окажешься.

TL;DR суть TRACE в том, чтобы вернуть в теле ответа запрос таким, каким сервер его получил (вот прям совсем-совсем таким, без каких-либо преобразований, вот практически эхо). И если злоумышленник каким-то образом заставит браузер отправить TRACE запрос, там, внезапно, окажутся наши кукисы, которые от джаваскрипта так-то охранять надо
...сим простым мановением руки, мы сокращаем объём строк втрое

Ифэррнилы здесь были бы просто охуенны, согласитесь
А, ну теперь понятно, почему между 0.16.3 и 0.17.3 (ещё в разработке) полтора года прошло. Я фактически с нуля всё переписал. Учитывая, что суммарно там всего 10к строк
🤯3🔥1😁1
У меня есть дефолтные заголовки, которые добавляются в ответ, если ты их не перегрузил (добавил в ответ вручную). Для этого, я сначала пишу заголовки, которые мне сунули, и ставлю флаг excluded на дефолтный, если я его там встретил. Мапу по традиции решил избегать, хотя надо будет ещё раз побенчить, заместо там обычный линейный поиск. O(nm), выходит. И это неприятно немного, но пока у меня там всё равно их всего один по-умолчанию (штуки по типу Date и Server всё равно лучше, удобнее и потенциально шустрее через миддлвари докидывать), то всё в общем-то довольно шустро.

К превеликому удивлению, золотая середина - бинпоиск - ситуацию только усугубил. Выигрыш в 30нс дал только кейс с 10 дефолтными заголовками (а это - дохуя). Грустно:(
В стд го шестнадцатиричку пишут через AppendUint(dst, val, base) - и работает это примерно как и обычный append. Корнеркейсом вынесли десятиричные числа меньше ста. Для всего остального - formatBits. Там уже три корнеркейса:
- Основа десятиричная;
- Основа - степень двойки;
- Всё остальное (1 < base <= 36)

Для десятиричной основы там творится какой-то ужас (и довольно медленный, к слову - взятие остатка в цикле, пока не спустимся до ста).

Вот кейс с основанием степенью двойки интересный. shift с bits.TrailingZeroes - это сколько бит представляет символ алфавита. 2 = 0b10 => 1 бит на символ, 16 = 0b10000 => 4 бита на символ. m - маска для индексирования алфавита, чтобы получить наш излюбленный overlapping overflow. Идентичный трюк где-то в старой мапе использовался, насколько я помню.

В цикле остаётся лишь взять нижние shift бит (как раз по нашей маске m), записать символ и битшифтом передвинуть следующие shift битиков в b. И так пока в b не останется числа на один разряд.
👍1
isPowerOfTwo тоже интересная, кстати. Если степень двойки, то в числе будет включён всего 1 бит. Если от него отнять единицу, то, следовательно, он сам выключится, но включатся все биты справа. Соответственно, побитовое И вернёт ноль в таком случае. Если ни один бит не включён (ноль), то побитовое И всегда даст ноль - условие выполнено, 2^0 учтён. Если же включено более одного бита, вычитание единицы сохранит позицию как минимум одного из них, что уже не даст ноль. Забавно, в общем.
Что делать
В стд го шестнадцатиричку пишут через AppendUint(dst, val, base) - и работает это примерно как и обычный append. Корнеркейсом вынесли десятиричные числа меньше ста. Для всего остального - formatBits. Там уже три корнеркейса: - Основа десятиричная; - Основа…
Ну так вот, чем оно интересно-то. Практически идентичным образом сделал у себя я - тот же LUT с алфавитом, та же маска, тот же шифт. Но в 2-4 раза быстрее. Почему - сам не знаю. Однако подозреваю, что
а) вариант в стд пишет себе на стэк перед тем, как скопировать в слайс, когда как я - сразу в ответ ебашу? Лишние байтодвижения у них.
б) я заинлайнил, и инструкции с константным операндом выполняются чуть быстрее?
в) может, компилятор в стд варианте не додумался до bounds check elision? Маловероятный вариант.
г) меньше инструкций? Я-то не жду, пока останется последний разряд - мне похуй, я хуярю нулями до талого.

Разбираться я в этом ебал, конечно. Будем считать, что я просто умнее.
👍1
Между делом просёк тему, что если в интерфейс сунуть тип размером с машинное слово (или меньше), он прям в itab и будет жить. Всё, что больше - летит на хип. Это если кратко подытожить ресёрч Даши. Столкнулся случайно, когда в структуру с единственным указателем докинул буль - и резко 16 b/op полетело.
1
На душе кошки скребут. Думаю, пойду гпт себе запущу, хоть с кем-то пообщаюсь. ollama run gpt-oss не хочет - сервер не робит, мол. А у меня-то в конфиге точно демон есть. Смотрю systemctl status - демон не найден. Хуясе. Ладно, он от юзера работал, --user делает дело. Ошибка - attribute "Type" in section [Unit] не понравился, мол. Возвращаюсь в конфиг никса. Тип у меня вписан в unitConfig. Почесал репу, погуглил, смотрю - а его в serviceConfig пишут. Ну я и вписал. Сделал ребилд, проверяю - робит. На радостях таки запускаю гпт.

А уже как-то и не хочется, уже как-то и полегчало. Как же хорошо, что здесь постоянно что-то приходится чинить!

Жаль только так и не смог майнкрафт запустить. Блядский никс.
😁4
🗿
Кодеки - тяжело. И это не их вина.

Последние часов 20-30 я работал исключительно над компрессией и декомпрессией. Их было непросто впихнуть. На 40% потому, что я не знал, как. И на 60% - потому что мне активно мешали.

И мешали мне io.Reader с io.Writer.


Нет, идея-то в корне хорошая: унифицируем интерфейс для ввода-вывода. Только вот на деле оказывается жизнь сложнее, чем предполагалось. io.ReadFrom, io.WriteTo - костыли как раз потому, что в какой-то момент внезапно оказалось, что у ридеров и врайтеров могут быть собственные внутренние буфера, из которых данные всё так же прекрасно читаются или пишутся! И даже не всегда нужно иметь лишний буфер-прослойку, чтобы данные между ридером и врайтером перекидывать!!!

Даже для потоковой обработки внезапно io.Reader не таким уж и удобным оказался. Начинаешь мешать ReadByte() (спасибо за непрямой вызов и кучу лишних инструкций ради 8 бит данных!) и io.ReadFull - и это неизбежно. Больше не можешь векторизировать поиск символа (а это неслабо бьёт по перфу). Смешнее только, когда оказывается, что ты вычитал немного того, что тебе уже не принадлежит, и по-хорошему бы их вернуть обратно. Я честно пытался переписать свой парсер, но получалась какая-то исключительная поебота. Оставил, как было: парсер получает сырой массив байт, диспатчит стейт и прыгает на нужную метку, возобновляет работу. Функция на 550 строк и 31 гото, и это работает просто хорошо. Старомодно, на расте невыразимо, но это удобно. Не надо никак изъёбываться с тем, как бы ридер засунуть, особенно в тестах: сунул столько, сколько надо, если хочешь протестировать промежуточный стейт - просто не суёшь ничего больше. Никакого unexpected EOF. И явно уж меньше if err != nil.

Ридер даже концептуально плох. В этом его вины мало, конечно: поскольку нет чего-то на подобии енума из раста, он возвращает ОБА n и err. Часто все сразу по привычке ставят if err != nil, но это неправильно: ридер МОЖЕТ вернуть n > 0 И err != nil. И в семантике чётко написано, что в таком случае, сначала данные должны быть обработаны, а только потом мы можем смотреть на ошибку. Я у себя это эсплуатирую, чтобы на один непрямой вызов меньше делать, но это совершенно неочевидно и далеко не всеми придерживается.

io.Writer ничем не лучше. Это мои компрессоры, как раз, и я с ними имел опыт первоклассного секса. Ебали, к сожалению, меня. Чтобы не стать счастливым владельцем лишнего промежуточного буфера (а это аллокации, при том потенциально регулярные), я молюсь всем Богам на предмет поддержки io.ReaderFrom или io.WriterTo. А худшее - мне пришлось раскроить сериализатор. Потому что компрессору нужно куда-то писать выхлоп, и мне его надо кормить в свой chunked writer. Чтобы без лишних движений байт, я добавил в chunked writer ещё и ReadFrom, что удобно примерно нихуя. Примерно аналогичный метод, но с немного другой логикой. Так в ReadFrom теперь ещё и не совсем понятно, как увеличивать нормально буфер - пришлось лакомиться эвристикой "буфер при чтении был заполнен на >98.64%".

Вишенка на торте - компрессор не закрывает тот врайтер, что я ему передаю. А у меня при закрытии как раз терминальный чанк пишется. То есть, оно всё ещё и чейнится - не чейнится. При том, то, что переданный поток не закрывается - это принято за хорошую практику.

А знаете, как можно было бы сделать лучше?

Сделать, как мой парсер - сунул байты, получил байты. Ну, ещё буфер суёшь, куда тебе результат напихивать. Всё.

И тогда мир оказывается настолько сильно проще! Ты больше не думаешь, где хранить буфер чтения, как бы не забыть про len переданного слайса (объёбывался несколько раз об это), потом ещё обрезать тот слайс до n. И чейнить внезапно оказывается не так уж и сложно: вместо того, чтобы между каждым врайтером держать промежуточный буфер, ты держишь только два! Из одного читает, в другой пишет - а для следующего просто меняем буфера местами. Больше не существует проблемы, которую решал бы io.Copy: ридера больше нет, у тебя на руках сразу массив со всеми данными!

io.Reader и io.Writer - это та вещь, которую я, наверное, ненавижу больше всего в Го. Даже сильнее if err != nil.
Using Haskell's standard network library, a listening socket is created with the non-blocking flag set. When a new connection is accepted from the listening socket, it is necessary to set the corresponding socket as non-blocking as well. The network library implements this by calling fcntl() twice: once to get the current flags and twice to set the flags with the non-blocking flag enabled.

On Linux, the non-blocking flag of a connected socket is always unset even if its listening socket is non-blocking. The system call accept4() is an extension version of accept() on Linux. It can set the non-blocking flag when accepting. So, if we use accept4(), we can avoid two unnecessary calls to fcntl(). Our patch to use accept4() on Linux has been already merged to the network library.

Из доки по Warp из мира хаскелла.

TL;DR в линуксе есть accept4(), который наследует новому подключению флаги листенера. Таким образом, nginx (ну и warp, понятно) избегают двух лишних сисколлов на каждое подключение. А это хорошо, учитывая, сколько там корутин может быть одновременно на одном потоке.
❤‍🔥5
ЧЕГО НАХУЙ

IE умудряется давать повод ненавидеть себя не только фронтам, но ещё и бэкам. Поразительно!
😁4🤬1
Slowloris - форма DoS, заключающаяся в передаче информации медленно и маленькими кусочками. Сервер потратит непропорционально больше времени на сам факт получения данных, на обработку самого ивента, нежели на разбор той жалкой пары байт, которые прилетели.

Защита строится по подобию троттлинга, только наоборот и низкоуровневее: подключения рубятся, если слишком медленные. Скажем, пару сот бит в секунду. Естественно, лимит работает только на периоды активной передачи данных, в idle состоянии хули его трогать-то.

А вот в индиго такого нет, кстати.