MOODUL 14 · TASE 5 — SPETSIALIST

⚡ Jõudluse Optimeerimine

Profileerimine, mälu haldus, rendering pipeline, Web Workers, Object Pooling, LOD — saavuta 60 FPS!

⏱️ ~8-10 tundi 📝 7 harjutust 🟣 Spetsialist

📊 Samm 1 — Profileerimine

profiler.ts — Jõudluse mõõtmine
class PerformanceProfiler {
  private timers: Map<string, number> = new Map();
  private history: Map<string, number[]> = new Map();
  private maxSamples = 120; // 2 sekundit @ 60fps

  begin(label: string): void {
    this.timers.set(label, performance.now());
  }

  end(label: string): number {
    const start = this.timers.get(label);
    if (start === undefined) return 0;
    const elapsed = performance.now() - start;

    if (!this.history.has(label)) this.history.set(label, []);
    const samples = this.history.get(label)!;
    samples.push(elapsed);
    if (samples.length > this.maxSamples) samples.shift();

    return elapsed;
  }

  getAverage(label: string): number {
    const samples = this.history.get(label);
    if (!samples || samples.length === 0) return 0;
    return samples.reduce((a, b) => a + b, 0) / samples.length;
  }

  getMax(label: string): number {
    const samples = this.history.get(label);
    if (!samples) return 0;
    return Math.max(...samples);
  }

  // Debug overlay
  drawStats(ctx: CanvasRenderingContext2D): void {
    ctx.save();
    ctx.fillStyle = 'rgba(0,0,0,0.7)';
    ctx.fillRect(10, 10, 250, 20 + this.history.size * 18);
    ctx.fillStyle = '#00ff00';
    ctx.font = '12px monospace';

    let y = 28;
    for (const [label, samples] of this.history) {
      const avg = this.getAverage(label);
      const max = this.getMax(label);
      const color = avg > 16 ? '#ff4444' : avg > 8 ? '#ffaa00' : '#00ff00';
      ctx.fillStyle = color;
      ctx.fillText(`${label}: ${avg.toFixed(2)}ms (max: ${max.toFixed(2)}ms)`, 20, y);
      y += 18;
    }
    ctx.restore();
  }
}

// Kasutamine:
// const profiler = new PerformanceProfiler();
// profiler.begin('physics');
// updatePhysics();
// profiler.end('physics');
// profiler.begin('render');
// render();
// profiler.end('render');

♻️ Samm 2 — Object Pooling

objectPool.ts — Objektide taaskasutus
class ObjectPool<T> {
  private pool: T[] = [];
  private active: Set<T> = new Set();
  private factory: () => T;
  private reset: (obj: T) => void;

  constructor(
    factory: () => T,
    reset: (obj: T) => void,
    initialSize = 50
  ) {
    this.factory = factory;
    this.reset = reset;
    // Eelloo objektid
    for (let i = 0; i < initialSize; i++) {
      this.pool.push(factory());
    }
  }

  acquire(): T {
    let obj: T;
    if (this.pool.length > 0) {
      obj = this.pool.pop()!;
    } else {
      obj = this.factory(); // Pool tühi → loo uus
      console.warn('ObjectPool: pool tühi, loon uue objekti');
    }
    this.reset(obj);
    this.active.add(obj);
    return obj;
  }

  release(obj: T): void {
    if (this.active.delete(obj)) {
      this.pool.push(obj);
    }
  }

  get activeCount(): number { return this.active.size; }
  get poolSize(): number { return this.pool.length; }
}

// Näide: Kuulide pool
interface Bullet {
  x: number; y: number;
  vx: number; vy: number;
  active: boolean;
}

const bulletPool = new ObjectPool<Bullet>(
  () => ({ x: 0, y: 0, vx: 0, vy: 0, active: false }),
  (b) => { b.active = true; },
  200
);

function shoot(x: number, y: number, angle: number): void {
  const bullet = bulletPool.acquire();
  bullet.x = x;
  bullet.y = y;
  bullet.vx = Math.cos(angle) * 500;
  bullet.vy = Math.sin(angle) * 500;
}

// Uuendus: taaskasuta "surnud" kuulid
function updateBullets(delta: number): void {
  for (const bullet of bulletPool['active']) {
    bullet.x += bullet.vx * delta;
    bullet.y += bullet.vy * delta;
    if (isOffScreen(bullet)) {
      bullet.active = false;
      bulletPool.release(bullet);
    }
  }
}

🧵 Samm 3 — Web Workers (Mitmelõimelisus)

gameWorker.ts — Raske arvutus teises lõimes
// ============ physics.worker.ts ============
// Eraldi fail — töötab teises lõimes!
self.onmessage = (e: MessageEvent) => {
  const { type, data } = e.data;

  switch (type) {
    case 'pathfind': {
      // Raske A* arvutus ei blokeeri main thread'i
      const path = aStarPathfind(data.grid, data.start, data.end);
      self.postMessage({ type: 'pathResult', data: path });
      break;
    }
    case 'generateTerrain': {
      const terrain = generatePerlinTerrain(data.width, data.height, data.seed);
      // Transferable: kiire üleandmine (zero-copy)
      self.postMessage(
        { type: 'terrainResult', data: terrain },
        [terrain.buffer]  // Transfer ownership
      );
      break;
    }
  }
};

// ============ main.ts ============
class WorkerManager {
  private worker: Worker;
  private callbacks: Map<string, (data: any) => void> = new Map();

  constructor(scriptUrl: string) {
    this.worker = new Worker(scriptUrl, { type: 'module' });
    this.worker.onmessage = (e) => {
      const { type, data } = e.data;
      const cb = this.callbacks.get(type);
      if (cb) {
        cb(data);
        this.callbacks.delete(type);
      }
    };
  }

  request<T>(type: string, data: any): Promise<T> {
    return new Promise((resolve) => {
      this.callbacks.set(type + 'Result', resolve);
      this.worker.postMessage({ type, data });
    });
  }

  terminate(): void {
    this.worker.terminate();
  }
}

// Kasutamine:
// const physics = new WorkerManager('/workers/physics.worker.js');
// const path = await physics.request('pathfind', { grid, start, end });
// Tulemus: main thread ei blokeeru!

🖥️ Samm 4 — Renderdamise Optimeerimine

renderOptimization.ts
// 1. Off-screen Canvas (eelrenderdamine)
class CachedSprite {
  private cache: OffscreenCanvas;
  private cacheCtx: OffscreenCanvasRenderingContext2D;

  constructor(private sourceImage: HTMLImageElement, private scale = 1) {
    const w = sourceImage.width * scale;
    const h = sourceImage.height * scale;
    this.cache = new OffscreenCanvas(w, h);
    this.cacheCtx = this.cache.getContext('2d')!;
    this.cacheCtx.drawImage(sourceImage, 0, 0, w, h);
  }

  draw(ctx: CanvasRenderingContext2D, x: number, y: number): void {
    ctx.drawImage(this.cache, x, y);
  }
}

// 2. Frustum culling (joonista ainult nähtavad)
function isInView(
  entity: { x: number; y: number; width: number; height: number },
  camera: { x: number; y: number; width: number; height: number }
): boolean {
  return (
    entity.x + entity.width > camera.x &&
    entity.x < camera.x + camera.width &&
    entity.y + entity.height > camera.y &&
    entity.y < camera.y + camera.height
  );
}

// 3. Batch rendering (grupeeritud joonistamine)
function batchRender(
  ctx: CanvasRenderingContext2D,
  sprites: Array<{ img: HTMLImageElement; x: number; y: number }>,
  camera: { x: number; y: number; width: number; height: number }
): void {
  // Sorteeri Z-järjekorras (taga → ees)
  sprites.sort((a, b) => a.y - b.y);

  // Joonista ainult nähtavad
  for (const s of sprites) {
    if (isInView({ x: s.x, y: s.y, width: 64, height: 64 }, camera)) {
      ctx.drawImage(s.img, s.x - camera.x, s.y - camera.y);
    }
  }
}

// 4. requestAnimationFrame + delta time (mitte setInterval!)
let lastTime = 0;
function gameLoop(timestamp: number): void {
  const delta = (timestamp - lastTime) / 1000;
  lastTime = timestamp;

  // Piira delta spike'e (nt tab vahetuse järel)
  const clampedDelta = Math.min(delta, 0.05); // max 50ms

  update(clampedDelta);
  render();
  requestAnimationFrame(gameLoop);
}

💾 Samm 5 — Mälu Haldus

memoryManagement.ts
// 1. Väldi GC pause — ära loo objekte game loopis!
// HALB:
function updateBad(): void {
  const velocity = new Vector2(0, 0); // ❌ Iga frame uus objekt!
  velocity.x = Math.cos(angle) * speed;
  velocity.y = Math.sin(angle) * speed;
}

// HEA:
const _tempVec = new Vector2(0, 0); // Korduvkasutatav
function updateGood(): void {
  _tempVec.set(Math.cos(angle) * speed, Math.sin(angle) * speed); // ✅
}

// 2. TypedArray suurte andmekogumite jaoks
// Objektide asemel kasuta struktuuri-massiive (SoA):
class ParticleSystem {
  private count = 0;
  private maxParticles: number;

  // Structure of Arrays (SoA) — cache-sõbralik!
  private x: Float32Array;
  private y: Float32Array;
  private vx: Float32Array;
  private vy: Float32Array;
  private life: Float32Array;

  constructor(max = 10000) {
    this.maxParticles = max;
    this.x = new Float32Array(max);
    this.y = new Float32Array(max);
    this.vx = new Float32Array(max);
    this.vy = new Float32Array(max);
    this.life = new Float32Array(max);
  }

  emit(x: number, y: number, vx: number, vy: number): void {
    if (this.count >= this.maxParticles) return;
    const i = this.count++;
    this.x[i] = x;
    this.y[i] = y;
    this.vx[i] = vx;
    this.vy[i] = vy;
    this.life[i] = 1;
  }

  update(dt: number): void {
    for (let i = this.count - 1; i >= 0; i--) {
      this.x[i] += this.vx[i] * dt;
      this.y[i] += this.vy[i] * dt;
      this.life[i] -= dt;
      if (this.life[i] <= 0) {
        // Swap-remove (O(1) kustutamine)
        this.count--;
        this.x[i] = this.x[this.count];
        this.y[i] = this.y[this.count];
        this.vx[i] = this.vx[this.count];
        this.vy[i] = this.vy[this.count];
        this.life[i] = this.life[this.count];
      }
    }
  }
}
// 10 000 partiklit ilma GC pausideta!

📝 Harjutused

  • Performance Profiler

    Loo profiler: begin/end, ajaloo graafik, värvikoodid (roheline/kollane/punane). Kuva reaalajas overlay mängu peal. FPS + frame time.

  • Object Pool geneerika

    Loo geneerine ObjectPool<T>: acquire, release, autoExpand. Kasuta kuulide, partiklite ja vaenlaste jaoks. Mõõda GC pause'ide vahe enne/pärast.

  • Web Worker pathfinding

    Liiguta A* algoritm Web Worker'isse. 100x100 grid, mitu agenti otsivad paralleelselt teed. Main thread jääb 60 FPS-le.

  • Renderdamise optimeerimine

    1000 sprite'i stseen: lisa frustum culling, batch rendering, off-screen cache. Mõõda FPS enne ja pärast igat optimeerimist.

  • TypedArray partiklid

    10 000 partiklit SoA (Structure of Arrays) struktuuriga. Swap-remove kustutamine. Võrdle AoS vs SoA jõudlust. Chrome DevTools Memory tab.

  • Spatial hashing + instanced rendering

    Spatial hash grid 10 000 entity jaoks. Ainult lähedased entity'd kontrollivad kokkupõrkeid. Three.js: InstancedMesh 5000 puud. FPS mõõtmine.

  • LOD süsteem

    Level-of-Detail: 3D objektid vahetavad detailsust kauguse põhjal (high → medium → low → billboard). Lisa fade-üleminekud. Mõõda draw calls.

← Moodul 13 Moodul 14b: Turvalisus →