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

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

Підписатись на RSS
📢 Канал в Telegram @stendap_sogodni
🦣 @stendap_sogodni@shevtsov.me в Федиверсі

17.01.2023

Краща оптимізація запиту до бази — це його не робити

Наступна оптимізація — уникнення запитів для нових акаунтів. Справа така. Для акаунта треба знати дату початку роботи, спираючись на журнал. Копатися в журналі — довго — декілька секунд. Тому я зробив кешування: оскільки дата початку роботи — історично сталий факт, то обчислювати її потрібно тільки раз. До речі, у Rails є приємний режим Rails.cache.fetch(key, skip_nil: true), при якому в кеш не потрапляють порожні значення. Тобто, як тільки в журналі є запис, в кеш потрапляє його дата. Чудово.

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

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

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


16.01.2023

Віконні функції PostgreSQL, або ж COUNT+LIMIT одним запитом

Сьогодні повернувся до роботи, а робота в мене зараз — це боротьба з Redshift. Тому маленька, але дуже цінна порада. Вона стосується тієї ситуації, коли потрібно для деякого запиту одночасно й обмежити кількість рядків, і дізнатись, скільки їх є взагалі. Тобто класичної пейджинації.

Я все життя просто робив окремий запит. Тільки що перевірив, і виходить, що не тільки я — найпопулярніша бібліотека для пейджинації у Rails - Kaminari - теж робить окремий запит. У нормальних умовах це і не погано, бо ніхто і не помітить уповільнення. Але якщо у вас пейджинується складний запит, виконання якого займає 3 секунди, то робити його другий раз вже не хочеться. Проте моніторинг, який я передбачливо налаштував перед відпусткою, саме таке і показав — що виклик API робить 2 майже однакових запити — один на COUNT, один на LIMIT - та замість трьох секунд триває шість.

Мені не хотілось вірити, що це найкраще, на що здатний Redshift, тож озброєний цією вірою, пішов шукати розвʼязок. Та знайшов, у так званих віконних функціях. Якщо просто, то віконна функція оперує не тільки поточним рядком, але й іншими рядками. Головне, вона виконується до операції LIMIT. В моєму випадку потрібна найпростіша функція: COUNT(*) OVER (). Вона значить: підрахувати рядки, використовуючи вікно без жодних обмежень, тобто всі рядки. Але з віконними функціями можна робити й цікавіші речі — наприклад, повертати в кожному рядку статистику по рядках зі схожими атрибутами. Віконні функції зустрічаються в секції SELECT, тобто результат буде повернений як стовпчик:

SELECT *, COUNT(*) OVER () AS total_count FROM big_view WHERE ...

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


15.01.2023

Як рахувати кількість підписників RSS з Vercel та Plausible

Сьогодні склав цікавий пазл та зробив для блогу збір аналітики по RSS. Колись для цього використовував різні проксі, наприклад, Feedburner. Та вже декілька років я віддаю перевагу RSS стрічці на власному домені, разом з самим блогом. А блог в мене давно розміщений на різних статичних хостингах, останнім часом — на Vercel. Все чудово, але статистику по відвідуваннях стрічок він не надасть. Не дасть її й безплатний план Cloudflare. Так що нарешті додав аналітику власними руками. Ну, точніше, збирає аналітику Plausible, а мені залишається тільки відправляти туди відповідні події.

Щоб відстежити запити до RSS стрічок, я підміняю їх на запити до хмарної функції. Для цього у Vercel є правила перезапису адрес. Функції можна писати прямо на TypeScript. У іншому це звичайні обробники за стандартом ExpressJS. Вони мають доступ до всіх файлів зі збірки проєкту. Звідти функція забирає RSS, згенерований Hugo, та віддає споживачу. Ну й ще робить запит до API Plausible. Класно, що весь проєкт разом можна запускати локально, командою vercel dev.

Нюанс: правило перезапису не може мати таку саму адресу, як і файл зі збірки; точніше, файли мають пріоритет. Тому, щоб перезаписати RSS стрічки, потрібно було перенести їх в інше місце; я приписав до них суфікс-розширення. Ще нюанс: функція не може перевищувати 50 МБ, а за замовчуванням у пакет потрапляють абсолютно всі файли. Проте опцією excludeFiles можна прибрати зайві; я просто прибрав світлини.

Далі, щоб зареєструвати подію, потрібно викликати API Plausible. Раніше цей API був напівприватним, прихованим, а тепер доступний офіційно. Ми його вже використовуємо для аналітики додатка iOS. У випадку з RSS, достатньо одного типу події, з відповідними властивостями. Це, звісно, агент браузера та IP користувача, а також URL стрічки (бо в мене їх декілька).

Тут теж нюанс: заголовок X-Forwarded-For, що містить IP користувача, не передається з CloudFlare через Vercel. Тобто Vercel його затиратиме. Але немає проблем, бо CloudFlare дублює заголовок своїм нестандартним CF-Connecting-IP, а він передається нормально.

Нарешті, агент браузера я також передаю як власну властивість події, тому що мені здається, що великі агрегатори RSS на кшталт Feedly можуть передавати в рядку агента внутрішню кількість підписників. Але це ще подивимось.

Отак. Здається, все просто, але це не враховуючи нюансів.


14.01.2023

Сни

Сьогодні на Старий Новий рік буде трохи дивний пост. Про сни.

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

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

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


13.01.2023

Огляд монітора Sony inZone M9

Набридло чекати на появу кращого 4K монітору, тому замовив 27-дюймовий Sony inZone M9, який вийшов минулого літа, та він нарешті приїхав з США. Обирав його довго, бо 4K монітори всі бракують тієї чи іншої функції, та й взагалі “маленьких” 4K моніторів на ринку мало. (До речі, в цьому році на виставках почали зʼявляться OLED монітори, тож, мабуть, тепер варто ще почекати.)

Про хороше: 27 дюймів та 4K, звісно. HDR! Локальне затемнення! Матова поверхня. Працює по USB-C через док (Caldigit TS3) з підтримкою HDR та VBR. Краще прокидається від сну, ніж LG Ultrafine. Немає засвітлення, як у LG Ultrafine. Багато входів. Вбудований KVM switch (це можна поруч поставити ноутбук та ігровий ПК та перемикати клавіатуру + мишу автоматично через монітор).

Про погане: 1) якась потворна ніжка — але я планував ставити його тільки на штатив VESA; (до речі, люблю ось цю модель штатива від Розетки.) 2) Прошивка оновлюється тільки з Windows. Та оновлення варто було зробити, тому що виправили нюанси пробудження монітора зі сну, а не дрібниці якісь. Що ж, відтягнув монітор до медіацентру, поклав на крісло, під’єднав до ПК, оновив. 3) Мак не вміє регулювати ані яскравість, ані гучність монітора. Гарно що є чудова утиліта Lunar, яка вміє. 4) До речі, динамік у ньому суто символічний, навіть в айфоні краще. 5) Локальне затемнення при роботі з текстом на темному фоні, або взагалі з однотонним темним фоном, створює плями підсвітки навколо яскравих елементів — навіть курсору. Але його можна відключати.

Якщо підсумувати, то, мабуть, не раджу купувати його для макбуків, бо монітор потребує “особливого набору навичок”. Шкода, бо апаратна частина вражає.


12.01.2023

Мій настінний календар

Сьогодні нарешті опублікував на GitHub свій генератор настінних календарів з багатою історією.

У 2015 році автор коміксу Wondermark поділився своїм настінним календарем. Ідея календаря проста: він складається з нескінченного потоку аркушів, кожний з яких повністю заповнений сіткою з днів. Ідея мене зачепила моментально. Дуже зручно бачити на стіні цілий рік. В мене він висить над кухонним столом. На ньому можна малювати, закреслювати дні, планувати — причому всією сімʼєю. Набагато наочніше, ніж в електронному календарі.

Після того, як оригінальні аркуші закінчились, я вирішив написати генератор, щоб надрукувати більше. Генератор збирає PDF-файл бібліотекою jsPDF. Формат PDF - це, по суті, векторний графічний формат: в ньому кожний рядок тексту та кожна фігура задаються та позиціюються окремо. У розверстці календаря є трошки ручних налаштувань, щоб все влізло, але окрім цього згенерувати сітку з днями зовсім нескладно. Також я вирішив додати до календаря свята та інші сімейні дати, відомі заздалегідь.

З таким календарем на стіні ми живемо вже років пʼять. Весь цей час (а точніше, кожний новорічний сезон) я хотів зробити з генератора календарів продукт. Минулого року нарешті спробував. Але виявилось, що генерація PDF - досить складний для монетизації процес. Тож генератор залишався неопублікованим.

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


11.01.2023

Антіпаттерн Reselect: один селектор залежить від даних іншого

const category = useSelector((s) => selectCategory(s, expense.categoryID));
const goal = useSelector(
  (s) => category?.goalID && selectGoal(category.goalID)
);

Антіпаттерн Reselect - коли один селектор залежить від даних іншого. Поганого тут те, що селектори створюються у момент виклику компоненти. А значить, щоб повністю отримати або оновити дані, доведеться неодмінно викликати її двічі — для отримання результату першого селектору, а потім другого. (От вам і несподіване уповільнення.)

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

Щоб спрямити антіпаттерн, треба сконструювати селектор, який відразу повертає всі необхідні значення. Якщо він повертатиме складений тип — обʼєкт чи масив — не забути зробити селектор стабільним. Тоді не тільки можна буде уникнути подвійного виклику компоненти, але й, можливо, взагалі її не викликати, якщо результат селектора не змінюється.

Як правило, компоненти React мають містити тільки логіку відображення даних. Логіка комбінації даних належить до шару Reselect.


10.01.2023

Інтернет у відсутності електроживлення

Як отримати інтернет за відсутності електроживлення? Тут варіанта три. Ідеальний — це оптика до квартири. Оптичне підключення не потребує підсилення, тому сигнал йде від провайдера напряму. Якщо у вас є живлення, та у провайдера теж — інтернет буде. Для цього вкрай необхідно, щоб оптичний кабель доходив аж до квартири — не всі провайдери так роблять, у мого Київстару оптика тільки до даху, тож без світла інтернет зникає. А ось інший місцевий провайдер дотягнув оптику до хати. Оптичний кабель закінчується адаптером, з якого виходить звичайна вита пара. Зазначу, що цей адаптер варто розмістити в коридорі або тамбурі, бо пересувати його буде неможливо без майстра та зварювального апарату. Але ж також не забувати, що адаптер треба буде живити, в тому числі від павербанка — про це далі.

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

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

Тепер — як живити роутер та оптичний адаптер? У обох є блоки живлення зі звичайною вилкою 220V. Якщо у вас є великий павербанк з інвертором та розеткою, то питання ніби вирішено. Але навіть якщо так, краще буде живити пристрої від джерела постійного струму (DC) - найстандартнішим з яких на цей час є USB. Річ у тім, що блок живлення роутера саме й перетворює 220V назад на постійний низьковольтовий струм. На перетворення туди й назад витрачається коштовна енергія. (До речі, тому й освітлення від павербанка краще живити від USB.)

Тож як під’єднати роутер до USB? Спочатку треба визначити, який саме розʼєм у роутера; для цього виміряти його діаметр, а ще подивитись на блоці живлення напругу та струм. Якщо роутеру потрібно не більше 12V - то його можна заживити від USB адаптером на кшталт такого. А більше — тільки від USB-C - тоді адаптер потрібен такий. Відповідно, павербанк має видавати достатню потужність, а головне — підтримувати напругу. Бо USB-C може видавати різну напругу, від 5V аж до 48V в найбільш сучасному стандарті — але на яку саме спроможний павербанк, можна дізнатись в його документації.

Як абсолютна протилежність всьому цьому - 4G-WiFi роутер, який можна вставити в будь-який павербанк з USB та отримати WiFi з мобільним звʼязком. Не так надійно, зате просто та дешево.


09.01.2023

Дискриміновані союзи у TypeScript як засіб розширення додатків

Продовжуючи тему Сінтри, з “Цілями та заощадженнями” зʼявляється новий різновид витрати — витрата з переводом на заощадження. До неї застосовується особлива логіка, наприклад — замість категорії відображатиметься заощадження. Логічно.

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

В типізованій мові могло б бути так же. Наприклад, якщо в TypeScript до типу Expense я додам нове поле goalID, то так само доведеться шукати всі місця використання цього типу. Але у TypeScript є краще рішення на таку ситуацію — дискриміновані союзи. Це спосіб, фактично, сказати, що у витрати є дві різних форми — або з полем categoryID, або полем goalID. Тепер весь код, що очікує бачити витрати з категоріями, стане помилковим, оскільки не враховує всі можливі варіанти типу. Щоб виправити його, потрібно додати до коду перевірку на тип витрати. Так автоматична перевірка типів замінить нам ручне тестування.

При цьому, що важливо, інформація про тип дискримінованого союзу береться з самого обʼєкту — зі значення або наявності поля-“дискримінанта”. Тож з боку JavaScript та, головне, з боку бази даних цей тип залишається звичайнісіньким обʼєктом JSON.


08.01.2023

Як я будую структури даних додатків на React за допомогою reselect

Сьогодні день Сінтри; просуваюсь до розділу “Ціли та накопичення”. Доречно буде розказати про мою улюблену схему організації потоку даних для React/Redux.

Я звик розташовувати у Redux дані у найбільш спрощеній формі. (Частково тому, що я віддаю перевагу архітектурі без бекенду, тому не маю можливості перенести логіку туди, щоб отримувати більш підготовані дані.) Далі всі форми, що необхідні для відображення компонент, створюються шаром селекторів, тобто функцій, що отримують на вхід стан Redux та повертають те, що потрібно. Компонентам залишається підписатись на результати селекторів хуком useSelector.

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

Другий — селектори reselect. Їхній результат кешується, але дещо нетривіально. Функція-селектор будується з функцій підготовки аргументів, та “робочої” функції, що обчислює результат. Поки аргументи не змінюються, “робоча” функція не буде викликана. Так, наприклад, можна побудувати селектор, що буде підраховувати загальну суму витрат — але тільки тоді, коли у стані змінюються витрати.

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

Але з селекторами, що повертають складні обʼєкти, є проблема: згенерований ними результат буде змінюватись за посиланням навіть коли він залишається сталим за значенням. У такому випадку всі залежні селектори та компоненти будуть перераховані зайвий раз. Ну, наприклад: в мене є селектор, що з масиву цілей робить обʼєкт-словник, проіндексований за ID. Він лежить в основі багатьох інших селекторів. Всі вони будуть перераховані кожний раз, коли є зміни в будь-якому місці вхідного масиву. Саме через такі марні витрати й починають гальмувати додатки на React. Я про це писав цілу статтю, висновок якої — що треба для таких випадків використовувати стабільні селектори, які додатково перевіряють, чи змінився обʼєкт за значенням.

Всі три різновиди селекторів мають інтерфейс звичайної функції, та є взаємозамінними, тому їх легко рефакторити або додавати нові.

До речі, саме таку архітектуру рекомендує сучасний Redux.