Skip to content

Measure and optimize memory usage for large collections and deep nesting #60

@Sam-Bolling

Description

@Sam-Bolling

Problem

The parser system (~1,714 lines across 4 files with 166 tests) has no memory usage benchmarking or profiling despite handling potentially large collections and deeply nested SWE Common structures. This means:

  • No memory footprint data: Unknown memory usage per feature, per collection, per nested level
  • No scaling data: Unknown how memory usage scales with collection size or nesting depth
  • No GC pressure data: Unknown garbage collection impact for large datasets
  • No leak detection: No systematic detection of memory leaks or retention issues
  • No optimization data: Cannot make informed decisions about memory optimization strategies

Real-World Impact:

  • Large collections: Parsing 10,000+ features - memory requirements unknown
  • Deep nesting: SWE DataRecord with 5+ levels - stack/heap usage unknown
  • Server-side: Memory limits affect how many concurrent parsing operations possible
  • Embedded devices: Must stay within strict memory constraints
  • Long-running processes: Memory leaks can cause crashes over time
  • Streaming scenarios: Cannot estimate buffer sizes or chunking strategies

Context

This issue was identified during the comprehensive validation conducted January 27-28, 2026.

Related Validation Issues: #10 (Multi-Format Parsers)

Work Item ID: 37 from Remaining Work Items

Repository: https://github.com/OS4CSAPI/ogc-client-CSAPI

Validated Commit: a71706b9592cad7a5ad06e6cf8ddc41fa5387732

Detailed Findings

1. No Memory Benchmarks Exist

Evidence from Issue #10 validation report:

Parser System: ~1,714 lines across 4 files

  • base.ts: 479 lines (SystemParser, extractGeometry, extractCommonProperties)
  • resources.ts: 494 lines (7 resource parsers, CollectionParser)
  • swe-common-parser.ts: 540 lines (15 component parsers, recursive parsing)
  • formats.ts: 162 lines (format detection)

Test Coverage: 166 tests (31 base + 79 resources + 56 swe-common)

Current Situation:

  • ✅ Comprehensive functional tests (166 tests)
  • ✅ Excellent parser functionality
  • ZERO memory measurements (no heap usage, GC pressure, retention data)
  • ❌ No collection size scaling analysis
  • ❌ No nesting depth analysis
  • ❌ No memory leak detection

2. Large Collection Parsing (Memory Unknown)

From Issue #10:

CollectionParser Implementation:

export class CollectionParser<T> extends CSAPIParser<T[]> {
  constructor(private itemParser: CSAPIParser<T>) {
    super();
  }

  parseGeoJSON(data: Feature | FeatureCollection): T[] {
    if (data.type === 'Feature') {
      return [this.itemParser.parseGeoJSON(data)];
    }
    return (data as FeatureCollection).features.map(feature =>
      this.itemParser.parseGeoJSON(feature)
    );
  }

  parseSensorML(data: Record<string, unknown>): T[] {
    if (Array.isArray(data)) {
      return data.map(item => this.itemParser.parseSensorML(item));
    }
    return [this.itemParser.parseSensorML(data)];
  }

  parseSWE(data: Record<string, unknown>): T[] {
    if (Array.isArray(data)) {
      return data.map(item => this.itemParser.parseSWE(item));
    }
    return [this.itemParser.parseSWE(data)];
  }
}

Memory Concerns:

  • Array allocation: Creates new array for results
  • Map operations: features.map() creates intermediate arrays
  • Feature objects: Each feature is full GeoJSON Feature object with properties
  • Accumulation: All features held in memory simultaneously

Memory Questions:

  1. Per-feature memory: How much memory per GeoJSON Feature? System? Deployment?
  2. Collection overhead: Fixed overhead for collection structure?
  3. Scaling: Linear, sublinear, or superlinear with collection size?
  4. Peak memory: At 1,000 features vs 10,000 features?
  5. GC frequency: How often does GC run during large collection parsing?
  6. Memory efficiency: Are temporary objects properly released?

3. Deep Nesting in SWE Common (Recursive Memory Unknown)

From Issue #10:

Recursive DataRecord Parsing:

export function parseDataRecordComponent(data: unknown): DataRecordComponent {
  // ... validation ...

  // Recursively parse nested components
  const fields = data.fields.map((field: any, index: number) => {
    if (!isObject(field)) {
      throw new ParseError(`DataRecord.fields[${index}] must be an object`);
    }

    if (!field.name || typeof field.name !== 'string') {
      throw new ParseError(`DataRecord.fields[${index}] must have a name property`);
    }

    if (!field.component) {
      if (!field.href) {
        throw new ParseError(`DataRecord.fields[${index}] must have either component or href`);
      }
      return field;
    }

    try {
      const parsedComponent = parseDataComponent(field.component);
      return {
        ...field,
        component: parsedComponent,
      };
    } catch (error) {
      if (error instanceof ParseError) {
        throw new ParseError(
          error.message,
          `fields[${index}].component${error.path ? '.' + error.path : ''}`
        );
      }
      throw error;
    }
  });

  return {
    ...data,
    fields,
  } as DataRecordComponent;
}

Recursive Parsing Examples:

// Test case showing deep nesting:
it('should recursively parse deeply nested structures', () => {
  const nested = {
    type: 'DataRecord',
    definition: 'http://example.com/def',
    label: 'Outer',
    fields: [
      {
        name: 'innerRecord',
        component: {
          type: 'DataRecord',
          definition: 'http://example.com/def/inner',
          label: 'Inner',
          fields: [
            {
              name: 'quantity',
              component: {
                type: 'Quantity',
                definition: 'http://example.com/def/temperature',
                label: 'Temperature',
                uom: { code: 'Cel' },
              },
            },
          ],
        },
      },
    ],
  };
  
  const result = parseDataRecordComponent(nested);
  // ... assertions ...
});

Memory Concerns:

  • Call stack depth: Each recursive call adds stack frame
  • Object spreading: { ...data }, { ...field } creates object copies
  • Path tracking: String concatenation for error paths (allocates strings)
  • Intermediate objects: Parsed components held during recursion

Memory Questions:

  1. Stack usage: How much stack per nesting level?
  2. Heap usage: How much heap per nested DataRecord?
  3. Maximum depth: What's safe nesting depth before stack overflow?
  4. Memory amplification: Does memory usage increase exponentially with depth?
  5. Garbage collection: Are intermediate objects properly collected during recursion?

4. SensorML→GeoJSON Conversion (Memory Overhead Unknown)

From Issue #10:

SystemParser SensorML Conversion:

parseSensorML(data: Record<string, unknown>): SystemFeature {
  const sml = data as unknown as SensorMLProcess;

  // Validate it's a physical system/component
  if (
    sml.type !== 'PhysicalSystem' &&
    sml.type !== 'PhysicalComponent'
  ) {
    throw new CSAPIParseError(
      `Expected PhysicalSystem or PhysicalComponent, got ${sml.type}`
    );
  }

  // Extract geometry from position
  const geometry = 'position' in sml ? extractGeometry(sml.position as Position) : undefined;

  // Build properties from SensorML metadata
  const properties: Record<string, unknown> = {
    ...extractCommonProperties(sml),
    featureType: 'System',
    systemType: sml.type === 'PhysicalSystem' ? 'platform' : 'sensor',
  };

  // Add inputs/outputs/parameters if present
  if ('inputs' in sml && sml.inputs) properties.inputs = sml.inputs;
  if ('outputs' in sml && sml.outputs) properties.outputs = sml.outputs;
  if ('parameters' in sml && sml.parameters) properties.parameters = sml.parameters;

  // Add components for systems
  if (sml.type === 'PhysicalSystem' && 'components' in sml && sml.components) {
    properties.components = sml.components;
  }

  return {
    type: 'Feature',
    id: sml.id || sml.uniqueId,
    geometry: geometry || null,
    properties,
  } as unknown as SystemFeature;
}

extractCommonProperties() Helper:

function extractCommonProperties(
  sml: DescribedObject
): Record<string, unknown> {
  const props: Record<string, unknown> = {};

  if (sml.id) props.id = sml.id;
  if (sml.uniqueId) props.uniqueId = sml.uniqueId;
  if (sml.label) props.name = sml.label;
  if (sml.description) props.description = sml.description;
  // Handle both string[] (schema-compliant) and Keyword[] (enhanced) formats
  if (sml.keywords) {
    props.keywords = Array.isArray(sml.keywords) && sml.keywords.length > 0 && typeof sml.keywords[0] === 'string'
      ? sml.keywords as string[]
      : (sml.keywords as any[]).map(k => typeof k === 'object' && k.value ? k.value : k);
  }
  if (sml.identifiers) props.identifiers = sml.identifiers;
  if (sml.classifiers) props.classifiers = sml.classifiers;
  if (sml.validTime) props.validTime = sml.validTime;
  if (sml.contacts) props.contacts = sml.contacts;
  if (sml.documents) props.documents = sml.documents;
  if (sml.securityConstraints) props.securityConstraints = sml.securityConstraints;
  if (sml.legalConstraints) props.legalConstraints = sml.legalConstraints;

  return props;
}

Memory Concerns:

  • Object spreading: { ...extractCommonProperties(sml) } duplicates properties
  • Property copying: Multiple conditional property assignments
  • Metadata arrays: identifiers, classifiers, contacts, documents arrays copied
  • Nested objects: components array may contain nested PhysicalComponent objects

Memory Questions:

  1. Conversion overhead: How much extra memory vs original SensorML?
  2. Temporary objects: Are intermediate objects released?
  3. Property duplication: Does spreading cause unnecessary copies?
  4. Metadata overhead: How much memory for identifiers/classifiers/contacts arrays?

5. Position Extraction (Multiple Object Allocations)

From Issue #10:

extractGeometry() Function (handles 8+ position types):

function extractGeometry(position?: Position): Geometry | undefined {
  if (!position) return undefined;

  // Check if it's a GeoJSON Point
  if (
    typeof position === 'object' &&
    'type' in position &&
    position.type === 'Point'
  ) {
    return position as Point;
  }

  // Check if it's a Pose (GeoPose with position and orientation)
  if (
    typeof position === 'object' &&
    'position' in position &&
    position.position &&
    typeof position.position === 'object'
  ) {
    const pos = position.position as { 
      lat?: number; 
      lon?: number; 
      h?: number;
      x?: number;
      y?: number;
      z?: number;
    };
    
    // GeoPose Basic-YPR (lat/lon/h)
    if (pos.lat !== undefined && pos.lon !== undefined) {
      return {
        type: 'Point',
        coordinates: [
          pos.lon,
          pos.lat,
          pos.h !== undefined ? pos.h : 0,
        ],
      } as Point;
    }
    
    // GeoPose with Cartesian coordinates (x/y/z)
    if (pos.x !== undefined && pos.y !== undefined) {
      return {
        type: 'Point',
        coordinates: [pos.x, pos.y, pos.z || 0],
      } as Point;
    }
  }

  // Check if it's a VectorComponent (SWE Common vector)
  // Check if it's a DataRecordComponent (SWE Common data record)
  // ... (8+ position type checks total)

  return undefined;
}

Memory Concerns:

  • New Point objects: Creates new GeoJSON Point for non-Point positions
  • Coordinates array: New array [lon, lat, h] allocated
  • Type checking: Multiple typeof checks and property accesses
  • Fallback chain: Checks 8+ types before giving up

Memory Questions:

  1. Point creation overhead: How much memory per new Point object?
  2. Coordinates array: Fixed 24 bytes (3 numbers) or more?
  3. Garbage collection: Are failed type checks generating temporary objects?
  4. Optimization potential: Could Point objects be pooled/reused?

6. Error Handling (Error Accumulation)

From Issue #10:

Error Handling Pattern:

parse(
  data: unknown,
  options: ParserOptions = {}
): ParseResult<T> {
  const format = detectFormat(options.contentType || null, data);
  const errors: string[] = [];
  const warnings: string[] = [];

  try {
    // ... parsing logic ...

    // Validate if requested
    if (options.validate) {
      const validationResult = this.validate(parsed, format.format);
      if (!validationResult.valid) {
        errors.push(...(validationResult.errors || []));
        if (options.strict) {
          throw new CSAPIParseError(
            `Validation failed: ${errors.join(', ')}`,
            format.format
          );
        }
      }
      warnings.push(...(validationResult.warnings || []));
    }

    return {
      data: parsed,
      format,
      errors: errors.length > 0 ? errors : undefined,
      warnings: warnings.length > 0 ? warnings : undefined,
    };
  } catch (error) {
    // ... error handling ...
  }
}

Memory Concerns:

  • Error arrays: errors: string[] and warnings: string[] allocated per parse
  • String concatenation: errors.join(', ') creates new string
  • Error spreading: errors.push(...validationResult.errors) copies arrays
  • Collection validation: Validation errors accumulate for each feature in collection

Memory Questions:

  1. Error overhead: How much memory for error/warning arrays?
  2. String allocation: How much memory for error messages?
  3. Collection errors: At 1,000 features with validation errors, total memory?
  4. Memory leak risk: Are error arrays properly released?

7. No Memory Leak Detection

Current Gaps:

  • ❌ No systematic memory leak testing
  • ❌ No long-running process simulation
  • ❌ No memory retention analysis
  • ❌ No heap snapshot comparison
  • ❌ No GC pressure measurement

Potential Leak Sources:

  1. Event listeners: If parsers register event listeners (not evident in code)
  2. Caches: If internal caching added later without cleanup
  3. Closures: Recursive parsing closures may retain references
  4. Error paths: Exceptions may prevent cleanup
  5. Validation state: Validators may retain state between calls

8. Parser System Context

From Issue #10:

Total: ~1,714 lines of parser code

  • SystemParser, DeploymentParser, ProcedureParser, SamplingFeatureParser
  • PropertyParser, DatastreamParser, ControlStreamParser
  • ObservationParser, CommandParser
  • CollectionParser (generic)
  • SWE Common parser (15 component parsers, recursive)
  • Format detection

Memory-Intensive Operations:

  1. Collection parsing: O(n) memory for n features
  2. Recursive parsing: O(d) stack for depth d
  3. SensorML conversion: Creates duplicate objects
  4. Position extraction: Creates new Point objects
  5. Validation: Accumulates error strings
  6. Format detection: Body inspection requires object traversal

Proposed Solution

1. Establish Benchmark Infrastructure (DEPENDS ON #55)

PREREQUISITE: This work item REQUIRES the benchmark infrastructure from work item #32 (Issue #55) to be completed first.

Once benchmark infrastructure exists:

2. Create Comprehensive Memory Benchmarks

Create benchmarks/memory.bench.ts (~600-900 lines) with:

Single Feature Memory Benchmarks:

  • Parse single GeoJSON System Feature (baseline)
  • Parse single GeoJSON Deployment with geometry
  • Parse single SensorML PhysicalSystem (with conversion)
  • Parse single SWE Quantity (simple component)
  • Parse single SWE DataRecord (nested 1 level)
  • Parse single SWE DataRecord (nested 3 levels)
  • Measure: heap used, external memory, array buffers

Collection Memory Benchmarks:

  • Parse 10 features (small collection)
  • Parse 100 features (medium collection)
  • Parse 1,000 features (large collection)
  • Parse 10,000 features (stress test)
  • Measure: total heap, heap delta per feature, GC frequency

Nesting Depth Memory Benchmarks:

  • Parse SWE DataRecord: 1 level deep (baseline)
  • Parse SWE DataRecord: 2 levels deep
  • Parse SWE DataRecord: 3 levels deep
  • Parse SWE DataRecord: 5 levels deep
  • Parse SWE DataRecord: 10 levels deep (stress test)
  • Measure: stack usage, heap usage, recursive call overhead

Format Conversion Memory Benchmarks:

  • Parse GeoJSON directly (baseline)
  • Parse SensorML → GeoJSON conversion (System)
  • Parse SensorML → GeoJSON conversion (Deployment)
  • Parse SensorML → GeoJSON conversion (Procedure)
  • Measure: conversion overhead, temporary object allocation

Validation Memory Benchmarks:

  • Parse without validation (baseline)
  • Parse with validation, no errors
  • Parse with validation, 10 errors
  • Parse with validation, 100 errors (collection)
  • Measure: validation overhead, error array memory

Position Extraction Memory Benchmarks:

  • Extract GeoJSON Point (passthrough)
  • Extract from GeoPose (create Point)
  • Extract from Vector (create Point)
  • Extract from DataRecord (create Point)
  • Measure: Point creation overhead, coordinates array memory

3. Create Memory Leak Detection Tests

Create benchmarks/memory-leaks.bench.ts (~300-500 lines) with:

Long-Running Process Simulation:

  • Parse 1,000 features sequentially
  • Parse 10,000 features sequentially
  • Parse in loop for 60 seconds continuous
  • Measure: heap growth over time, GC frequency

Memory Retention Analysis:

  • Parse features, release references, force GC
  • Check if heap returns to baseline
  • Identify retained objects via heap snapshot
  • Compare heap snapshots before/after

Parser State Cleanup:

  • Verify no state retained between parse calls
  • Check for leaked closures
  • Verify error arrays released
  • Check for leaked event listeners (if any)

Stress Testing:

  • Parse 100,000 features (extreme scale)
  • Parse with 100 concurrent parsers
  • Parse deeply nested structures (20+ levels)
  • Measure: peak memory, crash threshold

4. Create Memory Profiling Scripts

Create benchmarks/memory-profile.ts (~200-300 lines) with:

Heap Snapshot Capture:

  • Capture heap before parsing
  • Parse large collection
  • Capture heap after parsing
  • Compare snapshots to identify leaks

Memory Timeline:

  • Record memory usage every 100ms during parsing
  • Generate memory usage timeline graph
  • Identify memory spikes
  • Correlate with parsing operations

GC Pressure Analysis:

  • Count GC runs during parsing
  • Measure GC pause time
  • Calculate GC overhead percentage
  • Identify GC triggers

5. Analyze Memory Benchmark Results

Create benchmarks/memory-analysis.ts (~150-250 lines) with:

Memory Scaling Analysis:

  • Calculate memory per feature (linear regression)
  • Calculate memory per nesting level
  • Identify sublinear vs superlinear scaling
  • Determine practical limits

Identify Memory Bottlenecks:

  • Operations using >50% of total memory
  • Operations causing frequent GC
  • Operations with memory leaks
  • Operations with retention issues

Generate Recommendations:

  • Maximum practical collection sizes
  • Maximum safe nesting depth
  • Memory optimization opportunities
  • Streaming strategies for large datasets

6. Implement Targeted Optimizations (If Needed)

ONLY if benchmarks identify issues:

Optimization Candidates (benchmark-driven):

  • If collection parsing expensive: Implement streaming parser
  • If object spreading expensive: Use direct property assignment
  • If Point creation expensive: Implement object pooling
  • If error accumulation expensive: Use linked list instead of array
  • If validation expensive: Lazy error message generation

Optimization Guidelines:

  • Only optimize proven bottlenecks (>10MB overhead or GC >10% of time)
  • Measure before and after (verify improvement)
  • Document tradeoffs (code complexity vs memory savings)
  • Add regression tests (ensure optimization doesn't break functionality)

7. Document Memory Characteristics

Update README.md with new "Memory Usage" section (~200-300 lines):

Memory Overview:

  • Typical memory per feature: X KB
  • Collection memory scaling: O(n) with X KB per feature
  • Nesting memory scaling: O(d) with X KB per level
  • Peak memory for 10,000 features: X MB

Memory Scaling:

Single feature:      ~X KB
10 features:         ~X KB  (X KB per feature)
100 features:        ~X KB  (X KB per feature)
1,000 features:      ~X KB  (X KB per feature)
10,000 features:     ~X MB  (X KB per feature)

Nesting Memory:

1 level deep:        ~X KB
2 levels deep:       ~X KB  (X KB per level)
3 levels deep:       ~X KB  (X KB per level)
5 levels deep:       ~X KB  (X KB per level)
10 levels deep:      ~X KB  (X KB per level)

Practical Limits:

Small collections:   <100 features (<X MB)
Medium collections:  100-1,000 features (X-Y MB)
Large collections:   1,000-10,000 features (Y-Z MB)
Very large:          >10,000 features (>Z MB) - consider streaming

Best Practices:

  • Memory constraints: For <512 MB systems, limit to X features per parse
  • Streaming: For >10,000 features, consider chunked/streaming parsing
  • Nesting depth: Limit SWE DataRecord nesting to 5 levels for optimal performance
  • Validation: Disable validation for memory-constrained environments
  • GC tuning: Increase Node.js heap size for large collections (--max-old-space-size)

Performance Targets:

  • Good: <1 KB per feature
  • Acceptable: <10 KB per feature
  • Poor: >100 KB per feature (needs optimization)

8. Integrate with CI/CD

Add to .github/workflows/benchmarks.yml (coordinate with #55):

Benchmark Execution:

- name: Run memory benchmarks
  run: npm run bench:memory

- name: Run memory leak detection
  run: npm run bench:memory:leaks

- name: Run memory profiling
  run: npm run bench:memory:profile

Memory Regression Detection:

  • Compare against baseline (main branch)
  • Alert if memory usage >20% higher
  • Alert if GC frequency >20% higher
  • Alert if memory leaks detected

PR Comments:

  • Post memory benchmark results to PRs
  • Show comparison with base branch
  • Highlight regressions and improvements
  • Show memory timeline graphs

Acceptance Criteria

Benchmark Infrastructure (4 items)

  • ✅ Benchmark infrastructure from Add comprehensive performance benchmarking #55 is complete and available
  • Created benchmarks/memory.bench.ts with comprehensive memory benchmarks (~600-900 lines)
  • Created benchmarks/memory-leaks.bench.ts with leak detection tests (~300-500 lines)
  • Created benchmarks/memory-profile.ts with profiling scripts (~200-300 lines)

Single Feature Memory Benchmarks (7 items)

  • Benchmarked GeoJSON System Feature (baseline)
  • Benchmarked GeoJSON Deployment with geometry
  • Benchmarked SensorML→GeoJSON conversion (System)
  • Benchmarked SWE Quantity (simple component)
  • Benchmarked SWE DataRecord (nested 1 level)
  • Benchmarked SWE DataRecord (nested 3 levels)
  • Documented memory per feature for each type

Collection Memory Benchmarks (5 items)

  • Benchmarked 10 features (small collection)
  • Benchmarked 100 features (medium collection)
  • Benchmarked 1,000 features (large collection)
  • Benchmarked 10,000 features (stress test)
  • Documented memory scaling (linear/sublinear/superlinear)

Nesting Depth Memory Benchmarks (6 items)

  • Benchmarked 1 level deep (baseline)
  • Benchmarked 2 levels deep
  • Benchmarked 3 levels deep
  • Benchmarked 5 levels deep
  • Benchmarked 10 levels deep (stress test)
  • Documented safe maximum nesting depth

Format Conversion Memory Benchmarks (4 items)

  • Benchmarked GeoJSON direct (baseline)
  • Benchmarked SensorML→GeoJSON conversion overhead
  • Measured temporary object allocation
  • Documented conversion memory overhead percentage

Validation Memory Benchmarks (4 items)

  • Benchmarked parsing without validation (baseline)
  • Benchmarked parsing with validation, no errors
  • Benchmarked parsing with validation, 10 errors
  • Benchmarked parsing with validation, 100 errors (collection)

Position Extraction Memory Benchmarks (5 items)

  • Benchmarked GeoJSON Point (passthrough)
  • Benchmarked GeoPose extraction (create Point)
  • Benchmarked Vector extraction (create Point)
  • Benchmarked DataRecord extraction (create Point)
  • Documented Point creation overhead

Memory Leak Detection (5 items)

  • Tested long-running process (1,000 features sequential)
  • Tested extreme scale (10,000 features sequential)
  • Tested continuous parsing (60 seconds loop)
  • Verified heap returns to baseline after parsing
  • Identified and documented any memory leaks

Memory Profiling (5 items)

  • Captured heap snapshots before/after parsing
  • Generated memory usage timeline
  • Analyzed GC pressure (frequency, pause time, overhead)
  • Identified memory spikes and their causes
  • Documented profiling findings

Memory Analysis (5 items)

  • Analyzed memory scaling (per feature, per level)
  • Identified memory bottlenecks (>50% of total memory)
  • Calculated practical limits (max collection size, max depth)
  • Generated optimization recommendations
  • Documented current memory characteristics

Optimization (if needed) (4 items)

  • Identified optimization opportunities from benchmark data
  • Implemented targeted optimizations ONLY for proven bottlenecks
  • Re-benchmarked after optimization (verified improvement)
  • Added regression tests to prevent optimization from breaking functionality

Documentation (8 items)

  • Added "Memory Usage" section to README.md (~200-300 lines)
  • Documented typical memory per feature (KB)
  • Documented collection memory scaling (formula + table)
  • Documented nesting memory scaling (formula + table)
  • Documented practical limits (small/medium/large/very large)
  • Documented best practices (streaming, depth limits, GC tuning)
  • Documented performance targets (good/acceptable/poor thresholds)
  • Documented memory optimization history (if any optimizations made)

CI/CD Integration (4 items)

  • Added memory benchmarks to .github/workflows/benchmarks.yml
  • Configured memory regression detection (>20% higher = fail)
  • Added PR comment with memory benchmark results and comparison
  • Verified benchmarks run on every PR and main branch commit

Implementation Notes

Files to Create

Benchmark Files (~1,100-1,950 lines total):

  1. benchmarks/memory.bench.ts (~600-900 lines)

    • Single feature memory benchmarks (7 types)
    • Collection memory benchmarks (4 sizes)
    • Nesting depth benchmarks (5 levels)
    • Format conversion benchmarks
    • Validation memory benchmarks
    • Position extraction benchmarks
  2. benchmarks/memory-leaks.bench.ts (~300-500 lines)

    • Long-running process simulation
    • Memory retention analysis
    • Parser state cleanup verification
    • Stress testing
  3. benchmarks/memory-profile.ts (~200-300 lines)

    • Heap snapshot capture
    • Memory timeline generation
    • GC pressure analysis
  4. benchmarks/memory-analysis.ts (~150-250 lines)

    • Memory scaling analysis
    • Bottleneck identification
    • Recommendation generation
    • Results formatting

Files to Modify

README.md (~200-300 lines added):

  • New "Memory Usage" section with:
    • Memory overview
    • Scaling tables (collection and nesting)
    • Practical limits
    • Best practices
    • Performance targets

package.json (~12 lines):

{
  "scripts": {
    "bench:memory": "tsx benchmarks/memory.bench.ts",
    "bench:memory:leaks": "tsx benchmarks/memory-leaks.bench.ts",
    "bench:memory:profile": "tsx benchmarks/memory-profile.ts",
    "bench:memory:analyze": "tsx benchmarks/memory-analysis.ts"
  }
}

.github/workflows/benchmarks.yml (coordinate with #55):

  • Add memory benchmark execution
  • Add leak detection execution
  • Add regression detection
  • Add PR comment generation

Files to Reference

Parser Source Files (for accurate memory benchmarking):

  • src/ogc-api/csapi/parsers/base.ts (479 lines - SystemParser, extractGeometry)
  • src/ogc-api/csapi/parsers/resources.ts (494 lines - 7 resource parsers, CollectionParser)
  • src/ogc-api/csapi/parsers/swe-common-parser.ts (540 lines - 15 component parsers, recursive)
  • src/ogc-api/csapi/parsers/formats.ts (162 lines - format detection)

Test Fixtures (reuse existing test data):

  • src/ogc-api/csapi/parsers/base.spec.ts (has sample GeoJSON/SensorML data)
  • src/ogc-api/csapi/parsers/resources.spec.ts (has sample resource data)
  • src/ogc-api/csapi/parsers/swe-common-parser.spec.ts (has nested SWE data)

Technology Stack

Memory Measurement Tools:

  • Node.js process.memoryUsage() - heap, external, array buffers
  • V8 heap snapshot API - detailed object allocation
  • V8 --expose-gc flag - manual GC triggering
  • Tinybench (from Add comprehensive performance benchmarking #55) - benchmark framework

Memory Profiling Tools:

  • Chrome DevTools - heap snapshot visualization
  • heapdump npm package - heap snapshot capture
  • memwatch-next npm package - leak detection (optional)

Benchmark Priorities:

  • High: Collection scaling, nesting depth, memory leaks
  • Medium: Format conversion, validation overhead, position extraction
  • Low: Extreme scaling (>10,000), micro-optimizations

Performance Targets (Hypothetical - Measure to Confirm)

Memory per Feature:

  • Good: <1 KB per feature
  • Acceptable: <10 KB per feature
  • Poor: >100 KB per feature

Collection Memory Scaling:

  • Good: Linear (O(n))
  • Acceptable: Slightly superlinear (O(n log n))
  • Poor: Quadratic (O(n²))

Nesting Memory:

  • Good: <1 KB per level
  • Acceptable: <5 KB per level
  • Poor: >10 KB per level

GC Overhead:

  • Good: <5% of total parse time
  • Acceptable: <10% of total parse time
  • Poor: >20% of total parse time

Memory Leaks:

  • Good: No leaks detected
  • Acceptable: <1% heap growth over 1,000 parses
  • Poor: >5% heap growth (needs fixing)

Optimization Guidelines

ONLY optimize if benchmarks prove need:

  • Memory per feature >100 KB
  • Collection memory superlinear (>O(n log n))
  • Nesting memory >10 KB per level
  • GC overhead >20% of time
  • Memory leaks detected (>1% growth)

Optimization Approach:

  1. Identify bottleneck from benchmark data
  2. Profile with Chrome DevTools heap snapshot
  3. Implement targeted optimization
  4. Re-benchmark to verify improvement (>20% reduction)
  5. Add regression tests
  6. Document tradeoffs

Common Optimizations:

  • Object pooling: Reuse Point objects for position extraction
  • Streaming parser: Parse collections in chunks instead of all at once
  • Direct assignment: Avoid object spreading ({ ...obj }) when not needed
  • Lazy allocation: Delay error array creation until first error
  • Weak references: Use WeakMap for caches to allow GC

Dependencies

CRITICAL DEPENDENCY:

Why This Dependency Matters:

Testing Requirements

Benchmark Validation:

  • All benchmarks must run without errors
  • All benchmarks must complete in <120 seconds total
  • All benchmarks must produce consistent results (variance <20%)
  • Memory benchmarks must not cause out-of-memory errors

Regression Tests:

  • Add tests to verify optimizations don't break functionality
  • Rerun all 166 parser tests after any optimization
  • Verify parsing accuracy remains 100%
  • Verify no new memory leaks introduced

Caveats

Memory is Environment-Dependent:

  • Benchmarks run on specific hardware (document specs)
  • Results vary by Node.js version, CPU, memory
  • V8 GC behavior varies by configuration
  • Production memory usage may differ from benchmark environment
  • Document benchmark environment in README

Optimization Tradeoffs:

  • Lower memory may mean slower parsing (time/space tradeoff)
  • Object pooling adds complexity
  • Streaming parsers have different API
  • Lazy allocation may increase code complexity
  • Document all tradeoffs in optimization PRs

Memory Usage Context:

  • Parser memory typically <10% of total application memory
  • Network buffers typically dominate memory usage
  • JSON parsing (before CSAPI parsing) typically uses more memory
  • Focus on preventing leaks over micro-optimizations

Acceptable Memory Usage:

  • 1 KB per feature is excellent (10,000 features = 10 MB)
  • 10 KB per feature is acceptable (10,000 features = 100 MB)
  • 100 KB per feature needs investigation (10,000 features = 1 GB!)

Priority Justification

Priority: Low

Why Low Priority:

  1. No Known Memory Issues: No user complaints about high memory usage or crashes
  2. Functional Excellence: Parsers work correctly with comprehensive tests (166 tests)
  3. Expected Reasonable Memory: Parser code is clean and doesn't appear to have obvious leaks
  4. Depends on Infrastructure: Cannot start until Add comprehensive performance benchmarking #55 (benchmark infrastructure) is complete
  5. Educational Value: Primarily for documentation and preventing future memory issues

Why Still Important:

  1. Prevent Future Issues: Detect memory leaks before they cause production crashes
  2. Scalability Guidance: Help users understand limits for large collections and deep nesting
  3. Optimization Baseline: Establish memory baseline to detect regressions
  4. Resource Planning: Help users estimate memory requirements for their use cases
  5. Embedded/Mobile: Critical for memory-constrained environments

Impact if Not Addressed:

  • ⚠️ Unknown memory limits (users can't plan for large datasets)
  • ⚠️ No leak detection (potential production crashes over time)
  • ⚠️ No optimization guidance (can't prioritize memory improvements)
  • ⚠️ Unknown GC impact (can't estimate performance overhead)
  • ✅ Parsers still work correctly (functional quality not affected)
  • ✅ No known memory issues (no urgency)

Effort Estimate: 12-18 hours (after #55 complete)

  • Memory benchmark creation: 6-9 hours
  • Leak detection tests: 3-4 hours
  • Profiling setup: 2-3 hours
  • Analysis and documentation: 2-3 hours
  • CI/CD integration: 0.5-1 hour (reuse from Add comprehensive performance benchmarking #55)
  • Optimization (optional, if needed): 4-6 hours

When to Prioritize Higher:

  • If users report out-of-memory errors
  • If targeting embedded/mobile devices (strict memory limits)
  • If implementing streaming features (need memory baseline)
  • If memory leaks suspected (production stability at risk)
  • If adding caching features (need to measure memory impact)

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions