Nemrégiben több Ghostty felhasználó is arra panaszkodott, hogy az alkalmazás elképesztő mennyiségű memóriát zabál fel – egyeseknél akár 37 GB-ot is elfogyasztott mindössze 10 nap alatt. Ez engem is meglepett, hiszen egy termináltól ilyesmi nem várható. A jó hír viszont az, hogy a fejlesztők megtalálták a hibát, és már be is építették a javítást. Ebben a cikkben végigvezetlek azon, hogy pontosan mi okozta ezt a memóriaszivárgást, hogyan működik a Ghostty belső memóriakezelése, és milyen lépésekkel sikerült végül elcsípni ezt a makacs problémát.
Miért volt ilyen nehéz megtalálni a hibát?
A memória szivárgás már legalább a Ghostty 1.0 verziója óta jelen volt, de csak mostanában vált igazán látványossá. Ennek az az oka, hogy csak néhány népszerű parancssori alkalmazás – különösen a Claude Code – kezdett olyan körülményeket teremteni, amelyek nagy mennyiségben előhozták ezt a hibát. Ez a speciális körülményrendszer tette igazán trükkössé a probléma diagnosztizálását.
A javítás már elérhető az éjszakai (nightly) kiadásokban, és várhatóan a márciusban érkező 1.3-as verzióban is benne lesz.
A PageList: így kezeli Ghostty a terminál tartalmát
Ahhoz, hogy megértsük a hibát, először azt kell tudnunk, hogyan tárolja Ghostty a terminál tartalmát. Ehhez egy PageList nevű adatszerkezetet használ, ami egy kétszeresen láncolt lista (doubly-linked list) memóriaoldalakból (memory pages) áll. Ezek az oldalak tárolják magát a terminál tartalmat: karaktereket, stílusokat, linkeket és minden mást.
Fontos tisztázni, hogy ezek az “oldalak” nem egyszerű virtuális memóriaoldalak – hanem olyan összefüggő memóriablokkok, amelyek rendszeroldal méretre vannak igazítva és több rendszeroldalból állnak össze.
Kétféle oldal allokáció létezik:
- Standard oldalak: Ezek fix méretűek, egy memóriapoolból kerülnek kiadásra és oda is visszaadják őket újrafelhasználásra.
- Nem-standard oldalak: Ezek változó méretűek (általában nagyobbak), közvetlenül mmap-pel vannak lefoglalva és felszabadításukhoz munmap hívás szükséges – nem kerülnek vissza a poolba.
A pool használata azért fontos, mert az mmap hívások lassúak lehetnek; így ha van lehetőség újrahasznosítani egy már lefoglalt oldalt, az jelentős teljesítményelőnyt jelent.
A scrollback pruning – avagy hogyan takarítja ki Ghostty a régi tartalmat
A terminálokban megszokott funkció, hogy korlátozzák mennyi előzményt (scrollback history) tárolnak. Ghostty-ban van egy beállítás erre: ha eléri ezt a limitet, akkor elkezdi törölni a legrégebbi oldalakat.
Egy fontos optimalizáció viszont itt jön képbe: amikor eléri ezt a limitet, nem dobja ki azonnal az első oldalt és kér új memóriát hátulra – hanem egyszerűen áthelyezi ezt az első oldalt a lista végére és “kitakarítja” (metadata szinten), így újra felhasználja azt. Ez drasztikusan csökkenti az allokációk számát és gyorsabbá teszi az egész folyamatot.
A hiba gyökere: metadata és valós memória szinkronizációjának hiánya
Itt jön be az egész sztori lényege: amikor újrahasznosították ezt az oldalt scrollback pruning során, mindig visszaállították annak méretét standard méretre metadata szinten – de nem változtatták meg ténylegesen az alatta lévő memóriafoglalást! Így előfordult, hogy egy valójában nagyobb (nem-standard) oldal úgy lett kezelve mint egy standard méretű oldal.
Amikor később felszabadították ezeket az oldalakat (például amikor bezárták a terminált), akkor mivel metadata szerint standard méretűnek tűntek, visszaadták őket csak simán a poolnak – de nem hívták meg rájuk azt az erőforrás-felszabadító függvényt (munmap), ami ténylegesen felszabadítaná az operációs rendszer számára lefoglalt memóriát. Így ezeket az óriási memóriablokkokat soha nem engedték el – klasszikus memória szivárgás történt.
Hogyan néz ki ez lépésekben?
- Lefoglal egy nem-standard méretű oldalt (pl. sok emoji vagy link miatt).
- A scrollback pruning újrahasznosítja ezt az oldalt: metadata szerint visszaállítja standard méretre.
- Később felszabadításkor úgy kezeli mint standard oldalt és nem hívja meg rá a munmap-et.
- A nagy memória soha nem szabadul fel – így nő folyamatosan a fogyasztás.
Miért pont most lett ez probléma?
A kulcs itt Claude Code nevű CLI alkalmazás megjelenése volt. Ez ugyanis rengeteg olyan kimenetet generál, ami sok többkódpontú karakterből álló grapheme-t tartalmaz – emiatt gyakran kényszeríti Ghostty-t arra, hogy ne-standard méretű oldalakat használjon. Ráadásul Claude Code sok scrollback tartalmat is generál egyszerre. Ez együtt olyan helyzetet teremtett, ahol ez a régóta meglévő hiba tömegesen aktiválódott.
Szeretném hangsúlyozni: ez nem Claude Code hibája! Ő csak olyan helyzetbe hozta Ghostty-t, ami eddig rejtve maradt.
A megoldás: soha ne használd újra nem-standard oldalakat
A javítás egyszerű: ha scrollback pruning során találkozunk nem-standard méretű oldallal, akkor azt rendesen felszabadítjuk (munmap), majd újat kérünk standard méretben a poolból. Így biztosan nem marad bent fölösleges nagy memóriafoglalás.
if(first.data.memory.len > std_size){
self.destroyNode(first);
break prune;
}Bár lehetett volna bonyolultabb stratégiákat is alkalmazni (például mérni milyen gyakran fordulnak elő nem-standard oldalak és ehhez igazítani az optimalizációkat), jelenleg ez az egyszerű megoldás illeszkedik legjobban Ghostty eredeti elképzeléseihez és stabilan működik.
További fejlesztések: macOS virtuális memória címkék
A javítás részeként bekerült támogatás macOS-en speciális virtuális memória címkékhez (VM tags), amit a Mach kernel biztosít. Ez lehetővé teszi, hogy amikor memóriát foglalunk PageList számára, akkor azt külön címkével lássuk el – így könnyebb volt megtalálni és követni ezt a memóriaszivárgást különböző eszközökkel.
inline fn pageAllocator() Allocator {
if(builtin.is_test) return std.testing.allocator;
if(!builtin.target.os.tag.isDarwin()) return std.heap.page_allocator;
const mach = @import("../os/mach.zig");
return mach.taggedPageAllocator(.application_specific_1);
}Ezzel sokkal átláthatóbbá váltak a memóriakezelési folyamatok macOS alatt is.
Hogyan próbálják megelőzni hasonló hibákat?
- Debug build-ekben speciális leak-detektáló Zig allokátorokat használnak.
- Minden commit után futtatják Valgrind-et unit teszteken keresztül, hogy ne csak szivárgást hanem más memóriakezelési problémákat is kiszúrjanak.
- macOS GUI esetén Instruments eszközzel keresik aktívan a Swift kódlefedettség alatti szivárgásokat.
- Minden GTK-hoz kapcsolódó pull requestet Valgrind-del ellenőriznek GUI szinten is.
Bár ezekkel eddig jól mentek dolgok, ez a konkrét hiba pont azért csúszott át rajtuk mert nagyon speciális körülmények között jelentkezett – amiket korábban nem sikerült reprodukálni teszteken belül. A mostani javítás része egy reprodukciós teszt is lett, ami segít megelőzni hasonló regressziókat később.
Zárszó
Eddig ez volt Ghostty legnagyobb ismert memóriaszivárgása és az egyetlen olyan hiba is egyben, amit több felhasználó is megerősített. A fejlesztők továbbra is figyelik majd az esetleges újabb memóriajelentéseket és igyekeznek gyorsan reagálni rájuk. Az ilyen hibák kapcsán mindig kulcsfontosságú tudni reprodukálni őket – csak így lehet hatékonyan orvosolni őket!
Nagy köszönet @grishy-nak, aki végül megbízható reprodukciót adott ehhez az ügyhöz – így önállóan is elemezhettem és ellenőrizhettem mindent. Az ő elemzésük ugyanarra jutott mint én; együtt pedig biztosak lehettünk abban, hogy jó irányba haladunk.
Köszönet jár azoknak is akik részletes diagnosztikai adatokat küldtek be! A közösség által gyűjtött információk – például footprint output vagy VM régió számlálások – nagyon fontos nyomokat adtak ahhoz, hogy végül megtaláljuk bűnöst: magát a PageList-et.
Forrás: https://mitchellh.com/writing/ghostty-memory-leak-fix





