SharkTTY Desk 0.9.0이 오늘 출시되었습니다 — 새로운 기능에 사용자용 요약이 있고, 여기서 내려받을 수 있습니다. 거기서 우리가 앞세우는 기능은 게스트 공유 링크입니다: 계정 없이 링크나 QR 코드 하나로 누군가를 내 Mac에 초대할 수 있습니다. 이 글이 들려주는 건 다른 이야기입니다 — 새 기능만큼 시연하기 좋지 않은 무언가를 뒤쫓은 이야기, 앱 전체가 완전히 얼어붙어 무지개 커서가 멈추지 않고 도는, 그 원인을 밝히는 데 정말로 품이 든 이야기입니다. 서로 독립적인 세 가지 근본 원인, 세 가지 개별 수정, 전부 이 빌드에 담겼습니다.
원인 하나: UI 스레드에서의 블로킹 전송
Desk에서 다른 Mac을 보고 있을 때 화면과 입력은 WebRTC 데이터 채널을 타고 오가는데, 그 채널로 전송하는 코드는 동기적으로 실행되었습니다 — 그것도 UI 스레드에서요. 보통은 이게 전혀 보이지 않습니다. 전송은 거의 즉시 반환되니까요. 하지만 WebRTC의 SCTP 전송 계층은 128 KiB의 고정 크기 전송 버퍼를 유지하고, 상대편이 뒤처져서 이 버퍼가 가득 차면 — 즉 백프레셔가 걸리면 — 전송이 블로킹될 수 있습니다. 무기한으로요. 그리고 이 블로킹이 UI 스레드에서 일어나면, 그건 영상 스트림 하나의 끊김이 아니라 앱 전체입니다: 메뉴는 열리지 않고, 창은 다시 그려지지 않고, 상류 쪽이 물러설 때까지 커서가 계속 돕니다. WebSocket과 QUIC 전송 경로는 이 문제를 겪은 적이 없습니다 — 같은 블로킹 호출을 거치지 않으니까요.
수정은 간단합니다: 전송이 이제 논블로킹입니다. 우리가 가져가는 교훈은 단순합니다: 「항상 충분히 빨리 반환된다」는 가정이지 보장이 아니고, UI 스레드가 그 가정을 시험하는 자리여서는 안 됩니다. 최악의 경우가 「원격 피어가 버퍼를 비울 때까지 블로킹된다」인 호출이라면, 그 최악의 경우가 실제로 아무리 드물게 느껴졌더라도 렌더 루프 안에서 동기적으로 실행될 자격이 없습니다.
원인 둘: 아무도 비우지 않던 파이프
두 번째 원인은 오래되고 잘 알려진, 그러면서도 모르는 새 다시 끌어들이기 쉬운
유닉스의 함정입니다. 서브프로세스의 출력 파이프에는 작은 커널 버퍼(약 64 KB)가
있고, 아무도 그걸 읽지 않으면 버퍼가 가득 차고, 자식 프로세스는 더 쓰려다 블로킹되고,
그 자식이 끝나기를 기다리는 쪽도 함께 블로킹됩니다:
waitUntilExit는
절대 반환되지 않습니다. 기다리고 있는 프로세스가, 아무도 듣지 않는 파이프에 쓰다가
막혀 있으니까요.
정확히 이 모양에 들어맞는 호출 하나가, 잠금 화면 캡처 헬퍼가 켜져 있는 동안 1.5초마다 메인 스레드에서 돌고 있었습니다 — 주기적인 점검이었는데, 대상이 파이프를 가득 채우는 순간 앱 전체를 시계처럼 규칙적으로, 그것도 한 번이 아니라 반복해서 멈춰 세울 수 있었습니다. 이게 유독 골치 아팠던 이유가 바로 그겁니다: 드문 경계 사례가 아니라, 주기적인 타이머가 그대로 교착 상태에 정면으로 부딪힌 것이었죠.
수정은 두 부분으로 이루어집니다: 읽을 생각이 없는 출력은 이제 파이프가 아니라 널 디바이스로 보냅니다. 실제로 소비하는 출력은 프로세스를 기다리기 전에 비웁니다. 교훈: 만드는 파이프는 그걸 읽겠다는 계약입니다 — 읽을 생각이 없다면 만들지 마세요. 읽는다면, 쓰는 쪽을 기다리며 블로킹되기 전에 비우세요.
원인 셋: 쓸수록 느려지던 히스토리
세 번째는 멈춤이라기보다 서서히 새어 나가는 출혈에 가까웠습니다: 긴 mosh 터미널 세션은 처음에는 잘 동기화되다가, 오래 실행될수록 눈에 띄게 느려졌습니다. 원인은 알고리즘적인 것이었습니다 — 터미널 히스토리를 동기화 상태로 유지하는 비용이 히스토리 양에 대해 제곱, O(H²)으로 커졌던 겁니다. 락도 아니고 누수도 아니고, 그저 5분짜리 세션에는 아무 문제 없다가 5시간짜리 세션에서는 점점 더 비싸지는 방식이었을 뿐입니다 — 밖에서 보면 다른 두 원인이 일으키는 것과 똑같은 종류의 멈춤처럼 보이게 될 때까지요.
수정은 구조 공유를 쓰도록 히스토리를 재구성해서, 오래 실행되는 세션이 나이를 먹을수록 느려지는 대신 빠른 상태를 유지하게 합니다. 교훈: 제곱 비용은 어떤 빠른 테스트에서도, 어떤 데모에서도 보이지 않습니다. 세션이 H가 의미를 가질 만큼 충분히 오래 실행되어야만 드러나니까요 — 즉 그걸 발견하는 건 언제나 진짜 사용자가, 진짜로 가장 긴 세션에서 겪는 일이라는 뜻입니다. 일부러 찾으러 가지 않는 한요.
이제 https 형태가 된 공유 링크
0.9.0이 앞세우는 기능은 게스트 공유 링크입니다: Mac의 「공유」 패널에서 링크나 QR 코드로 누군가를 초대할 수 있고, 호스트가 만료 시점, 동시에 볼 수 있는 인원 수, 보기 전용 여부, 선택적인 계정 허용 목록을 제어하며 — 어떤 공유든 언제든 취소할 수 있습니다. 링크 자체는 Mac의 영구 연결 키를 절대 담지 않습니다: 게스트는 클라우드가 발급한 단기 티켓을 사용하고, 만료·동시 접속 제한·보기 전용 모드·취소를 검증하고 강제하는 건 클라우드가 아니라 여러분 Mac의 에이전트입니다. 아직 초기 단계인, 결제가 필요 없는 선택지로 공유를 시간 단위 독점 예약으로 제공할 수도 있습니다: 게스트가 UTC에 맞춘 빈 시간대를 골라 그 시간 동안 여러분의 Mac을 독점합니다.
0.9.0이 바꾸는 건 링크 자체의 형태입니다: 이제는 원시
ttyshare://
주소 대신 https URL —
https://safafish.com/s/#s=<code>&n=<name>
— 형태입니다. https 링크는 어떤 기기의 어떤 브라우저에서든 열려, 「SharkTTY에서
열기」를 제공하고, 아직 설치되지 않았다면 App Store를 안내하며, iPhone이나 iPad로
스캔할 수 있는 QR 코드를 보여 주는
중계 페이지로
이어집니다. 이미 앱이 설치된 기기에 이 https 링크를 다시 붙여 넣어도 마찬가지로
왕복됩니다. 아직 되지 않는 부분도 정확히 짚어 두겠습니다: 링크를 탭한다고 iOS 앱이
그 자체로 즉시 열리지는 않습니다 — 그 인계는 아직 출시하지 않은 앱 빌드에 달려
있습니다. 오늘 여러분이 얻는 건, 어떤 브라우저에서든 열려 SharkTTY로 링크를 넘겨주는
페이지입니다. 그것만으로도 게스트 절반의 기기가 알아보지 못할 URI 스킴보다는 채팅에
붙여 넣기에 훨씬 친절합니다.
0.9.0의 자잘한 개선 두 가지
언급할 만한 작은 개선 두 가지입니다. Desk의 터미널에서는 — SSH든 로컬 셸이든 mosh든 — Option을 누른 채 클릭하면 이제 셸 커서가 그 위치로 이동합니다. Terminal.app과 iTerm2가 쓰는 것과 같은 방식으로, 합성된 화살표 키를 통해 구현되었고, 기본 버퍼에서도 전체 화면 앱의 대체 화면에서도 작동합니다. 그리고 정밀 돋보기에는 작은 정확도 버그가 있었습니다: 스로틀링 창이 드래그의 마지막 이동을 놓치는 바람에, 돋보기가 커서가 실제로 멈춘 위치보다 살짝 뒤처져 자리 잡을 때가 있었습니다. 이제 정지 위치는 항상 마지막 업데이트를 받아서, 확대된 내용이 십자선 아래 정확히 놓입니다.
위 내용에 대해 질문이나 버그 신고가 있으신가요? 피드백 게시판이 가장 빠르게 저희에게 닿는 방법입니다.