Skip to content

Commit

Permalink
memory leak tracking & fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
deckerst committed Oct 19, 2023
1 parent 300d232 commit 4c07a9d
Show file tree
Hide file tree
Showing 44 changed files with 270 additions and 45 deletions.
6 changes: 6 additions & 0 deletions lib/main_common.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import 'dart:isolate';
import 'package:aves/app_flavor.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/widgets/aves_app.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:leak_tracker/leak_tracker.dart';

void mainCommon(AppFlavor flavor, {Map? debugIntentData}) {
// HttpClient.enableTimelineLogging = true; // enable network traffic logging
Expand Down Expand Up @@ -35,5 +37,9 @@ void mainCommon(AppFlavor flavor, {Map? debugIntentData}) {
// ErrorWidget.builder = (details) => ErrorWidget(details.exception);
// cf https://docs.flutter.dev/testing/errors

LeakTracking.start();
MemoryAllocations.instance.addListener(
(event) => LeakTracking.dispatchObjectEvent(event.toMap()),
);
runApp(AvesApp(flavor: flavor, debugIntentData: debugIntentData));
}
5 changes: 5 additions & 0 deletions lib/model/app/dependencies.dart
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,11 @@ class Dependencies {
licenseUrl: 'https://github.com/material-foundation/material-color-utilities/tree/main/dart/LICENSE',
sourceUrl: 'https://github.com/material-foundation/material-color-utilities/tree/main/dart',
),
Dependency(
name: 'Memory Leak Tracker',
license: bsd3,
sourceUrl: 'https://github.com/dart-lang/leak_tracker',
),
Dependency(
name: 'Path',
license: bsd3,
Expand Down
13 changes: 11 additions & 2 deletions lib/model/entry/entry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,7 @@ class AvesEntry with AvesEntryBase {
@override
final AChangeNotifier visualChangeNotifier = AChangeNotifier();

final AChangeNotifier metadataChangeNotifier = AChangeNotifier();
final AChangeNotifier addressChangeNotifier = AChangeNotifier();
final AChangeNotifier metadataChangeNotifier = AChangeNotifier(), addressChangeNotifier = AChangeNotifier();

AvesEntry({
required int? id,
Expand All @@ -72,6 +71,13 @@ class AvesEntry with AvesEntryBase {
required this.origin,
this.burstEntries,
}) : id = id ?? 0 {
if (kFlutterMemoryAllocationsEnabled) {
MemoryAllocations.instance.dispatchObjectCreated(
library: 'aves',
className: '$AvesEntry',
object: this,
);
}
this.path = path;
this.sourceTitle = sourceTitle;
this.dateModifiedSecs = dateModifiedSecs;
Expand Down Expand Up @@ -181,6 +187,9 @@ class AvesEntry with AvesEntryBase {
}

void dispose() {
if (kFlutterMemoryAllocationsEnabled) {
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
}
visualChangeNotifier.dispose();
metadataChangeNotifier.dispose();
addressChangeNotifier.dispose();
Expand Down
6 changes: 6 additions & 0 deletions lib/model/query.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ class Query extends ChangeNotifier {
}
}

@override
void dispose() {
_focusRequestNotifier.dispose();
super.dispose();
}

bool _enabled = false;

bool get enabled => _enabled;
Expand Down
2 changes: 2 additions & 0 deletions lib/model/source/collection_lens.dart
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ class CollectionLens with ChangeNotifier {
..forEach((sub) => sub.cancel())
..clear();
favourites.removeListener(_onFavouritesChanged);
filterChangeNotifier.dispose();
sortSectionChangeNotifier.dispose();
super.dispose();
}

Expand Down
11 changes: 11 additions & 0 deletions lib/services/analysis_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'package:aves/services/common/services.dart';
import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/view/view.dart';
import 'package:aves_model/aves_model.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';

Expand Down Expand Up @@ -92,12 +93,22 @@ class Analyzer {

Analyzer() {
debugPrint('$runtimeType create');
if (kFlutterMemoryAllocationsEnabled) {
MemoryAllocations.instance.dispatchObjectCreated(
library: 'aves',
className: '$Analyzer',
object: this,
);
}
_serviceStateNotifier.addListener(_onServiceStateChanged);
_source.stateNotifier.addListener(_onSourceStateChanged);
}

void dispose() {
debugPrint('$runtimeType dispose');
if (kFlutterMemoryAllocationsEnabled) {
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
}
_serviceStateNotifier.removeListener(_onServiceStateChanged);
_source.stateNotifier.removeListener(_onSourceStateChanged);
_stopUpdateTimer();
Expand Down
6 changes: 6 additions & 0 deletions lib/widgets/about/licenses.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ class _LicensesState extends State<Licenses> {
_sortPackages();
}

@override
void dispose() {
_expandedNotifier.dispose();
super.dispose();
}

void _sortPackages() {
int compare(Dependency a, Dependency b) => compareAsciiUpperCase(a.name, b.name);
_platform.sort(compare);
Expand Down
1 change: 0 additions & 1 deletion lib/widgets/collection/entry_set_action_delegate.dart
Original file line number Diff line number Diff line change
Expand Up @@ -644,7 +644,6 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
builder: (context) => MapPage(collection: mapCollection),
),
);
mapCollection.dispose();
}

void _goToSlideshow(BuildContext context) {
Expand Down
13 changes: 7 additions & 6 deletions lib/widgets/collection/grid/list_details_theme.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,20 +32,21 @@ class EntryListDetailsTheme extends StatelessWidget {
final titleStyle = textTheme.bodyMedium!;
final captionStyle = textTheme.bodySmall!;

final titleLineHeight = (RenderParagraph(
final titleLineHeightParagraph = RenderParagraph(
TextSpan(text: 'Fake Title', style: titleStyle),
textDirection: TextDirection.ltr,
textScaleFactor: textScaleFactor,
)..layout(const BoxConstraints(), parentUsesSize: true))
.getMaxIntrinsicHeight(double.infinity);
)..layout(const BoxConstraints(), parentUsesSize: true);
final titleLineHeight = titleLineHeightParagraph.getMaxIntrinsicHeight(double.infinity);
titleLineHeightParagraph.dispose();

final captionLineHeight = (RenderParagraph(
final captionLineHeightParagraph = RenderParagraph(
TextSpan(text: formatDateTime(DateTime.now(), locale, use24hour), style: captionStyle),
textDirection: TextDirection.ltr,
textScaleFactor: textScaleFactor,
strutStyle: AStyles.overflowStrut,
)..layout(const BoxConstraints(), parentUsesSize: true))
.getMaxIntrinsicHeight(double.infinity);
)..layout(const BoxConstraints(), parentUsesSize: true);
final captionLineHeight = captionLineHeightParagraph.getMaxIntrinsicHeight(double.infinity);

var titleMaxLines = 1;
var showDate = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ abstract class ChooserQuickButtonState<T extends ChooserQuickButton<U>, U> exten
void dispose() {
_animationController?.dispose();
_clearChooserOverlayEntry();
_chooserValueNotifier.dispose();
super.dispose();
}

Expand Down
7 changes: 4 additions & 3 deletions lib/widgets/common/basic/text/animated_diff.dart
Original file line number Diff line number Diff line change
Expand Up @@ -114,14 +114,15 @@ class _AnimatedDiffTextState extends State<AnimatedDiffText> with SingleTickerPr
}

Size textSize(String text) {
final para = RenderParagraph(
final paragraph = RenderParagraph(
TextSpan(text: text, style: widget.textStyle),
textDirection: Directionality.of(context),
textScaleFactor: MediaQuery.textScaleFactorOf(context),
strutStyle: widget.strutStyle,
)..layout(const BoxConstraints(), parentUsesSize: true);
final width = para.getMaxIntrinsicWidth(double.infinity);
final height = para.getMaxIntrinsicHeight(double.infinity);
final width = paragraph.getMaxIntrinsicWidth(double.infinity);
final height = paragraph.getMaxIntrinsicHeight(double.infinity);
paragraph.dispose();
return Size(width, height);
}

Expand Down
1 change: 1 addition & 0 deletions lib/widgets/common/basic/text/background_painter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class TextBackgroundPainter extends StatelessWidget {
TextSelection(baseOffset: 0, extentOffset: textLength),
boxHeightStyle: ui.BoxHeightStyle.max,
);
paragraph.dispose();

// merge boxes to avoid artifacts at box edges, from anti-aliasing and rounding hacks
final lineRects = groupBy<TextBox, double>(allBoxes, (v) => v.top).entries.map((kv) {
Expand Down
14 changes: 14 additions & 0 deletions lib/widgets/common/behaviour/pop/double_back.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,28 @@ import 'dart:async';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:overlay_support/overlay_support.dart';

class DoubleBackPopHandler {
bool _backOnce = false;
Timer? _backTimer;

DoubleBackPopHandler() {
if (kFlutterMemoryAllocationsEnabled) {
MemoryAllocations.instance.dispatchObjectCreated(
library: 'aves',
className: '$DoubleBackPopHandler',
object: this,
);
}
}

void dispose() {
if (kFlutterMemoryAllocationsEnabled) {
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
}
_stopBackTimer();
}

Expand Down
6 changes: 4 additions & 2 deletions lib/widgets/common/grid/header.dart
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ class SectionHeader<T> extends StatelessWidget {
}) {
final textScaleFactor = MediaQuery.textScaleFactorOf(context);
final maxContentWidth = maxWidth - (SectionHeader.padding.horizontal + SectionHeader.margin.horizontal);
final para = RenderParagraph(
final paragraph = RenderParagraph(
TextSpan(
children: [
// as of Flutter v3.7.7, `RenderParagraph` fails to lay out `WidgetSpan` offscreen
Expand All @@ -148,7 +148,9 @@ class SectionHeader<T> extends StatelessWidget {
textDirection: TextDirection.ltr,
textScaleFactor: textScaleFactor,
)..layout(BoxConstraints(maxWidth: maxContentWidth), parentUsesSize: true);
return para.getMaxIntrinsicHeight(maxContentWidth);
final height = paragraph.getMaxIntrinsicHeight(maxContentWidth);
paragraph.dispose();
return height;
}
}

Expand Down
5 changes: 3 additions & 2 deletions lib/widgets/common/identity/buttons/captioned_button.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,14 @@ class CaptionedButton extends StatefulWidget {
final width = getWidth(context);
var height = width;
if (showCaption) {
final para = RenderParagraph(
final paragraph = RenderParagraph(
TextSpan(text: text, style: CaptionedButtonText.textStyle(context)),
textDirection: TextDirection.ltr,
textScaleFactor: MediaQuery.textScaleFactorOf(context),
maxLines: CaptionedButtonText.maxLines,
)..layout(const BoxConstraints(), parentUsesSize: true);
height += para.getMaxIntrinsicHeight(width) + padding.vertical;
height += paragraph.getMaxIntrinsicHeight(width) + padding.vertical;
paragraph.dispose();
}
return Size(width, height);
}
Expand Down
1 change: 1 addition & 0 deletions lib/widgets/common/map/geo_map.dart
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ class _GeoMapState extends State<GeoMap> {

@override
void dispose() {
_clusterChangeNotifier.dispose();
_unregisterWidget(widget);
super.dispose();
}
Expand Down
14 changes: 13 additions & 1 deletion lib/widgets/common/search/delegate.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:aves/model/settings/settings.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/search/route.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

Expand All @@ -15,11 +16,22 @@ abstract class AvesSearchDelegate extends SearchDelegate {
String? initialQuery,
required super.searchFieldLabel,
}) {
if (kFlutterMemoryAllocationsEnabled) {
MemoryAllocations.instance.dispatchObjectCreated(
library: 'aves',
className: '$AvesSearchDelegate',
object: this,
);
}
query = initialQuery ?? '';
}

@mustCallSuper
void dispose() {}
void dispose() {
if (kFlutterMemoryAllocationsEnabled) {
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
}
}

@override
Widget? buildLeading(BuildContext context) {
Expand Down
1 change: 1 addition & 0 deletions lib/widgets/common/thumbnail/image.dart
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ class _ThumbnailImageState extends State<ThumbnailImage> {
}

void _registerWidget(ThumbnailImage widget) {
// TODO TLAD [leak] `widget.entry.visualChangeNotifier`
widget.entry.visualChangeNotifier.addListener(_onVisualChanged);
_initProvider();
}
Expand Down
11 changes: 11 additions & 0 deletions lib/widgets/common/tile_extent_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:math';

import 'package:aves/model/settings/settings.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart';

Expand All @@ -26,13 +27,23 @@ class TileExtentController {
required this.spacing,
required this.horizontalPadding,
}) {
if (kFlutterMemoryAllocationsEnabled) {
MemoryAllocations.instance.dispatchObjectCreated(
library: 'aves',
className: '$TileExtentController',
object: this,
);
}
// initialize extent to 0, so that it will be dynamically sized on first launch
extentNotifier = ValueNotifier(0);
userPreferredExtent = settings.getTileExtent(settingsRouteKey);
_subscriptions.add(settings.updateTileExtentStream.listen((_) => _onSettingsChanged()));
}

void dispose() {
if (kFlutterMemoryAllocationsEnabled) {
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
}
_subscriptions
..forEach((sub) => sub.cancel())
..clear();
Expand Down
19 changes: 19 additions & 0 deletions lib/widgets/debug/app_debug_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@ import 'package:aves/widgets/debug/report.dart';
import 'package:aves/widgets/debug/settings.dart';
import 'package:aves/widgets/debug/storage.dart';
import 'package:aves/widgets/viewer/info/common.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:provider/provider.dart';
import 'package:leak_tracker/leak_tracker.dart';

class AppDebugPage extends StatefulWidget {
static const routeName = '/debug';
Expand Down Expand Up @@ -133,6 +135,23 @@ class _AppDebugPageState extends State<AppDebugPage> {
},
title: const Text('Show tasks overlay'),
),
ElevatedButton(
onPressed: () => LeakTracking.collectLeaks().then((leaks) {
leaks.byType.forEach((type, reports) {
debugPrint('* leak type=$type');
groupBy(reports, (report) => report.type).forEach((reportType, typedReports) {
debugPrint(' * report type=$reportType');
groupBy(typedReports, (report) => report.trackedClass).forEach((trackedClass, classedReports) {
debugPrint(' trackedClass=$trackedClass reports=${classedReports.length}');
// classedReports.forEach((report) {
// debugPrint(' phase=${report.phase} retainingPath=${report.retainingPath} detailedPath=${report.detailedPath} context=${report.context}');
// });
});
});
});
}),
child: const Text('Collect leaks'),
),
ElevatedButton(
onPressed: () => source.init(loadTopEntriesFirst: false),
child: const Text('Source refresh (top off)'),
Expand Down
Loading

0 comments on commit 4c07a9d

Please sign in to comment.