Канал
У Льоні сьогодні
Нотатки архітектора систем та майстра на всі руки. Що я зробив, що з того вийшло, і що це все значить.
-
QR коди з зображенням на тлі
Сьогодні малював собі наліпку з QR кодом. (див. вище) Попередньо, ідея була приховати QR код в малюнку. Зараз можна побачити багато підходів, в тому числі розмальовку кодів нейромережею, але при цьому є фундаментальні обмеження, від яких не втечеш.
-
QR код починається з маркерів у кутах. Маркери обовʼязково мають бути присутні та заповнені одним кольором, бо саме за маркерами сканер знаходить код у світлині. Але не обовʼязково квадратними — головне утворити лінійну послідовність в 1-1-3-1-1 діапазонів темного та світлого. До речі, QR код може бути й світлим на темному. А також не обовʼязково чорно-білим, бо в справжньому світі немає нічого чорного або білого — хоча чим вище контрастність, тим краще код буде читатись.
-
А окрім маркерів, весь інший зміст кожної клітини, як я це розумію, читається за центральним “пікселем” (світлини з камери) — він має бути темним або світлим. Емпірично, решта пікселів не має значення, тому на тлі QR-коду може бути будь-яке зображення. Навіть якщо тло чорне, а в центрі біла крапочка, то відповідна клітина QR-коду читається як біла. А якщо тло в цьому місці світле, то взагалі ніякої крапочки не потрібно.
-
Є ще популярний метод поставити посередині QR-коду логотип. Він дуже просто працює - QR-код використовує надлишкове кодування інформації, тож до 30% коду можна закрити будь-чим. Тільки нюанс: маркери та деякі інші функціональні зони коду в ці 30% не входять — тільки клітини з даними. Тож насправді логотип обмежений ще більше.
-
До речі, в кодах з більшою “місткістю” маркери займають меншу частину площі. Навіть з коротким змістом можна обрати більшу роздільну здатність, та отримати більш “акуратний” код (якщо скористуватися бібліотекою, яка дозволяє власноруч обирати місткість, а правильніше, версію, коду). Тільки треба розуміти, що доведеться його друкувати більшим форматом, щоб камера могла роздивитись клітини.
Чомусь по генерації QR класні бібліотеки написані на Python. Є qrcode, а є amazing-qr. Є ще CuteR, який краще поєднує код з зображення.
-
-
Docker - це не система збірки
…Взагалі, вчорашні відкриття наштовхнули мене на наступний хід міркувань.
Раніше я дивився на Docker як на верхній рівень скриптів збірки. Тобто, щоб ми запускали
docker buildабоdocker compose, а все інше витікало звідти. А якщо так не виходить, то це погано, та треба докладати зусиль, щоб затягти всі аспекти збірки всерединуDockerfile. Наприклад, використовувати проміжні контейнери, монтування кешу, як я вчора писав, та інші засоби.Але насправді,
Dockerfileта docker buld не є гарною реалізацією системи збірки, тобто аналогомmake,rake, чи просто скриптів оболонки.-
Збірка потребує інструментів, які не потрібні в остаточному образі, а також утворює зайві артефакти. Наприклад, файли журналів можна знайти в багатьох публічно доступних образах, що не тільки збільшує їх розмір, але й може вести до витоку інформації.
-
Кеш, доступний в Docker через
RUN --cache, є обмеженим та потребує особливих дій. Наприклад, до нього немає доступу з хосту. -
Підготовка до збірки
Dockerfile- той крок, що називаєтьсяtransferring context- відбувається через копіювання файлів, що досить неефективно та може тривати десятки секунд. Можна замість того монтувати залежності черезRUN --mount, але то, як і кеш, потребує особливого ставлення.
Одним словом, відтепер буду використовувати
Dockerfileтільки для створення образу з вже готових артефактів. Та нічого страшного, якщоdocker buildбуде лише частиною більшого скрипту збірки. -
-
Пришвидшення збірки Go+Docker з 10 до 3 хвилин
Колись я хвалився нашим класним пакетом інтеграційних тестів. На жаль, чим повніше тестове оточення, тим повільніше його запуск, особливо на CI, з холодного старту. Отже, останнім часом намагався скоротити цю затримку.
Що не давало результатів: перенесення контейнерів баз даних з docker-compose в сервіси GitHub Actions: запуск триває однаково. Також кешування шарів Докера: щось на CI воно не гарно виходить.
А далі я помітив, що більшу частину збірки займала компіляція додатків на Go. Яка відбувалася як крок в
Dockerfile. Зазвичай Go компілює досить швидко — але то завдяки кешу. Бо Go це така мова, де всі залежності збираються з коду під час кожної компіляції — а тимчасові результати зберігаються в кеш. Є кеш — тоді Go може обмежитись тільки файлами, що були змінені. Немає кешу — та всі наші залежності підлягають компіляції. Щоб зрозуміти обсяг роботи: кеш для нашого проєкту займає 600 Мб!Проблема глибше: Dockerfile не має доступу ні до якого сталого кешу. Тобто є RUN –mount=type=cache, але такий кеш збережеться тільки між збірками на одній машині. Для CI це не допоможе. Втім, для локальної роботи вже добре.
Остаточним рішенням було винести компіляцію з Dockerfile та запускати окремою командою. Тобто спочатку
make, потімdocker compose up. Результати компіляції тепер просто монтуються до контейнерів.З таким підходом компіляція використовує кеш, який легко переноситься між запусками CI. Насправді навіть стандартна дія actions/setup-go кешує (між запусками) кеш компіляції — тож робити її власноруч не потрібно. Єдина різниця, що трохи ускладнився скрипт запуску.
-
Експорт даних з Reeder, та бази даних Realm
Мав задачу вивантажити зміст Reeder, в якому розташований мій список читання, а також закладки (а точніше, записи з “зірочками”). Вбудованої такої можливості немає — можна тільки експортувати список підписок в OPML, але це зовсім не те що мені потрібно. Отже, знайшов спосіб отримати доступ до бази даних програми, як вона є.
То виявилось несподівано просто, бо Reeder використовує базу даних Realm. У Realm є пакет NPM, тож мені залишилось тільки відкрити файл з базою, прочитати зміст та зберегти в JSON. Задача на пʼять хвилин. Ще є Realm Studio, але він дозволяє експорт тільки в форматі звʼязаного списку, що потребувало б подальшого перетворення.
Взагалі Realm - цікава база, яку я знаю доволі давно. Це одна з тих баз даних, які дозволяють зберігати дані локально, а вже потім синхронізувати. Тобто одна з можливих основ мобільного додатку, де важливе локальне зберігання. Цікаво, але в мене в macOS знайшлося тільки два додатки, які використовують Realm: це Reeder та… додаток Нової Пошти. Можливо, на телефоні їх знайшлося б більше, але взагалі не найпопулярніша технологія
Що знайшов зараз, чого раніше не бачив — це бібліотека IceCream для синхронізації Realm через CloudKit, тобто без впровадження додаткового сервісу синхронізації на платформах Apple.
-
Як не дати AWS Cloudwatch себе пограбувати
Давно вже не писав про Cloudwatch, хоча продовжую ним користуватись принаймні щотижня. Попри наявність “брендових” альтернатив, мені до вподоби те, що пропонує AWS у своєму рішенні для збору журналів та метрик. Є тільки один нюанс: можна швидко влетіти на багато грошей. Як і з усім в AWS.
Найризиковішим, як на мене, є утворення власних метрик. У метрик є “виміри” (dimension) - щоб під одним імʼям збирати показники по різних ресурсах. Кожне значення виміру власної метрики сплачується як окрема метрика. А це вже відчутні $0.30 на місяць. Якщо обрати такий вимір, у якого багато значень — наприклад, ID акаунта в нашій системі — то недовго почати витрачати сотні доларів на таку нібито тривіальну потребу.
Окремо підступний вимір - ID задачі ECS, або її ж IP адреса: вони не є сталими, тож за місяць перезапусків та деплоїв можна теж назбирати вагомий рахунок. Якщо вже збирати статистику за задачами, потрібно спочатку винайти сталий ідентифікатор.
Зазначу, що вбудовані метрики AWS нічого не коштують. Це ті, які неможливо відключити. Бо трапляються ще “розширені” метрики — вони рахуються як власні.
А ще дорого може вийти надсилання метрик. Тут $0.01 на тисячу запитів, але знов-таки, якщо наївно відправляти кожне значення окремо, то вони швидко накопичуються. Тому краще буферизувати на своєму боці та викликати
PutMetricDataрідше. До речі, оскільки для кожного значення в цьому запиті можна окремо вказати час, то нічого нам не заважає, наприклад, відправляти похвилинні метрики раз на пʼять хвилин.Зате потім зайшов на сторінку цін DataDog та зрозумів, що в AWS не найбільш заплутана цінова політика.
-
Amnesia: The Bunker та жанр "дослідницький хорор"
Закінчив на вихідних гру Amnesia: The Bunker, що вийшла цього року. Вона нагадала мені одну з моїх улюблених ігор - Subnautica. Я б сказав, що ці гри належать до піджанру жахів, який я називаю дослідницький хорор.
Ключовим аспектом дослідницького хорору є опанування оточення. Якщо в класичному хорорі метою є обхід рівня, пошук розвʼязка та просування далі, то в дослідницькому хорорі світ гри не змінюється від початку до кінця. Що змінюється, то це уявлення гравця про оточення, ризики та власні можливості.
Мета гри ставиться максимально широко — наприклад, “втекти”. Ігровий цикл являє собою експедицію - 1) спланувати вилазку з безпечного місця, 2) виконати вилазку, 3) повернутись в безпечне місце з результатами (ресурсами та знанням) та планувати знову. Замість звичайних для хорора “установок” (set piece), тут джерелом страху є постійно присутня загроза та особливо загроза втратити прогрес, бо збереження доступне тільки в безпечному місці.
🤿 Subnautica - це гра, що прикидається класичною пісочницею, але насправді є справжнім хорором, де головний противник — удушення. Бо весь процес гри відбувається під водою, де невпинно вибігає лічильник кисню. А ще є глибина та тиск та неприязна фауна… Якщо такий жанр цікавить, радив би зануритись у Subnautica без спойлерів та подарувати собі просто унікальний досвід дослідження морського світу від сонячного шельфу до тьмяних глибин.
Є ще продовження, Subnautica: Below Zero, але краще почати з першої, бо тут очевидно є припущення, що гравець вже знає що до чого та тому пропущений етап поступового знайомства з механізмами.
🪖 Amnesia: The Bunker так само складається з дослідження лабіринту темних підземних коридорів. Загроза тут банальніше — це бабай, який тебе зʼїсть… але реагує той бабай на галас, тобто ми вимушені бути якомога тихіше. Що не так просто, якщо навіть ліхтарик не світить без брязкучої динамо-машинки. Отже, доведеться балансувати між світлом, темрявою, тишею, гуркотом, та купою перешкод. Окремо мені подобається, що розробники рандомізують зміст, та навіть включили режим де ключові предмети розташовані випадково. Бо типова гра на дослідження — одноразова… далі хіба спідрани.
Які ще ігри з дослідницького хорору я знаю? В Minecraft можна так грати. В режимі виживання точно є ігровий цикл з експедиції за ресурсами вдень та відбудови бази вночі. Ще Dark Souls (перша частина) нагадує, оскільки там світ щільно повʼязаний, без телепортації по ньому доведеться багато разів побігати, а ціль ставиться туманно та її потрібно ще відшукати. (Та й перше проходження DS - неабиякий хорор.)
-
Аналіз поля Сапера за допомогою множин
…Вчорашнє рішення з обчисленням ймовірностей мін розвʼязує багато ситуацій, але не всі. Проте щоб досягти чогось більшого, недостатньо дивитись на комірки поодинці: ми витягли з них всю можливу інформацію. Подальший аналіз має дивитись на групи комірок з відомою кількістю мін та через операції над множинами досягати висновку про розташування мін.
???? 2334 1*11Наведу приклад: в цьому фрагменті поля ми не можемо визначити розташування мін за жодною з цифр. Але ми точно можемо сказати, що для виконання
2перші дві комірки повинні містити одну та тільки одну міну. А для виконання3перші три невідомі комірки повинні містити дві міни. Можна зробити висновок, що в третій комірці міна є гарантовано. Або навпаки: якби замість3стояла б2, то в третій комірці гарантовано не було б міни.Щоб алгоритмізувати такий підхід, потрібно ввести поняття множини комірок, а далі перетинати ці множини. Кожний перетин утворює три нові множини: множину спільних комірок та множину рештки з кожного боку. Залишається обчислити кількість мін в кожній з підмножин та маємо успіх. Під успіхом маю на увазі, що в деяких випадках отримуватимемо або множину зовсім без мін, або таку де кількість мін дорівнює кількості комірок.
Є тільки одне ускладнення: не завжди ми будемо отримувати точну кількість мін. Бо, якщо подивитись на приклад де дві цифри
3поруч, то про перетин їх множин можемо сказати тільки те, що в ньому не більше двох мін (що не дуже корисно). Та, не менше однієї — бо решткою від перетину з кожної сторони буде тільки одна комірка, значить, принаймні одна з мін припадає на спільну множину.Такі нерівності теж можуть бути корисними, оскільки через додаткові перетини можна дійти до точних значень. Взагалі, почати можна й з одного раунду перетинів, але в ідеалі треба обробляти й новоотримані множини, поки отримуємо нову інформацію. Благо масштаб задачі дозволяє робити всілякі перебори. Особливо якщо зберігати стан між кроками, бо поки що я обчислюю все наново кожен раз.
-
Сапер: відображення ймовірності
Перше, що я хотів додати у звичайного “Сапера” - це обчислення та відображення ймовірності мін. Бо просто цікаво роздивитись, а ще з цього можна зробити режим підказки або авторозвʼязувач.
Ймовірність — не така проста річ, як здається, та алгоритм її обчислення, власне, і являє собою розвʼязок гри. Тому треба було почати з основ.
-
Ймовірність кожної комірки на полі можна обчислити як частку від ділення кількості мін на кількість ще не відкритих комірок. Ну, це не тільки просто, але й не цікаво — хоч можна поспостерігати, як вона змінюється за ходом розкриття поля.
-
Далі можна зробити теж саме, але біля кожної цифри на краю: кількість невідмічених мін поділити на кількість невідкритих комірок. Різні цифри можуть давати різну ймовірність: беремо максимальну з тих, що знаємо.
-
Але є особливий випадок: якщо навколо деякої цифри вже не залишилось мін, то ймовірність ще не відкритих комірок стає 0. Такий нуль має пріоритет над іншими значеннями.
-
На цей час це вся логіка, хоча можна йти й далі. Власне, вже з таким набором евристик можна розвʼязувати нескладні ігри. (До речі, ще одна ідея на майбутнє: роздивитись ймовірність виграшу в залежності від кількості мін.)
(Ще настав час додати вибір складності. На жаль, на маленьких екранах поки немає щастя, бо
flex, який я використовую для центрування, псує прокрутку, коли зміст не влазить в екран.) -
-
Робота з вводом/виводом в Golang
В продовження вчорашньої теми…
Головна різниця в роботі вводу/виводу в Golang в тому, що тут код, що блокує, нічим не відрізняється від синхронного. Ніякої спеціальної відмітки для очікування не потрібно. Це, звісно, спрощує код. Бо ввід/вивід — це не тільки читання файлів, але й спілкування з клієнтами та з базою даних, та й взагалі з будь-яким зовнішнім ресурсом.
Можна подумати, що це властивість компільованої чи системної мови програмування, але ні: наприклад, в Ruby IO теж виглядає синхронно, а у Swift - потребує асинхронного коду або колбеків. Ось такий аспект дизайну мови.
Але то не єдина різниця між Go та JavaScript. Менш помітно те, що в JavaScript ввід/вивід працює за принципом push, а в Go - pull. В JavaScript ми можемо зробити запит на читання та займатись чимось іншим — допоки не буде викликаний наш обробник. В Go виклик
Readзупиняє виконання програми. З читанням файлів це менш помітно, але з сокетами ми можемо чекати досить довго.Тому в Go все ж використовують асинхронний код, але не конкретно для вводу/виводу, а для одночасного виконання. Наприклад, типовий сервер буде запускати горутіну для кожного підключеного клієнта: комунікація через кожне підключення відбуватиметься послідовно, та блокування очікуване.
Для читання та запису в Go є інтерфейси
io.Readerтаio.Writer. Так саме як і в JavaScript, вони можуть приховувати будь-яку логіку. На відміну від JavaScript, немає “дуплексного” інтерфейсу для перетворень. Щоб додати до даних перетворення, зазвичай загортають читач та повертають новий читач: наприклад, bufio.NewReader додасть до будь-якого читача буферизацію. -
Порівняння API вводу/виводу JavaScript та Go
Так вже вийшло, що останнім часом я спочатку працював з потоками в JavaScript (модуль stream), а потім — з модулем io в Golang. Обидва модулі реалізують ввід/вивід в загальному сенсі цього слова, але підхід зовсім різний та його цікаво порівняти.
Модуль
streamпобудований навколо асинхронних примітивів JavaScript - від подій до генераторів таasync/await. Важливо, що ввід/вивід завжди вимагають блокування, тобто пасивного очікування. А в JavaScript немає можливості блокувати синхронний код. Тому при використанні модуляstreamнам доведеться ускладнювати код — хоч сучасний підхід зasync/awaitмені подобається та майже не напружує розуміння.(Примітка: в JavaScript є декілька функцій, які здійснюють синхронний ввід/вивід, наприклад, fs.readFileSync. Вони реалізовані через вихід в C, тобто фактично “ламають” модель виконання. Ось детальне пояснення.)
Важлива функція абстракції вводу-виводу — це додавання логіки зі збереженням інтерфейсу. В JavaScript для цього є функція pipeline. Наприклад, нею можна додати до потоку запису в файл стискання. Або до читання з сокета — буферизацію.
Результат виконання
pipeline- теж потік, тобто споживачам коду не потрібно знати, що в нього всередині. Також приємно, щоpipelineприховує асинхронність всередині та залишає тільки зовнішню.Про Go - завтра.