Ders 09 / 19
Kod Bölme
Statik ve dinamik kenarların ayrılması; parça köklerinin belirlenmesi, ilk yüklemede inen ile sonradan inenin hesaplanması, istek başına maliyet ve aşırı bölünmüş parçaların birleştirilmesi.
İçindekiler
Bir önceki dersteki parçalar giriş noktalarından doğdu ve hepsi ilk yüklemede gerekliydi. Tarayıcı girişinin kapanışındaki altı modülün altısı da, kullanıcı henüz hiçbir şeye dokunmadan indi. Oysa Kuzey Yamaç Ölçüm İstasyonu uygulamasında bu doğru değildir: ölçüm listesi her ziyarette görünür, istasyon sorumlusunun paneli ise yalnız o panel açıldığında gerekir. Panelin grafik çizimi ve dışa aktarma kodu, sayfayı okumaya gelen ziyaretçinin indirdiği baytlar arasında bulunmamalıdır.
Bu ayrımı kaynakta işaretleyen bir yazım vardır. import() çağrısı bir bağımlılığı
çizgeye ekler ama onu ilk yüklemeden ayırır. Bu dersin sorusu, o işaretin çıktı parçalarını
nasıl değiştirdiği ve bölmenin nerede kâr getirip nerede zarara döndüğüdür.
İki Kenar Türü
Çizgede iki tür kenar vardır ve ayrım zamandadır.
Statik kenar, kaynağın en üstündeki içe aktarım bildirimidir. Modül sistemleri konusunda kurulan kural gereği bu bildirimler modül değerlendirilmeden önce çözülür: bağımlılık, kendisine bağlı olan modülden önce hazır olmak zorundadır. Statik kenarla bağlanan iki modül aynı anda gerekir.
Dinamik kenar, import() çağrısıdır. Çağrı bir söz döndürür; bağımlılık, çağrı
yürütülene kadar yüklenmez. Dinamik kenarla bağlanan iki modül aynı anda gerekmez ve
aralarında bir zaman sınırı vardır.
Bu zaman sınırı, çıktı tarafında bir dosya sınırına dönüşür. Dinamik içe aktarımın hedefi, kendi bağımlılıklarıyla birlikte ayrı bir dosyaya konabilir; o dosya, çağrı yürütüldüğünde indirilir. Böyle bir hedefe parça kökü (chunk root) denir. Bir derlemede parça kökleri kümesi, giriş noktaları ile dinamik içe aktarım hedeflerinin birleşimidir.
Panelin Ayrılması
Kaynak ağaca panel ve bağımlılıkları eklenir; tarayıcı girişi grafiği doğrudan içe aktarmak yerine paneli dinamik olarak ister. Aşağıdaki blok, bir önceki dersin kaynak ağacının üzerine yazar.
Aşağıdaki blok, Modül Paketleme dersinde kurulan kaynak/ ağacının üzerine yazar; betiği çalıştırmadan önce o dersin kurulum bloğu bu dizinde çalıştırılmış olmalıdır.
mkdir -p kaynak cat > kaynak/uyari.js <<'EOF' import { SICAKLIK_ALT, SICAKLIK_UST } from "./birim.js"; export const esikAsimi = (olcumler) => olcumler.filter((o) => o.sicaklik < SICAKLIK_ALT || o.sicaklik > SICAKLIK_UST); EOF cat > kaynak/disa-aktarma.js <<'EOF' import { ikiliBirim } from "./bicim.js"; import { gunAnahtari } from "./tarih.js"; const kacir = (a) => (a.includes(",") ? "\"" + a + "\"" : a); export function csvUret(olcumler) { const basliklar = ["gun", "damga", "sicaklik", "nem"]; const satirlar = olcumler.map((o) => [gunAnahtari(o.damga), o.damga, ikiliBirim(o), o.nem].map(kacir).join(",")); return [basliklar.join(","), ...satirlar].join("\n"); } EOF cat > kaynak/panel.js <<'EOF' import { sicaklikYolu } from "./grafik.js"; import { csvUret } from "./disa-aktarma.js"; import { baslik } from "./sablon.js"; export async function paneliCiz(kok, istasyon, olcumler) { const { esikAsimi } = await import("./uyari.js"); const asim = esikAsimi(olcumler); kok.innerHTML = baslik(istasyon + " paneli") + "<svg><path d=\"" + sicaklikYolu(olcumler, 320, 120) + "\"/></svg>" + "<p>" + asim.length + " asim</p>"; return { csv: () => csvUret(olcumler) }; } EOF cat > kaynak/giris-tarayici.js <<'EOF' import { liste, baslik } from "./sablon.js"; export function baslat(kok, istasyon, olcumler) { kok.innerHTML = baslik(istasyon) + liste(olcumler); return { async paneliAc(panelKok) { const { paneliCiz } = await import("./panel.js"); return paneliCiz(panelKok, istasyon, olcumler); }, }; } EOF
İki dinamik sınır oluştu: giriş noktası paneli, panel de eşik uyarısı modülünü dinamik olarak istiyor. İkincisi iç içe bir sınırdır ve maliyeti ayrıca ölçülecek.
Parça Köklerinden Parçalara
Bir önceki dersin imza ölçütü aynen geçerlidir, yalnız imzayı üreten küme değişir: giriş noktaları yerine parça kökleri. Bir modülün imzası, onu statik olarak erişebilen kökler kümesidir. İmzasında giriş noktası bulunan modül ilk yüklemede iner; bulunmayan modül, imzasındaki köklerden biri istendiğinde iner.
// bolme.mjs — statik ve dinamik kenarlardan parca cizgesi, maliyet ve birlestirme. import { readFileSync, statSync } from "node:fs"; import { dirname, join, relative } from "node:path"; const GIRIS = "kaynak/giris-tarayici.js"; const STATIK = /^\s*(?:import|export)[^"']*from\s*["']([^"']+)["']/gm; const DINAMIK = /\bimport\(\s*["']([^"']+)["']\s*\)/g; const cizge = new Map(); // yol -> { boyut, statik[], dinamik[] } const kuyruk = [GIRIS]; while (kuyruk.length > 0) { const yol = kuyruk.shift(); if (cizge.has(yol)) continue; const kaynak = readFileSync(yol, "utf8"); const coz = (e) => [...kaynak.matchAll(e)].map(([, b]) => relative(".", join(dirname(yol), b))); const statik = coz(STATIK), dinamik = coz(DINAMIK); cizge.set(yol, { boyut: statSync(yol).size, statik, dinamik }); kuyruk.push(...statik, ...dinamik); } // Parca koku: giris noktasi ve her dinamik ice aktarim hedefi. const kokler = [GIRIS, ...[...cizge.values()].flatMap((d) => d.dinamik)]; const kapanis = new Map(kokler.map((kok) => { const gorulen = new Set(), yigit = [kok]; while (yigit.length > 0) { const y = yigit.pop(); if (gorulen.has(y)) continue; gorulen.add(y); yigit.push(...cizge.get(y).statik); } return [kok, gorulen]; })); // Imza: modulu statik olarak isteyen kokler kumesi. Ayni imza = ayni parca. const gruplar = new Map(); for (const yol of cizge.keys()) { const anahtar = kokler.filter((k) => kapanis.get(k).has(yol)).join("|"); if (!gruplar.has(anahtar)) gruplar.set(anahtar, []); gruplar.get(anahtar).push(yol); } const ad = (y) => y.replace("kaynak/", "").replace(".js", ""); const bayt = (ms) => ms.reduce((t, y) => t + cizge.get(y).boyut, 0); let parcalar = [...gruplar].map(([anahtar, moduller], i) => ({ ad: "P" + (i + 1), kokler: anahtar.split("|"), moduller, boyut: bayt(moduller), ilk: anahtar.split("|").includes(GIRIS), })); const RTT = 60, BANT = 192; // ms; B/ms (1.5 Mbit/s) const maliyet = (liste) => liste.length * RTT + liste.reduce((t, p) => t + p.boyut, 0) / BANT; const eszamansiz = (liste, kok) => liste.filter((p) => p.kokler.includes(kok) && !p.ilk); function rapor(baslik, liste) { console.log(baslik); for (const p of liste.sort((a, b) => a.ad.localeCompare(b.ad))) { console.log(" " + p.ad + " " + String(p.boyut).padStart(4) + " B " + (p.ilk ? "ilk " : "eszamansiz ") + p.moduller.map(ad).sort().join(", ")); } const ilkListe = liste.filter((p) => p.ilk); console.log(" ilk yukleme " + ilkListe.length + " istek " + String(ilkListe.reduce((t, p) => t + p.boyut, 0)).padStart(4) + " B " + maliyet(ilkListe).toFixed(1).padStart(6) + " ms"); for (const kok of kokler.slice(1)) { const g = eszamansiz(liste, kok); if (g.length === 0) continue; console.log(" " + (ad(kok) + " siniri").padEnd(17) + g.length + " istek " + String(g.reduce((t, p) => t + p.boyut, 0)).padStart(4) + " B " + maliyet(g).toFixed(1).padStart(6) + " ms"); } } console.log("-- cizge --"); for (const [yol, d] of [...cizge].sort()) { console.log(" " + ad(yol).padEnd(16) + String(d.boyut).padStart(4) + " B statik: " + (d.statik.map(ad).join(", ") || "-") + " dinamik: " + (d.dinamik.map(ad).join(", ") || "-")); } console.log(" parca koku: " + kokler.map(ad).join(", ")); rapor("-- imzaya gore parcalar (RTT " + RTT + " ms, bant 1.5 Mbit/s) --", parcalar); // 1. Girisle birlikte inen parcalar ayri tutulmaz: hepsi tek ilk parcada birlesir. const ilkler = parcalar.filter((p) => p.ilk); if (ilkler.length > 1) { const birlesik = { ad: "I1", kokler: [...new Set(ilkler.flatMap((p) => p.kokler))], moduller: ilkler.flatMap((p) => p.moduller), boyut: bayt(ilkler.flatMap((p) => p.moduller)), ilk: true, }; parcalar = [birlesik, ...parcalar.filter((p) => !p.ilk)]; console.log("-- birlestirme: " + ilkler.map((p) => p.ad).join(" + ") + " -> I1 (hepsi ilk yuklemede iniyor)"); } // 2. Esigin altinda kalan ve tek yerden istenen eszamansiz parca, isteyenin parcasina girer. const ESIK = 300; for (const p of [...parcalar]) { if (p.ilk || p.boyut >= ESIK || p.kokler.length !== 1) continue; const isteyen = [...cizge].filter(([, d]) => d.dinamik.includes(p.kokler[0])).map(([y]) => y); const hedefler = [...new Set(isteyen.map((y) => parcalar.find((q) => q.moduller.includes(y))))]; if (hedefler.length !== 1 || hedefler[0].ilk) continue; const hedef = hedefler[0]; hedef.moduller.push(...p.moduller); hedef.boyut += p.boyut; parcalar = parcalar.filter((q) => q !== p); console.log("-- birlestirme: " + p.ad + " (" + p.boyut + " B < " + ESIK + " B, tek isteyen) -> " + hedef.ad); } rapor("-- birlestirme sonrasi --", parcalar);
$ node bolme.mjs -- cizge -- bicim 387 B statik: tarih, birim dinamik: - birim 186 B statik: - dinamik: - disa-aktarma 418 B statik: bicim, tarih dinamik: - giris-tarayici 316 B statik: sablon dinamik: panel grafik 458 B statik: birim dinamik: - panel 482 B statik: grafik, disa-aktarma, sablon dinamik: uyari sablon 229 B statik: bicim dinamik: - tarih 227 B statik: - dinamik: - uyari 179 B statik: birim dinamik: - parca koku: giris-tarayici, panel, uyari -- imzaya gore parcalar (RTT 60 ms, bant 1.5 Mbit/s) -- P1 316 B ilk giris-tarayici P2 843 B ilk bicim, sablon, tarih P3 1358 B eszamansiz disa-aktarma, grafik, panel P4 179 B eszamansiz uyari P5 186 B ilk birim ilk yukleme 3 istek 1345 B 187.0 ms panel siniri 1 istek 1358 B 67.1 ms uyari siniri 1 istek 179 B 60.9 ms -- birlestirme: P1 + P2 + P5 -> I1 (hepsi ilk yuklemede iniyor) -- birlestirme: P4 (179 B < 300 B, tek isteyen) -> P3 -- birlestirme sonrasi -- I1 1345 B ilk bicim, birim, giris-tarayici, sablon, tarih P3 1537 B eszamansiz disa-aktarma, grafik, panel, uyari ilk yukleme 1 istek 1345 B 67.0 ms panel siniri 1 istek 1537 B 68.0 ms
Çıktının Okunması
İlk tablo ham imza gruplamasını verir. Panelin kendisi, grafiği ve dışa aktarma modülü tek bir eşzamansız parçada toplanmıştır: üçünü de yalnız panel kökü ister. Şablon, biçim ve tarih modülleri hem giriş noktasından hem panelden erişilebildiği için ilk yüklemededir — paneli bekletmek için bir gerekçe yoktur, zaten inmişlerdir.
Bölmenin kazancı ilk satırdadır: toplam 2882 baytın 1345’i ilk yüklemede iner. Panelin 1537 baytı, o panel hiç açılmazsa hiç inmez. Oran küçük bir örnekte gösteriştir; gerçek bir uygulamada grafik çizimi ve dışa aktarma gibi modüllerin ağırlığı, ilk yüklemenin birkaç katı olabilir.
birim.js modülünün ayrı bir parçaya düşmesi imza ölçütünün doğrudan sonucudur: onu üç
kök de ister, dolayısıyla imzası diğerlerinden farklıdır. Ölçüt bu ayrımı doğru yapar ama
sonucu her zaman yararlı değildir; bir sonraki bölüm bunu düzeltir.
Aşırı Bölmenin Maliyeti
Her parça bir istektir. İstek başına gecikme, taşınan baytın süresinden büyük olabilir; maliyet satırları bu iki kalemi ayrı ayrı toplar. İlk yüklemede üç parça 187 milisaniye tutar, oysa aynı baytlar tek dosyada 67 milisaniyede iner. Aradaki 120 milisaniye, hiçbir şey kazandırmayan iki fazladan istektir.
Buradan iki birleştirme kuralı çıkar.
Birlikte inen parçalar ayrı tutulmaz. İmzasında giriş noktası bulunan her parça ilk yüklemede iner; bunları ayrı dosyalarda tutmanın tek etkisi istek sayısını artırmaktır. Üç parça tek bir ilk parçada birleşir ve ilk yükleme 187 milisaniyeden 67 milisaniyeye iner. Bu kuralın bir istisnası vardır ve önbelleklemeyle ilgilidir: iki farklı giriş noktasının paylaştığı bir parça — bir önceki dersteki tarayıcı ve sunucu ortaklığı gibi — ayrı tutulduğunda iki hedefte de bir kez indirilir. Aynı gerekçe sürümler arasında da geçerlidir ve beşinci derste ölçülecektir.
Eşiğin altında kalan parça ayrılmaz. Eşik uyarısı modülü 179 bayttır ve tek bir yerden istenir. Ayrı bir dosya olarak tutulduğunda kendisine 60,9 milisaniyelik bir istek maliyeti ödetir; panelin parçasına katıldığında ise panelin indirmesini 67,1 milisaniyeden 68,0 milisaniyeye çıkarır. Fark tek yönlüdür: bölme, kazandırdığından fazlasını götürür.
İç içe dinamik sınırın gizli bir maliyeti daha vardır. Eşik uyarısı, panel yüklendikten sonra istenir; iki istek sıralıdır. Kullanıcı paneli açtığında bekleme 67,1 + 60,9 = 128 milisaniyedir, birleştirmeden sonra 68,0 milisaniye. Sıralı istek zinciri, oluşturma modelleri konusunda istemcide oluşturmanın maliyeti olarak ölçülen aynı olgudur; parça düzeyinde de aynı biçimde çalışır.
Sınır Nereye Konur
Bölme sınırı koymanın ölçütü dört sorudur ve dördü de ölçülebilir.
İlk ekranda gerekli mi? Gerekliyse bölünmez; bölme yalnız gecikme ekler. Panel için yanıt hayırdır, ölçüm listesi için evet.
Kaçınılan bayt, eklenen isteği karşılıyor mu? Karşılamıyorsa bölünmez. Örnekteki eşik 300 bayttır ve gösterim içindir; gerçek bir eşik, ölçülen istek gecikmesi ile bant genişliğinden hesaplanır.
Kullanıcı eyleminin doğal bir beklemesi var mı? Bir düğmeye basıldıktan sonra kısa bir bekleme kabul edilebilir; sayfa ilk açılırken beklemek kabul edilmez. Bölme sınırları eylem sınırlarına yerleştirilir.
Kod ne sıklıkla isteniyor? Ziyaretçilerin çoğunun açtığı bir ekranın kodunu
geciktirmek, ilk yüklemeden kazanılanı gezinmede fazlasıyla geri verir. Uygulama Mimarisi
kursundaki kod bölme dersi bu takası rota sınırlarında ele almış ve önceden getirmeyi
çözüm olarak koymuştu; ölçüt aynıdır, sınırın kaynağı farklıdır — orada rota tanımı, burada
import() çağrısının bulunduğu yer.
Bölmenin Getirdiği Yükümlülükler
Bölünmüş bir çıktı, tek dosyalı bir çıktının taşımadığı üç yükümlülük getirir.
Parça adları çalışma zamanında çözülmelidir. Giriş parçası, import() çağrısı
yürütüldüğünde hangi dosyayı isteyeceğini bilmek zorundadır. Bu bilgi çıktıya bir eşleme
olarak yazılır; beşinci ders bu eşlemeyi içerik karmalı adlarla birlikte kuracak.
Bir parçanın yokluğu çalışma zamanı hatasıdır. Kullanıcı sayfayı açıkken yeni bir sürüm yayımlanırsa, eski sayfanın isteyeceği parça adları sunucuda bulunmayabilir. Bu, dağıtım konusundaki geri alma dersinin doğrudan konusudur ve çözümü eski sürümün dosyalarını bir süre daha yayında tutmaktır.
Yükleme sırası korunmalıdır. Bir eşzamansız parça, bağımlı olduğu ilk parça değerlendirilmeden çalıştırılamaz. Çıktıdaki yükleyici bu bağı izler; parça sayısı arttıkça izlenecek bağ sayısı da artar.
Özet
- Çizgedeki iki kenar türü zamanla ayrılır: statik içe aktarım aynı anda gerekliliği,
import()ise zaman sınırı olan bir bağımlılığı bildirir. - Parça kökleri kümesi giriş noktaları ile dinamik içe aktarım hedeflerinin birleşimidir; imzasında giriş noktası bulunan modül ilk yüklemede, bulunmayan modül kökü istendiğinde iner.
- Örnekte toplam 2882 baytın 1345’i ilk yüklemede iniyor; panelin 1537 baytı yalnız panel açılırsa indiriliyor.
- Her parça bir istektir: birlikte inen parçaları ayrı tutmak ilk yüklemeyi 67 milisaniyeden 187 milisaniyeye çıkarır, eşiğin altındaki bir parçayı ayırmak ise kazandırdığından fazlasını götürür.
- İç içe dinamik sınırlar istekleri sıralı hâle getirir; iki sınır tek parçada birleştirildiğinde bekleme 128 milisaniyeden 68 milisaniyeye iner.
- Bölme sınırı dört ölçütle konur: ilk ekranda gereklilik, kaçınılan baytın istek maliyetini karşılaması, eylemin doğal beklemesi ve kodun istenme sıklığı.
Sonraki Adım
Parçalar artık hem sayıca hem zamanca doğru ayrılmış durumda. Ama parçaların içeriği hâlâ modül düzeyinde belirleniyor: çizge bir modülün gerekli olduğunu söylüyor, içindeki hangi adın kullanıldığını söylemiyor. Paylaşılan parçadaki birim modülü dört ad dışa aktarıyor ve bunların hepsi kullanılmıyor; biçimlendirme modülünün ikinci işlevi yalnız dışa aktarma yolunda çağrılıyor. Sonraki ders çizgeyi modül düzeyinden ad düzeyine indirir: kullanılmayan dışa aktarımların elenmesi, bu elemenin hangi durumlarda yapılamadığı ve paketin bunu bildirimle nasıl mümkün kıldığı.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.