performance improvements
This commit is contained in:
@@ -33,7 +33,20 @@ part 'singbox_proxy_logs.g.dart';
|
||||
/// is well within budget for a debugging surface.
|
||||
const int _ringBufferCapacity = 2000;
|
||||
|
||||
/// Lower bound between two published snapshots. A chatty proxy can emit lines
|
||||
/// far faster than the screen can render them, and every publication copies the
|
||||
/// whole ring buffer, so coalescing bursts is free in terms of what the user
|
||||
/// actually sees.
|
||||
const _publishInterval = Duration(milliseconds: 100);
|
||||
|
||||
/// Snapshot of buffered log entries. Most-recent-last (chronological).
|
||||
///
|
||||
/// This notifier is `keepAlive` and subscribed from app start (see
|
||||
/// `main.dart`) so startup messages are retained even before any UI mounts.
|
||||
/// Appending and *publishing* are therefore deliberately decoupled: lines
|
||||
/// always land in [_buffer], but a new immutable snapshot is only produced
|
||||
/// while the log screen is on screen ([setLivePublishing]) and at most once
|
||||
/// per [_publishInterval].
|
||||
@Riverpod(keepAlive: true)
|
||||
class SingboxProxyLogs extends _$SingboxProxyLogs {
|
||||
final _buffer = Queue<ProxyLogMessage>();
|
||||
@@ -41,6 +54,28 @@ class SingboxProxyLogs extends _$SingboxProxyLogs {
|
||||
StreamSubscription<SingboxProxyLogMessage>? _singboxSubscription;
|
||||
StreamSubscription<TorLogMessage>? _torSubscription;
|
||||
|
||||
Timer? _publishTimer;
|
||||
bool _livePublishing = false;
|
||||
|
||||
/// Buffer contents materialized on demand, regardless of publication state.
|
||||
/// Lets a freshly mounted screen paint the backlog without waiting for the
|
||||
/// first throttled snapshot.
|
||||
List<ProxyLogMessage> get snapshot => List.unmodifiable(_buffer);
|
||||
|
||||
/// Enables/disables snapshot publication. Called by the log screen as it
|
||||
/// mounts and unmounts; flushes on both edges so the state left behind is
|
||||
/// always complete.
|
||||
void setLivePublishing(bool enabled) {
|
||||
if (_livePublishing == enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
_livePublishing = enabled;
|
||||
_publishTimer?.cancel();
|
||||
_publishTimer = null;
|
||||
_publish();
|
||||
}
|
||||
|
||||
void _append(ProxyLogMessage message) {
|
||||
_buffer.add(message);
|
||||
|
||||
@@ -48,11 +83,27 @@ class SingboxProxyLogs extends _$SingboxProxyLogs {
|
||||
_buffer.removeFirst();
|
||||
}
|
||||
|
||||
state = List.unmodifiable(_buffer);
|
||||
if (!_livePublishing || (_publishTimer?.isActive ?? false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
_publishTimer = Timer(_publishInterval, _publish);
|
||||
}
|
||||
|
||||
void _publish() {
|
||||
_publishTimer = null;
|
||||
|
||||
if (!ref.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
state = snapshot;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
_buffer.clear();
|
||||
_publishTimer?.cancel();
|
||||
_publishTimer = null;
|
||||
state = const [];
|
||||
}
|
||||
|
||||
@@ -69,10 +120,11 @@ class SingboxProxyLogs extends _$SingboxProxyLogs {
|
||||
);
|
||||
|
||||
ref.onDispose(() {
|
||||
_publishTimer?.cancel();
|
||||
unawaited(_singboxSubscription?.cancel());
|
||||
unawaited(_torSubscription?.cancel());
|
||||
});
|
||||
|
||||
return List.unmodifiable(_buffer);
|
||||
return snapshot;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,26 @@ class SingboxProxyLogsScreen extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final logs = ref.watch(singboxProxyLogsProvider);
|
||||
// The ring buffer fills from app start but only publishes snapshots while
|
||||
// this screen is up, so opt in for as long as we're mounted. Seeded from
|
||||
// the notifier's live buffer during the first build, so the backlog is
|
||||
// painted immediately rather than after the first published snapshot.
|
||||
final logsState = useState(
|
||||
useMemoized(() => ref.read(singboxProxyLogsProvider.notifier).snapshot),
|
||||
);
|
||||
|
||||
useEffect(() {
|
||||
final notifier = ref.read(singboxProxyLogsProvider.notifier);
|
||||
notifier.setLivePublishing(true);
|
||||
|
||||
return () => notifier.setLivePublishing(false);
|
||||
}, []);
|
||||
|
||||
ref.listen(singboxProxyLogsProvider, (previous, next) {
|
||||
logsState.value = next;
|
||||
});
|
||||
|
||||
final logs = logsState.value;
|
||||
final filter = useState<String?>(null);
|
||||
final autoScroll = useState(true);
|
||||
final scrollController = useScrollController();
|
||||
@@ -71,9 +90,14 @@ class SingboxProxyLogsScreen extends HookConsumerWidget {
|
||||
return () => scrollController.removeListener(onScroll);
|
||||
}, [scrollController]);
|
||||
|
||||
final filtered = filter.value == null
|
||||
? logs
|
||||
: logs.where((m) => m.level.toLowerCase() == filter.value).toList();
|
||||
// Memoized so scroll-driven rebuilds (autoScroll flipping) don't re-scan
|
||||
// up to 2000 entries; only a new snapshot or a filter change does.
|
||||
final filtered = useMemoized(
|
||||
() => filter.value == null
|
||||
? logs
|
||||
: logs.where((m) => m.level.toLowerCase() == filter.value).toList(),
|
||||
[logs, filter.value],
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
|
||||
Reference in New Issue
Block a user