perf: geocode pipeline — batch POST + parallel server, 200→1 roundtrip
JS preload(): replaced N individual GET /api/geocode (concurrency 2) with a single POST /api/geocode/batch — one HTTP roundtrip instead of up to N/2 sequential ones. schedLoc debounce reduced 600→200ms. Server geocode_batch(): ThreadPoolExecutor(max_workers=4) parallelises Nominatim/reverse_geocoder calls instead of sequential loop. Deduplicates coords before submitting futures. lru_cache absorbs duplicates within batch. Pi geocode_batch_route(): deduplicate coords, removed stale 'if label:' guard (label always non-empty after BUG-13 fix).
This commit is contained in:
@@ -212,6 +212,9 @@ def geocode_batch_route():
|
|||||||
coords = body.get("coords")
|
coords = body.get("coords")
|
||||||
if not isinstance(coords, list) or not coords:
|
if not isinstance(coords, list) or not coords:
|
||||||
return jsonify(error="expected {coords: [{lat, lon}, ...]}"), 400
|
return jsonify(error="expected {coords: [{lat, lon}, ...]}"), 400
|
||||||
|
# Deduplicate — same (lat4,lon4) would hit lru_cache anyway but avoids
|
||||||
|
# submitting redundant work.
|
||||||
|
seen = set()
|
||||||
result = {}
|
result = {}
|
||||||
for item in coords:
|
for item in coords:
|
||||||
try:
|
try:
|
||||||
@@ -221,9 +224,9 @@ def geocode_batch_route():
|
|||||||
continue
|
continue
|
||||||
lat4 = int(round(lat, 4) * 10000)
|
lat4 = int(round(lat, 4) * 10000)
|
||||||
lon4 = int(round(lon, 4) * 10000)
|
lon4 = int(round(lon, 4) * 10000)
|
||||||
label = _geocode_cached(lat4, lon4)
|
if (lat4, lon4) not in seen:
|
||||||
if label:
|
seen.add((lat4, lon4))
|
||||||
result[f"{lat:.3f},{lon:.3f}"] = label
|
result[f"{lat:.3f},{lon:.3f}"] = _geocode_cached(lat4, lon4)
|
||||||
return jsonify(result)
|
return jsonify(result)
|
||||||
|
|
||||||
@app.route('/api/dash/devices')
|
@app.route('/api/dash/devices')
|
||||||
|
|||||||
+30
-5
@@ -2,6 +2,7 @@
|
|||||||
uploaded by the Pi, storing them in Postgres + on-disk file storage.
|
uploaded by the Pi, storing them in Postgres + on-disk file storage.
|
||||||
Also serves the remote web portal for browsing and managing the configured car.
|
Also serves the remote web portal for browsing and managing the configured car.
|
||||||
"""
|
"""
|
||||||
|
import concurrent.futures
|
||||||
import functools
|
import functools
|
||||||
import hashlib
|
import hashlib
|
||||||
import os
|
import os
|
||||||
@@ -626,12 +627,20 @@ def geocode():
|
|||||||
@app.post("/api/geocode/batch")
|
@app.post("/api/geocode/batch")
|
||||||
@require_login
|
@require_login
|
||||||
def geocode_batch():
|
def geocode_batch():
|
||||||
"""Batch geocode — returns {key: label} for all unique coords in one request."""
|
"""Batch geocode — returns {key: label} for all unique coords in one request.
|
||||||
|
|
||||||
|
Parallelised with ThreadPoolExecutor (max_workers=4) so Nominatim HTTP
|
||||||
|
calls and reverse_geocoder lookups run concurrently instead of sequentially.
|
||||||
|
_geocode_cached's lru_cache absorbs duplicate coords within the batch."""
|
||||||
body = request.get_json(silent=True) or {}
|
body = request.get_json(silent=True) or {}
|
||||||
coords = body.get("coords")
|
coords = body.get("coords")
|
||||||
if not isinstance(coords, list) or not coords:
|
if not isinstance(coords, list) or not coords:
|
||||||
return jsonify(error="expected {coords: [{lat, lon}, ...]}"), 400
|
return jsonify(error="expected {coords: [{lat, lon}, ...]}"), 400
|
||||||
result = {}
|
|
||||||
|
# Deduplicate — same (lat4,lon4) would hit lru_cache anyway, but this
|
||||||
|
# avoids submitting redundant futures.
|
||||||
|
seen = set()
|
||||||
|
items = []
|
||||||
for item in coords:
|
for item in coords:
|
||||||
try:
|
try:
|
||||||
lat = float(item["lat"])
|
lat = float(item["lat"])
|
||||||
@@ -640,9 +649,25 @@ def geocode_batch():
|
|||||||
continue
|
continue
|
||||||
lat4 = int(round(lat, 4) * 10000)
|
lat4 = int(round(lat, 4) * 10000)
|
||||||
lon4 = int(round(lon, 4) * 10000)
|
lon4 = int(round(lon, 4) * 10000)
|
||||||
key = f"{lat:.3f},{lon:.3f}"
|
if (lat4, lon4) not in seen:
|
||||||
name = _geocode_cached(lat4, lon4)
|
seen.add((lat4, lon4))
|
||||||
result[key] = name
|
items.append((f"{lat:.3f},{lon:.3f}", lat4, lon4))
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
if not items:
|
||||||
|
return jsonify(result)
|
||||||
|
|
||||||
|
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as ex:
|
||||||
|
futures = {ex.submit(_geocode_cached, lat4, lon4): key for key, lat4, lon4 in items}
|
||||||
|
for future in concurrent.futures.as_completed(futures):
|
||||||
|
key = futures[future]
|
||||||
|
try:
|
||||||
|
result[key] = future.result(timeout=8)
|
||||||
|
except Exception:
|
||||||
|
# Last resort — raw coords if even the fallback failed
|
||||||
|
parts = key.split(",")
|
||||||
|
result[key] = f"{float(parts[0]):.5f}, {float(parts[1]):.5f}"
|
||||||
|
|
||||||
return jsonify(result)
|
return jsonify(result)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+18
-17
@@ -75,31 +75,32 @@
|
|||||||
function preload(pts) {
|
function preload(pts) {
|
||||||
if (_geoCtrl) _geoCtrl.abort();
|
if (_geoCtrl) _geoCtrl.abort();
|
||||||
_geoCtrl = new AbortController(); var sig = _geoCtrl.signal; _cache = {};
|
_geoCtrl = new AbortController(); var sig = _geoCtrl.signal; _cache = {};
|
||||||
var seen = {}, q = [], i;
|
var seen = {}, coords = [], i;
|
||||||
for (i = 0; i < pts.length; i++) {
|
for (i = 0; i < pts.length; i++) {
|
||||||
var k = gk(pts[i].latitude, pts[i].longitude);
|
var k = gk(pts[i].latitude, pts[i].longitude);
|
||||||
if (!k || seen[k]) continue; seen[k] = 1;
|
if (!k || seen[k]) continue; seen[k] = 1;
|
||||||
q.push({k: k, lat: pts[i].latitude, lon: pts[i].longitude});
|
coords.push({lat: pts[i].latitude, lon: pts[i].longitude});
|
||||||
}
|
}
|
||||||
if (!q.length) return;
|
if (!coords.length) return;
|
||||||
var C = 2, idx = 0;
|
// Single batch POST instead of N individual GETs — one roundtrip.
|
||||||
function nx() {
|
fetch("/api/geocode/batch", {
|
||||||
if (idx >= q.length || sig.aborted) return;
|
method: "POST",
|
||||||
var it = q[idx++], c = new AbortController();
|
headers: {"Content-Type": "application/json"},
|
||||||
var t = setTimeout(function () { c.abort(); }, 3000);
|
body: JSON.stringify({coords: coords}),
|
||||||
sig.addEventListener("abort", function () { c.abort(); });
|
signal: sig
|
||||||
fetch("/api/geocode?lat=" + it.lat + "&lon=" + it.lon, {signal: c.signal})
|
})
|
||||||
.then(function (r) { clearTimeout(t); return r.ok ? r.json() : null; })
|
.then(function (r) { return r.ok ? r.json() : {}; })
|
||||||
.then(function (d) { if (!sig.aborted) _cache[it.k] = (d && d.label) || null; })
|
.then(function (data) {
|
||||||
.catch(function () { clearTimeout(t); })
|
if (sig.aborted) return;
|
||||||
.finally(function () { if (!sig.aborted) nx(); });
|
for (var key in data) { if (data.hasOwnProperty(key)) _cache[key] = data[key]; }
|
||||||
}
|
})
|
||||||
for (var j = 0; j < C && j < q.length; j++) nx();
|
.catch(function () {});
|
||||||
}
|
}
|
||||||
function showLoc(lat, lon) {
|
function showLoc(lat, lon) {
|
||||||
var k = gk(lat, lon); if (!k || k === _lastKey) return; _lastKey = k;
|
var k = gk(lat, lon); if (!k || k === _lastKey) return; _lastKey = k;
|
||||||
var fb = lat.toFixed(5) + ", " + lon.toFixed(5);
|
var fb = lat.toFixed(5) + ", " + lon.toFixed(5);
|
||||||
if (_cache.hasOwnProperty(k)) { locEl.textContent = _cache[k] || fb; return; }
|
if (_cache.hasOwnProperty(k)) { locEl.textContent = _cache[k] || fb; return; }
|
||||||
|
// Fallback: individual fetch only if preload didn't cover this coord.
|
||||||
var c = new AbortController();
|
var c = new AbortController();
|
||||||
var t = setTimeout(function () { c.abort(); }, 3000);
|
var t = setTimeout(function () { c.abort(); }, 3000);
|
||||||
fetch("/api/geocode?lat=" + lat + "&lon=" + lon, {signal: c.signal})
|
fetch("/api/geocode?lat=" + lat + "&lon=" + lon, {signal: c.signal})
|
||||||
@@ -112,7 +113,7 @@
|
|||||||
}
|
}
|
||||||
function schedLoc(lat, lon) {
|
function schedLoc(lat, lon) {
|
||||||
clearTimeout(_geoTimer);
|
clearTimeout(_geoTimer);
|
||||||
_geoTimer = setTimeout(function () { showLoc(lat, lon); }, 600);
|
_geoTimer = setTimeout(function () { showLoc(lat, lon); }, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Format ───────────────────────────────────────────────────────────
|
// ── Format ───────────────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user