Стендап Сьогодні
Що я зробив, що я хочу зробити, і що це все значить.
Повсякденні здобутки в форматі стендапу.
Детальніше в статті
Підписатись на RSS · 📢 Канал в Telegram @stendap_sogodni
14.02.2025
Obsidian Canvas та Excalidraw
Obsidian Canvas - найбільш успішний з інструментів, що я спробував у 2024. Але в цього доповнення є суттєві недоліки. Головний з них — навіть не брак функціональності (хоча в порівнянні з іншими програмами тут сміховинні можливості стилізації.) А те, що Obsidian Canvas не має відкритих точок розширення. Дарма що це офіційний плагін, документовані тільки його CSS-стилі.
Та друге — ще більш тривожне — я не бачу, щоб над Canvas активно працювали. В нотатках до нових версій він майже не згадується. Тож я б сказав, стан Canvas такий, що треба бути задоволеним тим, що є, або не користуватися.
Отже, нещодавно я подивився на доповнення Excalidraw. Взагалі Excalidraw це векторний редактор для вебу, а це доповнення інтегрує його в Obsidian (де редактор працює вже з файлами з Obsidian, без жодної привʼязки до якогось сервера). Документи Excalidraw виглядають (примруживши очі) так само як і Canvas. Зате в Excalidraw багатші образотворчі можливості: не тільки вбудована стилізація, а й бібліотека фігур тощо.
Але можливо, головне це те, що в доповнення Excalidraw є відкритий API. Причому, як я розумію, зроблений конкретно для Obsidian.
Також документи Excalidraw зберігаються так, що їхній зміст видно в пошуку Obsidian (а документи Canvas - ні або неповноцінно.) Структура канви й там, і там знаходиться у JSON, з тою різницею, що в Excalidraw він містить більше атрибутів стилю, тому є більшим за розміром.
Я поки не вирішив, чи переїду на Excalidraw. З недоліків: тут немає форматування Markdown прямо на канві та не так зручно робити стрілочки (доводиться кожну створювати окремо, тоді як в Canvas можна одним рухом миші створити вузол, привʼязаний до поточного.)
13.02.2025
Історія однієї пакетної оптимізації
В тій історії про повільний API є нюанс. Це був внутрішній API між двома тісно повʼязаними сервісами — фактично, RPC, віддалений виклик процедури, бо один з сервісів був витягнутий з іншого (та переписаний на Go.)
Після цього в коді залишилися ще тисячі звернень до функцій, які стали тепер зовнішніми. Функції стосувалися обчислення метрик та раніше були суворо закешовані. Але нові вимоги додавали більше фільтрів та закешувати всі комбінації стало неможливим. Переписування на Go, те ще й зі станом в памʼяті робили обчислення швидкими, втім API накладав вагому ціну.
Перше та критичне покращення в таких обставинах — це впровадити стале підключення замість щоразового. Головне, що це уникає вище загаданого рукостискання TLS. Коли у вас швидкий API, то це реально може бути найдорожчою частиною. (Взагалі цікаво, що головні витрати на шифрування в інтернеті відбуваються в момент підключення.)
Друге — як не крути, тисяча запитів у 10 мс це вже 10 секунд. Звісно, з боку Go я міг робити їх паралельно, тому впровадив пакетні запити та переробив декілька місць. Це досить легко, поки запити відбуваються в циклі — наприклад, будується якийсь графік. Але ж як я казав, застосунок писався із вільним доступом до тих функцій…
Особливо важко було позбавитись серійних запитів у звітах. Там були величезні багатосторінкові шаблони, які зверталися до метрик як у себе вдома. Відокремити доступ до даних від коду звіту було непіднімною задачею.
Тоді я зробив хитрість. Спочатку запускав звіт із фальшивим джерелом даних, яке повертало нульові результати та запамʼятовувало кожний виклик. Потім — викликав справжній сервіс із пакетом викликів, які назбирав. Та нарешті — другий раз запускав той самий звіт, але тепер із підготованим набором даних. Звіт став швидким, а головне — я не перетворив розробку звітів у складну інженерну задачу. It just works!
12.02.2025
Оптимізація: треба йти глибше!
Далі, коли шукаєш причини уповільнення, мусиш ставити під сумнів кожний свій висновок та знаходити для нього обʼєктивні обґрунтування.
Вчора хотів написати пост “чому OpenSearch повільний, коли у вас надто багато шардів?” (Шард — це неподільний блок даних, як партиція; індекс — тобто колекція — складається з фіксованого набору шардів.) Всі радять робити шарди розміром у 10-50 Гб… але чи значить це, що робити їх меншими обовʼязково погано?
Виявилося, не дуже; дійсно, кожен шард коштує трохи памʼяті, але поки памʼять не вичерпана, додаткові шарди не суттєво впливають на швидкість операцій. Негативно — ні, але позитивно — теж ні, навіть попри те, що шард також є одиницею паралелізації. Бо щоб пришвидшити щось паралелізацією, потрібно більше ядер процесора, а не потоків.
Але це я знаю після того, як перевірив. А міг би залишитись із такою гадкою, витратити час на переформування індексів, та… нічого не отримати на заміну. Та це не перша та не остання ланка в розслідуванні! Перевіряти потрібно кожну.
Легко вказати на неприємну частину проєкту та сказати: проблема тут! (Мені, наприклад, доводилося витрачати час на алгоритмічну оптимізацію “некрасивого коду” там, де можна було обійтися як максимум кешем.) Але взагалі шукати причини — це ціле мистецтво. Треба знати, що важливо, що не важливо, та параноїдально собі не довіряти (що, до речі, називається науковим методом.)
Ото було колись — шукав, чому API повільний. Локально наче все було чудово, але більшість часу API проводив у серіалізації, тому я довго обирав оптимальний формат. А потім виявилося, що в продакшні є ще рукостискання TLS, та вся проблема зовсім не в серіалізації, а у встановленні підключення. Так що завжди перевіряти!
11.02.2025
Причина уповільнення вебзастосунка майже завжди в базі
Можу згадати тільки один випадок, де повільні запити до вебзастосунка були спричинені процесорним часом самого застосунку. Це коли я оптимізував css_parser. А в типових ситуаціях завжди потрібно зрозуміти, що робиться з базою.
Або це якийсь один жирний запит — тоді доведеться робити EXPLAIN
, шукати вірний індекс, може, навіть переосмислити схему даних. Але спочатку обовʼязково зрозуміти, який саме запит повільний: без конкретних цифр краще не продовжувати.
Або це багато запитів в циклі — це вже хиба логіки застосунку, та потрібно шукати, як ті запити обʼєднати. Робити запити у циклі майже завжди погана ідея — але зазвичай це не так очевидно, бо процес прихований в шарах логіки. Тут, як і в першому випадку, допоможе логування запитів. (А ще — інструмент на кшталт New Relic, Sentry, Scout тощо, який автоматично збиратиме статистику по запитах.)
В будь-якому разі, що там сам застосунок робить - JSON кодує, HTML малює — буде незрівнянно простіше. Така вона, модель класичного вебзастосунка — важку роботу він перекладає на базу. Помітне навантаження на процесор річ зазвичай передбачувана, бо потребує якоїсь алгоритмічної компоненти, а не тільки пересипання даних з місця на місце.
Отже, будь-яка робота з оптимізації у вебі починається з розуміння та налагодження бази даних.
10.02.2025
Де знайти час на власні проєкти?
Скільки я себе знаю, окрім роботи займаюся чимсь своїм. (Насправді, очевидно, свої проєкти були спочатку, а робота прийшла потім.) Стає питання, як взагалі знайти на це час?
🚥 Насамперед, зазначмо очевидне: досить часто — часу не знайти ніде. Навіть, чесно, можу порадити знайти якесь хобі, не повʼязане з компʼютером та в ідеалі повʼязане з живим спілкуванням. Але програмування така клята професія, що застосувати власні навички поза роботою надто легко та тому завжди хочеться. Якщо на те залишається енергія.
🐦🔥 Отже, я гадаю, що найкращий час для власних проєктів — це ранній ранок. Не тому, що я такий жайворонок, а тому, що тоді є час попрограмувати своє, а потім поснідати, дістатися до роботи, почитати кореспонденцію, та бути готовим програмувати далі. А вечір залишити на все інше.
🦉 Чому це краще, ніж після роботи? Бо робота більше виснажує, ніж годинка програмування для себе. Тому легше з проєкту перемикнутися на роботу, ніж навпаки. (Ну, принаймні це пояснює, чому містер Андерсон ставав Нео вночі, а не о шостій, після роботи.)
🛫 Та другий аспект: для успіху потрібна ясність. Для початку можна побути собі продакт-менеджером та розпланувати роботу, щоб в ту викроєну на проєкт годину було зрозуміло, чим зайнятись. Відсутність ясності вбиває проєкти частіше, ніж фактична відсутність часу.
09.02.2025
Планування роботи, оцінка часу виконання задач тощо
Не виходить з Omniwope сьогодні — наче довів до робочого стану, але для деяких постів нова контрольна сума розбігається з попередньою, зі старого скрипту. Тож треба спочатку зрозуміти, чому так.
🐌 Усвідомив сьогодні одну ваду мислення, яка мені добряче марнує час. Знаєте, є прокрастинація, коли замість діла граєш у гру та насолоджуєшся. А є — коли сидиш та намагаєшся примусити себе до роботи — на яку все одно немає настрою — тому час іде, користі ніякої, втім задоволення теж.
🎯 Отже, що я помітив. Я часто ставлю собі мету у форматі “сьогодні я зроблю це”. Наприклад, “сьогодні я опублікую Omniwope”. Потім чомусь ця справа не йде, а коли вже йде — то виявляється, що ніяк я її сьогодні не встигну: по-перше, там просто роботи більше ніж на день, по-друге, частину дня я вже змарнував.
🌴 Ясно, що йдеться не про весь день, а про частину вихідного, яку я можу витратити на власні проєкти. Але так само відбувається час від часу й з робочими задачами, або зі справами, на які був попередньо виділений весь день.
💀 А тепер постмортем: варто було б виробити звичку ніколи не ставити мету “зробити” багатокрокові та незрозумілі задачі. Зробити можна тільки кроки до такої задачі. Якщо з кроками нічого не ясно, першим можна попланувати — тільки не в голові!
⏱️ Та другий трюк/підхід: оскільки навіть з одним кроком не завжди ясно, скільки часу він займе (от в мене багато пішло на звірку нового та старого рушія) - то щоб цілий день не пручатись, можна спробувати ставити мету попрацювати над цим 20 хвилин. Чи три рази по 20 хвилин. Бо це вже гарантовано реальна мета, чи не так?
08.02.2025
Новини по HugoWope / OmniWope
Доробляю потрохи бот/скрипт/застосунок для ретрансляції блогу куди тільки можна.
Мій оригінальний скрипт займався “всім відразу” - читав пости з диска, розбирав метадані, будував дані для Telegram. Такий скрипт дуже важко зробити загально доступним, бо там навіть на назву розділу на сайті є завʼязка.
Це той випадок, коли “трохи узагальнити” не можна. Наприклад, параметризувати назву розділу — нічого не дасть, бо сама наявність окремого розділу для каналу є специфічним для мене.
Тому, коли я зробив прототип вивантаження змісту з Hugo у JSON, то це дійсно все спростило. Тепер навіть немає завʼязки на Hugo - бо дані можна підготувати звідки завгодно. (До речі, я вже думав про те, що цей JSON міг би бути натомість RSS. Але поки я хочу глибшої інтеграції ніж просто автопостинг RSS, та не хочу обтяжувати себе привʼязкою до конкретного формату з власною специфікою.)
Отже, тепер проєкт називається OmniWope (Write Once Publish Everywhere). Але почнеться з модулів для підтримки Hugo та Telegram; до якого невдовзі додасться ActivityPub, заради якого я все це роблю.
Сховище теж буде модульне, має влаштувати будь-яке сховище Key-Value. Але починаю я з єдиного файлу JSON. Та, до речі, поки відмовився від публікації з CI принаймні для себе, бо надто важко і це відразу охопити.
Я майже опублікував сьогодні весь проєкт, залишилося тільки зробити читання конфігурації. Але ж хіба то легко? Скоріше за все, долучу cobra - це практично фреймворк для застосунків командного рядка — його використовує Hugo та багато інших продуктів.
07.02.2025
Особливості помилок в Go: квазістектрейс
Вже пробачте, що я про Go зарядив (спойлер: завтра буде ще й оголошення невеличкого проєкту на Go), але сьогодні усвідомив один цікавий аспект. Цікавий скоріше тим, хто на Go не пише.
В Go помилка — це, в першому наближенні, так званий “таврований рядок”. Тобто зміст помилки — це рядок, але належність до типу error
чітко відрізняє її від “просто рядка”. У звичайних помилок немає більше нічого.
(Зокрема, в error
немає детальної типізації (ну як, вона може бути, бо error
- це тільки інтерфейс, але це буде нестандартно.) Щоб впізнати потрібну помилку, її роблять константою, та порівнюють за значенням.)
Також в error
немає стектрейсу (є бібліотеки, які його додають, але не загально прийнятні.) Але замість стектрейсу, в Go є загортання помилок. Коли вам повертають помилку, традиційно функцією fmt.Errorf додають до неї власну інформацію.
res, err := http.Request(myURL)
if err != nil {
return fmt.Errorf("loading URL failed: %w", err)
}
На верхньому рівні отримуємо щось на кшталт: failed to calculate balance: failed to load user: database request failed: io timeout
. Так виглядає типова помилка у Go. Та ось сьогодні збагнув, що це ж той самий стектрейс! Тільки ручної роботи. Можна грепати його частини та знайти, звідки вони.
З першого погляду, незрозуміла поробка. Але я знайшов пару переваг, порівняно зі стектрейсами. По-перше, такий загорнутий рядок відносно компактний, що важливо для логування. По-друге, стректрейси псуються при рівночасності (класична проблема інших мов), а загортання помилок — ні, його й з рівночасністю можна робити так, як зручно.
Власне, тому важливо завжди загортати помилки. Навіть є лінтер wrapcheck, який на це перевіряє.
06.02.2025
Go - що це за мова?
В мови Go, беззаперечно, є власний настрій. Та якщо його не вхопити, можна довго боротися з домовленостями та неписаними правилами, яких в Go дуже багато. Go - не та мова, де приємно виражати власний стиль.
Для мене Go - це мова синіх комірців. Не академічна. Не поетична. Не модна. Робітнича. Але при тому Go ніяк не є архаїчною мовою — такі аспекти, як керування залежностями, знаходяться на передній межі сучасності.
Go точно не є мовою, дружною до новачків, бо очікує від програміста розуміння деяких низькорівневих сутностей — як вказівники чи типи даних, та суворого дотримання деяких правил — як відсутність зайвих оголошень та імпортів.
Для мене Go - мова “бери та й роби”. Мова YAGNI. Мова “я встигну це за вихідні”. Мова “мене цікавить результат, а не процес.” Мова для інженерів, а не програмістів.
(Але якимсь чином цей підхід тут масштабується, бо на Go написані такі величезні проєкти, як Docker, Kubernetes чи Terraform, а також Hugo, Grafana, Ollama та багато всього іншого.)
Найкращий спосіб зануритися в Go - це мати реальну задачу, яку дійсно потрібно розвʼязати. Та рухатись до розвʼязку найбільш прямими та простими кроками, обовʼязково з оглядом на домовленості. І все тоді вийде.
05.02.2025
Контексти: слова-паразити в Go
Друга, новіша ідіома в Go, яка засмічує код — це контексти. Я вже писав, що контексти надійно розвʼязують складну проблему скасування операцій, в тому числі тайм-аутів. Але також передача контекстів зроблена… скажімо так, якби це придумали в нас на проєкті, то я б всіма силами протистояв.
(Це не така вже й уявна ситуація, бо одного разу я запропонував PR з повним покриттям контекстами для всіх функцій, що викликають інші функції, що приймають контекст. А потім сам же ж закрив той PR, бо код ставав значно гірше, а переваги значними не були.)
func (s *Service) GetUserBalance(ctx context.Context, id int) (*Results, error) {
cachedResults, err := s.cache.Get(ctx, s.cacheKey(id))
if err != nil { // і цього не забудемо
return nil, error
}
if cachedResults {
return cachedResults, nil
}
user, err := s.queries.GetUser(ctx, id)
// ... and so on
}
Причому контексти самі собою нічого не скасовують. Це задача самої функції. На практиці вони впливають тільки на операції вводу-виводу. Щоб скасувати, скажімо, тривалі обчислення, доведеться час від часу перевіряти стан контексту вручну.
Але головне, що, як і з помилками, обробка контекстів в 99 зі 100 випадків однакова: вони тільки передаються далі. Гадаю, було б краще, якби в Go 2 контекст передавався в кожну функцію сам собою, за замовчуванням. Або ще краще, якби його передача відбувалася тільки там, де компілятор бачить в контексті потребу (тобто залежність від функцій, які самі потребують контекст.)
Такі зміни могли б відбутися на рівні синтаксису, без зміни реалізації. Власне, навіщо? Бо мені не подобається, коли значна частина коду не несе змісту. Майже завжди передача контексту чи перевірка помилки не має нюансів, важливих для бізнес-логіки. Тому вони варті спрощення — так само як ми можемо не писати тип більшості змінних, чи опускати назви полів структури.