feat(talker_flutter): Performance optimizations and pause feature for TalkerView#488
feat(talker_flutter): Performance optimizations and pause feature for TalkerView#488jan-siroky wants to merge 1 commit intoFrezyx:masterfrom
Conversation
… TalkerView Performance improvements: - Replace StreamBuilder with throttled stream subscription (500ms trailing-edge) to prevent excessive widget rebuilds during high-frequency logging - Add cached filtering with change detection — only recompute filtered logs when source data or filter state actually changes - Add configurable maxDisplayedLogs parameter (default 500) to cap rendered entries and prevent UI freezes with large log histories - Replace ObjectKey with ValueKey(index) for faster widget element diffing - Wrap each log card in RepaintBoundary to isolate repaints - Remove redundant TalkerBuilder from MonitorButton — reuse the keys list already computed by the parent instead of scanning full history again - Add debounced search (300ms) in TalkerViewController to avoid rebuilds on every keystroke New feature — Pause/Resume: - Add pause button to TalkerView app bar (left of monitor icon) - When paused, freezes current log snapshot and stops processing new events - When resumed, returns to live log stream with filter state preserved - Pause state managed in TalkerViewController for external control All changes are backward compatible — new parameters are optional with defaults matching current behavior.
Reviewer's GuideRefactors TalkerView/TalkerScreen to significantly reduce rebuilds and filtering work via a throttled TalkerBuilder, cached filtering with a configurable maxDisplayedLogs cap, UI key/repaint optimizations, and introduces a pause/resume feature with debounced search and a cheaper monitor button implementation. Sequence diagram for throttled TalkerBuilder updatessequenceDiagram
participant Talker
participant TalkerBuilder
participant _TalkerBuilderState as TalkerBuilderState
participant FlutterFramework as Flutter
TalkerBuilder->>TalkerBuilderState: initState()
TalkerBuilderState->>Talker: stream.listen(callback)
loop HighFrequencyLogging
Talker-->>TalkerBuilderState: onLogEvent(TalkerData)
TalkerBuilderState->>TalkerBuilderState: _throttleTimer.cancel()
TalkerBuilderState->>TalkerBuilderState: _throttleTimer = Timer(500ms)
end
Note over TalkerBuilderState,Flutter: Only the last timer after the burst will fire
TalkerBuilderState-->>Flutter: setState() after 500ms
Flutter->>TalkerBuilderState: build(context)
TalkerBuilderState->>TalkerBuilder: widget.builder(context, talker.history)
TalkerBuilder-->>Flutter: Widget subtree rebuilt using latest history
Sequence diagram for pause and resume in TalkerViewsequenceDiagram
actor User
participant AppBar as TalkerViewAppBar
participant Controller as TalkerViewController
participant View as TalkerViewState
participant Builder as TalkerBuilder
User->>AppBar: tap pause button
AppBar->>Controller: onPauseTap() -> togglePause()
alt currently running
Controller->>Controller: _frozenLogs = List.from(_talker.history)
Controller->>Controller: _isPaused = true
else currently paused
Controller->>Controller: _frozenLogs = []
Controller->>Controller: _isPaused = false
end
Controller-->>View: notifyListeners()
View->>View: AnimatedBuilder rebuild
alt isPaused == true
View->>View: _buildLogList(frozenLogs, theme)
View-->>Builder: TalkerBuilder not built
else isPaused == false
View->>Builder: build with talker.history via TalkerBuilder
end
View->>AppBar: rebuild with
AppBar->>AppBar: update icon(color, play/pause)
AppBar->>AppBar: title includes (Paused) when isPaused == true
Class diagram for updated TalkerView and TalkerScreen structureclassDiagram
class Talker {
List~TalkerData~ history
Stream~TalkerData~ stream
}
class TalkerData {
String? key
Color getFlutterColor(TalkerScreenTheme theme)
}
class TalkerFilter {
List~String~ enabledKeys
String searchQuery
TalkerFilter copyWith(List~String~ enabledKeys, String searchQuery)
bool filter(TalkerData data)
}
class TalkerScreenTheme {
}
class TalkerViewController {
-Talker _talker
-TalkerFilter _uiFilter
-bool _expandedLogs
-bool _isLogOrderReversed
-bool _isPaused
-List~TalkerData~ _frozenLogs
-Timer? _searchDebounceTimer
+static Duration _searchDebounceDuration
+TalkerFilter get filter()
+set filter(TalkerFilter val)
+bool get expandedLogs()
+set expandedLogs(bool val)
+bool get isLogOrderReversed()
+set isLogOrderReversed(bool val)
+bool get isPaused()
+List~TalkerData~ get frozenLogs()
+void togglePause()
+void updateFilterSearchQuery(String query)
+void addFilterKey(String key)
+void removeFilterKey(String key)
+void setFilterKeys(List~String~ keys)
+void update()
+void dispose()
}
class TalkerBuilder {
+Talker talker
+TalkerWidgetBuilder builder
+createState() _TalkerBuilderState
}
class _TalkerBuilderState {
-StreamSubscription~TalkerData~? _subscription
-Timer? _throttleTimer
+static Duration _throttleDuration
+void initState()
+void didUpdateWidget(TalkerBuilder oldWidget)
+void dispose()
-void _subscribe()
-void _unsubscribe()
+Widget build(BuildContext context)
}
class TalkerView {
+Talker talker
+TalkerScreenTheme theme
+ScrollController? scrollController
+List~Widget~ customActions
+List~Widget~ customSettings
+bool isLogsExpanded
+bool isLogOrderReversed
+int maxDisplayedLogs
+createState() _TalkerViewState
}
class _TalkerViewState {
-TalkerViewController _controller
-List~TalkerData~ _cachedFiltered
-int _lastSourceLength
-int _lastFilterHash
+int get _filterHash
-List~TalkerData~ _getFilteredLogs(List~TalkerData~ source)
-Widget _buildLogList(List~TalkerData~ data, TalkerScreenTheme talkerTheme)
+Widget build(BuildContext context)
-void _onToggleKey(String key, bool selected)
-void _openSettings(BuildContext context, TalkerScreenTheme theme)
-void _cleanHistory()
-void _copyFilteredLogs(BuildContext context)
}
class TalkerScreen {
+Talker talker
+TalkerScreenTheme theme
+List~Widget~ customActions
+List~Widget~ customSettings
+bool isLogsExpanded
+bool isLogOrderReversed
+int maxDisplayedLogs
+Widget build(BuildContext context)
}
class TalkerViewAppBar {
+String? title
+Widget? leading
+Talker talker
+TalkerScreenTheme talkerTheme
+TalkerViewController controller
+List~String?~ keys
+List~String?~ uniqKeys
+VoidCallback onMonitorTap
+VoidCallback onSettingsTap
+VoidCallback onActionsTap
+VoidCallback onPauseTap
+bool isPaused
}
class _MonitorButton {
+Talker talker
+TalkerScreenTheme talkerTheme
+VoidCallback onPressed
+List~String?~ keys
+Widget build(BuildContext context)
}
TalkerBuilder --> Talker : uses
_TalkerBuilderState --> TalkerBuilder : stateOf
_TalkerBuilderState --> Talker : subscribesTo
TalkerView --> TalkerViewController : creates
_TalkerViewState --> TalkerViewController : uses
_TalkerViewState --> TalkerBuilder : buildsWith
_TalkerViewState --> TalkerViewAppBar : builds
TalkerScreen --> TalkerView : composes
TalkerViewAppBar --> TalkerViewController : controls
TalkerViewAppBar --> _MonitorButton : has
_MonitorButton --> Talker : readsHistoryViaKeys
TalkerViewController --> TalkerFilter : owns
TalkerViewController --> Talker : readsHistory
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 2 issues, and left some high level feedback:
- Consider making the
_throttleDurationinTalkerBuilderconfigurable via the constructor so callers can tune responsiveness vs. rebuild frequency for different use cases. - The
_filterHashcomputation inTalkerViewStatecurrently relies onenabledKeys.hashCode, which is identity-based for lists; using a content-based hash (e.g.,Object.hashAlloverenabledKeys) would make the cache invalidation more robust when the enabled key set changes.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- Consider making the `_throttleDuration` in `TalkerBuilder` configurable via the constructor so callers can tune responsiveness vs. rebuild frequency for different use cases.
- The `_filterHash` computation in `TalkerViewState` currently relies on `enabledKeys.hashCode`, which is identity-based for lists; using a content-based hash (e.g., `Object.hashAll` over `enabledKeys`) would make the cache invalidation more robust when the enabled key set changes.
## Individual Comments
### Comment 1
<location path="packages/talker_flutter/lib/src/ui/talker_view.dart" line_range="319" />
<code_context>
Clipboard.setData(ClipboardData(
- text: _getFilteredLogs(widget.talker.history)
- .text(timeFormat: widget.talker.settings.timeFormat)));
+ text: _cachedFiltered.text(
+ timeFormat: widget.talker.settings.timeFormat)));
_showSnackBar(context, 'All filtered logs copied in buffer');
</code_context>
<issue_to_address>
**question (bug_risk):** Copying filtered logs now respects the display cap, which may unintentionally truncate exported logs.
By switching from `_getFilteredLogs(widget.talker.history)` to `_cachedFiltered`, the export now uses the capped list (`maxDisplayedLogs`) instead of the full history. This means copied logs may be limited to the last N entries. If the goal is to export everything matching the filter, consider using an uncapped filter for export or providing a separate “copy visible” vs “copy all” option.
</issue_to_address>
### Comment 2
<location path="packages/talker_flutter/lib/src/ui/widgets/talker_view_appbar.dart" line_range="306-307" />
<code_context>
- color: talkerTheme.textColor,
- ),
- ),
+ final haveErrors = keys.any(
+ (k) => k == TalkerKey.error || k == TalkerKey.exception,
+ );
+ return Stack(
</code_context>
<issue_to_address>
**question (bug_risk):** Monitor error indicator now depends on keys of only the capped subset and on key naming, which may change semantics.
Previously `_MonitorButton` scanned the full `data` list for `TalkerError` / `TalkerException`. Now it infers `haveErrors` from `keys` built from the capped list in `_buildLogList`. As a result: (1) errors older than `maxDisplayedLogs` no longer affect the indicator, and (2) custom error/exception types without `TalkerKey.error` / `TalkerKey.exception` won’t trigger it. If this change isn’t deliberate, consider checking errors on the full data set or relaxing the condition used for the indicator.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| Clipboard.setData(ClipboardData( | ||
| text: _getFilteredLogs(widget.talker.history) | ||
| .text(timeFormat: widget.talker.settings.timeFormat))); | ||
| text: _cachedFiltered.text( |
There was a problem hiding this comment.
question (bug_risk): Copying filtered logs now respects the display cap, which may unintentionally truncate exported logs.
By switching from _getFilteredLogs(widget.talker.history) to _cachedFiltered, the export now uses the capped list (maxDisplayedLogs) instead of the full history. This means copied logs may be limited to the last N entries. If the goal is to export everything matching the filter, consider using an uncapped filter for export or providing a separate “copy visible” vs “copy all” option.
| final haveErrors = keys.any( | ||
| (k) => k == TalkerKey.error || k == TalkerKey.exception, |
There was a problem hiding this comment.
question (bug_risk): Monitor error indicator now depends on keys of only the capped subset and on key naming, which may change semantics.
Previously _MonitorButton scanned the full data list for TalkerError / TalkerException. Now it infers haveErrors from keys built from the capped list in _buildLogList. As a result: (1) errors older than maxDisplayedLogs no longer affect the indicator, and (2) custom error/exception types without TalkerKey.error / TalkerKey.exception won’t trigger it. If this change isn’t deliberate, consider checking errors on the full data set or relaxing the condition used for the indicator.
Summary
This PR addresses significant performance issues in
TalkerView/TalkerScreenthat cause UI freezes when log history grows large, and adds a built-in pause/resume feature.Performance Improvements
1. Throttled stream subscription replaces StreamBuilder (
talker_builder.dart)TalkerBuilderwas aStatelessWidgetwrapping a rawStreamBuilder, which rebuilds on every single log event — no throttling at allStatefulWidgetthat uses a trailing-edge throttle (500ms window): during high-frequency logging bursts, at most one rebuild fires after the burst settles2. Cached filtering with change detection (
talker_view.dart)_getFilteredLogs()iterated the entire log history and created a new filtered list on every single build3. Configurable
maxDisplayedLogsparameter (talker_view.dart,talker_screen.dart)0to disable the cap (not recommended for large histories)4.
ValueKey(index)replaces missing keys (talker_view.dart)ValueKey(index)enables fast integer-based element matching within each filtered snapshot5.
RepaintBoundaryper log card (talker_view.dart)TalkerDataCard(and customitemsBuilderoutput) is wrapped inRepaintBoundary6. Monitor button no longer spawns its own
TalkerBuilder(talker_view_appbar.dart)_MonitorButtonpreviously created a secondTalkerBuilderthat scanned the full history on every event just to check for errorskeyslist from the parent and checks for error/exception keys directly — zero additional overhead7. Debounced search (
talker_view_controller.dart)updateFilterSearchQuery()now debounces with a 300ms timerNew Feature: Pause / Resume
TalkerViewapp bar (left of the monitor icon)TalkerBuilderstream events are skippedTalkerViewController(isPaused,frozenLogs,togglePause()) so it can also be controlled externallyPerformance Impact
ValueKey(index)RepaintBoundaryper cardBackward Compatibility
All changes are fully backward compatible:
maxDisplayedLogsdefaults to500(same visual result for most apps, dramatically better performance)TalkerBuilderAPI unchanged (constructor signature identical)TalkerViewControllernew members (isPaused,frozenLogs,togglePause()) are additiveFiles Changed
packages/talker_flutter/lib/src/ui/talker_builder.dart— StreamBuilder → throttled StatefulWidgetpackages/talker_flutter/lib/src/ui/talker_view.dart— cached filtering, RepaintBoundary, ValueKey, maxDisplayedLogs, pause supportpackages/talker_flutter/lib/src/ui/talker_screen.dart— pass through maxDisplayedLogspackages/talker_flutter/lib/src/ui/widgets/talker_view_appbar.dart— pause button, optimized MonitorButtonpackages/talker_flutter/lib/src/controller/talker_view_controller.dart— pause state, debounced search, dispose cleanupSummary by Sourcery
Optimize TalkerView/TalkerScreen log rendering performance and add a pause/resume capability for the live log stream.
New Features:
Enhancements: