Core Leak Detection & Prevention
FASQ includes built-in leak detection tools to help identify and prevent memory leaks in your Flutter applications. These features are only available in debug mode (kDebugMode) to avoid performance overhead in production builds.
Overview
Section titled “Overview”Memory leaks can occur when Query objects are not properly disposed after use. This can happen when:
- Widgets don’t properly clean up queries on disposal
- Listeners are added but never removed
- Queries are kept alive by circular references
FASQ’s leak detection system provides:
- Debug Instrumentation: Automatic tracking of query creation and reference holders
- Leak Detection Tools: Utilities to identify leaked queries in tests
- Detailed Error Messages: Stack traces showing where leaks originate
Debug Instrumentation
Section titled “Debug Instrumentation”QueryDebugInfo
Section titled “QueryDebugInfo”Every Query instance in debug mode automatically captures:
- Creation Stack Trace: Where the query was created
- Reference Holders: What objects are keeping the query alive (listeners)
This information is exposed through the debugInfo getter:
final query = client.getQuery<String>('user', () async => fetchUser());final debugInfo = query.debugInfo;
if (debugInfo != null) { print('Created at: ${debugInfo.creationStack}'); print('Held by: ${debugInfo.referenceHolders.keys}');}QueryClient.activeQueryDebugInfo
Section titled “QueryClient.activeQueryDebugInfo”The QueryClient provides access to debug information for all active queries:
final client = QueryClient();final debugInfos = client.activeQueryDebugInfo;
for (final info in debugInfos) { print('Query created at: ${info.creationStack}'); print('Held by: ${info.referenceHolders.keys}');}Or get a map of query keys to debug info:
final debugInfoMap = client.activeQueryDebugInfoMap;
for (final entry in debugInfoMap.entries) { print('Query ${entry.key}:'); print(' Created at: ${entry.value.creationStack}'); print(' Held by: ${entry.value.referenceHolders.keys}');}LeakDetector
Section titled “LeakDetector”The LeakDetector class provides utilities for detecting memory leaks in tests.
Basic Usage
Section titled “Basic Usage”import 'package:fasq/fasq.dart';import 'package:fasq/src/testing/leak_detector.dart';import 'package:flutter_test/flutter_test.dart';
void main() { group('My Tests', () { late QueryClient client; late LeakDetector detector;
setUp(() { client = QueryClient(); detector = LeakDetector(); });
tearDown(() async { // Check for leaks after each test detector.expectNoLeakedQueries(client); await QueryClient.resetForTesting(); });
test('my test', () { final query = client.getQuery<String>('test', () async => 'data'); query.addListener();
// ... test code ...
query.removeListener(); // Important: clean up! }); });}expectNoLeakedQueries
Section titled “expectNoLeakedQueries”The primary method for leak detection. It throws a TestFailure if any queries are still active:
final detector = LeakDetector();final client = QueryClient();
// Create and use a queryfinal query = client.getQuery<String>('user', () async => fetchUser());query.addListener();
// ... use the query ...
// Clean upquery.removeListener();
// Verify no leaksdetector.expectNoLeakedQueries(client); // ✅ PassesIf a leak is detected, you’ll get a detailed error message:
Found 1 leaked query(ies):
Query: user──────────────────────────────────────────────────Created at:#0 Query.new (package:fasq/src/core/query.dart:123:45)#1 QueryClient.getQuery (package:fasq/src/core/query_client.dart:282:15)#2 main.<anonymous closure> (test/my_test.dart:15:20)
Held by 1 reference holder(s): - test-widget Stack trace: #0 Query.addListener (package:fasq/src/core/query.dart:340:12) #1 main.<anonymous closure> (test/my_test.dart:16:20) ... (5 more lines)Allowing Specific Queries
Section titled “Allowing Specific Queries”Sometimes you intentionally want to keep queries alive across tests. Use allowedLeakKeys:
final persistentQuery = client.getQuery<String>( 'persistent-data', () async => fetchPersistentData(),);
detector.expectNoLeakedQueries( client, allowedLeakKeys: {'persistent-data'}, // This query is allowed);Tracking Objects for Garbage Collection
Section titled “Tracking Objects for Garbage Collection”For advanced use cases, you can track arbitrary objects to verify they’re garbage collected:
final detector = LeakDetector();final query = client.getQuery<String>('test', () async => 'data');
// Track the query for GCdetector.trackForGc(query, debugLabel: 'test-query');
// Use and dispose the queryquery.addListener();query.removeListener();query.dispose();
// Make query unreachablequery = null;
// Wait for GC and verifyfinal allGc = await detector.verifyAllTrackedObjectsGc();expect(allGc, isTrue);Integration with Widget Tests
Section titled “Integration with Widget Tests”For widget tests, ensure queries are properly disposed after widget teardown:
testWidgets('widget test with leak detection', (tester) async { final detector = LeakDetector(); final client = QueryClient();
await tester.pumpWidget( MaterialApp( home: QueryBuilder<String>( queryKey: 'test'.toQueryKey(), queryFn: () async => 'data', builder: (context, state) => Text(state.data ?? 'loading'), ), ), );
await tester.pumpAndSettle();
// Remove the widget await tester.pumpWidget(Container());
// Verify no leaks detector.expectNoLeakedQueries(client);});Best Practices
Section titled “Best Practices”1. Always Clean Up in Tests
Section titled “1. Always Clean Up in Tests”test('my test', () { final query = client.getQuery<String>('test', () async => 'data'); query.addListener();
// ... test code ...
// Always clean up! query.removeListener(); query.dispose(); // Or let QueryClient handle it});2. Use tearDown for Automatic Checking
Section titled “2. Use tearDown for Automatic Checking”tearDown(() { detector.expectNoLeakedQueries(client);});3. Check After Widget Teardown
Section titled “3. Check After Widget Teardown”testWidgets('widget test', (tester) async { // ... test code ...
await tester.pumpWidget(Container()); // Remove widget
detector.expectNoLeakedQueries(client); // Check after teardown});4. Use Allowed Leaks Sparingly
Section titled “4. Use Allowed Leaks Sparingly”Only use allowedLeakKeys when you have a legitimate reason to keep queries alive:
// Good: Persistent cache that should survive testsdetector.expectNoLeakedQueries( client, allowedLeakKeys: {'persistent-cache'},);
// Bad: Using allowedLeakKeys to hide actual leaksdetector.expectNoLeakedQueries( client, allowedLeakKeys: {'leaked-query'}, // Fix the leak instead!);Debug Mode Only
Section titled “Debug Mode Only”All leak detection features are only available in debug mode. In release builds:
Query.debugInforeturnsnullQueryClient.activeQueryDebugInforeturns an empty iterableLeakDetectormethods still work but won’t have debug information
This ensures zero performance overhead in production.
Troubleshooting
Section titled “Troubleshooting””Found X leaked query(ies)” Error
Section titled “”Found X leaked query(ies)” Error”If you see this error:
- Check the creation stack trace: This shows where the query was created
- Check the reference holders: This shows what’s keeping the query alive
- Ensure proper cleanup: Make sure you’re calling
removeListener()and/ordispose()
Queries Not Being Disposed
Section titled “Queries Not Being Disposed”If queries aren’t being disposed even after removing listeners:
- Check disposal delay: Queries are disposed after a delay (default: 5 seconds) when reference count reaches zero
- Use
query.dispose(): For immediate disposal in tests - Use
client.removeQuery(): To force removal from the registry
False Positives
Section titled “False Positives”If you’re getting false positives:
- Wait for disposal delay: Queries aren’t disposed immediately
- Check for active listeners: Make sure all listeners are removed
- Use
allowedLeakKeys: For queries that should remain active
API Reference
Section titled “API Reference”QueryDebugInfo
Section titled “QueryDebugInfo”class QueryDebugInfo { final StackTrace? creationStack; final Map<Object, StackTrace> referenceHolders;}LeakDetector
Section titled “LeakDetector”class LeakDetector { // Track an object for garbage collection void trackForGc(Object object, {String? debugLabel});
// Verify all tracked objects are GC'd Future<bool> verifyAllTrackedObjectsGc({Duration timeout});
// Get list of leaked objects List<String> getLeakedObjects();
// Clear tracking information void clearTracking();
// Assert no queries are leaked void expectNoLeakedQueries( QueryClient client, { Set<String>? allowedLeakKeys, });}QueryClient
Section titled “QueryClient”// Get debug info for all active queriesIterable<QueryDebugInfo> get activeQueryDebugInfo;
// Get map of query keys to debug infoMap<String, QueryDebugInfo> get activeQueryDebugInfoMap;