SharkTTY Desk 0.9.0 вийшов сьогодні — у розділі Що нового є огляд для користувачів, а завантажити його можна тут. Функція, яку ми там виносимо на перший план, — це гостьові посилання спільного доступу: запросити когось на свій Mac за посиланням чи QR-кодом, без потреби в обліковому записі. Ця стаття розповідає іншу історію — закулісну, про полювання на дещо набагато менш ефектне, ніж нова функція: застосунок повністю зависає, кольоровий індикатор очікування крутиться без кінця, а причини цього довелося справді довго встановлювати. Три незалежні кореневі причини, три окремі виправлення — усі потрапили в цю збірку.
Причина перша: блокувальна відправка в потоці інтерфейсу
Коли ви переглядаєте інший Mac через Desk, відео та введення йдуть через канал даних WebRTC, і код, що надсилав дані в цей канал, виконувався синхронно — у потоці інтерфейсу. За звичайних умов це непомітно: відправка повертається майже миттєво. Але транспорт SCTP у WebRTC тримає буфер відправки фіксованого розміру, 128 KiB, і якщо інша сторона відстає й цей буфер заповнюється — тобто виникає зворотний тиск (backpressure) — відправка може заблокуватися. Безстроково. А коли це блокування відбувається в потоці інтерфейсу, це вже не затримка в одному відеопотоці — це весь застосунок: меню не відкриваються, вікна не перемальовуються, індикатор крутиться, доки щось вище за течією не поступиться. Транспортні шляхи WebSocket і QUIC ніколи не мали цієї проблеми — вони не проводять свої відправки через той самий блокувальний виклик.
Виправлення просте: відправки тепер неблокувальні. Урок, який ми з цього винесли, простий: «завжди повертається достатньо швидко» — це припущення, а не гарантія, і потік інтерфейсу ніколи не повинен бути тим, хто це припущення перевіряє. Будь-який виклик, найгірший випадок якого — «блокуватися, доки віддалений пір не спорожнить буфер», не має права виконуватися синхронно в циклі рендерингу, хай яким рідкісним цей найгірший випадок здавався на практиці.
Причина друга: канал, який ніхто не спорожняв
Друга причина — стара, добре відома пастка Unix, яку легко ненавмисно повернути.
Вихідний канал (pipe) підпроцесу має невеликий буфер ядра — близько 64 KB, — і
якщо його ніхто не читає, буфер заповнюється, дочірній процес блокується під час
спроби писати далі, а все, що чекає завершення цього процесу, теж блокується:
waitUntilExit
ніколи не повертається, бо процес, якого чекають, застряг, записуючи в канал,
який ніхто не слухає.
Один виклик саме такої форми виконувався в головному потоці кожні 1,5 секунди, поки був увімкнений помічник захоплення екрана блокування, — періодична перевірка, яка, щойно її ціль заповнювала свій канал, могла заклинити весь застосунок з точністю годинника, і не один раз, а повторно. Саме це робило її настільки руйнівною: не рідкісний крайній випадок, а періодичний таймер, що прямо натикався на взаємне блокування.
Виправлення складається з двох частин: вивід, який ми не збираємося читати, тепер іде на нуль-пристрій замість каналу, а вивід, який ми справді споживаємо, спорожняється перед очікуванням процесу. Урок: канал, який ви створюєте, — це контракт на його читання. Якщо не плануєте читати — не створюйте його. Якщо читаєте — спорожняйте його, перш ніж блокуватися в очікуванні того, хто пише.
Причина третя: історія, що уповільнювалася що довше нею користувалися
Третя причина була не так зависанням, як повільною кровотечею: довгі термінальні сеанси mosh спершу синхронізувалися нормально, а що довше тривали, то помітно повільніше. Причина була алгоритмічною: вартість підтримки історії термінала в синхронізованому стані зростала квадратично відносно обсягу історії, O(H²). Не блокування, не витік — просто підхід, який чудово працював для п'ятихвилинного сеансу і ставав дедалі дорожчим для п'ятигодинного, доки з боку не почав виглядати так само, як зависання, спричинене двома іншими причинами.
Виправлення перебудовує історію на структурне спільне використання, щоб довгі сеанси залишалися швидкими, а не сповільнювалися з часом. Урок: квадратична вартість непомітна в будь-якому швидкому тесті чи демонстрації, бо проявляється лише тоді, коли сеанс триває достатньо довго, щоб H почало мати значення — а це означає, що знаходять її завжди ваші справжні користувачі, на своїх справді найдовших сеансах, якщо тільки ви не шукаєте її навмисно.
Посилання спільного доступу тепер у форматі https
Функція, яку виносить на перший план 0.9.0, — гостьові посилання спільного доступу: з панелі «Спільний доступ» на вашому Mac можна запросити когось за посиланням або QR-кодом, а хост контролює термін дії, скільки людей можуть переглядати одночасно, чи це режим лише перегляду, і додатковий список дозволених облікових записів — і будь-який спільний доступ можна відкликати в будь-який момент. Саме посилання ніколи не несе постійний ключ з'єднання вашого Mac: гості обмінюють короткострокові квитки, видані хмарою, а перевіряє й забезпечує термін дії, обмеження одночасності, режим лише перегляду та відкликання агент на вашому Mac, а не хмара. Є також рання, безкоштовна на цьому етапі опція запропонувати спільний доступ як погодинне ексклюзивне бронювання: гість обирає вільний часовий слот, вирівняний за UTC, і отримує ваш Mac у своє повне розпорядження на цей час.
Що змінює 0.9.0 — це форму самого посилання: тепер це URL https —
https://safafish.com/s/#s=<code>&n=<name>
— замість необробленої адреси
ttyshare://.
Посилання https відкривається в будь-якому браузері, на будь-якому пристрої, і
веде на проміжну сторінку,
яка пропонує «Відкрити в SharkTTY», вказує на App Store, якщо застосунок ще не
встановлено, і показує QR-код для сканування з iPhone чи iPad. Вставити те саме
https-посилання назад у застосунок на пристрої, де він уже є, теж працює в обидва
боки. Скажімо чесно й про те, чого це ще не вміє: дотик до посилання не відкриває
миттєво застосунок iOS сам собою — ця передача залежить від збірки застосунку,
яку ми ще не випустили. Сьогодні ви отримуєте посилання, яке відкривається в
будь-якому браузері на сторінку, що передає його SharkTTY, — і це вже набагато
привітніше вставляти в чат, ніж схему URI, яку не розпізнає половина пристроїв
ваших гостей.
Також у 0.9.0
Дві менші деталі, варті згадки. У терміналах Desk — SSH, локальній оболонці та mosh однаково — утримання Option і клацання тепер переміщує курсор оболонки в те місце, той самий трюк, що й у Terminal.app та iTerm2, за допомогою синтезованих клавіш зі стрілками; це працює як в основному буфері, так і в повноекранних застосунках на альтернативному екрані. А в точній лупі була невелика похибка: вікно обмеження частоти оновлень могло відкидати останній рух перетягування, тож лупа іноді зупинялася трохи не там, де насправді зупинявся курсор. Тепер положення спокою завжди отримує останнє оновлення, тож збільшений вміст лягає точно під перехрестя.
Є питання чи звіти про баги щодо чогось із вищесказаного? Дошка відгуків — найшвидший спосіб зв'язатися з нами.