@mytec: iter3.4.0 start
This commit is contained in:
@@ -485,7 +485,16 @@ class CoverageService:
|
||||
)
|
||||
streets = _filter_osm_list_to_bbox(streets, min_lat, min_lon, max_lat, max_lon)
|
||||
water_bodies = _filter_osm_list_to_bbox(water_bodies, min_lat, min_lon, max_lat, max_lon)
|
||||
vegetation_areas = _filter_osm_list_to_bbox(vegetation_areas, min_lat, min_lon, max_lat, max_lon)
|
||||
# Cap vegetation at 5000 — each area requires O(samples × areas)
|
||||
# point-in-polygon checks per grid point. 20k+ areas with dominant
|
||||
# path enabled causes OOM via worker memory explosion.
|
||||
vegetation_areas = _filter_osm_list_to_bbox(
|
||||
vegetation_areas, min_lat, min_lon, max_lat, max_lon,
|
||||
max_count=5000,
|
||||
)
|
||||
|
||||
_clog(f"Filtered OSM data: {len(buildings)} bldgs, {len(streets)} streets, "
|
||||
f"{len(water_bodies)} water, {len(vegetation_areas)} veg")
|
||||
|
||||
# Build spatial index for buildings
|
||||
spatial_idx: Optional[SpatialIndex] = None
|
||||
@@ -650,10 +659,13 @@ class CoverageService:
|
||||
sites: List[SiteParams],
|
||||
settings: CoverageSettings,
|
||||
cancel_token: Optional[CancellationToken] = None,
|
||||
progress_fn: Optional[Callable[[str, float], None]] = None,
|
||||
) -> List[CoveragePoint]:
|
||||
"""
|
||||
Calculate combined coverage from multiple sites
|
||||
Best server (strongest signal) wins at each point
|
||||
|
||||
progress_fn(phase, pct): optional callback for progress updates (0.0-1.0).
|
||||
"""
|
||||
if not sites:
|
||||
return []
|
||||
@@ -661,10 +673,26 @@ class CoverageService:
|
||||
# Apply preset once
|
||||
settings = apply_preset(settings)
|
||||
|
||||
# Per-site progress tracking for averaged overall progress
|
||||
num_sites = len(sites)
|
||||
_site_progress = [0.0] * num_sites
|
||||
|
||||
def _make_site_progress(idx: int):
|
||||
"""Create a progress_fn for one site that reports scaled overall progress."""
|
||||
def _site_fn(phase: str, pct: float, _eta=None):
|
||||
_site_progress[idx] = pct
|
||||
if progress_fn:
|
||||
overall = sum(_site_progress) / num_sites
|
||||
progress_fn(f"Site {idx + 1}/{num_sites}: {phase}", overall)
|
||||
return _site_fn
|
||||
|
||||
# Get all individual coverages
|
||||
all_coverages = await asyncio.gather(*[
|
||||
self.calculate_coverage(site, settings, cancel_token)
|
||||
for site in sites
|
||||
self.calculate_coverage(
|
||||
site, settings, cancel_token,
|
||||
progress_fn=_make_site_progress(i) if progress_fn else None,
|
||||
)
|
||||
for i, site in enumerate(sites)
|
||||
])
|
||||
|
||||
# Combine by best signal
|
||||
@@ -751,7 +779,8 @@ class CoverageService:
|
||||
points = []
|
||||
timing = {"los": 0.0, "buildings": 0.0, "antenna": 0.0,
|
||||
"dominant_path": 0.0, "street_canyon": 0.0,
|
||||
"reflection": 0.0, "vegetation": 0.0}
|
||||
"reflection": 0.0, "vegetation": 0.0,
|
||||
"lod_none": 0, "lod_simplified": 0, "lod_full": 0}
|
||||
total = len(grid)
|
||||
log_interval = max(1, total // 20)
|
||||
|
||||
@@ -901,7 +930,6 @@ class CoverageService:
|
||||
|
||||
# LOD_NONE: skip dominant path entirely for distant points (>3km)
|
||||
if lod == LODLevel.NONE:
|
||||
timing.setdefault("lod_none", 0)
|
||||
timing["lod_none"] += 1
|
||||
else:
|
||||
t0 = time.time()
|
||||
@@ -909,12 +937,10 @@ class CoverageService:
|
||||
# LOD_SIMPLIFIED: limit buildings for mid-range points (1.5-3km)
|
||||
dp_buildings = nearby_buildings
|
||||
if lod == LODLevel.SIMPLIFIED:
|
||||
timing.setdefault("lod_simplified", 0)
|
||||
timing["lod_simplified"] += 1
|
||||
if len(nearby_buildings) > SIMPLIFIED_MAX_BUILDINGS:
|
||||
dp_buildings = nearby_buildings[:SIMPLIFIED_MAX_BUILDINGS]
|
||||
else:
|
||||
timing.setdefault("lod_full", 0)
|
||||
timing["lod_full"] += 1
|
||||
|
||||
# nearby_buildings already filtered via spatial index —
|
||||
|
||||
@@ -164,11 +164,16 @@ except ImportError:
|
||||
ray = None # type: ignore
|
||||
|
||||
|
||||
# ── Worker-level spatial index cache (persists across tasks in same worker) ──
|
||||
# ── Worker-level caches (persist across tasks in same worker process) ──
|
||||
|
||||
_worker_spatial_idx = None
|
||||
_worker_cache_key: Optional[str] = None
|
||||
|
||||
# Shared-memory buildings/OSM — unpickled once per worker, cached by key
|
||||
_worker_shared_buildings = None
|
||||
_worker_shared_osm_data = None
|
||||
_worker_shared_data_key: Optional[str] = None
|
||||
|
||||
|
||||
def _ray_process_chunk_impl(chunk, terrain_cache, buildings, osm_data, config):
|
||||
"""Implementation: process a chunk of (lat, lon, elevation) tuples.
|
||||
@@ -205,6 +210,7 @@ def _ray_process_chunk_impl(chunk, terrain_cache, buildings, osm_data, config):
|
||||
"los": 0.0, "buildings": 0.0, "antenna": 0.0,
|
||||
"dominant_path": 0.0, "street_canyon": 0.0,
|
||||
"reflection": 0.0, "vegetation": 0.0,
|
||||
"lod_none": 0, "lod_simplified": 0, "lod_full": 0,
|
||||
}
|
||||
|
||||
precomputed = config.get('precomputed')
|
||||
@@ -238,9 +244,14 @@ if RAY_AVAILABLE:
|
||||
|
||||
|
||||
def get_cpu_count() -> int:
|
||||
"""Get number of usable CPU cores, capped at 14."""
|
||||
"""Get number of usable CPU cores, capped at 6.
|
||||
|
||||
Each worker holds its own copy of buildings + OSM data + spatial index
|
||||
(~200-400 MB per worker). Capping at 6 prevents OOM on systems with
|
||||
8-16 GB RAM (especially WSL2 with limited memory allocation).
|
||||
"""
|
||||
try:
|
||||
return min(mp.cpu_count() or 4, 14)
|
||||
return min(mp.cpu_count() or 4, 6)
|
||||
except Exception:
|
||||
return 4
|
||||
|
||||
@@ -327,8 +338,25 @@ def calculate_coverage_parallel(
|
||||
except Exception as e:
|
||||
log_fn(f"Ray execution failed: {e} — falling back to sequential")
|
||||
|
||||
# Fallback: ProcessPoolExecutor with reduced workers to avoid MemoryError
|
||||
pool_workers = min(num_workers, 6)
|
||||
# Fallback: ProcessPoolExecutor (shared memory eliminates per-chunk pickle)
|
||||
pool_workers = num_workers
|
||||
|
||||
# Scale workers down based on data volume to prevent OOM.
|
||||
# Each worker unpickles + holds its own copy of buildings, OSM data, and
|
||||
# spatial index. With large datasets the per-worker memory can exceed
|
||||
# 300 MB, so reduce workers to keep total under ~2 GB.
|
||||
data_items = len(buildings) + len(streets) + len(water_bodies) + len(vegetation_areas)
|
||||
if data_items > 20000:
|
||||
pool_workers = min(pool_workers, 2)
|
||||
log_fn(f"Data volume high ({data_items} items) — capping workers at {pool_workers}")
|
||||
elif data_items > 10000:
|
||||
pool_workers = min(pool_workers, 3)
|
||||
log_fn(f"Data volume moderate ({data_items} items) — capping workers at {pool_workers}")
|
||||
elif data_items > 5000:
|
||||
pool_workers = min(pool_workers, 4)
|
||||
log_fn(f"Data volume elevated ({data_items} items) — capping workers at {pool_workers}")
|
||||
|
||||
log_fn(f"ProcessPool: {pool_workers} workers (cpu_count={num_workers}, data_items={data_items})")
|
||||
if pool_workers > 1 and total_points > 100:
|
||||
try:
|
||||
return _calculate_with_process_pool(
|
||||
@@ -338,6 +366,8 @@ def calculate_coverage_parallel(
|
||||
pool_workers, log_fn, cancel_token, precomputed,
|
||||
progress_fn,
|
||||
)
|
||||
except (MemoryError, OSError) as e:
|
||||
log_fn(f"ProcessPool OOM/OS error: {e} — falling back to sequential")
|
||||
except Exception as e:
|
||||
log_fn(f"ProcessPool failed: {e} — falling back to sequential")
|
||||
|
||||
@@ -396,8 +426,8 @@ def _calculate_with_ray(
|
||||
for lat, lon in grid
|
||||
]
|
||||
|
||||
# ~4 chunks per worker for granular progress
|
||||
chunk_size = max(1, len(items) // (num_workers * 4))
|
||||
# Larger chunks to amortize IPC overhead (was num_workers*4)
|
||||
chunk_size = max(1, min(400, len(items) // max(2, num_workers)))
|
||||
chunks = [items[i:i + chunk_size] for i in range(0, len(items), chunk_size)]
|
||||
log_fn(f"Submitting {len(chunks)} chunks of ~{chunk_size} points")
|
||||
|
||||
@@ -489,6 +519,7 @@ def _pool_worker_process_chunk(args):
|
||||
"los": 0.0, "buildings": 0.0, "antenna": 0.0,
|
||||
"dominant_path": 0.0, "street_canyon": 0.0,
|
||||
"reflection": 0.0, "vegetation": 0.0,
|
||||
"lod_none": 0, "lod_simplified": 0, "lod_full": 0,
|
||||
}
|
||||
|
||||
precomputed = config.get('precomputed')
|
||||
@@ -542,6 +573,28 @@ def _store_terrain_in_shm(terrain_cache: Dict[str, np.ndarray], log_fn) -> Tuple
|
||||
return blocks, refs
|
||||
|
||||
|
||||
def _store_pickle_in_shm(data, label: str, log_fn) -> Tuple[Optional[Any], Optional[dict]]:
|
||||
"""Pickle arbitrary data into a SharedMemory block.
|
||||
|
||||
Returns (shm_block, ref_dict) where ref_dict = {shm_name, size}.
|
||||
On failure returns (None, None) and caller should fall back to pickle.
|
||||
"""
|
||||
import multiprocessing.shared_memory as shm_mod
|
||||
import pickle
|
||||
|
||||
try:
|
||||
blob = pickle.dumps(data, protocol=pickle.HIGHEST_PROTOCOL)
|
||||
size = len(blob)
|
||||
block = shm_mod.SharedMemory(create=True, size=size)
|
||||
block.buf[:size] = blob
|
||||
mb = size / (1024 * 1024)
|
||||
log_fn(f"{label} in shared memory: {mb:.1f} MB")
|
||||
return block, {'shm_name': block.name, 'size': size}
|
||||
except Exception as e:
|
||||
log_fn(f"Failed to store {label} in shm: {e}")
|
||||
return None, None
|
||||
|
||||
|
||||
def _pool_worker_shm_chunk(args):
|
||||
"""Worker function that reads terrain from shared memory instead of pickle."""
|
||||
import multiprocessing.shared_memory as shm_mod
|
||||
@@ -585,6 +638,7 @@ def _pool_worker_shm_chunk(args):
|
||||
"los": 0.0, "buildings": 0.0, "antenna": 0.0,
|
||||
"dominant_path": 0.0, "street_canyon": 0.0,
|
||||
"reflection": 0.0, "vegetation": 0.0,
|
||||
"lod_none": 0, "lod_simplified": 0, "lod_full": 0,
|
||||
}
|
||||
|
||||
precomputed = config.get('precomputed')
|
||||
@@ -607,6 +661,200 @@ def _pool_worker_shm_chunk(args):
|
||||
return results
|
||||
|
||||
|
||||
_worker_chunk_count: int = 0 # per-worker chunk counter
|
||||
|
||||
|
||||
def _pool_worker_shm_shared(args):
|
||||
"""Worker: terrain + buildings + OSM all via shared memory.
|
||||
|
||||
Per-chunk args are tiny (~8 KB): just point coords, shm refs, and config.
|
||||
Buildings and OSM data are unpickled from shared memory ONCE per worker
|
||||
and cached in module globals for subsequent chunks.
|
||||
"""
|
||||
import multiprocessing.shared_memory as shm_mod
|
||||
import pickle
|
||||
|
||||
global _worker_chunk_count
|
||||
_worker_chunk_count += 1
|
||||
pid = os.getpid()
|
||||
t_worker_start = time.perf_counter()
|
||||
|
||||
chunk, terrain_shm_refs, shared_data_refs, config = args
|
||||
|
||||
# ── Reconstruct terrain from shared memory ──
|
||||
t0 = time.perf_counter()
|
||||
terrain_cache = {}
|
||||
for tile_name, ref in terrain_shm_refs.items():
|
||||
try:
|
||||
block = shm_mod.SharedMemory(name=ref['shm_name'])
|
||||
terrain_cache[tile_name] = np.ndarray(
|
||||
ref['shape'], dtype=ref['dtype'], buffer=block.buf,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
from app.services.terrain_service import terrain_service
|
||||
terrain_service._tile_cache = terrain_cache
|
||||
t_terrain_shm = time.perf_counter() - t0
|
||||
|
||||
# ── Read buildings + OSM from shared memory (cached per worker) ──
|
||||
global _worker_shared_buildings, _worker_shared_osm_data, _worker_shared_data_key
|
||||
global _worker_spatial_idx, _worker_cache_key
|
||||
|
||||
data_key = config.get('cache_key', '')
|
||||
cached = (_worker_shared_data_key == data_key)
|
||||
|
||||
t_unpickle_bld = 0.0
|
||||
t_unpickle_osm = 0.0
|
||||
t_spatial = 0.0
|
||||
|
||||
if not cached:
|
||||
# First chunk for this calculation — unpickle from shm
|
||||
buildings_ref = shared_data_refs.get('buildings')
|
||||
osm_ref = shared_data_refs.get('osm_data')
|
||||
|
||||
if buildings_ref:
|
||||
try:
|
||||
t0 = time.perf_counter()
|
||||
blk = shm_mod.SharedMemory(name=buildings_ref['shm_name'])
|
||||
_worker_shared_buildings = pickle.loads(bytes(blk.buf[:buildings_ref['size']]))
|
||||
t_unpickle_bld = time.perf_counter() - t0
|
||||
except Exception:
|
||||
_worker_shared_buildings = []
|
||||
else:
|
||||
_worker_shared_buildings = []
|
||||
|
||||
if osm_ref:
|
||||
try:
|
||||
t0 = time.perf_counter()
|
||||
blk = shm_mod.SharedMemory(name=osm_ref['shm_name'])
|
||||
_worker_shared_osm_data = pickle.loads(bytes(blk.buf[:osm_ref['size']]))
|
||||
t_unpickle_osm = time.perf_counter() - t0
|
||||
except Exception:
|
||||
_worker_shared_osm_data = {}
|
||||
else:
|
||||
_worker_shared_osm_data = {}
|
||||
|
||||
_worker_shared_data_key = data_key
|
||||
|
||||
# Rebuild spatial index for new data
|
||||
t0 = time.perf_counter()
|
||||
if _worker_shared_buildings:
|
||||
from app.services.spatial_index import SpatialIndex
|
||||
_worker_spatial_idx = SpatialIndex()
|
||||
_worker_spatial_idx.build(_worker_shared_buildings)
|
||||
else:
|
||||
_worker_spatial_idx = None
|
||||
_worker_cache_key = data_key
|
||||
t_spatial = time.perf_counter() - t0
|
||||
|
||||
print(
|
||||
f"[WORKER {pid}] Init: terrain_shm={t_terrain_shm*1000:.1f}ms "
|
||||
f"unpickle_bld={t_unpickle_bld*1000:.1f}ms "
|
||||
f"unpickle_osm={t_unpickle_osm*1000:.1f}ms "
|
||||
f"spatial={t_spatial*1000:.1f}ms "
|
||||
f"buildings={len(_worker_shared_buildings or [])} "
|
||||
f"tiles={len(terrain_cache)}",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
print(
|
||||
f"[WORKER {pid}] Processing chunk {_worker_chunk_count}, "
|
||||
f"cached={cached}, points={len(chunk)}",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
buildings = _worker_shared_buildings or []
|
||||
osm_data = _worker_shared_osm_data or {}
|
||||
|
||||
# ── Imports + object creation (timed) ──
|
||||
t0 = time.perf_counter()
|
||||
from app.services.coverage_service import CoverageService, SiteParams, CoverageSettings
|
||||
t_import = time.perf_counter() - t0
|
||||
|
||||
t0 = time.perf_counter()
|
||||
site = SiteParams(**config['site_dict'])
|
||||
settings = CoverageSettings(**config['settings_dict'])
|
||||
svc = CoverageService()
|
||||
t_pydantic = time.perf_counter() - t0
|
||||
|
||||
timing = {
|
||||
"los": 0.0, "buildings": 0.0, "antenna": 0.0,
|
||||
"dominant_path": 0.0, "street_canyon": 0.0,
|
||||
"reflection": 0.0, "vegetation": 0.0,
|
||||
"lod_none": 0, "lod_simplified": 0, "lod_full": 0,
|
||||
}
|
||||
|
||||
precomputed = config.get('precomputed')
|
||||
|
||||
streets = osm_data.get('streets', [])
|
||||
water = osm_data.get('water_bodies', [])
|
||||
veg = osm_data.get('vegetation_areas', [])
|
||||
site_elev = config['site_elevation']
|
||||
|
||||
t_init_done = time.perf_counter()
|
||||
init_ms = (t_init_done - t_worker_start) * 1000
|
||||
|
||||
# ── Process points with per-point profiling (first 3 only) ──
|
||||
results = []
|
||||
t_loop_start = time.perf_counter()
|
||||
t_model_dump_total = 0.0
|
||||
n_dumped = 0
|
||||
|
||||
for i, (lat, lon, point_elev) in enumerate(chunk):
|
||||
pre = precomputed.get((lat, lon)) if precomputed else None
|
||||
|
||||
# Snapshot timing dict before call (for first 3 points)
|
||||
if i < 3:
|
||||
timing_before = {k: v for k, v in timing.items()}
|
||||
t_pt = time.perf_counter()
|
||||
|
||||
point = svc._calculate_point_sync(
|
||||
site, lat, lon, settings,
|
||||
buildings, streets,
|
||||
_worker_spatial_idx, water, veg,
|
||||
site_elev, point_elev, timing,
|
||||
precomputed_distance=pre.get('distance') if pre else None,
|
||||
precomputed_path_loss=pre.get('path_loss') if pre else None,
|
||||
)
|
||||
|
||||
if i < 3:
|
||||
t_pt_done = time.perf_counter()
|
||||
pt_ms = (t_pt_done - t_pt) * 1000
|
||||
deltas = {k: (timing[k] - timing_before.get(k, 0)) * 1000 for k in timing}
|
||||
parts = " ".join(f"{k}={v:.2f}" for k, v in deltas.items() if v > 0.001)
|
||||
print(
|
||||
f"[WORKER {pid}] Point {i}: {pt_ms:.2f}ms "
|
||||
f"rsrp={point.rsrp:.1f} dist={point.distance:.0f}m "
|
||||
f"breakdown=[{parts}]",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
if point.rsrp >= settings.min_signal:
|
||||
t_md = time.perf_counter()
|
||||
results.append(point.model_dump())
|
||||
t_model_dump_total += time.perf_counter() - t_md
|
||||
n_dumped += 1
|
||||
|
||||
t_loop_done = time.perf_counter()
|
||||
loop_ms = (t_loop_done - t_loop_start) * 1000
|
||||
total_ms = (t_loop_done - t_worker_start) * 1000
|
||||
avg_pt = loop_ms / len(chunk) if chunk else 0
|
||||
avg_dump = (t_model_dump_total * 1000 / n_dumped) if n_dumped else 0
|
||||
|
||||
print(
|
||||
f"[WORKER {pid}] Chunk done: total={total_ms:.0f}ms "
|
||||
f"init={init_ms:.0f}ms loop={loop_ms:.0f}ms "
|
||||
f"avg_pt={avg_pt:.2f}ms model_dump={avg_dump:.2f}ms×{n_dumped} "
|
||||
f"import={t_import*1000:.1f}ms pydantic={t_pydantic*1000:.1f}ms "
|
||||
f"terrain_shm={t_terrain_shm*1000:.1f}ms "
|
||||
f"results={len(results)}/{len(chunk)}",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def _calculate_with_process_pool(
|
||||
grid, point_elevations, site_dict, settings_dict,
|
||||
terrain_cache, buildings, streets, water_bodies,
|
||||
@@ -616,23 +864,28 @@ def _calculate_with_process_pool(
|
||||
):
|
||||
"""Execute using ProcessPoolExecutor.
|
||||
|
||||
Uses shared memory for terrain tiles (zero-copy numpy views) to reduce
|
||||
memory usage compared to pickling full terrain arrays per worker.
|
||||
Uses shared memory for terrain tiles (zero-copy numpy views), buildings,
|
||||
and OSM data (pickle-once, read-many) to eliminate per-chunk serialization
|
||||
overhead.
|
||||
"""
|
||||
from concurrent.futures import ProcessPoolExecutor, as_completed
|
||||
|
||||
total_points = len(grid)
|
||||
|
||||
# Estimate pickle size for building data and cap workers accordingly
|
||||
building_count = len(buildings)
|
||||
if building_count > 10000:
|
||||
num_workers = min(num_workers, 3)
|
||||
log_fn(f"Large building set ({building_count}) — reducing workers to {num_workers}")
|
||||
elif building_count > 5000:
|
||||
num_workers = min(num_workers, 4)
|
||||
data_items = building_count + len(streets) + len(water_bodies) + len(vegetation_areas)
|
||||
|
||||
log_fn(f"ProcessPool mode: {total_points} points, {num_workers} workers, "
|
||||
f"{building_count} buildings")
|
||||
f"{building_count} buildings, {data_items} total OSM items")
|
||||
|
||||
# Log memory at start
|
||||
try:
|
||||
with open('/proc/self/status') as f:
|
||||
for line in f:
|
||||
if line.startswith('VmRSS:'):
|
||||
log_fn(f"Memory before calculation: {line.strip()}")
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Store terrain tiles in shared memory
|
||||
shm_blocks = []
|
||||
@@ -652,12 +905,31 @@ def _calculate_with_process_pool(
|
||||
log_fn(f"Shared memory setup failed ({e}), using pickle fallback")
|
||||
use_shm = False
|
||||
|
||||
# Store buildings + OSM data in shared memory (pickle once, read many)
|
||||
shared_data_refs = {}
|
||||
if use_shm:
|
||||
bld_block, bld_ref = _store_pickle_in_shm(buildings, "Buildings", log_fn)
|
||||
if bld_block:
|
||||
shm_blocks.append(bld_block)
|
||||
shared_data_refs['buildings'] = bld_ref
|
||||
|
||||
osm_data_dict = {
|
||||
'streets': streets,
|
||||
'water_bodies': water_bodies,
|
||||
'vegetation_areas': vegetation_areas,
|
||||
}
|
||||
osm_block, osm_ref = _store_pickle_in_shm(osm_data_dict, "OSM data", log_fn)
|
||||
if osm_block:
|
||||
shm_blocks.append(osm_block)
|
||||
shared_data_refs['osm_data'] = osm_ref
|
||||
|
||||
items = [
|
||||
(lat, lon, point_elevations.get((lat, lon), 0.0))
|
||||
for lat, lon in grid
|
||||
]
|
||||
|
||||
chunk_size = max(1, len(items) // (num_workers * 2))
|
||||
# Target larger chunks to amortize IPC overhead (was num_workers*2)
|
||||
chunk_size = max(1, min(400, len(items) // max(2, num_workers)))
|
||||
chunks = [items[i:i + chunk_size] for i in range(0, len(items), chunk_size)]
|
||||
log_fn(f"Submitting {len(chunks)} chunks of ~{chunk_size} points")
|
||||
|
||||
@@ -685,8 +957,21 @@ def _calculate_with_process_pool(
|
||||
pool = ProcessPoolExecutor(max_workers=num_workers, mp_context=ctx)
|
||||
_set_active_pool(pool)
|
||||
|
||||
if use_shm:
|
||||
# Shared memory path: pass shm refs instead of terrain data
|
||||
if use_shm and shared_data_refs:
|
||||
# Full shared memory path: terrain + buildings + OSM all via shm
|
||||
worker_fn = _pool_worker_shm_shared
|
||||
futures = {
|
||||
pool.submit(
|
||||
worker_fn,
|
||||
(chunk, terrain_shm_refs, shared_data_refs, config),
|
||||
): i
|
||||
for i, chunk in enumerate(chunks)
|
||||
}
|
||||
elif use_shm and data_items <= 2000:
|
||||
# Terrain-only shm — buildings/OSM pickled per chunk.
|
||||
# Only safe for small datasets; large datasets would OOM from
|
||||
# pickle copies (num_chunks × pickle_size).
|
||||
log_fn(f"Terrain-only shm (small data: {data_items} items)")
|
||||
worker_fn = _pool_worker_shm_chunk
|
||||
futures = {
|
||||
pool.submit(
|
||||
@@ -695,8 +980,9 @@ def _calculate_with_process_pool(
|
||||
): i
|
||||
for i, chunk in enumerate(chunks)
|
||||
}
|
||||
else:
|
||||
# Pickle fallback path
|
||||
elif data_items <= 2000:
|
||||
# Full pickle fallback — only safe for small datasets
|
||||
log_fn(f"Full pickle path (small data: {data_items} items)")
|
||||
futures = {
|
||||
pool.submit(
|
||||
_pool_worker_process_chunk,
|
||||
@@ -704,6 +990,14 @@ def _calculate_with_process_pool(
|
||||
): i
|
||||
for i, chunk in enumerate(chunks)
|
||||
}
|
||||
else:
|
||||
# Large dataset + shared memory failed → per-chunk pickle would OOM.
|
||||
# Bail out; caller will fall back to sequential.
|
||||
log_fn(f"Shared memory failed for large dataset ({data_items} items) "
|
||||
f"— skipping ProcessPool to avoid OOM")
|
||||
raise MemoryError(
|
||||
f"Cannot safely pickle {data_items} OSM items per chunk"
|
||||
)
|
||||
|
||||
completed_chunks = 0
|
||||
for future in as_completed(futures):
|
||||
@@ -730,6 +1024,9 @@ def _calculate_with_process_pool(
|
||||
if progress_fn:
|
||||
progress_fn("Calculating coverage", 0.40 + 0.55 * (completed_chunks / len(chunks)))
|
||||
|
||||
except MemoryError:
|
||||
raise # Propagate to caller for sequential fallback
|
||||
|
||||
except Exception as e:
|
||||
log_fn(f"ProcessPool error: {e}")
|
||||
|
||||
@@ -748,8 +1045,22 @@ def _calculate_with_process_pool(
|
||||
block.unlink()
|
||||
except Exception:
|
||||
pass
|
||||
# Release large local references before GC
|
||||
chunks = None # noqa: F841
|
||||
items = None # noqa: F841
|
||||
osm_data = None # noqa: F841
|
||||
shared_data_refs = None # noqa: F841
|
||||
# Force garbage collection to release memory from workers
|
||||
gc.collect()
|
||||
# Log memory after cleanup
|
||||
try:
|
||||
with open('/proc/self/status') as f:
|
||||
for line in f:
|
||||
if line.startswith('VmRSS:'):
|
||||
log_fn(f"Memory after cleanup: {line.strip()}")
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
calc_time = time.time() - t_calc
|
||||
log_fn(f"ProcessPool done: {calc_time:.1f}s, {len(all_results)} results "
|
||||
@@ -758,7 +1069,11 @@ def _calculate_with_process_pool(
|
||||
timing = {
|
||||
"parallel_total": calc_time,
|
||||
"workers": num_workers,
|
||||
"backend": "process_pool" + ("/shm" if use_shm else "/pickle"),
|
||||
"backend": "process_pool" + (
|
||||
"/shm_full" if (use_shm and shared_data_refs)
|
||||
else "/shm_terrain" if use_shm
|
||||
else "/pickle"
|
||||
),
|
||||
}
|
||||
return all_results, timing
|
||||
|
||||
@@ -791,6 +1106,7 @@ def _calculate_sequential(
|
||||
"los": 0.0, "buildings": 0.0, "antenna": 0.0,
|
||||
"dominant_path": 0.0, "street_canyon": 0.0,
|
||||
"reflection": 0.0, "vegetation": 0.0,
|
||||
"lod_none": 0, "lod_simplified": 0, "lod_full": 0,
|
||||
}
|
||||
|
||||
t0 = time.time()
|
||||
|
||||
Reference in New Issue
Block a user