Стендап Сьогодні
Що я зробив, що я хочу зробити, і що це все значить.
Повсякденні здобутки в форматі стендапу.
Детальніше в статті
Підписатись на RSS
📢
Канал в Telegram @stendap_sogodni
🦣
@stendap_sogodni@shevtsov.me в Федиверсі
16.09.2023
Автоматичне редагування документів в Obsidian
Повернувся до проєкту “автосадівника” дерева задач для Obsidian. На зараз ціль проста: щоб до задач автоматично додавався час створення та завершення, а також щоб завершені задачі переносились до “архівного” списку в кінці документа.
Сьогодні маю прогрес: базова версія працює. Проте зрозумів, що пара моїх підходів хибна. А саме:
Перетворення AST це чудовий підхід, але якщо інструмент не передбачає відтворення коду “як в оригіналі”, то покладатися на нього буде ненадійно.
Наприклад: Remark на виводі нормалізує символ списку — скажімо, до зірочки *
. Це само по собі не так погано. Але якщо поруч є два списки — один з зірочками, а інший з рисочками -
- то парсер бачив їх окремо. А коли другий список буде нормалізований у зірочки, то списки для парсера зливаються в один. Щоб виправити це, Remark додає між списками коментар HTML: <!---->
. Звісно, такого я зовсім не хочу.
Раніше я вже придумав тільки частково заміняти текст, тобто перетворювати тільки там, де є реальні зміни, але навіть часткова заміна має ризик зіпсувати документ… частково.
Напевно, все ж найнадійніший підхід це за допомогою AST тільки знаходити місце для зміни, а потім змінювати вхідний документ прямим підставлянням.
Тут наступне розуміння: в Obsidian активний документ варто редагувати тільки функцією editor.replaceRange. Вона безпосередню змінює зміст редактора та чудово співіснує з подальшим користуванням редактором. Мій перший намір був зберігати зміни в файл через vault.modify
, тобто “на диск”. Але це відразу почало створювати конфлікти з будь-якими змінами, які я одночасно зробив вручну. Не кажучи вже про те, що подія onModify
, яку я використовую для виконання свого коду, відбувалася й після моїх автоматичних змін теж (проте від цього нескладно захиститись.)
Нарешті, щоб зовсім не заважати користувачеві, ще треба перевіряти рядок, в якому знаходиться курсор (editor.getCursor
) та нічого в ньому не робити.
15.09.2023
Geometry Dash
Сьогодні буде про улюблену гру, але не мою, а сина - Geometry Dash. На перший погляд, вона майже нічого з себе не ставить — але при цьому у гри з 2013 року сабреддіт на 130 тисяч підписників та безліч щоденних відео на ютубі. Та й взагалі Google Trends каже, що вона лише в 7 разів менша за популярністю ніж світовий феномен Fortnite, при чому популярність GD тільки зростає, коли Fortnite йде на спад. Все це ознаки здорової спільноти, а значить, принаймні варто знати, що таке існує.
Механічно Geometry Dash є максимально простою грою: персонаж сам біжить по рівню, а все, що треба від тебе — це в правильний момент стрибати. Проте вона також є механічно надзвичайно складною: вимоги до точності стрибків з часом тільки зростають; перший рівень можна пройти за десяток спроб, але решта гри потребує годин тренувань. Тому зазвичай гравці порівнюють відсоток рівня, який вдалося пройти.
Можна зрозуміти, чому ця гра так захоплює (особливо дітей): завжди здається, що ось-ось, ще одна спроба — та ти пройдеш; завжди зрозуміло, що робити далі: вся гра — один нескінченний quick time event. Звісно, для стрімерів це теж непоганий формат.
Проте популярність Geometry Dash тільки наполовину полягає в простоті. Ключовою функцією тут є редактор рівнів та можливість ділитися ними онлайн. Створення рівнів це ціле мистецтво: тут не тільки можна “малювати”, а й залучити ряд логічних елементів-тригерів для утворення анімації та різних ефектів. Так люди фактично роблять цілі музичні відео на кшталт такого. Рівнів існує мільйони, з них 35 тисяч офіційно “оцінених” від тривіальних до “неможливих”. З часом виходять все “неможливіші” та гравці змагаються за те, хто скоріше пройде новий рівень.
А можна ще копіювати рівні та переробляти їх, або за допомогою мегахаку записувати макроси, що автоматизують проходження, або створювати рівні-випробування для друзів та змагатися, хто швидше пройде, або створювати рейтинги рівнів, або проходити випадкові чи нові рівні… Такий от цікавий куточок інтернет-культури зі своїми героями, подіями та історією.
14.09.2023
Чому "писати на тому, що знаєш" погана порада
На днях брав участь у дискусії чи Go гарна мова, щоб писати соціальну мережу. Чому ні, я вже все сказав, а тепер хочу поговорити про популярну відповідь “краще за все писати на тому, що знаєш”. Як людина, яка хотіла писати операційну систему на BASIC (даруйте, мені було 8 років!), можу сказати, що це погана ідея.
Взагалі, звісно, всі професійні програмісти пишуть на тому, що знають, щодня, та це виходить цілком справно. Але так відбувається тому, що ми не виходимо за межу вузької галузі, над якою працюємо.
Проте універсальних мов програмування не існує. Фахівці з фулстек веброзробки мають це знати — бо типовий додаток буде містити принаймні дві мови — для бекенду та для фронтенду. Але веб — це скоріше виключення, бо в більшості галузей розробник може все життя працювати одною мовою без нестачі проєктів.
Все то добре, поки ти в зоні комфорту. Але якщо область тобі невідома, то є шанси, що твоя робоча конячка не найкращий, або взагалі шкідливий вибір. Я б радив спочатку подивитись, що використовують типові проєкти — наприклад, через статті та презентації. Звідти й треба починати. Так, я не полюбляю Python, але якщо доведеться працювати з аналізом даних — візьму pandas.
Так, Railsware колись починалась як компанія, що пише на Ruby on Rails (та трішечки JavaScript зверху). Але зараз проєкти включають Golang, Node.js, Python, Java, та інші мови. Зовсім не тому, що проєктів на Rails не вистачає. А тому, що навіть в рамках одного продукту не все має сенс писати однією мовою. Компанія просто переросла прості проєкти.
Отже, якщо в тебе є мета робити проєкти з різних галузей, варто налаштуватись вивчати різні мови програмування. Або натомість заглиблюватись у свою галузь: теж гідне заняття. Тільки не намагатись все писати однією мовою; в академічних цілях це може бути цікаво, але в професійних — згубно.
13.09.2023
Профілювання С-коду всередині Ruby
Докопався до правди по вчорашньому питанню. Для цього треба було знайти, як профілювати не Ruby-код, а C-код, який складає методи, які ми порівнюємо.
Вдалося це зробити за допомогою цієї статті. Потрібно встановити інструменти профілювання GPerfTools - вони є у Homebrew. А потім скомпілювати Ruby з додаванням профілювання. На моєму Macbook M2 для цього достатньо увімкнути дві опції:
env RUBY_CFLAGS='-fno-omit-frame-pointer' LIBS="-lprofiler" asdf install ruby 3.2.1
# Потім:
env CPUPROFILE="out.profile" ruby payload.rb
pprof $(asdf which ruby) out.profile --svg > out.svg
Результати можна побачити на ілюстрації зверху. Обидві реалізації витрачають більшість часу у виділенні памʼяті. (Решта — це пошук регулярного виразу, який відбувається практично однаково.) Але там, де StringScanner
просто бере довжину збігу та копіює від початку, String#scan
викликає код, що створює обʼєкти MatchData
, а з них вже отримує розташування та довжину збігів. Та, профілювання ясно показує, що саме виділення памʼяті для MatchData
становить додаткові витрати часу, які ми бачили вчора.
Чому String#scan
потрібні обʼєкти MatchData
? Тому, що цей метод шукає фрагменти не тільки на початку, а й в будь-якому місці рядка. До того ж він підтримує регулярні вирази з підгрупами. А StringScanner
розрахований на єдиний випадок використання. Маємо висновок: у програмуванні спеціалізація коду веде до його оптимізації.
12.09.2023
Використання StringScanner для прискорення парсера на Ruby
Вже писав, як я прискорив бібліотеку для Ruby css_parser прибиранням регулярних виразів. Проте це було не єдине покращення того сезону. Нещодавня стаття Аарона Патерсона про клас StringScanner нагадала, що я теж залучав StringScanner, щоб прискорити парсер на 80%.
Зміни можна подивитись у цьому коміті. Як бачиш, вони мінімальні: просто замість методу String#scan
використовую клас StringScanner. Цей клас існує ще з Ruby 1.8 - саме час його вивчити. :)
Бо мій канал не для того, щоб просто сказати, який клас хороший. Розберімося, чому. Перший ключ в тому, що він цілком написаний на C. Проте так само і String#scan. Взагалі в Ruby багато низькорівневих методів реалізовані на C, що трохи розвіює міф про те, що Ruby обовʼязково має бути повільніше за мови, що компілюються.
На жаль, профілювання коду Ruby не пояснить нам різницю між цими реалізаціями. Зате ось gist з бенчмарком, який показує, що на тривіальному прикладі StringScanner
навіть на 170% швидше. Тобто різниця полягає не в виконанні регулярного виразу (тут тривіалізованому), а в тому, що відбувається окрім нього. Єдине, що обидва методи роблять окрім регулярки — це повертають знайдений рядок.
Я здогадуюсь, що різниця у виділенні памʼяті. Якщо пропустити мій бенчмарк через benchmark-memory, то виявиться, що String#scan
виділяє в 5 разів більше памʼяті. Здається (без подальшого профілювання), що String#scan
(тобто rb_str_scan) містить більш узагальнений код, а StringScanner#scan
(тобто strscan_scan) - зоптимізований рівно під повертання результату з початкової позиції. (Так, до речі, є різниця в тому, що StringScanner шукає тільки на початку рядка — але зазвичай це саме те, що нам потрібно.)
Висновок тут, як і в попередній історії, такий, що виділення памʼяті всередині циклу може бути вузьким місцем та впливати не тільки на витрати ОЗУ, але також й на швидкість виконання.
11.09.2023
Інтуїція про бази даних. Чому OpenSearch агрегує дані швидше, ніж Redshift?
Отримав сьогодні таке питання — напівжартома — бо дійсно, те, що в Redshift займає десять секунд, в OpenSearch може бути виконано миттєво. Йдеться про агрегаційний запит на кшталт “підрахувати статистику суми поля по часовій шкалі.” Думаю, дуже важливо інтуїтивно розуміти, в чому саме різниця. Я вже писав про архітектуру OpenSearch/ElasticSearch, але що по цьому конкретному випадку?
Моя перша здогадка була — що OpenSearch тримає ці дані в памʼяті, а Redshift ні. Проте при перевірці виявилось, що це не зовсім так. Насправді OpenSearch не завантажує всі індекси в памʼять. Зате індекси на диску зберігаються в такому форматі, який готовий для прямого читання в структури памʼяті. Для того є механізм mmap, який уможливлює швидкий доступ до файлу в памʼяті без зайвого копіювання даних. Про це можна почитати в документації до модуля Store.
Проте mmap
використовується, напевно, для читання індексів в усіх базах даних. Справжня причина швидкості OpenSearch не в тому, що індекси розташовані в памʼяті.
Причина, на мою думку, в тому, що Redshift, як реляційна база даних, підтримує незрівнянно ширший спектр операцій. Тому вона має складний конвеєр даних, який не потрібний для простих агрегацій. Почнемо з того, що Redshift компілює кожний запит — тільки це може зайняти від 7 секунд. Це та наступні етапи виконання розраховані на маленьку кількість складних аналітичних запитів, які будуть обʼєднувати великі масиви даних реляційними операціями.
В той час як OpenSearch, хоч теж розрахований на великі обсяги даних та на масивну паралелізацію, але обмежений простими запитами, які працюють на рівні індивідуальних документів. Тож моя остаточна відповідь - OpenSearch швидше, бо простіше ніж Redshift. Такий тут компроміс.
10.09.2023
В що я граю останнім часом
Цієї осені дуже складно обрати між безліччю смачних релізів: вже не можу вирішити між Baldur’s Gate 3, Armored Core 6 та Starfield. Поки граю в інше:
-
Ashes: Aftergow - продовження мода для Doom, про який я вже писав. Деталізація оточень бездоганна. Тут просто приємно мандрувати постапокаліптичними торговельними центрами, ресторанами та станціями.
-
Quake 2 Remastered - ремастер, що вийшов нещодавно, додав до Quake 2 рівно достатньо сучасного оздоблення. Велика роздільна здатність, покращене освітлення, всі старі та ціле нове доповнення. Quake 2 - це чудова коридорна стрілялка та нічого зайвого. Класика як вона є.
-
Detroit: Become Human - раніше уникав цієї гри, бо зовсім не резоную з її темою. Проте ось що в ній цікаво: сюжет має десятки суттєвих розгалужень в залежності від твоїх дій — правильних та хибних — та після кожної глави можна побачити дерево рішень. Чого тут не вистачає, так це можливості швидко перестрибнути частини сюжету, які вже бачив. Бо я б хотів поекспериментувати з різними комбінаціями. Оскільки в Detroit немає “game over”, а гра завжди йде далі, то зараз роблю найцікавіший експеримент: дивлюся, наскільки можна зіпсувати сюжет поганими рішеннями.
-
The Centennial Case: A Shijima Story - FMV-детектив (тобто фактично, кінодетектив, в якому розвʼязки маєш обрати ти). Логічні ланцюги інколи ведуть не туди, куди думаєш, але, як каже протагоніст в перші хвилини, “в детективній історії траплятимуться підказки до таємниці, та якщо уважно стежити за ними, вони приведуть тебе до відповіді”. Тому приємно, що навіть коли не доходиш до розвʼязку сам, то принаймні ясно, які прикмети не помітив. До того ж гра є утішною цифровою подорожжю в Японію — бо зроблена японцями про японців для японців (наполегливо рекомендую увімкнути японську озвучку.)
09.09.2023
Typescript обмежує
На мою думку, найстрашніша помилка при переході з JavaScript на TypeScript - це очікувати, що доведеться тільки додати до свого коду типи, а структуру кода та підходи можна не змінювати. Так і зʼявляються десятиповерхові типи, неможливі для розуміння.
-
Підхід з гнучкими аргументами, дуже популярний в JavaScript. Наприклад, коли функція може приймати опції та колбек, або тільки опції, або тільки колбек, або колбек як одну з опцій… Така функція утворює складні для розуміння типи з багатьма альтернативами — а значить, поганий досвід використання. Згадаємо, що такий підхід зʼявився саме як спроба покращити життя споживачам, які можуть помилитись з порядком аргументів — проблема, яку вже вирішує TypeScript. Тому краще зупинитись на одній манері виклику функції- тоді у споживачів буде чітке розуміння.
-
Обʼєкти з нечітко визначеним набором полів. Теж типове явище в JavaScript. Поля наповнюються в різні моменти життєвого циклу обʼєкта. Зазвичай ми здогадуємось про наявність того чи іншого поля за контекстом. Наївний підхід в TS - це перевіряти кожне поле на наявність — або не перевіряти зовсім, а присипати код оператором
!
. Це нівелює всі переваги від використання TypeScript. Краще робити вузькі типи для кожної конкретної ситуації. Це також допоможе формалізувати логіку та описати перебіг даних. -
Функції, які доповнюють обʼєкти додатковими атрибутами. Як широко відомий приклад — функція connect в Redux. Доповнення обʼєктів призводить до жахливих типів; бо, наприклад. доведеться обмежувати, що вхідний обʼєкт не мав атрибутів, що додаються. Краще не доповнювати обʼєкти, а повертати нові. Або принаймні якщо доповнювати, то фіксованим набором атрибутів.
Так само як складні тести сигналять про поганий факторинг коду, складні типи сигналять про нечіткий життєвий цикл даних. Такі та інші “нечіткі” ситуації треба перекладати на код з простішими типами. А також не писати нового коду на TypeScript зі своїми нечіткими підходами з JavaScript.
08.09.2023
Завантаження всіх записів з Кафки
Стикнувся сьогодні з ще однією особливістю Кафки. А саме: у топіків Кафки немає кінця. Якщо дивитись не неї як на чергу, то це логічно. Проте оскільки Кафка також є сховищем даних, то все ж трохи дивно не знати, де ті дані закінчується.
API Кафки побудоване на очікуванні нових даних, якщо їх немає напоготові. Типовий споживач буде просто отримувати дані, поки його не зупинять. Але що якщо ми хочемо звантажити все, що є на цей момент — наприклад, для експорту чи резервної копії? Навіть якщо встановити порожній інтервал очікування, ми все одно ризикуємо потроху отримувати все новіші записи, які надходять, поки ми обробляємо попередні.
Натомість потрібно встановити власний критерій зупинки. Простіше за все дивитися на поле Timestamp
, яке є у кожного запису. Коли таймстемп перебільшить час початку споживання — значить, пора зупинятись.
Взагалі, корисно знати, що окрім власне ключа та змісту у записів ще є відбиток часу та можуть бути метадані. Ці дані легше (та швидше!) отримати, ніж займатися розбором змісту.
Залишається ще тільки один нюанс: треба памʼятати, що топік ділиться на розділи, а споживач отримує записи паралельно з різних розділів. Записи в різних розділах ніяк не повʼязані та можуть йти в довільній послідовності. Тому зупиняти споживання треба не тоді, коли побачиш новий таймстемп, а коли закінчаться старі.
07.09.2023
Як типи заміняють тести
Є два різновиди помилок: помилки інтеграції — коли код поєднаний неправильно, та помилки логіки — коли код використаний правильно, але задача була зрозуміла або реалізована невірно.
В ідеалі все, чим має займатись інженер — це реалізація бізнес-логіки. В будь-якому разі на логіку доведеться писати тести. Але незалежно від мови, при цьому ми будемо неправильно розуміти та використати код, тобто робитимемо помилки інтеграції — будемо передавати рядок замість обʼєкту, повертати null
де очікується значення, і так далі. Витрати на помилки інтеграції не вирішують бізнес-задачі — для бізнесу вони є марними.
В мові з динамічною типізацією перевірити, що код написаний правильно, можна тільки запустивши його. Тому хороший набір тестів має запускати весь код та хоча б в більшості комбінацій параметрів. Навіть юніт-тести в динамічно типізованій мові здебільшого викликають ті модулі, на які мають залежність, бо без того тест не виконує половину своїх обовʼязків.
В мові зі статичною типізацією саме типи й підтверджують, що код інтегрований вірно. А саме, що функції приймають та повертають значення таких типів, як це очікується. До того ж перевірка типами застосовується до абсолютно всього коду, при тому, що зазвичай немає потреби всюди явно оголошувати типи — компілятори добре вміють їх виводити автоматично. Це звільняє нам час краще перевірити логіку. Тобто призводить до якіснішого коду.
Звісно, моделі типів не є строго статичними чи динамічними, вони існують на шкалі, тому в кожній мові баланс типів та тестів свій.