-
Notifications
You must be signed in to change notification settings - Fork 0
Description
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:
- Per-feature memory: How much memory per GeoJSON Feature? System? Deployment?
- Collection overhead: Fixed overhead for collection structure?
- Scaling: Linear, sublinear, or superlinear with collection size?
- Peak memory: At 1,000 features vs 10,000 features?
- GC frequency: How often does GC run during large collection parsing?
- 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:
- Stack usage: How much stack per nesting level?
- Heap usage: How much heap per nested DataRecord?
- Maximum depth: What's safe nesting depth before stack overflow?
- Memory amplification: Does memory usage increase exponentially with depth?
- 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:
- Conversion overhead: How much extra memory vs original SensorML?
- Temporary objects: Are intermediate objects released?
- Property duplication: Does spreading cause unnecessary copies?
- 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
typeofchecks and property accesses - Fallback chain: Checks 8+ types before giving up
Memory Questions:
- Point creation overhead: How much memory per new Point object?
- Coordinates array: Fixed 24 bytes (3 numbers) or more?
- Garbage collection: Are failed type checks generating temporary objects?
- 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[]andwarnings: 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:
- Error overhead: How much memory for error/warning arrays?
- String allocation: How much memory for error messages?
- Collection errors: At 1,000 features with validation errors, total memory?
- 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:
- Event listeners: If parsers register event listeners (not evident in code)
- Caches: If internal caching added later without cleanup
- Closures: Recursive parsing closures may retain references
- Error paths: Exceptions may prevent cleanup
- 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:
- Collection parsing: O(n) memory for n features
- Recursive parsing: O(d) stack for depth d
- SensorML conversion: Creates duplicate objects
- Position extraction: Creates new Point objects
- Validation: Accumulates error strings
- 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:
- Import Tinybench framework (from Add comprehensive performance benchmarking #55)
- Use Node.js
process.memoryUsage()for heap tracking - Use V8 heap snapshot API for detailed analysis
- Integrate with CI/CD pipeline
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:profileMemory 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.tswith comprehensive memory benchmarks (~600-900 lines) - Created
benchmarks/memory-leaks.bench.tswith leak detection tests (~300-500 lines) - Created
benchmarks/memory-profile.tswith 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):
-
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
-
benchmarks/memory-leaks.bench.ts(~300-500 lines)- Long-running process simulation
- Memory retention analysis
- Parser state cleanup verification
- Stress testing
-
benchmarks/memory-profile.ts(~200-300 lines)- Heap snapshot capture
- Memory timeline generation
- GC pressure analysis
-
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-gcflag - manual GC triggering - Tinybench (from Add comprehensive performance benchmarking #55) - benchmark framework
Memory Profiling Tools:
- Chrome DevTools - heap snapshot visualization
heapdumpnpm package - heap snapshot capturememwatch-nextnpm 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:
- Identify bottleneck from benchmark data
- Profile with Chrome DevTools heap snapshot
- Implement targeted optimization
- Re-benchmark to verify improvement (>20% reduction)
- Add regression tests
- 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:
- REQUIRES work item Update parser test count from 108 to 166 tests #32 (Issue Add comprehensive performance benchmarking #55) - Comprehensive performance benchmarking infrastructure
- Cannot start until benchmark framework, utilities, and CI/CD integration are complete
Why This Dependency Matters:
- Reuses Tinybench setup from Add comprehensive performance benchmarking #55
- Uses shared benchmark utilities (stats, reporter, regression detection)
- Integrates with established CI/CD pipeline
- Follows consistent benchmarking patterns
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:
- No Known Memory Issues: No user complaints about high memory usage or crashes
- Functional Excellence: Parsers work correctly with comprehensive tests (166 tests)
- Expected Reasonable Memory: Parser code is clean and doesn't appear to have obvious leaks
- Depends on Infrastructure: Cannot start until Add comprehensive performance benchmarking #55 (benchmark infrastructure) is complete
- Educational Value: Primarily for documentation and preventing future memory issues
Why Still Important:
- Prevent Future Issues: Detect memory leaks before they cause production crashes
- Scalability Guidance: Help users understand limits for large collections and deep nesting
- Optimization Baseline: Establish memory baseline to detect regressions
- Resource Planning: Help users estimate memory requirements for their use cases
- 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)