Volver al blog de desarrollo Actualización de ingeniería

Tres maneras de congelar una app de Mac — y cómo Desk 0.9.0 las corrige todas

SharkTTY Desk 0.9.0 ya está aquí — Novedades tiene el resumen para usuarios, y puedes descargarlo aquí. Ahí la función que encabezamos son los enlaces de invitado: invitar a alguien a tu Mac con un enlace o un código QR, sin necesidad de cuenta. Esta entrada cuenta otra historia — la de detrás de escena, la caza de algo mucho menos vistoso que una función nueva: la app entera congelándose por completo, con el cursor arcoíris girando sin parar, por razones que costó de verdad rastrear. Tres causas raíz independientes, tres arreglos separados, todos en esta build.

Causa uno: un envío bloqueante en el hilo de la interfaz

Cuando ves otro Mac desde Desk, el vídeo y la entrada viajan por un canal de datos WebRTC, y el código que enviaba por ese canal se ejecutaba de forma síncrona — en el hilo de la interfaz. En condiciones normales eso es invisible: el envío vuelve casi al instante. Pero el transporte SCTP de WebRTC mantiene un búfer de envío de tamaño fijo, 128 KiB, y si el otro lado se retrasa y ese búfer se llena — contrapresión —, el envío puede bloquearse. Indefinidamente. Y cuando ese bloqueo ocurre en el hilo de la interfaz, no es un tirón en un flujo de vídeo: es toda la app. Los menús no se abren, las ventanas no se redibujan, la rueda gira hasta que algo aguas arriba cede. Las rutas de transporte WebSocket y QUIC nunca tuvieron este problema — no hacen pasar sus envíos por la misma llamada bloqueante.

El arreglo: los envíos ahora son no bloqueantes. La lección que nos llevamos es sencilla: «siempre vuelve lo bastante rápido» es una suposición, no una garantía, y el hilo de la interfaz nunca debería ser quien la ponga a prueba. Cualquier llamada cuyo peor caso sea «bloquearse hasta que el otro extremo vacíe un búfer» no tiene nada que hacer ejecutándose de forma síncrona en el bucle de renderizado, por poco frecuente que pareciera ese peor caso en la práctica.

Causa dos: una tubería que nadie vaciaba

La segunda causa es una trampa antigua y muy conocida de Unix, fácil de reintroducir sin darse cuenta. La tubería de salida de un subproceso tiene un búfer de kernel pequeño — unos 64 KB — y si nadie la lee, el búfer se llena, el proceso hijo se bloquea intentando seguir escribiendo, y cualquiera que espere a que ese hijo termine también se bloquea: waitUntilExit nunca vuelve, porque el proceso que espera está atascado escribiendo en una tubería que nadie escucha.

Una llamada con exactamente esta forma se ejecutaba en el hilo principal cada 1,5 segundos siempre que el asistente de captura de pantalla bloqueada estaba activado — una comprobación periódica que, en cuanto su objetivo llenaba su tubería, podía atascar toda la app con la precisión de un reloj, y no una sola vez sino repetidamente. Eso es lo que la hacía tan disruptiva: no un caso límite raro, sino un temporizador recurrente estrellándose directamente contra un interbloqueo.

El arreglo tiene dos partes: la salida que no pensamos leer ahora va al dispositivo nulo en lugar de a una tubería, y la salida que sí consumimos se vacía antes de esperar al proceso. Lección: la tubería que creas es un contrato para leerla — si no piensas leerla, no la crees; si la lees, vacíala antes de bloquearte esperando a quien escribe en ella.

Causa tres: un historial que se volvía más lento cuanto más lo usabas

La tercera no era tanto un cuelgue como una hemorragia lenta: las sesiones largas de terminal mosh sincronizaban bien al principio y se volvían perceptiblemente más lentas cuanto más duraban. La causa era algorítmica: el coste de mantener sincronizado el historial de la terminal escalaba de forma cuadrática con la cantidad de historial, O(H²). Ni un lock ni una fuga, solo un enfoque que funcionaba de maravilla en una sesión de cinco minutos y se volvía cada vez más caro en una de cinco horas — hasta que, visto desde fuera, parecía el mismo tipo de congelación que causaban las otras dos.

El arreglo reestructura el historial para usar compartición estructural, de modo que las sesiones largas se mantienen rápidas en lugar de ir más lentas con el tiempo. Lección: el coste cuadrático es invisible en cualquier prueba rápida y en cualquier demo, porque solo aparece cuando una sesión ha corrido lo suficiente como para que H importe — lo que significa que quien lo encuentra siempre son tus usuarios reales, en sus sesiones más largas de verdad, a menos que lo busques a propósito.

Los enlaces de compartir, ahora en formato https

La función que encabeza 0.9.0 son los enlaces de invitado: desde el panel de Compartir en tu Mac, puedes invitar a alguien con un enlace o un código QR, con el anfitrión controlando la caducidad, cuántas personas pueden estar viendo a la vez, si es solo de visualización, y una lista de cuentas permitidas opcional — y cualquier share puede revocarse en cualquier momento. El enlace en sí nunca lleva la clave de conexión permanente de tu Mac: los invitados canjean tickets de corta duración emitidos por la nube, y es el agente en tu Mac, no la nube, quien verifica y hace cumplir la caducidad, los límites de concurrencia, el modo de solo visualización y la revocación. También hay una opción temprana y gratuita para ofrecer un share como reserva exclusiva por horas, donde el invitado elige una franja horaria libre alineada a UTC y consigue tu Mac en exclusiva durante esa ventana.

Lo que cambia 0.9.0 es la forma del propio enlace: ahora es una URL https — https://safafish.com/s/#s=<code>&n=<name> — en lugar de una dirección ttyshare:// en crudo. Un enlace https se abre en cualquier navegador, en cualquier dispositivo, en una página de traspaso que ofrece «Abrir en SharkTTY», señala la App Store si aún no está instalada, y muestra un código QR para escanear desde un iPhone o iPad. Pegar el enlace https de vuelta en la app en un dispositivo que ya la tiene también funciona de ida y vuelta. Para ser precisos sobre lo que esto todavía no es: tocar el enlace no abre al instante la app de iOS por sí solo — ese traspaso depende de una build de la app que aún no hemos publicado. Lo que tienes hoy es un enlace que se abre en cualquier navegador hacia una página que se lo entrega a SharkTTY, lo cual ya es mucho más amigable para pegar en un chat que un esquema de URI que la mitad de los dispositivos de tus invitados no reconocerán.

También en 0.9.0

Dos detalles menores que merecen mención. En las terminales de Desk — SSH, shell local y mosh por igual — mantener pulsada Option y hacer clic ahora mueve el cursor de la shell a ese punto, el mismo truco que hacen Terminal.app e iTerm2, mediante teclas de flecha sintetizadas; funciona tanto en el búfer principal como en apps a pantalla completa, en la pantalla alterna. Y la lupa de precisión tenía un pequeño bug: una ventana de limitación podía descartar el último movimiento de un arrastre, así que la lupa a veces se quedaba un poco por detrás de donde realmente se detenía el cursor. Ahora la posición de reposo siempre recibe su actualización, así que el contenido ampliado queda exactamente bajo la mira.

¿Preguntas o informes de errores sobre algo de lo anterior? El tablón de comentarios es la forma más rápida de contactarnos.