Steven
Steven6 min lukuaika

Syntyi ja korjattiin kahdessatoista minuutissa

GeekBye v1.8.10:n muutosloki sanoo että se korjasi kaatumisen näppäinoikoteitä muokatessa. Niin se teki — mutta kaatuminen esiteltiin ja korjattiin samassa pull requestissa, kahdentoista minuutin välein, eikä se koskaan tavoittanut ainuttakaan käyttäjää. Todellinen tarina on luotettavuuskaskadi joka sen tuotti: pieni, oikea muutos siihen milloin oikotiet keskeytetään, ja React-purkubugi joka putosi yhdestä rivistä jonka piti tehdä editorista turvallisempi.

Ohjelmistokehitys
React
Desktop
GeekBye-julkaisut
Syntyi ja korjattiin kahdessatoista minuutissa

Tässä on rehellisin lause jonka voin kirjoittaa GeekBye v1.8.10:stä: kaatuminen jonka korjaamisesta se on kuuluisa ei koskaan tapahtunut kenellekään. Muutosloki sanoo "Fixed a crash while editing keyboard shortcuts and switching windows mid-edit," mikä on totta ja lukee kuin bugi joka toimitettiin, puri käyttäjiä ja paikattiin jatkojulkaisussa. Ei se ollut. Kaatumisen esitteli yksi commit ja poisti toinen samassa pull requestissa, kahdentoista minuutin välein, samana perjantaiaamuna. Se eli kokonaan yhden haaran sisällä. Julkaisumuistiinpanot kuvaavat arven joka muodostui ja parani ennen kuin kukaan repon ulkopuolella saattoi nähdä sitä.

Se ei ole muutoslokin epäonnistuminen; se on iteratiivisen työn rehellinen muoto, ja se kannattaa kertoa koska nuo kaksitoista minuuttia sisältävät aidosti opettavaisen React-bugin. Mutta ymmärtääksesi kaatumisen sinun on ymmärrettävä mitä varten julkaisu oikeastaan oli — koska kaatuminen oli oheisvahinkoa jonkin muun korjaamisesta.

Mitä varten julkaisu oikeastaan oli

GeekBye antaa sinun uudelleensitoa näppäinoikotiensa asetuksissa. Sillä aikaa kun tallennat uutta yhdistelmää, sovelluksen on estettävä globaalien oikoteidensa laukeaminen — muuten Cmd+B:n painaminen sen määräämiseksi vain vaihtaisi ikkunaa sen sijaan että se kaapattaisiin. Joten editori keskeyttää globaalit oikotiet IPC:n yli: renderöijä kutsuu window.electronAPI.setShortcutsSuspended(true), ja pääprosessin ShortcutsHelper vastaa kutsumalla unregisterAll(), pudottaen jokaisen globalShortcut-rekisteröinnin niin että näppäinpainallus tavoittaa tallennussyötteen sen sijaan että laukaisisi toiminnon. Jatkaessa se kutsuu registerGlobalShortcuts() laittaakseen ne takaisin.

Bugi joka aloitti tämän kaiken (ongelma #233) koski tuon keskeytyksen laajuutta. Vanha koodi keskeytti oikotiet koko sen ajaksi kun oikotieasetussivu oli auki — ei vain silloin kun aktiivisesti tallensit näppäintä. Suurimman osan ajasta se on näkymätöntä. Mutta jos avasit sivun, aloit tehdä jotain muuta ja navigoit pois muokkausta koskaan lopettamatta, sovellus saattoi jäädä kaikki globaalit oikotiensa rekisteröimättöminä — hiljaa kuolleina — kunnes palasit takaisin. Commit joka sen korjaa on avoin oireesta: liian laaja keskeytys uhkasi "making the app appear broken if the user forgot they had the page open."

Korjaus on yhden rivin kavennus: keskeytä vain kun riviä oikeasti muokataan — avaimena !!editingShortcut, hook-tila joka pitää tallennettavan rivin id:tä — sen sijaan että koko käynnin ajaksi. Hyvä muutos. Mutta "keskeytä vain muokkauksen aikana" nostaa välittömän kysymyksen: mikä lasketaan muokkauksen lopettamiseksi? Näppäimen painaminen lopettaa sen. Escape:n painaminen peruuttaa sen. Mutta entä jos vain… klikkaat pois tai vaihdat kokonaan toiseen sovellukseen kesken tallennuksen? Jos mikään ei peruuta muokkausta, jäät tallentamaan oikotietä ikkunaan joka ei ole fokuksessa, ja oikotiet pysyvät keskeytettyinä. Joten sama PR lisäsi turvaventtiilin: peruuta muokkaus kun fokus lähtee. Ja se on rivi joka kaatui.

Kahdentoista minuutin bugi

Turvaventtiili oli useEffect tiedostossa ShortcutsSettings.tsx joka, sillä aikaa kun riviä muokattiin, kuunteli blur:ia:

const handleBlur = () => cancelEditing()
window.addEventListener('blur', handleBlur)
inputRef.current?.addEventListener('blur', handleBlur) // the landmine

Kaksi kuuntelijaa. Yksi window:ssa — laukeaa kun vaihdat toiseen sovellukseen. Yksi kiinnitetty suoraan, raakana DOM-kuuntelijana, tallennus-<input>:iin — tarkoitettu laukeamaan kun klikkaat pois toiseen elementtiin. Ne näyttävät tarpeettomilta ja vaarattomilta. Eivät ole.

Kävele läpi sekvenssi "switching windows mid-edit," mikä on täsmälleen muutoslokin ilmaus:

  1. Tallennat oikotietä; <input> on fokuksessa. Painat Cmd+Tab toiseen sovellukseen.
  2. Ikkunan blur laukeaa. handleBlur ajaa cancelEditing():n, joka asettaa editingShortcut:n takaisin null:iksi.
  3. Tuon tilan asettaminen irrottaa <input>:n — rivi poistuu muokkaustilasta, joten syöte poistetaan DOM:sta.
  4. Fokuksessa olevan elementin poistaminen DOM:sta lähettää synkronisesti blur-tapahtuman siihen elementtiin. Raaka inputRef-kuuntelija nappaa sen ja kutsuu cancelEditing():n uudelleen — tällä kertaa keskellä React:n commit-vaihetta, sillä aikaa kun komponenttipuuta puretaan.
  5. Tilan asettajan kutsuminen fiber:issä joka on kesken purkua kompastuu yhteen React:n sisäisistä invarianteista, ja se heittää: "Should have a queue". Asetusnäkymä kaatuu.

Bugi ei ole ikkunakuuntelija eikä se oikeastaan ole "peruuta blur:ssa" -konsepti. Se on se että toinen kahdesta kuuntelijasta oli raaka addEventListener React:n hallitsemassa elementissä. React ei tiedä siitä kuuntelijasta, ei siivoa sitä irrotuksessa, ja — ratkaisevasti — selain laukaisee blur:n juuri sen DOM-poiston aikana jota React suorittaa, joten käsittelijä palaa tilalogiikkaasi pahimmalla mahdollisella hetkellä. Korjaus-commitin viesti sanoo sen tarkemmin kuin minä osaan: "when cancelEditing unmounts the input, the blur fires and calls cancelEditing again during React's render cycle."

Korjaus

bf28a50, kaksitoista minuuttia kaatumisen syntymän jälkeen, tekee kaksi pientä asiaa.

Ensiksi, se poistaa raa'an syötekuuntelijan ja pitää vain ikkunan, joka on ainoa joka ylipäätään tarvitsi olla manuaalinen DOM-kuuntelija (ei ole React:n onBlur:ia sille että "the whole window lost focus"):

const handleWindowBlur = () => cancelEditing()
window.addEventListener('blur', handleWindowBlur)
return () => {
  window.removeEventListener('blur', handleWindowBlur)
}

Toiseksi, se siirtää klikkaa-pois-tapauksen — "user clicked another element" — React:n omaan onBlur-propiin syötteessä, ja lykkää sitä tikin verran:

onBlur={() => {
  // Deferred to avoid state updates during React's render cycle.
  setTimeout(() => cancelEditing(), 0)
}}

Molemmat osat ovat tärkeitä. React:n onBlur-propin käyttäminen raa'an kuuntelijan sijaan tarkoittaa että React omistaa käsittelijän elinkaaren eikä jätä sitä roikkumaan. Ja setTimeout(…, 0) takaa että vaikka tämän blur:n laukaiseekin irrotus, cancelEditing()-kutsu laskeutuu seuraavalle tikille — nykyisen renderöinnin ja commitin päätyttyä — joten se ei voi koskaan päivittää tilaa komponentissa joka on yhä purkautumassa. Jos komponentti on siihen mennessä jo poissa, lykätty kutsu on vaaraton no-op.

Siinä on koko korjaus. Poista kuuntelija jota React ei nähnyt; anna React:n ajastaa se joka jäi.

Miksi tämä on sitä hyvää tylsyyttä

On houkutus arkistoida tämä "triviaaliksi" — setTimeout, poistettu rivi — ja jatkaa eteenpäin. Mutta sen muoto on yksi yleisimmistä tavoista joilla React-työpöytäsovellukset kaatuvat, ja se kannattaa sisäistää:

  • Kaatuminen ilmestyy vain elinkaaren rajalla (irrotus), laukaistuna tapahtumasta jota kehys ei ajasta (natiivi blur DOM-poiston aikana). Se ei näy klikkaustestissä; se tarvitsee tarkan "fokus lähtee muokkauksen aikana" -polun.
  • Juurisyy on kahden omistusmallin sekoittaminen — raa'at DOM-tapahtumat ja React:n synteettiset, ajastetut tapahtumat — samassa elementissä. Sillä hetkellä kun käsittelijäsi muuttaa tilaa joka voi irrottaa sen elementin, raaka polku muuttuu uudelleensyötyväksi.
  • Korjaus ei ole "lisää vartijalippu" (vaikka isMounted-ref olisi peittänyt sen); se on raa'an kuuntelijan poistaminen kokonaan, mikä on pienempi muutos ja oikea. Vähennystä taas.

Ja meta-oppitunti, se jonka ympärillä koko tämä sarja pyörii: lue diff, älä julkaisumuistiinpanoa. "Fixed a crash while editing keyboard shortcuts" on tarkka, mutta se piilottaa että kaatuminen oli itseaiheutettu, saman aamun ja ei koskaan toimitettu — ja että todellinen julkaisu on hiljainen oikeellisuusvoitto siitä milloin globaali oikotie keskeytetään.

Mitä muuta toimitettiin

Kaksi asiaa lisää ratsasti mukana v1.8.10:ssä, molemmat rivin arvoisia. Oikotiejärjestelmä sai myös itsekorjautumisen unen/heräämisen yli: macOS voi hiljaa pudottaa sovelluksen globaalien oikoteiden rekisteröinnit kun kone nukkuu, joten jatkokäsittelijä nyt tarkistaa ja rekisteröi ne uudelleen, mikä tarkoittaa että oikotiesi toimivat yhä läppärin herättyä ilman uudelleenkäynnistystä. Ja AI-puolella asiakas tuli fiksummaksi käyttörajojen suhteen — suoratoistopyynnöt jotka osuvat 429:ään perääntyvät nyt eksponentiaalisesti ja nollaavat laskurinsa kun lopetat tallennuksen, sen sijaan että odottaisivat ulos kiinteän viiveen. Kolme luotettavuuskorjausta eri alijärjestelmiin, joista yksi on kaatuminen joka ei koskaan ollut kaatuminen.

Edellinen luku v1-tarinassa: kokouksen tulostaminen PDF:ksi ilman PDF-kirjastoa (v1.8.9); ja koko kaarelle, ohjelmiston toimittamisen anatomia täydellisyyteen asti.

Aiheeseen liittyvät artikkelit

Kolme verbiä, jotka pitävät Web Audion elossa
Steven
Steven7 min lukuaika

Kolme verbiä, jotka pitävät Web Audion elossa

Kaksi GeekBye-julkaisua, kahden kuukauden välein ja kahdessa eri tiedostossa, opettivat ääni-koodillemme saman opetuksen vastakkaisista päistä: lakkaa kohtelemasta selaimen AudioContextia kertakäyttöisenä. Yksi julkaisu oppi kutsumaan resume() kontekstille, jonka macOS oli hiljaa keskeyttänyt kesken tallennuksen; toinen oppi kutsumaan suspend() eikä close(), niin että peräkkäiset istunnot lakkaavat törmäämästä Chromiumin noin-kuuden-kontekstin kattoon. Resume, suspend, close — siinä on koko juoni.

Ohjelmistokehitys
Audio
Desktop
Puhelun erottaminen avoimesta sovelluksesta
Steven
Steven7 min lukuaika

Puhelun erottaminen avoimesta sovelluksesta

GeekBye voi huomata, että olet liittynyt videokokoukseen, ja tarjoutua tallentamaan sen. Tunnistus osoittautuu helpommaksi puoliskoksi — Swift-binääri, joka lukee ikkunoiden otsikoita joka kymmenes sekunti. Vaikea puolisko on tarkkuus: olla laukeamatta kun Zoom on vain auki, olla tarjoamatta kokousta jota jo tallennat, ja olla mykistämättä mikrofonia puhelussa jossa oikeasti olet. Kolme julkaisua, ja jokainen niistä on vartija jonka oli opittava olemaan kukistamatta itseään.

Ohjelmistokehitys
macOS
Desktop
Backendin poistaminen latauspolusta
Steven
Steven6 min lukuaika

Backendin poistaminen latauspolusta

GeekBye tallentaa näyttösi ja tallentaa videon Google Driveesi. Ensimmäinen versio toimitti jokaisen tallenteen GeekByen omien palvelimien kautta matkalla sinne; yksi julkaisu myöhemmin tiedosto meni suoraan koneeltasi Driveen, ja backend alennettiin yhden osoittimen pitämiseen. Kiinnostava osa on se, miten vähän koodia 'suora, jatkettava' versio oikeasti sisältää — koska jatkettavuus tuli välityspalvelimen poistamisesta, ei sen kirjoittamisesta.

Ohjelmistokehitys
Arkkitehtuuri
Desktop