This document tracks the performance optimization journey of the airspace rendering pipeline in The Paragliding App. The pipeline processes aviation airspace data from OpenAIP, stores it in SQLite, and renders it on maps with polygon clipping to eliminate overlaps.
Code locations are symbol names, not line numbers. They were line ranges until 2026-08-29, by which point every one of them was wrong — four pointed past the end of a file that had since shrunk to 1234 lines, and the two that were still in range named unrelated code. Cite something greppable.
| Stage | Optimization | Implementation Details | Performance Impact | Code Location |
|---|---|---|---|---|
| 1. SQL Filtering | Move filtering to database | Filter by type, class, altitude, and bounds in the SQL query instead of post-processing | Reduces data loaded by ~70-90% | AirspaceDiskCache.getGeometriesInBounds() |
| 2. Pre-computed Altitudes | Store altitude in feet | lower_altitude_ft INTEGER column avoids runtime conversion |
Eliminates ~1ms per 100 airspaces | airspace_disk_cache.dart, geometry table schema |
| 3. Viewport Culling | SQL spatial indices | Compound index idx_geometry_spatial over the four bounds columns |
Fast spatial queries (~5ms for 1000s) | idx_geometry_spatial in _onCreate |
| 4. Direct Pipeline | Skip GeoJSON parsing | Build Clipper data straight from database BLOBs, no intermediate JSON or LatLng |
Saves ~20-30ms per load | AirspaceDiskCache._createClipperData() |
| 5. Altitude Sorting | Pre-sort for clipping | Sort airspaces by altitude once for optimal clipping order | Enables early exit optimization | _applyPolygonClippingOptimized() |
| 6. Early Exit | Stop at higher altitudes | Break loop when lower altitude ≥ current altitude | Reduces comparisons by ~40-60% | _applyPolygonClippingOptimized() |
| 7. Bounds Pre-check | Inline bbox check | Direct coordinate comparison before polygon operations | Skips ~50-70% of polygon ops | _applyPolygonClippingOptimized() |
| 8. Altitude Array | Cache-friendly layout | Altitudes pre-extracted into a contiguous Int32List |
Better CPU cache locality | ClippingBatch.altitudes |
| 9. Binary Storage | Compressed coordinates | Superseded by #10 — see note below. | — | — |
| 10. Int32 Coordinates | Direct Clipper2 pipeline | Int32 BLOBs (scaled by 10^7) with zero-copy Int32List.view into the Clipper data |
25-40% faster clipping, 85% memory reduction | coordinates_binary / polygon_offsets columns, _createClipperData() |
#9 no longer describes the code. It recorded Float32 binary arrays with GZIP compression; #10 replaced that with uncompressed Int32 BLOBs, which is what the cache stores today (
coordinates_binary BLOB -- Int32 array (scaled by 10^7)). There is no gzip in the disk cache. Both were listed as “completed” side by side, which reads as the two being layered rather than one having replaced the other.
The original pipeline had excessive conversions and memory allocations:
Database (Float32) → Float32List → LatLng objects → Int64 → Clipper2
This created:
Direct Int32 pipeline with ClipperData wrapper:
Database (Int32) → Int32List.view → ClipperData → Point64 → Clipper2
| Metric | Before | After | Improvement |
|---|---|---|---|
| Clipping Time (1344 polygons) | ~2000-2500ms | 1508-1727ms | 25-40% faster |
| Memory Allocations | 50,000+ LatLng objects | 0 intermediate objects | 100% reduction |
| Conversion Overhead | ~35-45ms per 1000 | ~2-5ms per 1000 | 85% reduction |
| Memory per Coordinate | ~56 bytes | ~8 bytes | 85% reduction |
-- Multiple separate indices (suboptimal)
CREATE INDEX idx_geometry_spatial ON airspace_geometry(
bounds_west, bounds_east, bounds_south, bounds_north
);
CREATE INDEX idx_geometry_spatial_altitude ON airspace_geometry(
lower_altitude_ft, bounds_west, bounds_east, bounds_south, bounds_north
);
-- Create optimized covering index for the most common query pattern
CREATE INDEX idx_geometry_spatial_covering ON airspace_geometry(
bounds_west,
bounds_east,
bounds_south,
bounds_north,
lower_altitude_ft,
type_code,
id,
coordinates_binary,
polygon_offsets
);
Expected Impact:
Add grid cell column for coarse filtering before exact spatial query:
-- Add grid cell column (10x10 degree grid)
ALTER TABLE airspace_geometry ADD COLUMN grid_cell INTEGER;
-- Update grid cells
UPDATE airspace_geometry
SET grid_cell =
(CAST((bounds_west + 180) / 10 AS INTEGER)) * 100 +
(CAST((bounds_south + 90) / 10 AS INTEGER));
-- Create grid index
CREATE INDEX idx_geometry_grid ON airspace_geometry(
grid_cell, bounds_west, bounds_east
);
-- Optimized query with grid filtering
SELECT * FROM airspace_geometry
WHERE grid_cell IN (?, ?, ?, ?) -- Pre-computed grid cells
AND bounds_west <= ? AND bounds_east >= ?
AND bounds_south <= ? AND bounds_north >= ?
AND lower_altitude_ft <= ?
ORDER BY lower_altitude_ft ASC;
Expected Impact:
Based on production logging from CLIPPING_DETAILED_PERFORMANCE:
| Metric | Baseline (O(n²)) | Current | Improvement |
|---|---|---|---|
| Theoretical comparisons | n*(n-1)/2 | - | - |
| Actual comparisons | 100% of theoretical | 30-40% of theoretical | 60-70% reduction |
| Altitude rejections | 0 | 40-60% of comparisons | Early exit working |
| Bounds rejections | 0 | 50-70% of remaining | Spatial filtering effective |
| Empty clipping lists | 0 | ~20% of airspaces | Skip unnecessary ops |
| Actual clipping operations | 100% | ~20% of airspaces | 80% reduction |
Reason: Too complex for current scale (<5000 polygons)
Reason: Limited Dart support, buggy in AOT compilation
Reason: Not available in Flutter environment
Reason: Overkill for current polygon counts
LoggingService.structured('CLIPPING_PERFORMANCE', {
'polygons_input': count,
'polygons_output': count,
'clipping_time_ms': ms,
'total_comparisons': count,
'altitude_rejections': count,
'bounds_rejections': count,
});
# Enable performance logging
LoggingService.enablePerformanceLogging = true;
# Look for structured logs
grep "CLIPPING_PERFORMANCE" dev_data/flutter.log
grep "AIRSPACE_CLIPPING" dev_data/flutter.log
file.dart:line citations with symbol names:
every one was wrong, four pointing past the end of a file that had shrunk to 1234 lines,
and the two still in range naming unrelated codeLast Updated: 2026-08-29 Next Review: After covering index implementation