İçeriğe geç
academia.sh

Ders 15 / 20

Kare Hızı ve Profilleme

Ağacı üç kez yeniden üreten bir güncellemenin sıfır komut verebildiğini ölçer. Yeniden çizim ile komut ayrılır; belleme sayacı düşürür, komutu değil.

Yayın Güncelleme

Bu derste

  • Yeniden çizim sayısı ile komut sayısını ayrı iki ölçü olarak okur
  • Aynı durumdan üç kez üretilen ağacın sıfır komut verdiğini ölçer
  • Bellemenin yeniden çizim sayısını düşürdüğünü ama komutu değiştirmediğini gösterir
  • Takılmanın bedelinin kural dışında, JS tarafındaki işte durduğunu açıklar
İçindekiler

Önceki ders başa eklenen bir ögenin komut sayısını dörtten bire indirebildiğini gösterdi. Orada ağaç değişiyordu ve soru değişikliğin satırlara nasıl dağıldığıydı. Bu derste ağaç değişmez: aynı durumdan üç kez üretilir. Kullanıcı yine de takılma yaşayabilir, çünkü ağacı üretmek de bir iştir. Bu ders o işin ağaçta görünmediğini, ama ölçülebildiğini anlatır.

Kursun sorusu değişmedi: bu güncelleme görünüm ağacındaki düğüme ne yapıyor? Yanıtlar korunan, güncellenen, kurulan ve sökülen; korunan dışındakilerin toplamı komuttur. Kural, çekirdek dosyada aşağıda verilmiştir.

// fate.mjs — bir guncellemenin gorunum agacindaki her dugume ne yaptigini siniflandirir
export const FATES = ["korunan", "guncellenen", "kurulan", "sokulen"];

export function el(type, props = {}, children = [], key = null) {
  return { type, props, children, key };
}

function segment(node) {
  return node.key === null ? node.type : `${node.type}[${node.key}]`;
}

function identity(node, index) {
  return node.key === null ? `#${index}` : `key:${node.key}`;
}

function sameProps(a, b) {
  const names = [...new Set([...Object.keys(a), ...Object.keys(b)])].sort();
  return names.every((n) => JSON.stringify(a[n]) === JSON.stringify(b[n]));
}

function whole(node, path, fate, ops) {
  const here = `${path}/${segment(node)}`;
  ops.push({ fate, path: here });
  node.children.forEach((child) => whole(child, here, fate, ops));
}

export function reconcile(prev, next, path = "", ops = []) {
  if (prev === null) {
    whole(next, path, "kurulan", ops);
    return ops;
  }
  if (next === null) {
    whole(prev, path, "sokulen", ops);
    return ops;
  }
  if (prev.type !== next.type || prev.key !== next.key) {
    whole(prev, path, "sokulen", ops);
    whole(next, path, "kurulan", ops);
    return ops;
  }
  const here = `${path}/${segment(next)}`;
  ops.push({ fate: sameProps(prev.props, next.props) ? "korunan" : "guncellenen", path: here });
  const before = new Map(prev.children.map((child, i) => [identity(child, i), child]));
  const matched = new Set();
  next.children.forEach((child, i) => {
    const id = identity(child, i);
    matched.add(id);
    reconcile(before.has(id) ? before.get(id) : null, child, here, ops);
  });
  prev.children.forEach((child, i) => {
    if (!matched.has(identity(child, i))) reconcile(child, null, here, ops);
  });
  return ops;
}

export function printOps(ops) {
  ops.forEach((op) => console.log(`${op.fate}: ${op.path}`));
}

export function countFates(ops) {
  for (const name of FATES) {
    console.log(`${name}: ${ops.filter((op) => op.fate === name).length}`);
  }
  console.log(`komut: ${ops.filter((op) => op.fate !== "korunan").length}`);
}

Ekranı ağaca çeviren işlev ayrı bir dosyada durur. Ekranda bir başlık, bir grafik ve bir alt not vardır. İşlev her çağrıldığında ders-yerel bir sayacı artırır; sayaç yeniden çizimleri sayar. Sayaç ağaçta değildir ve kuralın girdisi olmaz: kural yalnız iki ağacı karşılaştırır.

// screen.mjs — bir ekrani gorunum agacina cevirir ve yeniden cizimleri sayar
import { el } from "./fate.mjs";

export const counter = { renders: 0 };

export function renderScreen(state) {
  counter.renders += 1;
  return el("Screen", {}, [
    el("Header", { title: state.title }),
    el("Chart", { points: state.points }),
    el("Footer", { note: state.note }),
  ]);
}

export function memoRender(render) {
  let lastKey = null;
  let lastTree = null;
  return (state) => {
    const key = JSON.stringify(state);
    if (key !== lastKey) {
      lastKey = key;
      lastTree = render(state);
    }
    return lastTree;
  };
}

renderScreen durumu ağaca çevirir. memoRender bir çizim işlevini sarar: durum öncekiyle aynıysa ağacı yeniden üretmez, öncekini döndürür. Bu bir bellemedir. Yeniden çizim ile komut arasındaki fark bu iki işlevle ölçülür.

Üç Yeniden Çizim

İlk ölçümde aynı durumdan ağaç üç kez üretilir. Üç ağaç iki güncelleme verir: birinciden ikinciye, ikinciden üçüncüye. Kural iki güncellemeyi aynı ops dizisinde biriktirir ve sayar; yeniden çizim sayısı ayrıca sayaçtan okunur.

// main.mjs
import { reconcile, countFates } from "./fate.mjs";
import { renderScreen, counter } from "./screen.mjs";

const state = { title: "Satislar", points: [3, 5, 8], note: "gunluk" };
const first = renderScreen(state);
const second = renderScreen(state);
const third = renderScreen(state);

const ops = [];
reconcile(first, second, "", ops);
reconcile(second, third, "", ops);
console.log(`yeniden cizim: ${counter.renders}`);
countFates(ops);
yeniden cizim: 3
korunan: 8
guncellenen: 0
kurulan: 0
sokulen: 0
komut: 0

Üç yeniden çizim, sıfır komut. Dört düğümün her biri iki güncellemede korunan çıktı, sekiz kayıt. Ağaç üç kez üretildi, ama üretilen ağaçlar birbirine eşitti ve yerel tarafa gidecek hiçbir iş çıkmadı.

M10/K03’te bir işlevin kaç kez koştuğu sayılıyordu. Buradaki sayaç aynı şeyi sayar, ama akıbet o sayıya bakmaz. Yeniden çizim komut değildir; üç yeniden çizim sıfır komut verebilir. Yeniden çizim ağacın üretilmesidir; komut ise iki ağaç karşılaştırıldıktan sonra yerel tarafa giden iştir. İkisi aynı yönde de, ayrı yönlerde de hareket eder: bir yeniden çizim yüz komut da verebilir, hiç komut da.

Belleme

Sayaç üçse, bu üçün gereksiz olduğu görülebilir: durum hiç değişmedi. Belleme bu gereksizliği keser. Aynı üç çağrı memoRender ile sarılmış çizim işleviyle yapılır; sayaç sıfırdan başlar.

// main.mjs
import { reconcile, countFates } from "./fate.mjs";
import { renderScreen, memoRender, counter } from "./screen.mjs";

const render = memoRender(renderScreen);
const state = { title: "Satislar", points: [3, 5, 8], note: "gunluk" };
const first = render(state);
const second = render(state);
const third = render(state);

const ops = [];
reconcile(first, second, "", ops);
reconcile(second, third, "", ops);
console.log(`yeniden cizim: ${counter.renders}`);
countFates(ops);
yeniden cizim: 1
korunan: 8
guncellenen: 0
kurulan: 0
sokulen: 0
komut: 0

Yeniden çizim üçten bire düştü; komut sıfırdı, sıfır kaldı. Belleme iş yaptı, ama işin ağaçtaki akıbete etkisi yok: komut zaten sıfırdı. Bellemenin değeri ağaçta değil, ağacı üretmenin kendi işinde. Üretilen ağaç büyük ve pahalıysa belleme o işi atlar; kural bunu görmez, çünkü iki ağacın eşit olduğunu iki durumda da aynı şekilde söyler.

Burada dikkat edilecek nokta bellemenin anahtarıdır. memoRender durumu içeriğiyle karşılaştırır. Gerçek çerçevelerde karşılaştırma çoğu zaman özelliklerin kimliğiyle, yani aynı nesne olup olmadığıyla yapılır. Her çağrıda yeni bir dizi ya da yeni bir işlev üreten bir üst ekran, içeriği aynı olsa bile belleyeni atlatır. Bu fark varsayım bloğunda söylenir; model ölçmez.

Gerçek bir ekranda bellenen şey genellikle ağır bir alt ağaçtır, örneğin grafik. Aşağıdaki parça grafiği bellenmiş bir bileşen işlevi olarak yazar ve ekran, grafiğe verdiği diziyi yalnız satırlar değişince yeniden hesaplar. Model bu parçayı çalıştırmaz.

// Platform kodu: SalesScreen
const Chart = React.memo(function Chart({ points }) {
  return <ChartView data={points} />;
});

function SalesScreen({ state }) {
  const points = useMemo(() => state.rows.map((r) => r.total), [state.rows]);
  return <Chart points={points} />;
}

React.memo grafiği sarar: özellikleri öncekiyle aynıysa grafik yeniden çizilmez. useMemo ise points dizisini yalnız state.rows değişince yeniden kurar; dizi aynı kalırsa sarılmış grafik yeni bir özellik görmez.

Değişen Veri

Bellemenin yaptığı şeyi anlamak için tersini de ölçmek gerekir: veri gerçekten değişince ne oluyor? Grafiğin noktaları değişir, başlık ve alt not aynı kalır.

// main.mjs
import { reconcile, printOps } from "./fate.mjs";
import { renderScreen } from "./screen.mjs";

const before = renderScreen({ title: "Satislar", points: [3, 5, 8], note: "gunluk" });
const after = renderScreen({ title: "Satislar", points: [3, 5, 9], note: "gunluk" });
printOps(reconcile(before, after));
korunan: /Screen
korunan: /Screen/Header
guncellenen: /Screen/Chart
korunan: /Screen/Footer

Tek komut gitti: /Screen/Chart güncellenen. Başlık ve alt not korunan kaldı; ekran kökü de özelliksiz olduğu için korunan. Veri değişince yeniden çizim zorunludur ve komut da zorunludur. Bellemenin işi, veri değişmediğinde yeniden çizimi atlamaktır; veri değişince atlayacağı bir şey yoktur.

Profilleme

Takılma, yeniden çizim sayısından değil, yeniden çizimin sürdüğü zamandan doğar. Ekran her kare için belirli bir sürede yeni görüntüyü hazırlamak zorundadır; JS tarafındaki iş bu süreye sığmazsa kare kaçırılır. Kaçırılan kare kullanıcının gördüğü takılmadır. Bu ders süre yazmaz, çünkü süre modelde yoktur; ölçüm gerçek aygıtta, profil aracıyla yapılır.

Profil aracı iki şeyi ayırır. Birincisi JS tarafındaki iştir: durumun işlenmesi, ağacın üretilmesi, iki ağacın karşılaştırılması. İkincisi yerel taraftaki iştir: komutların yerel görünümlere uygulanması, düzenin hesaplanması, çizim. Bir ekran sıfır komut verse de JS tarafındaki iş yüzünden takılabilir; çok komut verse de yerel taraf yetişebilir. Sıcak yol denen yer, bu iki işin kareden kareye en çok tekrarlandığı yerdir: kaydırma, yazı yazma, sürükleme.

Profil aracıyla ölçülen yer bir sarmalayanla işaretlenebilir. Aşağıdaki parça grafiği bir profil sarmalayanının içine alır ve her çizimin süresini bir toplayıcıya geçirir. Model bu parçayı çalıştırmaz.

// Platform kodu: ProfiledChart
function ProfiledChart({ points }) {
  return (
    <Profiler id="chart" onRender={(id, phase, duration) => collect(id, duration)}>
      <Chart points={points} />
    </Profiler>
  );
}

Bu parçanın verdiği süre, bu grafiğin JS tarafındaki çizim işidir. Komut sayısıyla aynı şey değildir; ikisi birlikte okunur. Süre yüksek, komut sıfırsa belleme aranır. Süre düşük, komut çoksa ağacın kendisi değişiyordur ve anahtarlar gözden geçirilir.

Profil Sarmalayanını Satır Satır Okumak

Yukarıdaki parçanın her satırı bir şey yapar.

  • <Profiler id="chart" ...> sarmalayanı açar ve ölçümün adını verir. Aynı ekranda birden çok sarmalayan olabilir; id hangisinin konuştuğunu söyler.
  • onRender={(id, phase, duration) => collect(id, duration)} her çizim bittiğinde çağrılan işlevdir. phase çizimin ilk çizim mi, güncelleme mi, yoksa bir güncellemenin içinde başlayan güncelleme mi olduğunu, duration ise bu çizimin JS tarafındaki işini verir. collect toplayıcıdır; uygulamanın kendi işlevidir.
  • <Chart points={points} /> ölçülen alt ağaçtır. Sarmalayan yalnız içindekini ölçer; başlık ya da alt not bu sayıya girmez.

Sarmalayanın ağaçta düğüm olmadığına dikkat etmek gerekir. Ölçüm düzeneği görünüm ağacını değiştirmez; model de bu yüzden /Screen/Chart yolunu aynı bırakır. Bir ölçüm aracı ağaca düğüm ekliyorsa araç ölçtüğü şeyi bozuyordur.

Yaygın Yanılgılar

İlk yanılgı, çok yeniden çizimin takılma demek olduğudur. Üç yeniden çizim sıfır komut verebildiği için sayı tek başına bir şey söylemez. Yeniden çizim yalnız JS tarafında işlenen bir iştir; yerel tarafa bir şey gitmediği sürece ekranda hiçbir görünüm değişmez. Takılma, ancak yeniden çizimin işi kare süresine sığmadığında olur ve bunu sayı değil profil gösterir.

İkinci yanılgı, her şeyi bellemenin güvenli olduğudur. Belleme bir karşılaştırma ister: memoRender her çağrıda durumu bir anahtara çevirir ve öncekiyle karşılaştırır. Durum nadiren aynı kalıyorsa bu karşılaştırma boşa harcanan iştir ve ağacı yine de üretirsiniz. Belleme, durumun sık sık aynı kaldığı yerde kazandırır; her yere serpiştirilmesi bir kazanç değil, bir maliyettir.

Üçüncü yanılgı, sıfır komutun her zaman iyi olduğudur. Veri gerçekten değiştiğinde sıfır komut, ekranın eski veriyi göstermeye devam ettiği anlamına gelir. Değişen Veri ölçümünde grafiğin güncellenen çıkması bir kusur değil, doğru davranıştır. İyi olan, gereksiz komutun olmamasıdır; komutun hiç olmaması değil.

Sınırlayıcı Ölçüm

Son ölçümde başlık değişir, grafik değişmez. Üç yeniden çizim yapılır, iki güncelleme sayılır. Başlık her güncellemede yeni bir değer alır; grafik, alt not ve kök aynı kalır. Kural grafiğin yolunu süzerek gösterir.

// main.mjs
import { reconcile, printOps, countFates } from "./fate.mjs";
import { renderScreen, counter } from "./screen.mjs";

const points = [3, 5, 8];
const first = renderScreen({ title: "Satislar", points, note: "gunluk" });
const second = renderScreen({ title: "Satislar (hafta)", points, note: "gunluk" });
const third = renderScreen({ title: "Satislar (ay)", points, note: "gunluk" });

const ops = [];
reconcile(first, second, "", ops);
reconcile(second, third, "", ops);
console.log(`yeniden cizim: ${counter.renders}`);
printOps(ops.filter((op) => op.path === "/Screen/Chart"));
countFates(ops);
yeniden cizim: 3
korunan: /Screen/Chart
korunan: /Screen/Chart
korunan: 6
guncellenen: 2
kurulan: 0
sokulen: 0
komut: 2

Odak düğüm /Screen/Chart: iki güncellemede de korunan. Grafik üç yeniden çizimin her birinde ağaca girdi ve üç kez bir önceki grafikle karşılaştırıldı, ama özellikleri aynı olduğu için yerel tarafa hiçbir şey gitmedi. Başlık iki güncellemede güncellenen oldu; komutların ikisi de başlığa gitti.

Bu ölçümün sınırı, dersin asıl uyarısıdır. Grafik üç kez yeniden çizildi ve yeniden çizim bir iştir: grafiğin noktaları üç kez işlenmiş olabilir, ağır bir hesap üç kez koşmuş olabilir. Kural bunu görmez. Kural yalnız ağaçta görüneni görür; bir düğümün korunan çıkması, o düğümün üretilmesinin ucuz olduğunu söylemez. Takılma, korunan bir düğümün arkasında yaşayabilir. Bunu bulmak için profil aracı gerekir; ağaç farkı bulamaz.

BU2. Model, bu akıbetin kuralını kurar; model çerçevenin kendisi değildir. Modelde yeniden çizim, bir işlevin ağacı yeniden üretmesidir ve sayaç bu çağrıları sayar; gerçek çerçevede bir ekranın hangi koşulda yeniden çizileceğini durum, bağlam ve üst ekran birlikte belirler ve model bunları kurmaz. Modeldeki memoRender durumu içeriğiyle karşılaştırır; gerçek bellemeler çoğu zaman nesne kimliğiyle karşılaştırır ve bu fark ölçülmez. Kare süresi, JS tarafı ile yerel taraf arasındaki işin dağılımı ve kaçırılan kare modelde yoktur.

Özet

  • Bu dersin akıbeti: korunan. /Screen/Chart düğümü, ağaç üç kez yeniden üretildiği ve başlık değiştiği hâlde iki güncellemede de korunan kaldı.
  • Aynı durumdan üç kez üretilen ağaç sıfır komut verdi; yeniden çizim komut değildir.
  • Belleme yeniden çizim sayısını üçten bire indirdi, komut sıfır kaldı; bellemenin değeri ağacı üretme işindedir.
  • Grafiğin noktaları değişince /Screen/Chart güncellenen oldu ve tek komut gitti; belleme veri değişince bir şey atlamaz.
  • Takılmanın bedeli JS tarafındaki işte durur; kural yalnız ağaçta görüneni görür ve bu işi profil aracı ölçer.

Sonraki Adım

Bu derste ağaç hep aynı ekranın ağacıydı ve ekran en baştan oradaydı. Uygulamanın bütün ekranlarını açılışta yüklemek bu varsayımı büyütür: yüklenen kod da bir bedeldir. Sıradaki ders, Paket Boyutu ve Başlangıç, bir ekranın kodunu ertelemenin düğümünü ne zaman kurduğunu ölçer.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat