fix: memory leaks (#3995)

* feat: add memory leak monitor

* fix: memory leaks

* feat: dump call stack in memory leak detector

* chore: disable memory leak detector
This commit is contained in:
Lucas.Xu
2023-11-26 15:10:48 +08:00
committed by GitHub
parent 7fb1b4f43f
commit 79b1515c3a
17 changed files with 225 additions and 62 deletions

View File

@ -32,6 +32,12 @@ class FlowyEmojiSearchBar extends StatefulWidget {
class _FlowyEmojiSearchBarState extends State<FlowyEmojiSearchBar> { class _FlowyEmojiSearchBarState extends State<FlowyEmojiSearchBar> {
final TextEditingController controller = TextEditingController(); final TextEditingController controller = TextEditingController();
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return Padding(

View File

@ -3,8 +3,8 @@ import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'field_controller.dart'; import 'field_controller.dart';
@ -109,6 +109,7 @@ class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
@override @override
Future<void> close() { Future<void> close() {
_singleFieldListener.stop(); _singleFieldListener.stop();
return super.close(); return super.close();
} }
} }
@ -116,11 +117,11 @@ class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
@freezed @freezed
class FieldEditorEvent with _$FieldEditorEvent { class FieldEditorEvent with _$FieldEditorEvent {
const factory FieldEditorEvent.initial() = _InitialField; const factory FieldEditorEvent.initial() = _InitialField;
const factory FieldEditorEvent.didReceiveFieldChanged(String fieldId) = const factory FieldEditorEvent.didReceiveFieldChanged(final String fieldId) =
_DidReceiveFieldChanged; _DidReceiveFieldChanged;
const factory FieldEditorEvent.switchFieldType(FieldType fieldType) = const factory FieldEditorEvent.switchFieldType(final FieldType fieldType) =
_SwitchFieldType; _SwitchFieldType;
const factory FieldEditorEvent.renameField(String name) = _RenameField; const factory FieldEditorEvent.renameField(final String name) = _RenameField;
const factory FieldEditorEvent.toggleFieldVisibility() = const factory FieldEditorEvent.toggleFieldVisibility() =
_ToggleFieldVisiblity; _ToggleFieldVisiblity;
const factory FieldEditorEvent.deleteField() = _DeleteField; const factory FieldEditorEvent.deleteField() = _DeleteField;
@ -130,6 +131,6 @@ class FieldEditorEvent with _$FieldEditorEvent {
@freezed @freezed
class FieldEditorState with _$FieldEditorState { class FieldEditorState with _$FieldEditorState {
const factory FieldEditorState({ const factory FieldEditorState({
required FieldInfo field, required final FieldInfo field,
}) = _FieldEditorState; }) = _FieldEditorState;
} }

View File

@ -1,10 +1,10 @@
import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:flowy_infra/notifier.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:dartz/dartz.dart';
import 'package:protobuf/protobuf.dart' hide FieldInfo;
import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:dartz/dartz.dart';
import 'package:flowy_infra/notifier.dart';
import 'package:protobuf/protobuf.dart' hide FieldInfo;
import '../field_service.dart'; import '../field_service.dart';
import 'type_option_context.dart'; import 'type_option_context.dart';
@ -110,4 +110,8 @@ class TypeOptionController {
void removeFieldListener(void Function() listener) { void removeFieldListener(void Function() listener) {
_fieldNotifier.removeListener(listener); _fieldNotifier.removeListener(listener);
} }
void dispose() {
_fieldNotifier.dispose();
}
} }

View File

@ -12,8 +12,8 @@ import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/
import 'package:appflowy/plugins/database_view/tab_bar/tab_bar_view.dart'; import 'package:appflowy/plugins/database_view/tab_bar/tab_bar_view.dart';
import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart'; import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart';
import 'package:appflowy/util/platform_extension.dart'; import 'package:appflowy/util/platform_extension.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_board/appflowy_board.dart'; import 'package:appflowy_board/appflowy_board.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@ -25,11 +25,11 @@ import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import '../../widgets/card/cells/card_cell.dart'; import '../../widgets/card/card.dart';
import '../../widgets/card/card_cell_builder.dart'; import '../../widgets/card/card_cell_builder.dart';
import '../../widgets/card/cells/card_cell.dart';
import '../../widgets/row/cell_builder.dart'; import '../../widgets/row/cell_builder.dart';
import '../application/board_bloc.dart'; import '../application/board_bloc.dart';
import '../../widgets/card/card.dart';
import 'toolbar/board_setting_bar.dart'; import 'toolbar/board_setting_bar.dart';
import 'widgets/board_hidden_groups.dart'; import 'widgets/board_hidden_groups.dart';
@ -383,11 +383,15 @@ class _BoardTrailingState extends State<BoardTrailing> {
} }
return KeyEventResult.ignored; return KeyEventResult.ignored;
}, },
)..addListener(() { )..addListener(_onFocusChanged);
if (!_focusNode.hasFocus) { }
_cancelAddNewGroup();
} @override
}); void dispose() {
_focusNode.removeListener(_onFocusChanged);
_focusNode.dispose();
_textController.dispose();
super.dispose();
} }
@override @override
@ -452,4 +456,10 @@ class _BoardTrailingState extends State<BoardTrailing> {
), ),
); );
} }
void _onFocusChanged() {
if (!_focusNode.hasFocus) {
_cancelAddNewGroup();
}
}
} }

View File

@ -9,12 +9,13 @@ import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.da
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/common/type_option_separator.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/common/type_option_separator.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pbenum.dart';
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'field_type_option_editor.dart'; import 'field_type_option_editor.dart';

View File

@ -1,10 +1,12 @@
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import "package:appflowy/generated/locale_keys.g.dart";
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart'; import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database_view/application/row/row_controller.dart'; import 'package:appflowy/plugins/database_view/application/row/row_controller.dart';
import 'package:appflowy/plugins/database_view/application/row/row_service.dart'; import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
import 'package:appflowy/plugins/database_view/grid/application/row/row_bloc.dart'; import 'package:appflowy/plugins/database_view/grid/application/row/row_bloc.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart'; import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
@ -13,11 +15,9 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../../../../widgets/row/accessory/cell_accessory.dart'; import '../../../../widgets/row/accessory/cell_accessory.dart';
import '../../layout/sizes.dart';
import '../../../../widgets/row/cells/cell_container.dart'; import '../../../../widgets/row/cells/cell_container.dart';
import '../../layout/sizes.dart';
import 'action.dart'; import 'action.dart';
import "package:appflowy/generated/locale_keys.g.dart";
import 'package:easy_localization/easy_localization.dart';
class GridRow extends StatefulWidget { class GridRow extends StatefulWidget {
final RowId viewId; final RowId viewId;
@ -267,7 +267,6 @@ class RowContent extends StatelessWidget {
return cellByFieldId.values.map( return cellByFieldId.values.map(
(cellId) { (cellId) {
final GridCellWidget child = builder.build(cellId); final GridCellWidget child = builder.build(cellId);
return CellContainer( return CellContainer(
width: cellId.fieldInfo.fieldSettings?.width.toDouble() ?? 140, width: cellId.fieldInfo.fieldSettings?.width.toDouble() ?? 140,
isPrimary: cellId.fieldInfo.field.isPrimary, isPrimary: cellId.fieldInfo.field.isPrimary,

View File

@ -5,15 +5,15 @@ import 'package:appflowy/plugins/database_view/grid/presentation/widgets/row/act
import 'package:appflowy/util/platform_extension.dart'; import 'package:appflowy/util/platform_extension.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'card_bloc.dart'; import 'card_bloc.dart';
import 'cells/card_cell.dart';
import 'card_cell_builder.dart'; import 'card_cell_builder.dart';
import 'cells/card_cell.dart';
import 'container/accessory.dart'; import 'container/accessory.dart';
import 'container/card_container.dart'; import 'container/card_container.dart';
@ -193,7 +193,7 @@ class _RowCardState<T> extends State<RowCard<T>> {
} }
} }
class _CardContent<CustomCardData> extends StatelessWidget { class _CardContent<CustomCardData> extends StatefulWidget {
const _CardContent({ const _CardContent({
super.key, super.key,
required this.rowNotifier, required this.rowNotifier,
@ -211,26 +211,44 @@ class _CardContent<CustomCardData> extends StatelessWidget {
final CustomCardData? cardData; final CustomCardData? cardData;
final RowCardStyleConfiguration styleConfiguration; final RowCardStyleConfiguration styleConfiguration;
@override
State<_CardContent<CustomCardData>> createState() =>
_CardContentState<CustomCardData>();
}
class _CardContentState<CustomCardData>
extends State<_CardContent<CustomCardData>> {
final List<EditableCardNotifier> _notifiers = [];
@override
void dispose() {
for (final element in _notifiers) {
element.dispose();
}
_notifiers.clear();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (styleConfiguration.hoverStyle != null) { if (widget.styleConfiguration.hoverStyle != null) {
return FlowyHover( return FlowyHover(
style: styleConfiguration.hoverStyle, style: widget.styleConfiguration.hoverStyle,
buildWhenOnHover: () => !rowNotifier.isEditing.value, buildWhenOnHover: () => !widget.rowNotifier.isEditing.value,
child: Padding( child: Padding(
padding: styleConfiguration.cardPadding, padding: widget.styleConfiguration.cardPadding,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: _makeCells(context, cells), children: _makeCells(context, widget.cells),
), ),
), ),
); );
} }
return Padding( return Padding(
padding: styleConfiguration.cardPadding, padding: widget.styleConfiguration.cardPadding,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: _makeCells(context, cells), children: _makeCells(context, widget.cells),
), ),
); );
} }
@ -241,26 +259,28 @@ class _CardContent<CustomCardData> extends StatelessWidget {
) { ) {
final List<Widget> children = []; final List<Widget> children = [];
// Remove all the cell listeners. // Remove all the cell listeners.
rowNotifier.unbind(); widget.rowNotifier.unbind();
cells.asMap().forEach((int index, DatabaseCellContext cellContext) { cells.asMap().forEach((int index, DatabaseCellContext cellContext) {
final isEditing = index == 0 ? rowNotifier.isEditing.value : false; final isEditing = index == 0 ? widget.rowNotifier.isEditing.value : false;
final cellNotifier = EditableCardNotifier(isEditing: isEditing); final cellNotifier = EditableCardNotifier(isEditing: isEditing);
if (index == 0) { if (index == 0) {
// Only use the first cell to receive user's input when click the edit // Only use the first cell to receive user's input when click the edit
// button // button
rowNotifier.bindCell(cellContext, cellNotifier); widget.rowNotifier.bindCell(cellContext, cellNotifier);
} else {
_notifiers.add(cellNotifier);
} }
final child = Padding( final child = Padding(
key: cellContext.key(), key: cellContext.key(),
padding: styleConfiguration.cellPadding, padding: widget.styleConfiguration.cellPadding,
child: cellBuilder.buildCell( child: widget.cellBuilder.buildCell(
cellContext: cellContext, cellContext: cellContext,
cellNotifier: cellNotifier, cellNotifier: cellNotifier,
renderHook: renderHook, renderHook: widget.renderHook,
cardData: cardData, cardData: widget.cardData,
hasNotes: !cellContext.rowMeta.isDocumentEmpty, hasNotes: !cellContext.rowMeta.isDocumentEmpty,
), ),
); );

View File

@ -1,9 +1,9 @@
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart'; import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database_view/application/row/row_service.dart'; import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/timestamp_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/timestamp_entities.pb.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -160,11 +160,8 @@ class EditableRowNotifier {
} }
void dispose() { void dispose() {
for (final notifier in _cells.values) { unbind();
notifier.dispose(); isEditing.dispose();
}
_cells.clear();
} }
} }

View File

@ -2,8 +2,9 @@ import 'package:appflowy/mobile/presentation/database/card/row/cells/cells.dart'
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
import 'package:appflowy/util/platform_extension.dart'; import 'package:appflowy/util/platform_extension.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import '../../application/cell/cell_service.dart'; import '../../application/cell/cell_service.dart';
import 'accessory/cell_accessory.dart'; import 'accessory/cell_accessory.dart';
import 'accessory/cell_shortcuts.dart'; import 'accessory/cell_shortcuts.dart';
@ -263,7 +264,9 @@ abstract class GridCellState<T extends GridCellWidget> extends State<T> {
@override @override
void dispose() { void dispose() {
widget.onAccessoryHover.dispose();
widget.requestFocus.removeAllListener(); widget.requestFocus.removeAllListener();
widget.requestFocus.dispose();
super.dispose(); super.dispose();
} }
@ -336,6 +339,7 @@ class RequestFocusListener extends ChangeNotifier {
void removeAllListener() { void removeAllListener() {
if (_listener != null) { if (_listener != null) {
removeListener(_listener!); removeListener(_listener!);
_listener = null;
} }
} }

View File

@ -1,9 +1,11 @@
import 'dart:async'; import 'dart:async';
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/text_cell/text_cell_bloc.dart'; import 'package:appflowy/plugins/database_view/widgets/row/cells/text_cell/text_cell_bloc.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../grid/presentation/layout/sizes.dart'; import '../../../../grid/presentation/layout/sizes.dart';
import '../../cell_builder.dart'; import '../../cell_builder.dart';
@ -141,6 +143,7 @@ class _GridTextCellState extends GridEditableTextCell<GridTextCell> {
@override @override
Future<void> dispose() async { Future<void> dispose() async {
_controller.dispose();
_cellBloc.close(); _cellBloc.close();
super.dispose(); super.dispose();
} }

View File

@ -166,6 +166,8 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
EditorStyleCustomizer get styleCustomizer => widget.styleCustomizer; EditorStyleCustomizer get styleCustomizer => widget.styleCustomizer;
DocumentBloc get documentBloc => context.read<DocumentBloc>(); DocumentBloc get documentBloc => context.read<DocumentBloc>();
late final EditorScrollController editorScrollController;
Future<bool> showSlashMenu(editorState) async { Future<bool> showSlashMenu(editorState) async {
final result = await customSlashCommand( final result = await customSlashCommand(
slashMenuItems, slashMenuItems,
@ -186,6 +188,12 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
slashMenuItems = _customSlashMenuItems(); slashMenuItems = _customSlashMenuItems();
effectiveScrollController = widget.scrollController ?? ScrollController(); effectiveScrollController = widget.scrollController ?? ScrollController();
editorScrollController = EditorScrollController(
editorState: widget.editorState,
shrinkWrap: widget.shrinkWrap,
scrollController: effectiveScrollController,
);
// keep the previous font style when typing new text. // keep the previous font style when typing new text.
supportSlashMenuNodeWhiteList.addAll([ supportSlashMenuNodeWhiteList.addAll([
ToggleListBlockKeys.type, ToggleListBlockKeys.type,
@ -207,7 +215,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
effectiveScrollController.dispose(); effectiveScrollController.dispose();
} }
inlineActionsService.dispose(); inlineActionsService.dispose();
editorScrollController.dispose();
widget.editorState.dispose(); widget.editorState.dispose();
super.dispose(); super.dispose();
@ -225,12 +233,6 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
_setRTLToolbarItems(isRTL); _setRTLToolbarItems(isRTL);
final editorScrollController = EditorScrollController(
editorState: widget.editorState,
shrinkWrap: widget.shrinkWrap,
scrollController: effectiveScrollController,
);
final editor = Directionality( final editor = Directionality(
textDirection: textDirection, textDirection: textDirection,
child: AppFlowyEditor( child: AppFlowyEditor(

View File

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:appflowy/startup/tasks/memory_leak_detector.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart'; import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy_backend/appflowy_backend.dart'; import 'package:appflowy_backend/appflowy_backend.dart';
import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/log.dart';
@ -63,6 +64,9 @@ class FlowyRunner {
// this task should be first task, for handling platform errors. // this task should be first task, for handling platform errors.
// don't catch errors in test mode // don't catch errors in test mode
if (!mode.isUnitTest) const PlatformErrorCatcherTask(), if (!mode.isUnitTest) const PlatformErrorCatcherTask(),
// this task should be second task, for handling memory leak.
// there's a flag named _enable in memory_leak_detector.dart. If it's false, the task will be ignored.
MemoryLeakDetectorTask(),
// localization // localization
const InitLocalizationTask(), const InitLocalizationTask(),
// init the app window // init the app window

View File

@ -0,0 +1,97 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:leak_tracker/leak_tracker.dart';
import '../startup.dart';
bool _enable = false;
class MemoryLeakDetectorTask extends LaunchTask {
MemoryLeakDetectorTask();
Timer? _timer;
@override
Future<void> initialize(LaunchContext context) async {
if (!kDebugMode || !_enable) {
return;
}
LeakTracking.start();
LeakTracking.phase = const PhaseSettings(
leakDiagnosticConfig: LeakDiagnosticConfig(
collectRetainingPathForNotGCed: true,
collectStackTraceOnStart: true,
),
);
MemoryAllocations.instance.addListener((p0) {
LeakTracking.dispatchObjectEvent(p0.toMap());
});
_timer = Timer.periodic(const Duration(seconds: 1), (_) async {
final summary = await LeakTracking.checkLeaks();
if (summary.isEmpty) {
return;
}
final details = await LeakTracking.collectLeaks();
dumpDetails(LeakType.notDisposed, details);
// dumpDetails(LeakType.notGCed, details);
});
}
@override
Future<void> dispose() async {
if (!kDebugMode || !_enable) {
return;
}
_timer?.cancel();
_timer = null;
LeakTracking.stop();
}
final _dumpablePackages = [
'package:appflowy/',
];
void dumpDetails(LeakType type, Leaks leaks) {
final summary = '${type.desc}: ${switch (type) {
LeakType.notDisposed => '${leaks.notDisposed.length}',
LeakType.notGCed => '${leaks.notGCed.length}',
LeakType.gcedLate => '${leaks.gcedLate.length}'
}}';
debugPrint(summary);
final details = switch (type) {
LeakType.notDisposed => leaks.notDisposed,
LeakType.notGCed => leaks.notGCed,
LeakType.gcedLate => leaks.gcedLate
};
for (final value in details) {
final stack = value.context![ContextKeys.startCallstack]! as StackTrace;
final stackInAppFlowy = stack
.toString()
.split('\n')
.where(
(stack) =>
// ignore current file call stack
!stack.contains('memory_leak_detector') &&
_dumpablePackages.any((pkg) => stack.contains(pkg)),
)
.join('\n');
// ignore the untreatable leak
if (stackInAppFlowy.isEmpty) {
continue;
}
final object = value.type;
debugPrint('''
$object ${type.desc}
$stackInAppFlowy
''');
}
}
}
extension on LeakType {
String get desc => switch (this) {
LeakType.notDisposed => 'not disposed',
LeakType.notGCed => 'not GCed',
LeakType.gcedLate => 'GCed late'
};
}

View File

@ -2,8 +2,8 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart'; import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
import 'package:appflowy/util/google_font_family_extension.dart'; import 'package:appflowy/util/google_font_family_extension.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy/workspace/application/appearance_defaults.dart'; import 'package:appflowy/workspace/application/appearance_defaults.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
@ -83,6 +83,12 @@ class _FontFamilyDropDownState extends State<FontFamilyDropDown> {
final List<String> availableFonts = GoogleFonts.asMap().keys.toList(); final List<String> availableFonts = GoogleFonts.asMap().keys.toList();
final ValueNotifier<String> query = ValueNotifier(''); final ValueNotifier<String> query = ValueNotifier('');
@override
void dispose() {
query.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ThemeValueDropDown( return ThemeValueDropDown(

View File

@ -426,10 +426,16 @@ class _AIAccessKeyInputState extends State<_AIAccessKeyInput> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
textEditingController.text = widget.accessKey; textEditingController.text = widget.accessKey;
} }
@override
void dispose() {
textEditingController.dispose();
debounce.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return TextField( return TextField(
@ -470,12 +476,6 @@ class _AIAccessKeyInputState extends State<_AIAccessKeyInput> {
}, },
); );
} }
@override
void dispose() {
debounce.dispose();
super.dispose();
}
} }
typedef SelectIconCallback = void Function(String iconUrl, bool isSelected); typedef SelectIconCallback = void Function(String iconUrl, bool isSelected);

View File

@ -979,6 +979,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.0.4" version: "0.0.4"
leak_tracker:
dependency: "direct main"
description:
name: leak_tracker
sha256: b63ca5cc296c7509d71f6d4a8cb6085eec8461970c503f3ef3c5c541bc3f0a9a
url: "https://pub.dev"
source: hosted
version: "9.0.6"
linked_scroll_controller: linked_scroll_controller:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -128,6 +128,7 @@ dependencies:
image_picker: ^1.0.4 image_picker: ^1.0.4
image_gallery_saver: ^2.0.3 image_gallery_saver: ^2.0.3
cached_network_image: ^3.3.0 cached_network_image: ^3.3.0
leak_tracker: ^9.0.6
dev_dependencies: dev_dependencies:
flutter_lints: ^2.0.1 flutter_lints: ^2.0.1