Стендап Сьогодні

Що я зробив, що я хочу зробити, і що це все значить.
Повсякденні здобутки в форматі стендапу.
Детальніше в статті

Підписатись на RSS · 📢 Канал в Telegram @stendap_sogodni

18.03.2024

Відкритий код Дії

Якщо ще не чули, то вихідний код “Дії"опублікували на GitHub. Ось мої думки без поглибленого, поки, аналізу.


17.03.2024

Найкраща у світі печена картопля

🥔☘️ Сьогодні раптом рецепт моєї улюбленої печеної картоплі (через день Святого Патрика, може? А насправді це чистий збіг.) Рецепт я побачив у Kenji Lopez-Alt та трохи адаптував для простоти.


16.03.2024

Запуск одноразових скриптів на Fly.io

Люблю хостинг Fly.io за його простоту (а ще за те, що до $5 на місяць вони не беруть оплату.) Тому, коли довелося запустити бенчмарк десь у хмарах, то саме Fly.io й взяв, хоч це не типове його використання.

Чому цей хостинг? Бо він побудований на Docker, але також підтримує стале сховище, доступ по SSH, та швидке масштабування, включаючи до нуля.

Отже, з чого почати: нам буде потрібний “застосунок” - він триматиме всі дані. flyctl app init. Застосунок матиме конфігураційний файл, також Dockerfile, та повинний запускати хоч якийсь “сервіс” - я бачив tail -f /dev/null як мінімальне рішення. Потім запускаємо: flyctl app deploy.

А далі flyctl ssh console відкриває звичайнісінький ssh, в якому будемо запускати бенчмарк чи інший скрипт. Для масштабування — команда flyctl scale вміє як змінити кількість ядер та памʼяті, так і кількість віртуальних машин.Нам потрібна лише одна, але дуже корисно масштабувати в нуль — так можна недорого запускати бенчмарки до 16 ядер та 128 Гб.

Залишається питання великих файлів. Пакувати гігабайти в образ Docker буде дуже повільно, та ще й відбуватиметься на кожну збірку. Тут є два шляхи: загальний — завантажити файл кудись на S3 та забирати звідти. Але краще — приєднати до машини том сталого сховища, та розмістити файли там — flyctl volumes create. Та, на відміну від, наприклад, AWS, до томів є легкий доступ — або через ssh, або навіть через SFTP командою flyctl ssh sftp shell.

…Я вже звик до того, що в AWS все робиться через шари бюрократії та непрямостей, тож радію, коли за 10 хвилин можна від чистого аркуша дійти до рішення.


15.03.2024

Місяць зі стохастичним таймтрекером

Мій таймтрекер живий та просувається до релізу. Ось такий прогрес за останні пару тижнів:

1: Повністю переробив інтерфейс вводу тегів. Початкова версія була примітивна та складалася з двох списків: використаних тегів та всіх інших; більшість роботи ж лягало на рядок пошуку, бо знайти в переліку з сотні тегів потрібний — це нереально.

Поточна версія пропонує ймовірні теги. Це або теги з попередніх записів, або ті, що вже зустрічалися в такий самий день тижня та годину. Такі теги додаються одним натиском. Можливість знайти тег з загального списку залишається, але як додаткова; там же ж можна й створити тег, якщо його ще не існує.

2: Шукаю нові шляхи аналізу тегів. Моя мета (на цей час) - допомогти краще використовувати свій час. Для цього в мене вже є графіки часу за тегом, проте їх не має сенсу робити детальніше, ніж по тижнях. Таких графіків достатньо для оцінки змін, але не для розуміння, що саме потрібно міняти.

Щоб побачити нагоди для змін, розробив аналіз тегів на суміжність. Наприклад: відкриваю тег “робота” та бачу що за ним часто слідує тег “в телефоні”. Що значить, напевно, що телефон під час роботи варто тримати якомога далі та не відвертатися на нього (нагадаю, що в цій системі трекінгу навіть якщо на “хвилинку” взяв телефон та у цей час прийшов пінг, то він заноситься як “в телефоні”, а не “робота”. ) Така аналітика вже допоможе виправляти свою поведінку.

Також додав таку метрику, як середню безперервну тривалість тегу. Так можна виявити заняття, які особливо затягують (або навпаки, затягують недостатньо.) В мене ось знайшлося, що Baldur’s Gate 3 затягує. Висновок: не сідати грати, аж допоки 1) не закінчиш всі задачі та 2) не підготуєш відомої точки зупинки.


14.03.2024

Особливості процесорів Apple Silicon

Як писав, вимірювання швидкості паралельних програм на Apple Silicon має неочевидні аспекти, через які я б радив взагалі не вимірювати нічого локально, а використовувати хмарну віртуальну машину.

Нюанс головний: в процесорі Apple Silicon є два типи ядер: швидкі та економні. Вони відрізняються швидкістю, розміром кеша, та, звісно, витратами енергії. Економні ядра — чудова інновація, бо вони дозволяють заощадити батарею під час виконання фонових, не вибагливих до швидкості процесів. Але… процеси чи потоки, що потрапляють на економні ядра, будуть виконуватись повільніше.

Причому примусово використовувати тільки швидкі ядра ми не можемо. Процеси по ядрах розподіляє операційна система. Для того в процесу є пріоритет (клас QoS); найменший пріоритет мають фонові задачі, які завжди виконуються на економних ядрах, а найбільший — графічні програми, робота яких нам буквально видна. Пріоритет можна побачити, зокрема, в htop - це стовпчик PRI. (Взагалі, htop - найкращий монітор процесів і на Apple Silicon теж.)

Все це гарно для користування, проте шкодить бенчмаркінгу. Зверх того, треба мати на увазі стандартні обмеження — запускати тести тільки при живленні від мережі, зупинити зайві процеси, перевірити охолодження (до речі: в macOS є процес kernel_task, який буквально відбирає час CPU, коли той перегрівається. Бачиш високу активність kernel_task - шукай причину перегріву.)

…При всьому цьому, мені так і не вдалося зробити розвʼязок з ракторами, який не був би у 2 рази повільніше за процеси — попри те, що процеси в мене комунікують рядками JSON, а рактори безпосередньо передають значення в памʼяті. Експериментальна технологія експериментальна.

(О, а ще — і я це перевіряв — запущена в фоні база даних має нижчий пріоритет, ніж запущена з термінала, та це впливає на швидкість тестів, які її використовують.)

Раджу ось цю низку статей, де в дрібних деталях розписано про поведінку Apple Silicon.


13.03.2024

1BRC на Ruby: нарешті, правильна паралелизація

Так, виявляється, читати з файлу одночасно з декількох місць — значно повільніше, ніж робити це послідовно. А значить, щоб обробити файл паралельно, мусимо побудувати складніший скрипт з пулом потоків та розподілом роботи.

Скрипт буде читати з файлу блок за блоком (тут як раз стає до нагоди те, що я вже зробив поблокову обробку) та віддавати їх потокам, а потім збирати результати. Типова архітектура fan-out. Зробив версії з ракторами та з процесами; різниця в тому, що процеси спілкуються через “труби” IO.pipe, а у ракторів є своя семантика обміну повідомленнями, яка дуже нагадує мені Golang, бо обмін є синхронним (тобто як відправник, так і отримувач блокують до готовності протилежної сторони.) Код тут.

Наступне відкриття: рівночасні програми на Apple Silicon поводяться неочевидно, тобто, не залучають всі ресурси в системі. Так, мені не вдалося використати 100% ядер; з наявних на моєму MacBook Air 4 “швидких” та 4 “економних” ядер, два швидких залишались вільними, незалежно від кількості ракторів. Макбуки надто розумні — про це можна окремий пост. Навіть так, паралельне рішення працювало у 2-3 рази швидше за найкраще послідовне — нарешті, помітне покращення, хоч і далеке від ідеального.

Щоб уникнути еплівських нюансів, переніс тестовий скрипт на Fly.io. Про це теж треба окремий пост, але головне, що на Fly.io паралельна версія з fork реально дала восьмикратний приріст, тобто за кількістю ядер. А з іншого боку, розвʼязки з ракторами просто скидають SEGFAULT - чому я не дуже здивований, бо вони “експериментальні” - хоча могло б бути й краще.

Якщо підсумувати, то так, паралелізувати розвʼязки на Ruby можна, але робити це на процесах (тобто з fork) та добре продумати обіг даних. Або взяти готові рішення на кшталт Sidekiq та не вигадувати велосипеда.


12.03.2024

Паралелізація 1BRC на Ruby: несподівані результати

Останнє, що я хотів спробувати після минулих експериментів, це паралелізувати розвʼязок, проілюструвати, що це набагато простіше, ніж оптимізувати, та закрити питання. Вийшло трохи не так.

Про паралелізацію. Вочевидь, ми повинні розбити файл на декілька розділів та обробляти їх паралельно, а наприкінці обʼєднати результати. Обʼєднати нескладно, але з розбиттям виникає нюанс: потрібно зробити так, щоб кожний розділ починався з нового рядка.

Зробив так: розділяю розмір файлу на рівні частини, а потім на початку кожної частини зчитую до початку наступного рядка те, що насправді є останнім рядком попереднього розділу. Тому підготовка розділів відбувається з кінця на початок. (Та, може, потрібно додати, що для паралельного читання ми відкриваємо файл окремо для кожного розділу.)

Я знаю три способи технічної реалізації: потоки (Thread), рактори (Ractor) та процеси (fork). Очікування були, що потоки будуть трошечки швидше, а рактори та процеси дадуть близьке до кратного пришвидшення.

Вийшло не так. Потоки вийшли у 8 разів повільніше послідовного обчислення, а рактори та процеси — десь у 5 разів повільніше. Оце диво! Невже паралельне читання з файлу настільки обмежує? Бо я інакше взагалі не розумію, як паралельні обчислення, які помітно навантажують процесор, можуть бути настільки повільними. Поки залишу з тим висновком, що паралелізація теж не магічна та може навіть в рази уповільнити код.

Код тут. Відрізняється тільки манера запуску та збору результатів.


11.03.2024

Debug на Ruby з VSCode

Чесно, я останній раз впевнено дебажив ще на Borland Pascal та можливо ще C++ у Visual Studio (звичайному, не Code, а версії десь так 2010). Тоді програма була програмою: не було ані тестів, ані вебсерверів, ані “живого перезавантаження”. Потім в моєму житті трапився веб, а дебаг став рідкісним.

Ось, сьогодні вирішив замість звичного розсипання по коду puts, зʼясувати, чи можна у 2024 дебажити Ruby з VSCode. Виявилося, що можна. Та й не дуже складно.

Нам потрібно буде доповнення VSCode rdbg Ruby Debugger. Воно працює разом з офіційним гемом debug, проте цей гем достатньо мати встановленим — включати в проєкт його не потрібно.

У цього доповнення є два режими роботи. Той, де воно запускає програму для дебагу, в мене не запускався — здається, через те, що мій термінал fish. Інший режим — підключення до програми — спрацював. Його й рекомендую, бо, як на мене, такий підхід більш прозорий.

Отже, спочатку маємо запустити програму. Для того є команда rdbg.

rdbg -O -c bundle exec rspec # ... і так далі

Важливо, що так можна запустити будь-яку програму на Ruby, хоч RSpec, хоч Rails, хоч просто власний скрипт. Я думаю, RSpec то найкращий вибір, бо там і контекст готовий, і легко запустити той модуль, який потрібний, в ізоляції.

Точки зупинки можна створювати засобами редактору, нічого в коді писати не треба. Потім стартуємо програму, запускаємо доповнення в режимі attach, та насолоджуємося. Покрокове виконання, стеження за значеннями (тобто виконання довільних виразів в поточному контексті), точки зупинки з умовами (теж довільними виразами на Ruby)… Працює бездоганно!


10.03.2024

Доповнення до класів у Swift

Коли шукаєш розвʼязки всіляких дрібниць на Swift, можна швидко натрапити на конструкцію extension. Доповнення це завидна можливість мови, яка дозволяє прозоро та безпечно розширити функціональність класів, які вже існують, включаючи такі, що створені не нами. Точніше, саме для чужих класів доповнення й існують.

Ось, наприклад, доповнення, яке додає до символу функцію isEmoji. Так, далі можна писати "😀".isSingleEmoji", що дуже зручно.

Рубісти відразу впізнають такий спосіб розробки. В нас цілий Active Support так побудований — одна з фундаментальних частин сучасного Ruby.

Проте міксини в Ruby та доповнення у Swift мають одну принципову різницю. А саме, доповнення не впливають на внутрішню роботу класу. Ми не маємо доступу до приватних частин класу, та можемо тільки доповнювати його функціями, яких не було до того. Новий код буде обовʼязково навколо, а не всередині старого.

Технічно, замість доповнення можна було просто створити новий модуль (EmojiDetector.isEmoji(character:)). Тільки, по-перше, писати більше, а по-друге, завдяки доповненням можна ще й реалізувати інтерфейси, тобто підігнати готовий клас під нову потребу.


09.03.2024

1BRC: чому Ruby повільний

Таке просте (не легке!) завдання, як 1 Billion Row Challenge, дає нагоду роздивитися причини повільності мови. Однак ruby-prof приховує їх — бо більшість часу цей скрипт витрачає не в Ruby. Тому взяв в руки профайлер коду C та пішов глибше.

Виходить, що переважну більшість часу збігає на виділення (та вивільнення) памʼяті. Виявилося, що в операціях над рядками дуже важко уникнути виділення памʼяті практично на кожному кроці. Там, де в низькорівневій мові можна створити сталий буфер та використовувати його повторно, в Ruby на кожне читання з файлу утворюється новий. Навіть якщо переписати алгоритм з буферизацією — все одно на витягання кожного елемента потрібні нові рядки.

Є один очевидний спосіб уникнути цього: обробляти рядок на місці байт за байтом. Тут зʼявляється друга причина — цикли в Ruby мають ненульову ціну. Робити ітерацію на кожний рядок — нормально. А на кожний байт — тобто на порядок більше ітерацій — вже стає найдорожчою частиною програми.

Нарешті, я знайшов як працювати з рядками без виділення памʼяті. А саме: операцією String#byteslice, яка копіює байти з рядка в рядок. Якщо рядок-отримувач має достатньо місткості, памʼять виділятися не буде. (Тут варто знати, що в рядків в Ruby є довжина, а є ще місткість, тобто розмір вже виділеної памʼяті.)

Додати цю операцію до StringScanner#skip_until та IO#readpartial та отримуємо сканування файлу абсолютно без виділення памʼяті! От тільки код стає дуже плутаним (ось він) та — головне — працює все одно повільніше за вчорашній розвʼязок через кількість необхідних додаткових операцій.

Отже, нічого нового: Ruby непрактично повільний на циклах з мільярдів ітерацій, або там, де потрібні мільярди виділень памʼяті. Якщо проєкт такого потребує — пора перейти на низькорівневу мову.