Skip to content

Memory Management Examples

Examples demonstrating correct memory management patterns.

Correct Load/Unload Patterns

Basic Pattern: Load Once, Use Many Times

typescript
import { HtmlLayoutParser, CharLayout } from 'html-layout-parser/web';

// ✅ CORRECT: Load font once, use for multiple parses
async function correctPattern() {
  const parser = new HtmlLayoutParser();
  await parser.init();

  try {
    // Load font ONCE
    const fontResponse = await fetch('/fonts/arial.ttf');
    const fontData = new Uint8Array(await fontResponse.arrayBuffer());
    const fontId = parser.loadFont(fontData, 'Arial');
    parser.setDefaultFont(fontId);

    // Use for MANY parses
    const documents = ['<div>Doc 1</div>', '<div>Doc 2</div>', '<div>Doc 3</div>'];

    for (const html of documents) {
      const layouts = parser.parse(html, { viewportWidth: 800 });
      console.log(`Parsed ${layouts.length} characters`);
    }

  } finally {
    parser.destroy();
  }
}

// ❌ INCORRECT: Loading/unloading font for each parse
async function incorrectPattern() {
  const parser = new HtmlLayoutParser();
  await parser.init();

  try {
    const documents = ['<div>Doc 1</div>', '<div>Doc 2</div>'];

    for (const html of documents) {
      // ❌ BAD: Loading font for each document
      const fontResponse = await fetch('/fonts/arial.ttf');
      const fontData = new Uint8Array(await fontResponse.arrayBuffer());
      const fontId = parser.loadFont(fontData, 'Arial');
      parser.setDefaultFont(fontId);

      const layouts = parser.parse(html, { viewportWidth: 800 });

      // ❌ BAD: Unloading immediately
      parser.unloadFont(fontId);
    }
  } finally {
    parser.destroy();
  }
}

Memory Monitoring

Basic Memory Monitoring

typescript
import { HtmlLayoutParser, MemoryMetrics } from 'html-layout-parser/web';

function logMemoryMetrics(parser: HtmlLayoutParser): void {
  const metrics = parser.getMemoryMetrics();
  
  if (metrics) {
    const totalMB = (metrics.totalMemoryUsage / 1024 / 1024).toFixed(2);
    console.log(`Total Memory: ${totalMB} MB`);
    console.log(`Font Count: ${metrics.fontCount}`);
    
    for (const font of metrics.fonts) {
      const fontMB = (font.memoryUsage / 1024 / 1024).toFixed(2);
      console.log(`  - ${font.name} (ID: ${font.id}): ${fontMB} MB`);
    }
  }
}

async function memoryMonitoringExample() {
  const parser = new HtmlLayoutParser();
  await parser.init();

  try {
    const fontResponse = await fetch('/fonts/arial.ttf');
    const fontData = new Uint8Array(await fontResponse.arrayBuffer());
    parser.loadFont(fontData, 'Arial');
    parser.setDefaultFont(1);

    console.log('=== After Font Load ===');
    logMemoryMetrics(parser);

    for (let i = 0; i < 100; i++) {
      parser.parse(`<div>Document ${i}</div>`, { viewportWidth: 800 });
    }

    console.log('\n=== After Parsing 100 Documents ===');
    logMemoryMetrics(parser);

    if (parser.checkMemoryThreshold()) {
      console.warn('⚠️ Memory usage exceeds 50MB threshold!');
    }

  } finally {
    parser.destroy();
  }
}

Continuous Memory Monitoring

typescript
import { HtmlLayoutParser, MemoryMetrics } from 'html-layout-parser/web';

class MemoryMonitor {
  private parser: HtmlLayoutParser;
  private intervalId: NodeJS.Timeout | null = null;
  private warningThresholdMB: number;
  private criticalThresholdMB: number;
  private onWarning?: (metrics: MemoryMetrics) => void;
  private onCritical?: (metrics: MemoryMetrics) => void;

  constructor(
    parser: HtmlLayoutParser,
    options: {
      warningThresholdMB?: number;
      criticalThresholdMB?: number;
      onWarning?: (metrics: MemoryMetrics) => void;
      onCritical?: (metrics: MemoryMetrics) => void;
    } = {}
  ) {
    this.parser = parser;
    this.warningThresholdMB = options.warningThresholdMB || 40;
    this.criticalThresholdMB = options.criticalThresholdMB || 50;
    this.onWarning = options.onWarning;
    this.onCritical = options.onCritical;
  }

  start(intervalMs: number = 5000): void {
    if (this.intervalId) return;
    this.intervalId = setInterval(() => this.check(), intervalMs);
  }

  stop(): void {
    if (this.intervalId) {
      clearInterval(this.intervalId);
      this.intervalId = null;
    }
  }

  check(): MemoryMetrics | null {
    const metrics = this.parser.getMemoryMetrics();
    if (!metrics) return null;

    const usageMB = metrics.totalMemoryUsage / 1024 / 1024;

    if (usageMB >= this.criticalThresholdMB) {
      console.error(`🔴 CRITICAL: ${usageMB.toFixed(2)} MB`);
      this.onCritical?.(metrics);
    } else if (usageMB >= this.warningThresholdMB) {
      console.warn(`🟡 WARNING: ${usageMB.toFixed(2)} MB`);
      this.onWarning?.(metrics);
    }

    return metrics;
  }

  getStatus(): { usageMB: number; status: 'ok' | 'warning' | 'critical' } {
    const metrics = this.parser.getMemoryMetrics();
    const usageMB = metrics ? metrics.totalMemoryUsage / 1024 / 1024 : 0;

    let status: 'ok' | 'warning' | 'critical' = 'ok';
    if (usageMB >= this.criticalThresholdMB) status = 'critical';
    else if (usageMB >= this.warningThresholdMB) status = 'warning';

    return { usageMB, status };
  }
}

Resource Cleanup

Using try/finally for Guaranteed Cleanup

typescript
import { HtmlLayoutParser } from 'html-layout-parser/web';

// ✅ CORRECT: Always use try/finally for cleanup
async function guaranteedCleanup() {
  const parser = new HtmlLayoutParser();
  
  try {
    await parser.init();

    const fontResponse = await fetch('/fonts/arial.ttf');
    const fontData = new Uint8Array(await fontResponse.arrayBuffer());
    parser.loadFont(fontData, 'Arial');
    parser.setDefaultFont(1);

    const layouts = parser.parse('<div>Hello</div>', { viewportWidth: 800 });
    return layouts;

  } finally {
    // This ALWAYS runs, even if an error occurred
    parser.destroy();
  }
}

// Wrapper function that ensures cleanup
async function withParser<T>(
  fn: (parser: HtmlLayoutParser) => Promise<T>
): Promise<T> {
  const parser = new HtmlLayoutParser();
  
  try {
    await parser.init();
    return await fn(parser);
  } finally {
    parser.destroy();
  }
}

// Usage
async function cleanupWrapperExample() {
  const result = await withParser(async (parser) => {
    const fontResponse = await fetch('/fonts/arial.ttf');
    const fontData = new Uint8Array(await fontResponse.arrayBuffer());
    parser.loadFont(fontData, 'Arial');
    parser.setDefaultFont(1);

    return parser.parse('<div>Hello</div>', { viewportWidth: 800 });
  });

  console.log(`Parsed ${result.length} characters`);
}

Cleanup in Class-Based Applications

typescript
import { HtmlLayoutParser, CharLayout } from 'html-layout-parser/web';

class DocumentRenderer {
  private parser: HtmlLayoutParser | null = null;
  private initialized = false;
  private destroyed = false;

  async init(): Promise<void> {
    if (this.initialized || this.destroyed) return;

    this.parser = new HtmlLayoutParser();
    await this.parser.init();

    const fontResponse = await fetch('/fonts/arial.ttf');
    const fontData = new Uint8Array(await fontResponse.arrayBuffer());
    this.parser.loadFont(fontData, 'Arial');
    this.parser.setDefaultFont(1);

    this.initialized = true;
  }

  render(html: string, css?: string): CharLayout[] {
    if (!this.parser || this.destroyed) {
      throw new Error('Renderer not initialized or already destroyed');
    }

    return this.parser.parse(html, { viewportWidth: 800, css });
  }

  destroy(): void {
    if (this.destroyed) return;

    if (this.parser) {
      this.parser.destroy();
      this.parser = null;
    }

    this.initialized = false;
    this.destroyed = true;
  }
}

// Usage
async function classCleanupExample() {
  const renderer = new DocumentRenderer();

  try {
    await renderer.init();
    const layouts = renderer.render('<div>Hello</div>');
    console.log(`Rendered ${layouts.length} characters`);
  } finally {
    renderer.destroy();
  }
}

Long-Running Applications

Singleton Pattern

typescript
import { HtmlLayoutParser, CharLayout, MemoryMetrics } from 'html-layout-parser/web';

class ParserSingleton {
  private static instance: ParserSingleton | null = null;
  private parser: HtmlLayoutParser;
  private initialized = false;
  private loadedFonts: Map<string, number> = new Map();

  private constructor() {
    this.parser = new HtmlLayoutParser();
  }

  static getInstance(): ParserSingleton {
    if (!ParserSingleton.instance) {
      ParserSingleton.instance = new ParserSingleton();
    }
    return ParserSingleton.instance;
  }

  async ensureInitialized(): Promise<void> {
    if (this.initialized) return;
    await this.parser.init();
    this.initialized = true;
  }

  async loadFont(fontData: Uint8Array, fontName: string): Promise<number> {
    await this.ensureInitialized();

    if (this.loadedFonts.has(fontName)) {
      return this.loadedFonts.get(fontName)!;
    }

    const fontId = this.parser.loadFont(fontData, fontName);
    if (fontId > 0) {
      this.loadedFonts.set(fontName, fontId);
    }
    return fontId;
  }

  setDefaultFont(fontName: string): boolean {
    const fontId = this.loadedFonts.get(fontName);
    if (fontId) {
      this.parser.setDefaultFont(fontId);
      return true;
    }
    return false;
  }

  parse(html: string, options: { viewportWidth: number; css?: string }): CharLayout[] {
    if (!this.initialized) throw new Error('Parser not initialized');
    return this.parser.parse(html, options);
  }

  performMaintenance(): void {
    if (this.parser.checkMemoryThreshold()) {
      console.warn('Memory threshold exceeded');
    }

    const metrics = this.parser.getMemoryMetrics();
    if (metrics) {
      console.log(`Maintenance: ${metrics.fontCount} fonts, ${(metrics.totalMemoryUsage / 1024 / 1024).toFixed(2)} MB`);
    }
  }

  static destroy(): void {
    if (ParserSingleton.instance) {
      ParserSingleton.instance.parser.destroy();
      ParserSingleton.instance.loadedFonts.clear();
      ParserSingleton.instance.initialized = false;
      ParserSingleton.instance = null;
    }
  }
}

Common Mistakes to Avoid

typescript
import { HtmlLayoutParser } from 'html-layout-parser/web';

// ❌ MISTAKE 1: Forgetting to destroy
async function mistake1() {
  const parser = new HtmlLayoutParser();
  await parser.init();
  const layouts = parser.parse('<div>Hello</div>', { viewportWidth: 800 });
  return layouts;
  // Parser never destroyed - MEMORY LEAK!
}

// ❌ MISTAKE 2: Loading same font multiple times
async function mistake2() {
  const parser = new HtmlLayoutParser();
  await parser.init();

  const fontData = new Uint8Array(await (await fetch('/fonts/arial.ttf')).arrayBuffer());
  
  // Loading same font 3 times - wastes memory!
  parser.loadFont(fontData, 'Arial');
  parser.loadFont(fontData, 'Arial');
  parser.loadFont(fontData, 'Arial');

  parser.destroy();
}

// ❌ MISTAKE 3: Not handling errors
async function mistake3() {
  const parser = new HtmlLayoutParser();
  await parser.init();

  // If fetch fails, parser is never destroyed
  const fontData = new Uint8Array(await (await fetch('/fonts/arial.ttf')).arrayBuffer());
  parser.loadFont(fontData, 'Arial');
  
  parser.destroy();
}

// ❌ MISTAKE 4: Using parser after destroy
async function mistake4() {
  const parser = new HtmlLayoutParser();
  await parser.init();
  
  parser.destroy();
  
  // This will fail!
  const layouts = parser.parse('<div>Hello</div>', { viewportWidth: 800 });
}

// ✅ CORRECT: All mistakes fixed
async function correct() {
  const parser = new HtmlLayoutParser();
  const loadedFonts = new Map<string, number>();

  try {
    await parser.init();

    try {
      const fontData = new Uint8Array(await (await fetch('/fonts/arial.ttf')).arrayBuffer());
      
      // Check if already loaded
      if (!loadedFonts.has('Arial')) {
        const fontId = parser.loadFont(fontData, 'Arial');
        if (fontId > 0) {
          loadedFonts.set('Arial', fontId);
          parser.setDefaultFont(fontId);
        }
      }
    } catch (error) {
      console.error('Failed to load font:', error);
    }

    const layouts = parser.parse('<div>Hello</div>', { viewportWidth: 800 });
    return layouts;

  } finally {
    parser.destroy();
  }
}

Released under the MIT License.