Стендап Сьогодні
Що я зробив, що я хочу зробити, і що це все значить.
Повсякденні здобутки в форматі стендапу.
Детальніше в статті
Підписатись на RSS · 📢 Канал в Telegram @stendap_sogodni
27.09.2024
Флешкартки Anki для запамʼятовування колег
Флешкартки “обличчя => імʼя”? Чом би й ні! Вийшов напівкріповий, напівкорисний помічник для корпоративів. А ще це такий проєкт, що й не дуже поділишся, бо залежить від вашої специфіки. Так що ділюся ідеєю.
Перелік колег в мене легко забрати зі Slack. Ну як — легко. Технічно вони там всі є, передивляйся скільки хочеш. Але з автоматизацією складно. В Slack можна отримати ключ тільки через застосунки. Та виявилося, що в нас тепер потрібно просити дозволу на додавання нових застосунків. Вирішив, що цей ранковий хак не вартий такої церемонії та пішов шукати інших шляхів.
З браузера в сеансі користувача теж не так легко викликати API - очевидно що це можливо, проте fetch
напряму не запрацював. Тоді знайшов обхідний шлях: є тестер API, в який можна встромити ключ, забраний з сеансу браузера, та тоді тестер цілком нормально віддає результати. Це хоч і не автоматичний шлях, але принаймні можна забрати весь перелік користувачів за 2 запити. (А ще в того переліку немає фільтрів, окрім як за командою. Навіть видалених користувачів не можна сховати.)
Тепер до Anki. Чомусь красивої офіційної інформації про формат не знайшов. Хоча всередині сидить просто база SQLite та файли картинок. Знайшов гем anki2, яким легко згенерувати той файл .apkg
, який імпортується в Anki. Формат карток — ніби HTML-lite. Єдине нарікання — що гем генерує випадкові ідентифікатори карток, тож коли перегенеруєш теку, то створюються дублікати — хоча сам Anki вміє домішувати нові картки.
До речі, я користуюся мобільним Anki для iOS, він дорогущий, але зручний - зокрема автоматичною реалізацією розподіленого повторення. В цілому флешкартки рекомендую для вивчення будь-чого.
26.09.2024
Торт
🎂 Син замовив на день народження торт їжачок. А потім я здогадався спитати, з чим робити його та отримав список інгредієнтів: горіхи, кава, тирамісу, крем як в київському торті, коктейльна вишня, ананас… лікер Блю Кюрасао.
Це звучить трохи неможливо (та я спочатку пошкодував що спитав), але насправді навіть з такими вимогами можна працювати. Торт складається з: коржів, крему внутрішнього, крему зовнішнього, прикрас. Залишається знайти рецепти для кожної частини.
Знайшов оцей рецепт торта з кавою та волоським горіхом. То вже кава, горіхи, та навіть масляний крем. Але куди взяти тирамісу? Оскільки в коржах вже є кава, придумали замінити внутрішній крем на маскарпоне. Знайшов навіть рецепт торта тирамісу, щоб взяти з нього крем, бо мені гадалося що типовий крем від тирамісу буде надто вологим.
Вишню та ананаси взяли на прикраси. (Ананаси я майже відмовився брати, але вони з дружиною мене вмовили.) А що робити з блю кюрасао? Якщо ви не знаєте, то це апельсиновий лікер з синьою фарбою. Єдине, куди він більш-менш вписувався — це в зовнішній масляний крем. Знайшов ще один рецепт гавайського торта з таким кремом.
(Взагалі лікер надто слабкий для крему — на смак та за кольором. А багато його не додати через алкоголь. Колір я виправив барвником, але для смаку гарно було б додати апельсинову есенцію або бітер.)
Проте найскладніший для мене момент настав, коли треба було перетворити торт на їжачка. Як кумедно, що підбір рецептів та розвʼязання вимог — це моє — як на роботі — а в дизайн лізти не хотілося. Тут допомогла дружина, все що бачите зверху то її старання. Моє, інженерне, все всередині. Зате було смачно! 🦔
25.09.2024
Рефакторинг в чистому полі
Всі красиві інструкції з розробки починаються з постановки результату та кроків до нього. Але в мене часто трапляються задачі, де результат визначений дуже абстрактно. Наприклад: замінити стару бібліотеку для логінгу на нову. Або: впровадити систему суворої рівночасності для Swift 6.
Зазвичай з такою задачею починаєш щось робити, та потроху приходиш до результату. На жаль, шанси 50 на 50, що посередині загрузнеш в змінах, а код все ще не працює. Одне чіпляється за інше, оновлення тягнуть залежності, а ті — зміни інтерфейсів, і так далі. Чим більше “нестабільних” змін, тим важче робити наступні: ламається перевірка коду, тести, залишається спиратися тільки на власне уявлення.
Я знайшов, що легше в такому випадку відкотитись назад, скласти список вже відомих кроків, та робити їх по одному. На перший погляд, шкода “починати спочатку”, проте розуміння плану все спрощує, та робить деякі зміни суто механічними. Плюс, з Git все зміни можна відкласти в окрему гілку.
Наприклад, при впровадженні рівночасності було потрібно замість обʼєктів моделі передавати їхні ідентифікатори. Таку зміну легко зробити ізольовано та вже про неї не думати. Також легко обмежити інтерфейси, які стануть рівночасними, та спростити подальшу роботу — тобто легко, коли компілятор у свідомості та робить підказки, а не посередині величезного рефакторингу.
От би наступного разу згадати, коли сидітиму третій день з тисячами рядків змін та купою помилок компілятора.
24.09.2024
Виконання вимог клієнтів
У продуктах B2B завжди доводиться робити чимало змін суто через вимоги того чи іншого клієнта. Це абсолютно нормально. Мистецтвом інженера тут є впровадження змін так, щоб не перетворити код в мішанину не повʼязаних шматочків логіки.
В ідеалі можна знайти спосіб узагальнити функціонал. Уявимо платформу для продажу. Наступний клієнт просить додати звіт в якомусь форматі XML. Після дослідження виявляємо, що формат нішевий, але поширений. Чудово — додаємо загальну опцію до експорту звіту, отримуємо нову здібність для нашого продукту.
Але не всі запити можливо або розумно узагальнювати. Не кожне узагальнення корисне. Навіть навпаки — більшість узагальнень тільки все ускладнюють. Легко зрозуміти повністю конкретизований код, або така абстракція, яку можна зрозуміти без конкретного прикладу. Посередині лежать напівузагальнення, які не спираються на специфічні обставини, але потребують їх знання.
Наприклад, новий клієнт — автомагазин — хоче надавати знижку на придбання разом шин та дисків. Можна узагальнити від конкретних категорій, зробити модель на кшталт Знижка(категоріяА, категоріяБ)
. Ніби корисно. Але чи буде точно така формула потрібна іншим клієнтам? Не впевнений. Гарно було б зробити універсальний рушій правил знижок — гадаю, в реальних платформах таке є. Проте рушій знижок — задача на пару місяців (та ще не зрозуміло, чи вірна.) За першою потребою ми його робити не будемо.
В таких випадках я б краще додав конкретний та стислий блок коду, можливо, навіть з ID клієнта та категорій, та поки не городив би таблиць в базі та іншої архітектури. Зі специфічними змінами легше впоратись, коли вони ізольовані.
23.09.2024
Семафори
🚥 Подолав сьогодні найскладніший для рівночасності модуль. То є мій порт GIF2MP4.swift, якій побудований на бібліотеці AVFoundation
. Вона ще не була перероблена не тільки на async/await
, а, здається, навіть на Swift (бо більшість бібліотек Apple для Swift почали життя як прямий порт з Objective C, а потім були переосмислені на ідіоматичний Swift.)
Проблемний код був навколо класу AVAssetWriterInput
- він збирає відео по кадрах. Зрозуміло, що це споживає багато ресурсів, тому кадри ми передаємо в міру потреби. Для того є метод requestMediaDataWhenReady. Умовно, ми передаємо йому “тіло циклу”, який генеруватиме кадри.
Все б добре, але у Swift 6 більше не можна передати в той блок ані джерело кадрів, ані навіть змінну-лічильник — нічого змінного. Дилема: як зробити цикл без стану?
Я придумав інвертувати цю інверсію керування. В мене тепер requestMediaDataWhenReady
тільки перемикає семафор. А в головному методі звичайний цикл по кадрах, з одним нюансом: перед ітерацією чекає на семафор.
Вийшло так. Мені навіть подобається! Причому головний метод є синхронним та не потребує колбеку. Нагадує Ruby. Звісно, цей метод блокує аж поки не перекодує все відео, тож він має сенс тільки всередині рівночасного коду (а мені тільки це і треба).
(До речі, ці обʼєкти DispatchSemaphore можна передавати в інший потік, що дуже логічно. А ще є Atomic - контейнер для передачі значень.)
А ще цікаво, що код з семафорами повільніше за async/await
, оскільки у Swift await здатний робити “синхронний” виклик - якщо є можливість — а семафор завжди є повільною зміною контексту. Ну, у моєму випадку рішення з async/await
неможливе, тож маємо що маємо.
22.09.2024
Ще про ізоляцію даних у Swift
Ох і Swift! Такої суворої системи ізоляції я ще не бачив (ну, хіба в Rust, але там я далеко не заходив.) Не очікував такого від мови, яка для екосистеми Apple значить те ж саме, що JavaScript для браузерів — тобто це перша та головна мова, з якою стикаються всі.
Так, я звик до async/await
в JavaScript. Як і до “розфарбовування функцій” - тобто обовʼязкового маркування функцій, де є await
. Але якщо в JS async/await
зʼявляється практично тільки там, де є ввід/вивід — то у Swift будь-яка заявка на рівночасність потребує async/await
та розфарбовування.
Причому рівночасність у Swift нам потрібна, щоб робити програми, де не гальмує інтерфейс. Втім, я б радив почати з @MainActor
всюди, а потім впроваджувати акторів обережно та обмежено, бо вони досипають в код несподіваних ускладнень. Наприклад:
-
Так виходить, що й конструктори, й звернення до атрибутів, й передача замикань — все це “розфарбовується” за актором-власником, і все потребує особливого ставлення — тобто якісь виклики стають
async
, а якісь ні. Переорганізація коду змушує це переглядати, разом з місцями виклику. -
Якщо в класі є атрибут, що належить до іншого актору, та ми його ініціалізуємо, то сам конструктор стає асинхронним (оскільки містить асинхронний конструктор атрибута.)
-
Також не всі типи можна передавати з актора акторові; це змусить вас переписати потоки даних. Зокрема моделі SwiftData потребують ізоляції, тож замість моделей доведеться передавати ідентифікатори. Ще не можна передавати нічого змінного. Хочеш повернути результати — роби це у функціональному стилі, без мутації.
-
Взагалі понад усе дратує саме те, що рефакторинг коду акторів змінює цілі шари функцій з синхронних в асинхронні та навпаки. Чого практично не трапляється в JavaScript, бо там
async/await
випливає з потреб коду (наприклад, в читанні файлів), а у Swift - суто з його організації. Отак.
21.09.2024
Оновлення до Swift 6 та його моделі рівночасності
Оновлював свої застосунки на Swift до свіжого Swift 6. Це вимагає дотримання моделі рівночасності. Досвід був цікавий. Простий застосунок майже не потребує змін. Але деякі функції вимагають переписування, навіть якщо в моєму коді немає акторів.
Мій застосунок з HealthKit був найбільш неприємним. HealthKit отримав новий набір API з асинхронністю, що чудово. От тільки старі API викликають непрозору помилку в асемблерному коді та ще й без стека. Суть помилки, як я зрештою зрозумів — що колбек був викликаний не в очікуваній “черзі”. А саме, в черзі @MainActor
, а не в фоновій. Довелось переписати весь код з колбеками на API з async
, причому розшукувати його вручну. Результатом я задоволений, але досвід розробки максимально вражий.
Ще в мене є застосунок для перегляду світлин, в ньому є фонове завантаження. Тут модель акторів дійсно корисна. Вирішив для себе, що найзручніше це зробити глобальний актор для менеджера завантажень, та призначити йому всі класи, що стосуються завантажень. Тоді внутрішні операції в цій області відбуваються синхронно, а на стику з інтерфейсом, тобто @MainActor
, зʼявляється асинхронність.
Взагалі ментальна модель безпеки Swift така, що кожна змінна належить одному з акторів. Тому ми думаємо не про послідовність та синхронізацію операцій, а про власність над даними. Та цікаво, що власність не залежить від розташування змінних, наприклад, посередині нашого актора може бути атрибут чи функція з власністю @MainActor
.
Та другий аспект — це типи Sendable
- які дозволено передавати з актора в актора, або захопити в замиканнях з іншого актору. З цим доводиться помізкувати. Наприклад, мав проблеми з AVAssetWriterInput. Там API з колбеком “дай мені ще кадрів”. Цей колбек важко уявити без зовнішнього стану. Але ось проблема: якщо я викликаю метод всередині актору, то колбек все одно буде викликаний в іншій черзі — та в ньому не дозволено змінювати захоплені змінні. Знайшов цікаве рішення, де будь-яку змінну можна зробити Sendable
, якщо додати до неї обгортку та замок.
Принаймні це оновлення є хорошою вправою та новим для мене способом думати про безпеку рівночасних операцій.
20.09.2024
Кожний сеанс продукту — це тестування на юзабіліті
Я собі давно виробив звичку, що коли хтось інший використовує мій продукт, а я опиняюсь в ролі спостерігача, то звертаю увагу на незручні аспекти та занотовую їх. Звісно, це ціла дисципліна — перевірка на користувачах — але не обовʼязково для того збирати фокус-групи.
Інженери рідко опиняються поруч зі справжніми користувачами, і це прикро, бо велика частина продуктового досвіду створюється саме інженерами. Але навколо багато сурогатних користувачів, та моя теза в тому, що будь-яке використання продукту несе інформацію.
Мені особливо подобається спостерігати за тестувальниками. Дорогою до багів можна помітити погану навігацію чи нестачу інформації на сторінці. А час від часу й баг виявляється тільки незрозумілою фічею.
Також спонтанне випробування юзабіліті трапляється в роботі парою, на демо мітингах, у спілкуванні з підтримкою. Навіть за собою можна спостерігати, але це важче, бо ми звикаємо до незручностей.
В юзабіліті аксіома “клієнт завжди правий” перетворюється в “всі помилки користувача випливають з недоліків продукту”. Або з позитивнішого боку: вчити доведеться кожного користувача окремо, а зробити проєкт доступнішим достатньо один раз для всіх.
19.09.2024
SIMD: оптимізація, але не для нашого коду
В темі прискорення арифметичного коду зʼявляється (принаймні, в мене) ідея застосування інструкцій SIMD: “одна інструкція на багато значень”. Я вже писав, що NumPy їх використовує, але ж так само можна й власноруч оптимізувати код. В теорії…
Проблема перша: що ці інструкції потребують суворого дотримання формату вхідних даних. В деяких випадках це легко. Якщо працюєш з 3D-координатами або з кольорами, то багато операцій легко перекладаються на SIMD (що вже зробили популярні бібліотеки.). Ось гарна стаття. Але в загальному випадку доведеться на кожну арифметичну операцію дивитися окремо. До того ж достатній обсяг пересувань перекриє виграш від SIMD.
Проблема друга: операції SIMD працюють не над всім вашим масивом, а тільки над фіксованою частиною (наприклад, 256 байтів.) Це не так, як з NumPy, де скалярна арифметика “просто” стає векторною. Доведеться думати, як ділити масив та куди зберігати проміжні значення.
Виходить, переписувати програми на SIMD не тільки важко — це ще й робить програму менш зрозумілою. Рідка програма заслуговує на такі зусилля. В деяких випадках SIMD може додати компілятор.
Але здебільшого, SIMD використовується в серйозних бібліотеках для обробки масивів даних. Не тільки математичних — наприклад, є simdjson для розбору JSON. Або будь-який аудіо/відео кодек — саме для кодеків й придумали SIMD, до речі. Або нейронні мережі, бо там все зводиться до множення матриць.
А коли в нас задача на кшталт “виконати більш-менш складний арифметичний вираз над кожним елементом масиву”, то SIMD не допоможе.
18.09.2024
Оптимізація паралельними обчисленнями
Щоб вже закінчити тему з оптимізацією, додав розвʼязок з паралельними обчисленнями до gist.
Вийшло знову несподівано. Я очікував, що, оскільки програма складається на 100% з арифметики, то розпаралелити її можна практично ідеально, тобто ділити час виконання на число ядер.
Технічно програма працює за ідіомами Go: декілька горутін, в кожної є канал на вхід та канал на вихід. Кожна горутіна обробляє частину масиву даних; також є головний потік, який керує процесом та агрегує результати.
Вийшло так, що впровадження горутін уповільнило програму приблизно на 60%. Це якщо горутіна одна, що мало б бути еквівалентно непаралельному рішенню. Стільки часу уходить на синхронізацію. Особливість нашої програми в тому, що кожна ітерація спирається на показник, похідний від усіх результатів попередньої. Виходить, що десь 3000 разів на секунду контроль переходить між головною та робочою горутіною.
Хороші новини, що збільшення кількості горутін все ж дає очікуваний пропорційний приріст.. Тобто в чотири потоки програма все ж на 60% швидше за послідовну — але не на 75, як хотілося б. Більше в мене ядер немає, але гадаю, чим більше ядер — тим виправданіше паралелізація.