DiskLens: de la idee la Mac App Store in sase zile

Pe 19 iunie 2026 am facut primul commit la un analizor de spatiu pe disc pentru macOS. Pe 22 l-am trimis la App Store review, pe 23 era aprobat, iar pe 25 era live la 2,99 $. Sase zile de la prima linie de cod la aplicatie publicata, si prima mea aplicatie macOS. Partea interesanta nu e viteza, ci ce am gasit pe drum: un singur syscall care a taiat consumul de CPU la mai putin de jumatate, un scan care raporta 1,48 TB pe un disc de 500 GB, si doi oameni de pe Reddit care mi-au scris efectiv urmatoarele doua versiuni.

Fereastra DiskLens pe macOS: bara laterala cu vizualizarile, cardul cu spatiul volumului, un grafic inelar cu spatiul pe categorii si lista tipurilor de fisiere dupa marime.
Dashboard-ul: spatiul volumului, categoriile si tipurile de fisiere dupa marime.

Ideea

Pe Windows aveam un obicei: cand se umplea discul, deschideam un analizor care imi arata instant unde s-a dus spatiul, cu folderele afisate imediat si marimile umplute din mers. Pe Mac nu aveam echivalentul pe care sa-l vreau: alternativele erau ori scumpe, ori aratau ca in 2010, ori vindeau abonament pentru ceva ce faci de doua ori pe an.

Asa ca specificatia a fost scrisa in termeni de comportament, nu de functionalitati: alegi un folder, vezi rezultatul cum se umple live, si intelegi din prima privire ce ocupa spatiul. Nativ, macOS 26, cu chrome-ul de sistem la zi. Pret unic, fara abonament. Pentru un utilitar pe care il deschizi rar, abonamentul e ostil si atat.

O nota despre nume, fiindca e o capcana ieftina si nu ma asteptam la ea: numele de lucru din primele zile era al unui produs care exista deja pe Windows. L-am schimbat inainte de publicare, si abia atunci am descoperit ca jumatate din alternativele la care ma gandeam erau deja luate de alte produse. „Nu stiu de vreun produs cu numele asta" nu e o verificare de marca. Cauta pe App Store si in registrele de marci inainte sa fixezi numele in bundle id, in domeniu si in iconita.

Motorul: un singur syscall per director

Prima versiune a scanerului folosea abordarea evidenta: contentsOfDirectory ca sa listezi, apoi resourceValues pe fiecare fisier ca sa-i afli marimea. Merge, dar inseamna un getattrlist per fisier, iar pe sute de mii de fisiere se simte.

Am masurat trei variante pe /Applications (452.000 de fisiere in 86.000 de directoare, cache cald, un singur fir de executie):

VariantaCPUObservatie
contentsOfDirectory + resourceValues per fisier7,5 sabordarea evidenta
FileManager.enumerator cu prefetch de chei7,4 spractic nicio diferenta
getattrlistbulk2,8 sun syscall per director

Concluzia care conteaza: prefetch-ul de chei pe enumerator nu ajuta, fiindca nu produce cache-hit la resourceValues, deci platesti tot un syscall per fisier. Singurul lucru care a mutat acul a fost getattrlistbulk: deschizi directorul o data si ceri toate atributele pentru toate intrarile dintr-un foc.

let fd = open(path, O_RDONLY | O_DIRECTORY)
getattrlistbulk(fd, &attrs, &buf, buf.count, FSOPT_PACK_INVAL_ATTRS)

2,8 secunde in loc de 7,5, adica aproape doua treimi mai putin CPU, si un timp real de rulare mai mic cu peste o treime. Pretul e ca lucrezi direct cu un buffer binar, iar acolo e prima capcana serioasa.

Layout-ul nu e fix

Fiecare intrare din bufferul returnat are un layout care depinde de tipul intrarii. Atributele de director lipsesc la fisiere, cele de fisier lipsesc la directoare, chiar si cu FSOPT_PACK_INVAL_ATTRS. Daca parsezi cu offseturi fixe, la prima intrare de alt tip te desincronizezi si citesti gunoi: marimi absurde, apoi crash.

Corect e sa citesti intai ATTR_CMN_RETURNED_ATTRS (primul camp dupa lungime) si sa avansezi fiecare camp conditionat de bitul lui. Lungimea intrarii te resincronizeaza intre inregistrari, deci o intrare parsata gresit nu contamineaza restul.

1,48 TB pe un disc de 500 GB

Prima scanare a intregului disc a raportat 1,48 TB pe un SSD de 500 GB. Doua cauze diferite, ambele instructive.

Prima: scanul intra in volume care nu erau pe discul local: un share SMB, un Time Machine pe retea, simulatoare, imagini de disc montate. Solutia parea evidenta: compara identificatorul de volum al copilului cu al parintelui. Nu merge: getattrlistbulk nu traverseaza punctul de montare, deci raporteaza identificatorul stub-ului, adica al volumului parinte. Share-ul SMB trecea de orice verificare pe fsid si se scanau linistit zeci de GB de retea.

Raspunsul corect e sa intrebi direct sistemul daca directorul e punct de montare:

if isDir, excludeMounts, mountStatus & DIR_MNTSTATUS_MNTPOINT != 0 { continue }

A doua: pe macOS modern exista un firmlink, /.nofollow, care expune o copie integrala a radacinii. La ls -ld pare un director gol de 64 de octeti; in realitate /.nofollow/Users are acelasi inode ca /Users. Rezultatul: tot discul numarat de doua ori, vreo 620 GB aparuti ca un folder cu nume ciudat. Fixul e cel pe care il foloseste si du: tii minte perechea (device, inode) pentru fiecare director si nu recursezi de doua ori in acelasi obiect. Firmlink-urile normale (/Users, /Applications) nu sunt puncte de montare, deci raman scanate, exact ce vrei.

Ce cere de fapt App Store-ul

Codul a fost partea previzibila. Publicarea are o lista de pasi pe care nu-i vezi pana nu-i lovesti, si care nu sunt intr-un singur tutorial cap-coada.

  1. Doua certificate distincte: unul care semneaza aplicatia si unul care semneaza pachetul de instalare. Al doilea nu apare la o cautare obisnuita de identitati de semnare, fiindca e cu politica de installer (usor de crezut ca lipseste).
  2. App ID inregistrat plus un profil de provisioning dedicat pentru App Store, copiat unde il cauta sistemul.
  3. Un manifest de confidentialitate, PrivacyInfo.xcprivacy. Fara el pachetul e respins automat, inainte sa-l vada un om.
  4. Build de distributie facut obligatoriu prin xcodebuild.
  5. Upload prin Transporter, unde e o capcana de care m-am lovit direct.

Manifestul de confidentialitate cere coduri de motiv pentru fiecare API „cu motiv obligatoriu" pe care il atingi. Pentru un analizor de disc, astea sunt:

APICodDe ce
File timestampDDA9.1 + 3B52.1afisez data modificarii; citesc metadate pe fisiere alese de user
Disk spaceE174.1arat spatiul liber ca sa ajut userul sa elibereze
User defaultsCA92.1preferinte, doar in aplicatie

Codurile trebuie sa fie cele reale din documentatia Apple. Un asistent mi-a propus, foarte convingator, un cod care pur si simplu nu exista. Verifica-le la sursa.

Capcana cu build-ul m-a costat un pachet aruncat: aveam un script propriu de build cu swiftc, rapid si comod pentru development. Numai ca el nu compileaza catalogul de iconite in formatul nou si nu declanseaza chrome-ul Liquid Glass al sistemului. Aplicatia iesea cu iconita veche si fara aspectul nativ: corecta functional, gresita vizual. Orice build care pleaca spre lume trece prin xcodebuild.

./package-mas.sh
pkgutil --check-signature build/export/DiskLens.pkg

Iar in Transporter: Validate nu inseamna Deliver. Validate verifica pachetul si il arunca. Am asteptat un timp bun sa apara build-ul in App Store Connect, dupa o validare perfect curata, pana mi-am dat seama ca nu-l urcasem niciodata.

Ultimul blocaj n-a fost tehnic deloc. Aplicatia a trecut review-ul din prima si a stat cateva zile la „Ready for Distribution" fara sa poata fi vanduta, fiindca lipsea acordul pentru aplicatii platite din sectiunea de business, cu tot ce vine cu el (formular fiscal american inclusiv). Daca vinzi din Romania, incepe hartogaraia inainte sa termini codul, nu dupa.

Sandbox-ul: ce nu poti face, oricat ai vrea

Aplicatia din magazin ruleaza in sandbox si poate citi doar folderele alese explicit de user, cu acces pastrat intre lansari prin bookmark-uri. Consecinta dura: nu poate scana tot discul. Accesul complet la disc e rezervat aplicatiilor nesandboxate, distribuite direct, iar o aplicatie de App Store care incearca sa-l ceara e respinsa. De-aia analizoarele serioase de disc se descarca de pe site-ul lor, nu din magazin. Am ales sa livrez ambele: build sandboxed in magazin, si un build notarizat, separat, pentru cine vrea discul intreg.

Capcana zilnica a sandbox-ului e mai subtila. In interiorul lui, FileManager iti da caile din containerul aplicatiei, in timp ce panoul de selectie de fisiere iti da calea reala. Compari doua siruri care arata la fel dar nu sunt, si te intrebi de ce nu se potriveste nimic. Home-ul real se ia din passwd:

let home = getpwuid(getuid())?.pointee.pw_dir

Reddit, sau cum doi oameni au scris versiunea urmatoare

Cand a devenit live, am postat-o intr-un subreddit de aplicatii macOS. Fara buget, fara lista de emailuri, fara nimic: un post si atat. Reactia n-a fost spectaculoasa ca volum, dar doi oameni au facut mai mult pentru produs decat orice plan de marketing.

Primul, un client care platise, mi-a scris o lista de sase lucruri care il deranjau concret. Nu „nu-mi place", ci: lista de tipuri de fisiere e taiata la opt intrari si ramane spatiu gol; graficul inelar nu-mi spune ce felie e sub cursor; harta nu ma lasa sa intru intr-un folder; cand redeschid aplicatia nu mai vad nimic din ce ma uitam. Toate erau adevarate. Toate erau lucruri pe care le vedeam zilnic si incetasem sa le mai observ. Aia a devenit versiunea urmatoare, aproape integral.

Una singura din lista am refuzat-o, si merita explicat de ce: cerea ca aplicatia sa scaneze automat la pornire. Ar fi rezolvat simptomul (redeschid si e gol), dar ar fi pornit munca grea pe disc de fiecare data cand deschizi aplicatia, poate pentru alt folder decat vrei. Solutia corecta la nemultumirea reala e sa tii minte rezultatul si sa-l arati instant, fara sa scanezi. Feedback-ul iti spune unde doare, nu neaparat ce sa faci.

Al doilea a raportat ceva ce nu puteam reproduce: scanul folderului personal se bloca peste 30 de minute. La mine mergea. Diferenta era ca el avea OneDrive si Google Drive instalate, eu nu. In loc sa modific scanerul pe ghicite, am construit un build separat, semnat si notarizat, care logheaza fiecare director cu timpul petrecut in el si marcheaza orice depaseste un prag. I l-am trimis, a rulat scanul pana s-a blocat, mi-a trimis logul.

Cauza era clara in log: pe folderele sincronizate din cloud, apelul care citeste atributele blocheaza, uneori la nesfarsit, fiindca providerul incearca sa materializeze continutul. Fiecare apel blocat tinea ocupat un fir din pool, iar cateva astfel de foldere opreau tot scanul. Fixul are doua parti: nu recursa in directoarele marcate ca „fara continut local" (citirea flagului nu declanseaza descarcarea), si arata marimile din cloud intr-o sectiune separata, citite din indexul Spotlight, fara sa atingi fisierele.

Bonus contraintuitiv: Spotlight nu poate spune cat ocupa un fisier pe disc. Pentru un placeholder nedescarcat raporteaza marimea logica chiar si la campul care ar trebui sa fie marimea fizica. Verificat pe un fisier cu zero blocuri alocate, raportat totusi cu zeci de kiloocteti.

Al treilea mesaj, mult mai tarziu, a fost o singura propozitie: „backup-urile de la iMazing imi umplu rezultatele la duplicate". Asta a devenit o functie de excludere de foldere. Cererile bune de la utilizatori sunt aproape mereu de o propozitie.

Cifrele, fara hype

Cronologia completa:

DataCe s-a intamplat
19 iunie 2026primul commit
21 iuniescanerul rescris pe getattrlistbulk
22 iunietrimis la App Store review
23 iunieaprobat din prima
25 iunielive in magazin, 2,99 $
27–28 iunieprimul feedback de pe Reddit
4 iulieprimul update trimis
8 iulieal doilea update, live

Cifrele reale dintr-o zi obisnuita, la aproape o luna de la lansare: 768 de afisari in magazin, 231 de vizualizari ale paginii de produs si 29 de descarcari noi. Rata de conversie: 7,98%, la o medie uzuala in magazin de 2–5%.

Conversia buna imi spune ca pagina de produs isi face treaba: cine ajunge acolo, cumpara. Volumul imi spune ca ajung putini: problema e vizibilitatea, nu produsul. Sunt doua concluzii diferite, cu solutii diferite, si e usor sa le confunzi cand te uiti la un singur numar.

Am pus si intrebarea prosteasca, fiindca mi-o pusesem singur: exista aplicatii cu un milion de descarcari in prima zi. Da: gratuite, pe iPhone, de la studiouri cu bugete de marketing. Un utilitar platit pentru macOS traieste in alt univers: se masoara in vanzari constante pe luni, nu intr-o zi virala. Cel mai cunoscut concurent din nisa asta nu face milioane; face vanzari mici si constante de ani de zile. Asta e plafonul realist, si e ok.

Emotiile, fiindca fac parte din poveste

Cele doua zile de asteptare a review-ului au fost disproportionat de stresante fata de miza reala. Verificam statusul de mai multe ori pe zi. Nu pentru bani (la 2,99 $ nu se pune problema), ci fiindca era prima data cand ceva scris de mine urma sa fie judecat de o organizatie care poate spune „nu" fara sa explice.

A trecut din prima. Bucuria a tinut vreo doua ore, dupa care a venit sentimentul cu care nu ma asteptam sa ma intalnesc: si acum ce? Aplicatia era live si nu se intampla nimic. Nimeni nu stia ca exista. Publicarea nu e finalul, e momentul in care incepe partea pentru care un inginer nu e antrenat.

Si o observatie despre primul „nu merge la mine": doare mai putin decat crezi si e mai util decat orice lauda. Omul care mi-a scris ca i se blocheaza scanul mi-a dat, de fapt, o functie intreaga pe care n-as fi construit-o niciodata singur: nu aveam cloud instalat, deci pentru mine bug-ul nu exista.

Ce as face la fel data viitoare

DiskLens e in Mac App Store, cu detalii pe disklens.vtun.ro. Cere macOS 26, costa 2,99 $ o singura data si nu colecteaza nimic.

← Toate articolele