chasing excess tile fetching and lack of correct cache clearing - NOT WORKING

This commit is contained in:
stopflock
2025-08-23 12:27:04 -05:00
parent f6adffc84e
commit a2bc3309c0
4 changed files with 86 additions and 20 deletions

View File

@@ -9,6 +9,17 @@ import '../network_status.dart';
/// Global semaphore to limit simultaneous tile fetches
final _tileFetchSemaphore = _SimpleSemaphore(4); // Max 4 concurrent
/// Cancellation token to invalidate all pending requests
int _globalCancelToken = 0;
/// Clear queued tile requests and cancel all retries
void clearOSMTileQueue() {
final oldToken = _globalCancelToken;
_globalCancelToken++; // Invalidate all pending requests and retries
final clearedCount = _tileFetchSemaphore.clearQueue();
debugPrint('[OSMTiles] Cancel token: $oldToken -> $_globalCancelToken, cleared $clearedCount queued');
}
/// Fetches a tile from OSM, with in-memory retries/backoff, and global concurrency limit.
/// Returns tile image bytes, or throws on persistent failure.
Future<List<int>> fetchOSMTile({
@@ -25,13 +36,31 @@ Future<List<int>> fetchOSMTile({
kTileFetchSecondDelayMs + random.nextInt(kTileFetchJitter2Ms),
kTileFetchThirdDelayMs + random.nextInt(kTileFetchJitter3Ms),
];
// Remember the cancel token when we start this request
final requestCancelToken = _globalCancelToken;
print('[fetchOSMTile] START $z/$x/$y with token $requestCancelToken (global: $_globalCancelToken)');
while (true) {
// Check if this request was cancelled
if (requestCancelToken != _globalCancelToken) {
print('[fetchOSMTile] CANCELLED $z/$x/$y (token: $requestCancelToken vs $_globalCancelToken)');
throw Exception('Tile request cancelled');
}
await _tileFetchSemaphore.acquire();
try {
print('[fetchOSMTile] FETCH $z/$x/$y');
attempt++;
final resp = await http.get(Uri.parse(url));
print('[fetchOSMTile] HTTP ${resp.statusCode} for $z/$x/$y, length=${resp.bodyBytes.length}');
// Check cancellation after HTTP request completes - this is the key check!
if (requestCancelToken != _globalCancelToken) {
print('[fetchOSMTile] CANCELLED $z/$x/$y after HTTP (token: $requestCancelToken vs $_globalCancelToken)');
throw Exception('Tile request cancelled');
}
if (resp.statusCode == 200 && resp.bodyBytes.isNotEmpty) {
print('[fetchOSMTile] SUCCESS $z/$x/$y');
NetworkStatus.instance.reportOsmTileSuccess();
@@ -42,6 +71,11 @@ Future<List<int>> fetchOSMTile({
throw HttpException('Failed to fetch tile $z/$x/$y: status ${resp.statusCode}');
}
} catch (e) {
// Don't retry cancelled requests
if (e.toString().contains('cancelled')) {
rethrow;
}
print('[fetchOSMTile] Exception $z/$x/$y: $e');
// Report network issues on connection errors
@@ -55,9 +89,22 @@ Future<List<int>> fetchOSMTile({
print("[fetchOSMTile] Failed for $z/$x/$y after $attempt attempts: $e");
rethrow;
}
final delay = delays[attempt - 1].clamp(0, 60000);
print("[fetchOSMTile] Attempt $attempt for $z/$x/$y failed: $e. Retrying in ${delay}ms.");
// Check cancellation before and after delay
if (requestCancelToken != _globalCancelToken) {
print('[fetchOSMTile] CANCELLED $z/$x/$y before retry');
throw Exception('Tile request cancelled');
}
await Future.delayed(Duration(milliseconds: delay));
if (requestCancelToken != _globalCancelToken) {
print('[fetchOSMTile] CANCELLED $z/$x/$y after retry delay');
throw Exception('Tile request cancelled');
}
} finally {
_tileFetchSemaphore.release();
}
@@ -90,4 +137,11 @@ class _SimpleSemaphore {
_current--;
}
}
/// Clear all queued requests (call when view changes significantly)
int clearQueue() {
final clearedCount = _queue.length;
_queue.clear();
return clearedCount;
}
}

View File

@@ -60,25 +60,21 @@ class NetworkStatus extends ChangeNotifier {
/// Report successful operations to potentially clear issues faster
void reportOsmTileSuccess() {
// Don't immediately clear on single success, but reduce recovery time
// Clear issues immediately on success (they were likely temporary)
if (_osmTilesHaveIssues) {
debugPrint('[NetworkStatus] OSM tile server issues cleared after success');
_osmTilesHaveIssues = false;
_osmRecoveryTimer?.cancel();
_osmRecoveryTimer = Timer(const Duration(seconds: 30), () {
_osmTilesHaveIssues = false;
notifyListeners();
debugPrint('[NetworkStatus] OSM tile server issues cleared after success');
});
notifyListeners();
}
}
void reportOverpassSuccess() {
if (_overpassHasIssues) {
debugPrint('[NetworkStatus] Overpass API issues cleared after success');
_overpassHasIssues = false;
_overpassRecoveryTimer?.cancel();
_overpassRecoveryTimer = Timer(const Duration(seconds: 30), () {
_overpassHasIssues = false;
notifyListeners();
debugPrint('[NetworkStatus] Overpass API issues cleared after success');
});
notifyListeners();
}
}

View File

@@ -15,6 +15,7 @@ import 'camera_provider_with_cache.dart';
import 'map/camera_markers.dart';
import 'map/direction_cones.dart';
import 'map/map_overlays.dart';
import 'network_status_indicator.dart';
import '../dev_config.dart';
class MapView extends StatefulWidget {
@@ -95,6 +96,7 @@ class _MapViewState extends State<MapView> {
void _onTilesCached() {
// When new tiles are cached, just trigger a widget rebuild
// This should cause the TileLayer to re-render with cached tiles
debugPrint('[MapView] Tile cached callback triggered, calling setState');
if (mounted) {
setState(() {});
}
@@ -176,12 +178,6 @@ class _MapViewState extends State<MapView> {
return ids1.length == ids2.length && ids1.containsAll(ids2);
}
@override
Widget build(BuildContext context) {
final appState = context.watch<AppState>();
@@ -270,6 +266,11 @@ class _MapViewState extends State<MapView> {
if (session != null) {
appState.updateSession(target: pos.center);
}
// Simple approach: cancel tiles on ANY significant view change
final tileProvider = Provider.of<TileProviderWithCache>(context, listen: false);
tileProvider.cancelAllTileRequests();
// Request more cameras on any map movement/zoom at valid zoom level
// This ensures cameras load even when zooming without panning (like with zoom buttons)
if (pos.zoom >= 10) {
@@ -323,6 +324,9 @@ class _MapViewState extends State<MapView> {
uploadMode: appState.uploadMode,
session: session,
),
// Network status indicator (top-left)
const NetworkStatusIndicator(),
],
);
}

View File

@@ -2,6 +2,7 @@ import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import '../services/map_data_provider.dart';
import '../services/map_data_submodules/tiles_from_osm.dart';
/// In-memory tile cache and async provider for custom tiles.
class TileProviderWithCache extends TileProvider with ChangeNotifier {
@@ -11,7 +12,7 @@ class TileProviderWithCache extends TileProvider with ChangeNotifier {
bool _disposed = false;
int _disposeCount = 0;
VoidCallback? _onTilesCachedCallback;
TileProviderWithCache();
/// Set a callback to be called when tiles are cached (used by MapView for refresh)
@@ -19,6 +20,12 @@ class TileProviderWithCache extends TileProvider with ChangeNotifier {
_onTilesCachedCallback = callback;
}
/// Cancel ALL pending tile requests - delegates to OSM tile fetcher
void cancelAllTileRequests() {
clearOSMTileQueue(); // This handles all the cancellation logic
debugPrint('[TileProviderWithCache] Cancelled all tile requests');
}
@override
void dispose() {
_disposeCount++;
@@ -73,12 +80,17 @@ class TileProviderWithCache extends TileProvider with ChangeNotifier {
if (!_disposed && hasListeners) {
notifyListeners(); // This updates any listening widgets
}
// Trigger map refresh callback to force tile re-rendering
// Trigger map refresh callback to force tile re-rendering
debugPrint('[TileProviderWithCache] Tile cached: $key, calling refresh callback');
_onTilesCachedCallback?.call();
}
// If bytes were empty, don't cache (will re-attempt next time)
} catch (e) {
// Do NOT cache a failed or empty tile! Placeholder tiles will be evicted on online transition.
// Cancelled requests will throw exceptions from fetchOSMTile(), just ignore them
if (e.toString().contains('cancelled')) {
debugPrint('[TileProviderWithCache] Tile request was cancelled: $key');
}
// Don't cache failed tiles regardless of reason
}
}
}