12 KiB
macOS / Apple Silicon Performans Optimizasyonu — Araştırma Raporu
Tarih: 2026-07-21 Tetikleyici: MacBook Air M2 (Mac14,2), macOS 26.1 (25B78) üzerinde 40 dakikalık crawl sonrası post-crawl fazında main process V8 heap OOM crash'i (
hata.txt— EXC_BREAKPOINT / brk 0,LowMemoryNotification→FatalProcessOutOfMemory,node::sqlite::StatementSync::Alliçinde). Kapsam: FreeCrawl SEO'nun düşük donanımlı Apple Silicon Mac'lerde (8–16 GB unified memory, fansız MacBook Air) darboğazsız çalışması.Kanıt durumu: 26 kaynak tarandı, 125 iddia çıkarıldı. Doğrulama fazının bir kısmı oturum limitine takıldığından 25 iddianın 6'sı 3-0 çapraz doğrulamadan geçti, 3'ü çürütüldü (rapora alınmadı), kalanlar kaynak alıntılı ancak tek kaynaklı — ⚠️ ile işaretli.
1. En kritik bulgu: Crash'in tavanı RAM değil, Electron'un 4 GB V8 kafesi ✅
hata.txt'deki çökme 8 GB'lık Air'de değil, 64 GB'lık Mac Studio'da da aynen yaşanırdı.
- Electron 14'ten beri pointer compression etkin: V8 heap'i process başına maksimum 4 GB ile sınırlı — makinedeki fiziksel RAM'den bağımsız. (electronjs.org/blog/v8-memory-cage) ✅ 3-0
- Electron 21'den beri sandboxed pointers de etkin; crash raporundaki
Memory Tag 255 = 1.3 TBbu sandbox'ın sanal adres rezervasyonu — gerçek RAM tüketimi değil, zararsız. (v8.dev/blog/sandbox) ✅ 3-0 (rezervasyon boyutu ⚠️) - ⚠️
--max-old-space-sizeişe yaramaz: pointer-compression kafesi flag'i ezer;app.commandLine.appendSwitch('js-flags', ...)main process için no-op (isolate zaten yaratılmış). Maintainer'lara göre runtime workaround yok; tek çıkış özel Electron derlemesi. (electron#31330) - ⚠️ En sinsi detay: Electron'da ArrayBuffer/Buffer verisi de Node.js'in aksine kafes içinde tutuluyor (maintainer nornagon'un tespiti). "Büyük veriyi Buffer'a al, heap rahatlasın" taktiği Electron'da çalışmaz.
- ⚠️ Mimari sonuç:
worker_threadsaynı process'te yaşadığından main isolate + reader worker'lar + writer worker'lar tek 4 GB kafesi paylaşıyor. Worker havuzları CPU paralelliği sağlıyor ama bellek tavanını genişletmiyor; worker→mainpostMessagestructured clone anlık olarak veriyi ikiye katlıyor.
Windows/Intel kafasından fark: "Page file büyür, idare eder" varsayımı Electron'da her platformda geçersiz — kafes 4 GB. Düşük RAM'li Mac bunu sadece daha erken hissettirir (compression/swap baskısı kafes OOM'undan önce başlar).
Gerçek çözüm — veri hacmini heap'e hiç sokmamak:
StatementSync.prototype.iterate()— tam bu amaçla eklendi, Node v22.13.0'a backport edildi, Electron 41.2.2'nin Node 22.x'inde mevcut. (nodejs.org/api/sqlite.html, nodejs/node#54213)- Gerçekten büyük heap gerektiren işler için maintainer önerisi: işi Electron dışına taşımak.
Pratik karşılığı
utilityProcess— ayrı OS process = ayrı 4 GB kafes + ayrı crash domain'i.
2. procRole: Background — App Nap + E-core hapsi ✅
Crash raporundaki Role: Background tesadüf değil; crawl arka planda çalışıyordu:
- App Nap koşulları: ön planda değil + görünür içerik güncellenmiyor + ses yok + assertion alınmamış → aday. ✅ 3-0
- App Nap üç mekanizmayla vurur: ✅ 3-0
- Priority reduction → daha az CPU payı
- Timer throttling →
setInterval/setTimeouttabanlı PQueue iş dağıtımı seyrekleşir - I/O throttling → SQLite disk yazışları kısılır
- Resmi çıkış: NSProcessInfo activity assertion → sistem App Nap'e sokmaz. ✅ 3-0
Electron karşılığı:
powerSaveBlocker.start('prevent-app-suspension'). (Apple App Nap docs) - ⚠️ Ödünleşim:
prevent-app-suspensionsistem uykusunu da engeller; "sadece App Nap'i engelle" granülerliği Electron'da yok ('not planned' kapatıldı). Crawl aktifken aç, bitincestop(). - ⚠️ E-core hapsi (Apple Silicon'a özgü): QoS
background(9) thread'ler yalnızca E-core'larda çalışır; P-core'lar boşta olsa bile terfi yok, dışarıdan terfi de mümkün değil. (eclecticlight.co) - ⚠️ App Nap'e girmiş uygulamalar bellek baskısında compression/swap için öncelikli hedef.
3. Fansız Air'de sustained load — termal adaptasyon ⚠️
- macOS termal durum API'si:
nominal / fair / serious / critical. Apple önerisi:fair'de proaktif azalt,serious'ta CPU+I/O düşür,critical'de minimuma in. (Apple thermal docs) - Native modül gerekmez: Electron 41.2.2'de
powerMonitor.getCurrentThermalState()ve'thermal-state-change'event'i mevcut (electron.d.ts'ten doğrulandı). - Uyarı: desteklenmeyen sistemler her zaman
nominaldöndürür → tek sinyal olarak kullanma; mevcut adaptive-throttle (yanıt süresi bazlı) ile birleştir. - Önerilen merdiven:
fair→ concurrency %75,serious→ %50,critical→ minimum.
4. SQLite × APFS — Windows'tan gerçekten farklı davranan katman ⚠️
- macOS'ta
fsync()dayanıklılık garantisi vermez — güç kesintisi garantisi içinF_FULLFSYNCgerekir; o da çok pahalı. Apple'ın sistem SQLite'ı bilefullfsync=on'u sessizceF_BARRIERFSYNC'e çevirir. (mjtsai.com, avi.im/blag/2025/sqlite-fsync) - Crawl projesi için doğru nokta:
WAL+synchronous=NORMAL— corruption-safe, fsync yalnızca checkpoint'te; kaybedilebilecek şey son commit'ler (yeniden fetch edilebilir veri).fullfsyncaçma. - WAL sınırsız büyüyebilir: 40+ dk sürekli yazan crawl'da
wal_autocheckpoint+ faz aralarındaPRAGMA wal_checkpoint(TRUNCATE). (phiresky) mmap_size(256 MB–1 GB): read syscall'larını page-cache mapping'e çevirir; yalnızca sanal bellek rezerve eder.- Apple Silicon 16 KB VM sayfası kullanır — 1 MB'lık TEXT blob'lar için
page_size=8192/16384düşünülebilir (yeni DB'lerde; mevcutlara VACUUM gerekir). - Sorgu hijyeni: body kolonunu list/aggregate sorgularından dışla; Google eşiği "4 KB'ın birkaç katı üstü dosyaya" — 1 MB gövdeler için uzun vadede dosya + path kolonu meşru seçenek.
5. macOS bellek yönetimi — düşük RAM'li Mac'te ne olur? ⚠️
- Kademeli davranış: önce memory compression (10.9+), sonra swap; process kill son çare
ve önce düşük öncelik bandı. Jetsam'in masaüstü agresifliği konusunda kaynaklar çelişkili
(iki karşıt iddia da çürütüldü) — net olan: bizim crash sistem kill'i değil, V8'in 4 GB
tavanına çarpması (
LowMemoryNotification→ abort zinciri kanıtı). DISPATCH_SOURCE_TYPE_MEMORYPRESSURE(10.9+) native baskı bildirimi verir; native köprüsüz pratik alternatif: büyük materialization öncesiv8.getHeapStatistics()headroom kontrolü.- Unified memory'de RAM aynı zamanda VRAM: 8 GB Air'de efektif bellek nominalden az.
6. macOS 26 (Tahoe) — bilinen Electron sorunları ⚠️
| Sorun | Durum | FreeCrawl'a etkisi |
|---|---|---|
_cornerMask private-API override → WindowServer GPU spike, sistem geneli kasma |
Electron 36+'ya backport (PR #48376, 2025-09-26) ile düzeltildi | Yok — 41.2.2 düzeltmeyi içerir (theregister, #48311) |
MAS/App Sandbox + arm64 + macOS 26'da başlangıçta EXC_BREAKPOINT (MAP_JIT sıkılaşması hipotezi) |
Açık; Apple DTS "JIT rehberi değişmedi" diyor, maintainer onayı yok | App Store dağıtımı planlanırsa kritik; sandbox'sız DMG'de üremiyor (#51351, Apple forums) |
| 26.2'de Electron uygulamalarının crash log'suz SIGKILL'lenmesi | Tek kullanıcı raporu, 'not planned', doğrulanmamış | İzlenmeli (#49261) |
AutoFill (NSAutoFillHeuristicController) scroll yavaşlığı |
26.0.1'de kısmen düzeltildi | Düşük |
Teşhis ipucu: Bu regresyon sınıfının istisna imzası bizimkiyle aynı (EXC_BREAKPOINT /
brk 0, V8 içinde). Ayrım: OOM'da stack'te LowMemoryNotification + node::sqlite frame'leri
var ve çökme dakikalar sonra; JIT regresyonunda çökme başlangıçta ve JIT/compiler thread'inde.
7. Eylem planı ve durum
| # | Öncelik | İş | Durum |
|---|---|---|---|
| 1 | P0 | recomputeBoilerplateCoverage: .all(sampleSize) → 64'lük keyset batch + SUBSTR(body,1,200000); dbCall ile writer worker'a taşındı (tepe bellek ~2–4 GB → ~25 MB) |
✅ 2026-07-21 |
| 2 | P0 | Diğer toplu url_sources.body okuyucuları tarandı — seoTextCorpus body'yi zaten dışlıyor, duplicate clustering yalnızca simhash okuyor; başka toplu okuyucu yok |
✅ 2026-07-21 |
| 3 | P0 | Crawl sırasında powerSaveBlocker('prevent-app-suspension') — ref-count'lu, done + yeni stopped event'iyle bırakılıyor |
✅ 2026-07-21 |
| 4 | P1 | callReaderOrFallback: ağır metotlarda (HEAVY_METHODS) main-thread fallback kaldırıldı — rejection döner, sidebar eski sayıları korur ve sonraki tick'te yeniden dener |
✅ 2026-07-21 |
| 5 | P1 | Termal-adaptif concurrency: Crawler.setThrottleScale (dış tavan, lag-adaptasyonuyla kompoze) + macOS'ta powerMonitor thermal-state-change → nominal 1 / fair 0.75 / serious 0.5 / critical 0.25; crawl başlangıcında mevcut durum da uygulanır |
✅ 2026-07-21 |
| 6 | P1 | SQLite pragma seti — inceleme sonucu zaten mevcuttu (WAL, synchronous=NORMAL, mmap_size=30GB, wal_autocheckpoint=2000, busy_timeout, temp_store=MEMORY); eksik olan tek parça eklendi: post-crawl wal_checkpoint(TRUNCATE) (writer worker üzerinden) |
✅ 2026-07-21 |
| 7 | P2 | DB worker'ları worker_threads → utilityProcess (ayrı 4 GB kafes + crash izolasyonu) — bilinçli ertelendi (2026-07-21): crawl yazma yolunu değiştiren en riskli iş, gerçek crawl smoke-testleriyle doğrulanabileceği ayrı bir oturuma bırakıldı; watchdog SAB'ı cross-process çalışmadığından heartbeat'lerin mesaj tabanlıya çevrilmesi de gerekecek |
⏸ |
| 8 | P2 | Heap headroom guard: ensureHeapHeadroom (core, v8.getHeapStatistics tabanlı, 256 MB rezerv) — XLSX export satır biriktirme döngüsü (50K'da bir) + workbook build öncesi + Log Analyzer export'una uygulandı; OOM abort yerine anlaşılır "use CSV" hatası |
✅ 2026-07-21 |
| 9 | P2 | Sorgu/export hijyeni: (a) iterateUrlsByCategory + iterateIndexableUrls sahte streamer'dı (içeride .all()) — keyset-paginated gerçek streaming'e çevrildi (1M-URL CSV export tepe belleği ~yüzlerce MB → sabit); (b) queryUrls'a server-side limit clamp (≤10K); (c) SELECT * bilinçli korundu — urls'ta sınırsız kolon yok, mapping tüm kolonları tüketiyor, 187 kolonluk el listesi drift riski; (d) bonus: proje şifreleme/çözme tam-dosya-Buffer'dan 8 MB streaming'e çevrildi (format aynı; 9 senaryoluk round-trip testiyle doğrulandı) |
✅ 2026-07-21 |
| 10 | P2 | Uzun vade: url_sources.body'yi DB dışına (dosya + path) taşıma — şema migrasyonu + tüm okuma/yazma yollarını değiştiren büyük iş; P0–P2 sonrası acil bellek baskısı kalmadığından ihtiyaç doğana kadar ertelendi |
⏸ |