Саша говорит


Kanal geosi va tili: Belarus, Ruscha


Поток сознания @ruliov

Связанные каналы

Kanal geosi va tili
Belarus, Ruscha
Statistika
Postlar filtri


Пишу сейчас код, который собирается в wasm, насобирал чутка лайфхаков.

Можно сделать пакет, который использую и из wasm-пакетов и там где всё это дело выполняю (в том числе с такими GAT в местах где типы расходятся), в котором завести трейты на все функции, которые могут экспортироваться из wasm'а. А затем реализовывать их для пустых структур, так сигнатуры функций не разойдутся.

В пакете общих утилит сделал тип Annotated, у которого #[repr(transparent)] (т.е. по сути это алиас для типа и в итоговой сборке ничего от него не останется). Им размечаю указатели, чтобы было понятнее, что за ними должно лежать.

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

А затем можно сделать impl на таким образом аннотированный указатель, который возьмёт и всё распарсит, красота.

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




Нашёл себе занятие на время когда ну совсем ничего делать не хочется — посидеть и потыкать неовим.

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

Ищешь команды чтобы использовать LSP, назначаешь новые кейбиндинги, перезагружаешь, продолжаешь.

Первое что сделал — добавил в конфиг lazy, чтобы добавлять другие плагины. Затем stylua-nvim чтобы вызвав :lua require('stylua-nvim').format_file() отформатировало lua-файл на котором здесь пишутся конфиги.

Затем поставил WhichKey чтобы были подсказки чего дальше можно нажать, пока нублю. Переназначил leader key на запятую чтобы поближе была, сделал биндинг ,\fl который форматирует lua-файл (страшный такой потому что решил что мусорные команды буду в лидер-бэкслеш запихивать, ну и Format Lua).

Накрутил привычный monokai, доставил дерево файлов, Telescope (ничего такая тулза умеющая искать практически по чему угодно и такое ощущение что другие плагины могут с ним затем интегрироваться и добавлять свои источники поиска), lspconfig ибо было лень разбираться как включать нативный самому.

Наделал на все эти плагины тоже биндингов, чтобы быстро искать файлы/найти текущий файл в дереве/создать рядом файл/поиски определений и прочего по LSP/навигация по ошибкам LSP. И теперь даже похоже на что-то, что можно использовать, хотя пока явно далеко до того, чтобы пересесть с vscode/RustRover, нужно больше опыта, чтобы все навигации руки сами исполняли не думая.

Нужно ещё посмотреть почему все хвалят harpoon и что это такое вообще. И научиться/наделать биндингов под мощные прыжки вроде «телепортани меня на первую строку кода после импортов», «го по всем функциям подряд пройдёмся».

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


Не помню, рассказывал ли про эту игрушку, но Ventoy на флешке это магия какая-то.

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

Да, потыкаю арч на старом ноуте.


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

Некоторые ветки выполнения у меня до сих пор под todo!() и с таким подходом довольно легко сделать тест, который в них заходит — нужно просто поменять зерно, либо другие параметры.

Что я могу сделать написав временный тест, который в цикле выполняет тест с разными сидами, пока не упадёт там где мне нужно (ну или где не нужно, но тоже хорошо, что упало).

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


https://just.systems/man/en/

> оно как make, только выпилили костыли из восьмидесятых и добавили возможность писать скрипты на других языках


https://flink.apache.org/
https://www.fluvio.io/
https://www.arroyo.dev/

Пусть тут ссылочки полежат, все похожи на то, что я пилю, нужно будет поизучать их фичи.

#diffbelt


Всё больше погружаюсь в FFI-сторону Rust, для которой нужно думать, кто будет владеть памятью (тот кто снаружи вызывает наши extern-функции или код внутри) и постоянно обмазываться unsafe-блоками (т.к. снаружи нам приходят указатели и наружу нужно выдавать их же, а работа с сырыми указателями в расте возможна только в unsafe-блоках, которые говорят «начиная отсюда ты сам за себя, добро пожаловать в чудесный мир C++, где никогда не знаешь, есть у тебя undefined behavior или нет»).

И чем дальше, тем всё страшнее и страшнее. Я наивно думал, что память это такой большой массив байт. И, например, если у нас есть массив u32 (32-битных беззнаковых целых чисел), то это то же самое что массив u8 (байт) с длиной умноженной на 4.

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

struct MyStruct {
two_bytes: u16,
single_byte: u8,
}

fn main() {
println!("{}", mem::size_of::());
}

Выведет 4 вместо 3, хотя храним мы там двухбайтовое и однобайтовые значения. Можно перед объявлением структуры написать #[repr(packed)] и оно начнёт писать 3, т.к. мы попросили экономить место и не вставлять «лишние» пробелы.

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

Т.е. когда мы положили в структуру u16 у которого выравнивание равно двум и u8, у которого выравнивание равно единице, компилятору нужно сделать так, чтобы u16 всегда лежал по чётному адресу. А понятие размера структуры это не её физический размер, а расстояние между двумя элементами массива из этих структур. В таком случае если размер был бы 3, то каждый второй элемент массива имел бы u16 который лежит по нечётному адресу. Что, насколько я понял, геморрой для процессора, т.к. ему потенциально придётся два куска памяти читать, а потом склеивать концы, чтобы получить значение с которым он будет работать.

Вернёмся к примеру про массивы. Выравнивание элементов массива из u32 равно 4, а если он состоит из байт — то 1. И если начать интерпретировать каждые 4 байта в байтовом массиве как u32, то с вероятностью в 75% всё будет плохо когда адрес не будет делиться на 4 (хотя нам может повезти с аллокатором, который будет выделять память только с каким-то большим выравниванием).

Т.е. мы не можем, например, сделать функцию которой просто сказать «дай мне указатель на X * 7 байт», затем записать по этому указателю 7 произвольных структур размером X, а затем интерпретировать эту память не как байты, как как массив из 7 структур. Т.к. у этих структур может быть выравнивание не равное единице. А если у нас не массивы, а динамически расширяемые вектора, то ещё веселее — при реаллокации оно будет думать что прошлая аллокация была выровнена по X, а не байтам и будет деаллоцировать и аллоцировать память с этим выравниваем. А дальше зависит от реализации аллокатора, который может сделать что угодно, если выравнивания не совпадают.

Что делать? Ну, у меня, хвала котам, в коде пока нет функций которые бы просили аллоцировать N байт а затем использовали их не как байты. Типов данных которые нужно выставлять наружу по FFI и делать из них массивы не так уж и много, можно отдельных функций аллокаций для каждого написать. А было бы много — тогда да, пришлось бы особенно для векторов какие-то хаки придумывать, как его аллоцировать для нужного Layout, чтобы потом оно сошлось с тем, что мы там храним на самом деле.

#rust


Купил полную версию какой-то мобильной приложухи для рисования mind maps, ни разу не пожалел. Можно с дивана накидывать, чего там сделать то нужно, а потом уже хоть примерно понимать, с какой стороны подступиться.


О, в телеграм подсветку кода для конкретных языков завезли, теперь заживём. Ещё бы спойлеры разворачивающиеся, чтобы не постить полотно на два экрана/не использовать пастебины всякие (Мерлин пошарил недавно, кстати, вообще топовый — https://code.xxut.ru/ ).

Не вижу только, где настроить, чтобы был Monokai.


Нашёл наконец юзкейс, где нужны GAT (generic associated types). Есть проблема: нужно пошарить типы между кодом который компилируется в WASM (натив) и кодом, который этот wasm запускает (хост).

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

А на хосте нам нужны те же структуры, но внутри них нам нужны не сырые указатели (т.к. у нас другое адресное пространство), а специальные, в данном случае WasmPtr вместо *mut T. Ну и вот, делаем трейт:

pub trait PtrImpl {
type Ptr: Copy;
}

Он говорит «у реализации трейта должен быть определён generic-тип Ptr, где T можно клонировать, а сам Ptr можно побайтово скопировать и ничего плохого не произойдёт».

А затем мы можем сделать две структуры без полей: NativePtrImpl и WasmPtrImpl. И реализовать для них этот трейт:

impl PtrImpl for NativePtrImpl {
type Ptr = *mut T;
}

impl PtrImpl for WasmPtrImpl {
type Ptr = WasmPtrCopy;
}

При создании структур параметризируем их этим трейтом и вместо указателей используем P::Ptr:

pub struct BytesVecFull {
pub ptr: P::Ptr,
pub len: i32,
pub capacity: i32,
}

Готово, теперь по-умолчанию в нативном коде там будут указатели, а если нужно использовать из хоста, то пишем BytesVecFull:: и все указатели становятся другого типа. Ну и с вложенными структурами оно автомагически тоже работает, просто параметризируем их P.

#rust


Alexander Ruliov dan repost
Ну, у меня там в базе есть три вида трансформаций данных:

▪️ MultiMapFilter(A -> B) — при изменениях коллекции A каждая пара (ключА, значениеБ) превращается в сколько-то пар (ключБ, значениеБ) в коллекции B, ну или удаляется (на примере: строки логов превращаются в JSON/Protobuf логов чтобы дальше с ними удобнее было работать).
▪️ Aggregate(A, B) — при изменениях коллекции A каждая пара (ключА, значениеА) маппится в ключБ, а затем над всеми изменениями пар которые смаппились в ключБ производится свёртка (reduce), где аккумулятор — значениеБ и результат его обновляет. Ну и там ещё есть вариации аггрегаций, когда можно параллелить reduce'ы и потом мержить их между собой и когда нельзя (если в значениеБ просто счётчики которые можно сложить между собой, например, то можно и параллелить).
▪️ ComplexTransform(A, B, C) — по сути, Aggregate(MultiMapFilter(A, B), C), такая штука, например, умеет обрабатывая diff'ы коллекции считать перцентили и количество уникальных сущностей в каком-то периоде времени.

Ещё пока придумываю, как сделать Merge((A, B, ...), C) в более или менее декларативном стиле и можно будет считать всё что душе угодно в реалтайме и смотреть на результаты)


#diffbelt


Взял библиотеку которая умеет выполнять WASM — сразу вступил в баг https://github.com/wasmerio/wasmer/issues/4282

Придётся вместо возвращения кортежа передавать в функцию указатель на созданную ранее на стеке структуру. Хотя может оно и к лучшему, пошарю нормально типы этих структур между хостом и внутренним кодом.


Тех, кто придумал хоткеи вроде Cmd+Q, закрывающие IDE/браузер нужно на принудительные работы отправлять.


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

А сами трансформации сделал так, как где-то ранее писал. Это отдельный модуль, где можно сказать «я хочу совершить MapFilter из коллекции A в коллекцию B» (создать экземпляр трансформации). Затем у него есть единственный метод .run(inputs: Vec). Возвращает он либо ошибку, либо одно из трёх:

▪️ Actions(Vec) — вот тебе задания, которые тебе нужно выполнить.
▪️ Finished — ты молодец, преобразование завершено.
▪️ Consumed — понял, принял, давай ещё Input'ов.

Задание пока бывает одним из двух типов: «сходи в такой-то метод базы с такими-то параметрами, верни мне результат» и «выполни функцию X с такими-то данными».

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

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


Велосипедить свой ЯП оказалось ошибкой. Я то его напилил, но хочется, например, не только хешмапы создавать, а и просто структурки с полями, чтобы в рантайме оно не смотрело в словарь для обращения к ключу, а уже знало в каком месте будет лежать нужное значение. И как начинаю думать, как их там оптимальнее реализовать — понимаю что работы привалит ёмаё.

Как буду делать следующий подход, сотру всё что наделал и начну интеграцию с wasm. Буду то же самое что на ямлах костылил рядышком на расте запиливать, а в конфиге указывать путь до wat-файлика.

#diffbelt


IDEA могёт (точнее это превью RustRover, но не суть).

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


Вот им жалко букв было под инструкции? У x86 всё более или менее понятно было, а тут приходится смотреть на неё, пытаться расшифровать, затем плюнуть и читать по тексту, что оно делает.

Кстати, сильно советую почитать https://marabos.nl/atomics/ , даже если не планируете на Rust писать. Хорошо про memory ordering объясняется, что вроде бы даже понятно. Я сам ещё не дочитал, но и про разницы архитектур вот тоже интересно довольно пошло.


Какой-то угар. Накидал бенчмарк пре-создания регэкспа против его динамического создания каждый раз:

https://www.measurethat.net/Benchmarks/Show/27718/0/pre-created-regexp-vs-created-every-time

На MacBook Pro с i7 выдаёт 11 миллионов проверок в секунду против 3 миллионов.

... а на айфоне 21 миллион против 11 миллионов. Почему мак оказался медленнее телефона для прогона регэкспов? Я как-то не думал, что такие драматические разницы между архитектурами бывают. Или это Safari лучше регэкспы компилит, чем хромиум с v8?

UPD: запустил на маке в сафари, получил 10 лямов против 5.

20 ta oxirgi post ko‘rsatilgan.