From 775955df5cbdae86f82b20ca33b12c842924d217 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Sun, 10 Dec 2023 20:26:23 +0700 Subject: [PATCH] feat: optimize editor memory usage (#4120) --- .../appflowy_flutter/devtools_options.yaml | 1 + frontend/appflowy_flutter/ios/Podfile.lock | 12 +- .../widgets/row/row_document.dart | 67 ++++---- .../document/application/doc_bloc.dart | 72 ++++----- .../lib/plugins/document/document_page.dart | 143 ++++++++---------- .../document/presentation/editor_page.dart | 1 - .../startup/tasks/memory_leak_detector.dart | 89 +++++++---- .../application/doc/doc_listener.dart | 6 +- .../favorite/favorite_listener.dart | 1 + .../home/desktop_home_screen.dart | 9 +- .../appflowy_backend/lib/rust_stream.dart | 6 +- frontend/appflowy_flutter/pubspec.lock | 6 +- frontend/appflowy_flutter/pubspec.yaml | 2 +- 13 files changed, 213 insertions(+), 202 deletions(-) create mode 100644 frontend/appflowy_flutter/devtools_options.yaml diff --git a/frontend/appflowy_flutter/devtools_options.yaml b/frontend/appflowy_flutter/devtools_options.yaml new file mode 100644 index 0000000000..7e7e7f67de --- /dev/null +++ b/frontend/appflowy_flutter/devtools_options.yaml @@ -0,0 +1 @@ +extensions: diff --git a/frontend/appflowy_flutter/ios/Podfile.lock b/frontend/appflowy_flutter/ios/Podfile.lock index 9c90ae776c..9057a91ab5 100644 --- a/frontend/appflowy_flutter/ios/Podfile.lock +++ b/frontend/appflowy_flutter/ios/Podfile.lock @@ -173,7 +173,7 @@ SPEC CHECKSUMS: app_links: 5ef33d0d295a89d9d16bb81b0e3b0d5f70d6c875 appflowy_backend: 144c20d8bfb298c4e10fa3fa6701a9f41bf98b88 connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d - device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea + device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 file_picker: 15fd9539e4eb735dc54bae8c0534a7a9511a03de @@ -186,20 +186,20 @@ SPEC CHECKSUMS: integration_test: 13825b8a9334a850581300559b8839134b124670 irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9 keyboard_height_plugin: 43fa8bba20fd5c4fdeed5076466b8b9d43cc6b86 - package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7 - path_provider_foundation: eaf5b3e458fc0e5fbb9940fb09980e853fe058b8 + package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85 + path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 rich_clipboard_ios: 7588abe18f881a6d0e9ec0b12e51cae2761e8942 SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84 - shared_preferences_foundation: e2dae3258e06f44cc55f49d42024fd8dd03c590c + shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 sign_in_with_apple: f3bf75217ea4c2c8b91823f225d70230119b8440 sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a super_native_extensions: 4916b3c627a9c7fffdc48a23a9eca0b1ac228fa7 SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780 Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 - url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 + url_launcher_ios: bf5ce03e0e2088bad9cc378ea97fa0ed5b49673b webview_flutter_wkwebview: 2e2d318f21a5e036e2c3f26171342e95908bd60a PODFILE CHECKSUM: 8c681999c7764593c94846b2a64b44d86f7a27ac -COCOAPODS: 1.11.3 +COCOAPODS: 1.12.1 diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_document.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_document.dart index 2d4b155aea..b4d28f1b4a 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_document.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_document.dart @@ -3,6 +3,7 @@ import 'package:appflowy/plugins/database_view/grid/application/row/row_document import 'package:appflowy/plugins/document/application/doc_bloc.dart'; import 'package:appflowy/plugins/document/presentation/editor_page.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart'; +import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/widget/error_page.dart'; @@ -98,43 +99,37 @@ class _RowEditorState extends State { }, child: BlocBuilder( builder: (context, state) { - return state.loadingState.when( - loading: () => const Center( - child: CircularProgressIndicator.adaptive(), - ), - finish: (result) { - return result.fold( - (error) => FlowyErrorPage.message( - error.toString(), - howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(), + if (state.isLoading) { + return const Center(child: CircularProgressIndicator.adaptive()); + } + + final editorState = state.editorState; + final error = state.error; + if (error != null || editorState == null) { + Log.error(error); + return FlowyErrorPage.message( + error.toString(), + howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(), + ); + } + return IntrinsicHeight( + child: Container( + constraints: const BoxConstraints(minHeight: 300), + child: AppFlowyEditorPage( + shrinkWrap: true, + autoFocus: false, + editorState: editorState, + scrollController: widget.scrollController, + styleCustomizer: EditorStyleCustomizer( + context: context, + padding: const EdgeInsets.only(left: 16, right: 54), ), - (_) { - final editorState = documentBloc.editorState; - if (editorState == null) { - return const SizedBox.shrink(); - } - return IntrinsicHeight( - child: Container( - constraints: const BoxConstraints(minHeight: 300), - child: AppFlowyEditorPage( - shrinkWrap: true, - autoFocus: false, - editorState: editorState, - scrollController: widget.scrollController, - styleCustomizer: EditorStyleCustomizer( - context: context, - padding: const EdgeInsets.only(left: 16, right: 54), - ), - showParagraphPlaceholder: (editorState, node) => - editorState.document.isEmpty, - placeholderText: (node) => - LocaleKeys.cardDetails_notesPlaceholder.tr(), - ), - ), - ); - }, - ); - }, + showParagraphPlaceholder: (editorState, node) => + editorState.document.isEmpty, + placeholderText: (node) => + LocaleKeys.cardDetails_notesPlaceholder.tr(), + ), + ), ); }, ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/doc_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/application/doc_bloc.dart index 9f05835307..c3aa568847 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/doc_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/doc_bloc.dart @@ -47,15 +47,15 @@ class DocumentBloc extends Bloc { documentService: _documentService, ); - EditorState? editorState; StreamSubscription? _subscription; @override Future close() async { + await _documentListener.stop(); await _viewListener.stop(); await _subscription?.cancel(); await _documentService.closeDocument(view: view); - editorState?.cancelSubscription(); + state.editorState?.dispose(); return super.close(); } @@ -65,9 +65,25 @@ class DocumentBloc extends Bloc { ) async { await event.map( initial: (Initial value) async { - final state = await _fetchDocumentState(); - await _subscribe(state); - emit(state); + final editorState = await _fetchDocumentState(); + _onViewChanged(); + _onDocumentChanged(); + editorState.fold( + (l) => emit( + state.copyWith( + error: l, + editorState: null, + isLoading: false, + ), + ), + (r) => emit( + state.copyWith( + error: null, + editorState: r, + isLoading: false, + ), + ), + ); }, moveToTrash: (MoveToTrash value) async { emit(state.copyWith(isDeleted: true)); @@ -88,18 +104,6 @@ class DocumentBloc extends Bloc { ); } - Future _subscribe(DocumentState state) async { - _onViewChanged(); - _onDocumentChanged(); - - // create the editor state - await state.loadingState.whenOrNull( - finish: (data) async => data.map((r) { - _initAppFlowyEditorState(r); - }), - ); - } - /// subscribe to the view(document page) change void _onViewChanged() { _viewListener.start( @@ -122,22 +126,22 @@ class DocumentBloc extends Bloc { } /// Fetch document - Future _fetchDocumentState() async { + Future> _fetchDocumentState() async { final result = await _documentService.openDocument(viewId: view.id); - return state.copyWith( - loadingState: DocumentLoadingState.finish(result), + return result.fold( + (l) => left(l), + (r) async => right(await _initAppFlowyEditorState(r)), ); } - Future _initAppFlowyEditorState(DocumentDataPB data) async { + Future _initAppFlowyEditorState(DocumentDataPB data) async { final document = data.toDocument(); if (document == null) { assert(false, 'document is null'); - return; + return null; } final editorState = EditorState(document: document); - this.editorState = editorState; // subscribe to the document change from the editor _subscription = editorState.transactionStream.listen((event) async { @@ -164,6 +168,8 @@ class DocumentBloc extends Bloc { // Log.debug(log); }; } + + return editorState; } Future applyRules() async { @@ -172,7 +178,7 @@ class DocumentBloc extends Bloc { } Future ensureLastNodeIsEditable() async { - final editorState = this.editorState; + final editorState = state.editorState; if (editorState == null) { return; } @@ -187,7 +193,7 @@ class DocumentBloc extends Bloc { } Future ensureAtLeastOneParagraphExists() async { - final editorState = this.editorState; + final editorState = state.editorState; if (editorState == null) { return; } @@ -233,26 +239,22 @@ class DocumentEvent with _$DocumentEvent { @freezed class DocumentState with _$DocumentState { const factory DocumentState({ - required DocumentLoadingState loadingState, required bool isDeleted, required bool forceClose, + required bool isLoading, bool? isDocumentEmpty, UserProfilePB? userProfilePB, + EditorState? editorState, + FlowyError? error, }) = _DocumentState; factory DocumentState.initial() => const DocumentState( - loadingState: _Loading(), isDeleted: false, forceClose: false, isDocumentEmpty: null, userProfilePB: null, + editorState: null, + error: null, + isLoading: true, ); } - -@freezed -class DocumentLoadingState with _$DocumentLoadingState { - const factory DocumentLoadingState.loading() = _Loading; - const factory DocumentLoadingState.finish( - Either successOrFail, - ) = _Finish; -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart index 20c905eb9d..ecbc9656f9 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart @@ -1,30 +1,20 @@ -import 'dart:convert'; -import 'dart:io'; - import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/application/doc_bloc.dart'; import 'package:appflowy/plugins/document/presentation/banner.dart'; import 'package:appflowy/plugins/document/presentation/editor_page.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart'; -import 'package:appflowy/plugins/document/presentation/export_page_widget.dart'; import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/util/base64_string.dart'; import 'package:appflowy/workspace/application/notifications/notification_action.dart'; import 'package:appflowy/workspace/application/notifications/notification_action_bloc.dart'; import 'package:appflowy/workspace/application/view/prelude.dart'; -import 'package:appflowy/workspace/presentation/home/toast.dart'; import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart' - hide DocumentEvent; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/file_picker/file_picker_service.dart'; import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:path/path.dart' as p; enum EditorNotificationType { undo, @@ -57,81 +47,66 @@ class DocumentPage extends StatefulWidget { } class _DocumentPageState extends State { - late final DocumentBloc documentBloc; - EditorState? editorState; - @override void initState() { super.initState(); - documentBloc = getIt(param1: widget.view) - ..add(const DocumentEvent.initial()); - // The appflowy editor use Intl as localization, set the default language as fallback. Intl.defaultLocale = 'en_US'; } - @override - void dispose() { - documentBloc.close(); - super.dispose(); - } - @override Widget build(BuildContext context) { return MultiBlocProvider( providers: [ BlocProvider.value(value: getIt()), - BlocProvider.value(value: documentBloc), - ], - child: BlocListener( - listener: _onNotificationAction, - child: BlocBuilder( - builder: (context, state) => state.loadingState.when( - loading: () => - const Center(child: CircularProgressIndicator.adaptive()), - finish: (result) => result.fold( - (error) { - Log.error(error); - return FlowyErrorPage.message( - error.toString(), - howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(), - ); - }, - (data) { - if (state.forceClose) { - widget.onDeleted(); - return const SizedBox.shrink(); - } else if (documentBloc.editorState == null) { - return Center( - child: ExportPageWidget( - onTap: () async => await _exportPage(data), - ), - ); - } else { - editorState = documentBloc.editorState!; - return _buildEditorPage( - context, - state, - ); - } - }, - ), - ), + BlocProvider( + create: (_) => DocumentBloc(view: widget.view) + ..add(const DocumentEvent.initial()), ), + ], + child: BlocBuilder( + builder: (context, state) { + if (state.isLoading) { + return const Center(child: CircularProgressIndicator.adaptive()); + } + + final editorState = state.editorState; + final error = state.error; + if (error != null || editorState == null) { + Log.error(error); + return FlowyErrorPage.message( + error.toString(), + howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(), + ); + } + + if (state.forceClose) { + widget.onDeleted(); + return const SizedBox.shrink(); + } + + return BlocListener( + listener: _onNotificationAction, + child: _buildEditorPage( + context, + state, + ), + ); + }, ), ); } Widget _buildEditorPage(BuildContext context, DocumentState state) { final appflowyEditorPage = AppFlowyEditorPage( - editorState: editorState!, + editorState: state.editorState!, styleCustomizer: EditorStyleCustomizer( context: context, // the 44 is the width of the left action list padding: EditorStyleCustomizer.documentPadding, ), - header: _buildCoverAndIcon(context), + header: _buildCoverAndIcon(context, state.editorState!), ); return Column( @@ -144,19 +119,20 @@ class _DocumentPageState extends State { Widget _buildBanner(BuildContext context) { return DocumentBanner( - onRestore: () => documentBloc.add(const DocumentEvent.restorePage()), - onDelete: () => documentBloc.add(const DocumentEvent.deletePermanently()), + onRestore: () => context.read().add( + const DocumentEvent.restorePage(), + ), + onDelete: () => context.read().add( + const DocumentEvent.deletePermanently(), + ), ); } - Widget _buildCoverAndIcon(BuildContext context) { - if (editorState == null) { - return const Placeholder(); - } - final page = editorState!.document.root; + Widget _buildCoverAndIcon(BuildContext context, EditorState editorState) { + final page = editorState.document.root; return DocumentHeaderNodeWidget( node: page, - editorState: editorState!, + editorState: editorState, view: widget.view, onIconChanged: (icon) async { await ViewBackendService.updateViewIcon( @@ -167,20 +143,20 @@ class _DocumentPageState extends State { ); } - Future _exportPage(DocumentDataPB data) async { - final picker = getIt(); - final dir = await picker.getDirectoryPath(); - if (dir == null) { - return; - } - final path = p.join(dir, '${documentBloc.view.name}.json'); - const encoder = JsonEncoder.withIndent(' '); - final json = encoder.convert(data.toProto3Json()); - await File(path).writeAsString(json.base64.base64); - if (mounted) { - showSnackBarMessage(context, 'Export success to $path'); - } - } + // Future _exportPage(DocumentDataPB data) async { + // final picker = getIt(); + // final dir = await picker.getDirectoryPath(); + // if (dir == null) { + // return; + // } + // final path = p.join(dir, '${documentBloc.view.name}.json'); + // const encoder = JsonEncoder.withIndent(' '); + // final json = encoder.convert(data.toProto3Json()); + // await File(path).writeAsString(json.base64.base64); + // if (mounted) { + // showSnackBarMessage(context, 'Export success to $path'); + // } + // } Future _onNotificationAction( BuildContext context, @@ -189,8 +165,9 @@ class _DocumentPageState extends State { if (state.action != null && state.action!.type == ActionType.jumpToBlock) { final path = state.action?.arguments?[ActionArgumentKeys.nodePath.name]; + final editorState = context.read().state.editorState; if (editorState != null && widget.view.id == state.action?.objectId) { - editorState!.updateSelectionWithReason( + editorState.updateSelectionWithReason( Selection.collapsed( Position(path: [path]), ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index c201d455de..5d3f203285 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -220,7 +220,6 @@ class _AppFlowyEditorPageState extends State { } inlineActionsService.dispose(); editorScrollController.dispose(); - widget.editorState.dispose(); super.dispose(); } diff --git a/frontend/appflowy_flutter/lib/startup/tasks/memory_leak_detector.dart b/frontend/appflowy_flutter/lib/startup/tasks/memory_leak_detector.dart index 22d0c62f77..a4ae27a7fc 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/memory_leak_detector.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/memory_leak_detector.dart @@ -5,7 +5,15 @@ import 'package:leak_tracker/leak_tracker.dart'; import '../startup.dart'; -bool _enable = false; +bool enableMemoryLeakDetect = false; +bool dumpMemoryLeakPerSecond = false; + +void dumpMemoryLeak({ + LeakType type = LeakType.notDisposed, +}) async { + final details = await LeakTracking.collectLeaks(); + details.dumpDetails(type); +} class MemoryLeakDetectorTask extends LaunchTask { MemoryLeakDetectorTask(); @@ -14,9 +22,10 @@ class MemoryLeakDetectorTask extends LaunchTask { @override Future initialize(LaunchContext context) async { - if (!kDebugMode || !_enable) { + if (!kDebugMode || !enableMemoryLeakDetect) { return; } + LeakTracking.start(); LeakTracking.phase = const PhaseSettings( leakDiagnosticConfig: LeakDiagnosticConfig( @@ -24,45 +33,67 @@ class MemoryLeakDetectorTask extends LaunchTask { 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); - }); + + // dump memory leak per second if needed + if (dumpMemoryLeakPerSecond) { + _timer = Timer.periodic(const Duration(seconds: 1), (_) async { + final summary = await LeakTracking.checkLeaks(); + if (summary.isEmpty) { + return; + } + + dumpMemoryLeak(); + }); + } } @override Future dispose() async { - if (!kDebugMode || !_enable) { + if (!kDebugMode || !enableMemoryLeakDetect) { return; } - _timer?.cancel(); - _timer = null; + + if (dumpMemoryLeakPerSecond) { + _timer?.cancel(); + _timer = null; + } + LeakTracking.stop(); } +} - final _dumpablePackages = [ - 'package:appflowy/', - ]; - void dumpDetails(LeakType type, Leaks leaks) { +extension on LeakType { + String get desc => switch (this) { + LeakType.notDisposed => 'not disposed', + LeakType.notGCed => 'not GCed', + LeakType.gcedLate => 'GCed late' + }; +} + +final _dumpablePackages = [ + 'package:appflowy/', + 'package:appflowy_editor/', +]; + +extension on Leaks { + void dumpDetails(LeakType type) { final summary = '${type.desc}: ${switch (type) { - LeakType.notDisposed => '${leaks.notDisposed.length}', - LeakType.notGCed => '${leaks.notGCed.length}', - LeakType.gcedLate => '${leaks.gcedLate.length}' + LeakType.notDisposed => '${notDisposed.length}', + LeakType.notGCed => '${notGCed.length}', + LeakType.gcedLate => '${gcedLate.length}' }}'; debugPrint(summary); final details = switch (type) { - LeakType.notDisposed => leaks.notDisposed, - LeakType.notGCed => leaks.notGCed, - LeakType.gcedLate => leaks.gcedLate + LeakType.notDisposed => notDisposed, + LeakType.notGCed => notGCed, + LeakType.gcedLate => gcedLate }; + + // only dump the code in appflowy for (final value in details) { final stack = value.context![ContextKeys.startCallstack]! as StackTrace; final stackInAppFlowy = stack @@ -75,10 +106,12 @@ class MemoryLeakDetectorTask extends LaunchTask { _dumpablePackages.any((pkg) => stack.contains(pkg)), ) .join('\n'); + // ignore the untreatable leak if (stackInAppFlowy.isEmpty) { continue; } + final object = value.type; debugPrint(''' $object ${type.desc} @@ -87,11 +120,3 @@ $stackInAppFlowy } } } - -extension on LeakType { - String get desc => switch (this) { - LeakType.notDisposed => 'not disposed', - LeakType.notGCed => 'not GCed', - LeakType.gcedLate => 'GCed late' - }; -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/doc/doc_listener.dart b/frontend/appflowy_flutter/lib/workspace/application/doc/doc_listener.dart index 9f74162120..b7a1e2d285 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/doc/doc_listener.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/doc/doc_listener.dart @@ -1,11 +1,12 @@ import 'dart:async'; import 'dart:typed_data'; + import 'package:appflowy/core/notification/document_notification.dart'; import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart'; -import 'package:dartz/dartz.dart'; -import 'package:appflowy_backend/protobuf/flowy-notification/subject.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-notification/subject.pb.dart'; import 'package:appflowy_backend/rust_stream.dart'; +import 'package:dartz/dartz.dart'; class DocumentListener { DocumentListener({ @@ -50,5 +51,6 @@ class DocumentListener { Future stop() async { await _subscription?.cancel(); + _subscription = null; } } diff --git a/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_listener.dart b/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_listener.dart index c04ddb7a71..6c002ab878 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_listener.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_listener.dart @@ -60,6 +60,7 @@ class FavoriteListener { Future stop() async { _parser = null; await _streamSubscription?.cancel(); + _streamSubscription = null; _favoriteUpdated = null; } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/desktop_home_screen.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/desktop_home_screen.dart index c21758cb66..d198f06c4c 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/desktop_home_screen.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/desktop_home_screen.dart @@ -1,12 +1,13 @@ import 'package:appflowy/plugins/blank/blank.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/startup/tasks/memory_leak_detector.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; -import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; import 'package:appflowy/workspace/application/home/home_bloc.dart'; import 'package:appflowy/workspace/application/home/home_service.dart'; import 'package:appflowy/workspace/application/home/home_setting_bloc.dart'; +import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart'; @@ -122,6 +123,12 @@ class DesktopHomeScreen extends StatelessWidget { }, ), ), + floatingActionButton: enableMemoryLeakDetect + ? FloatingActionButton( + onPressed: () async => dumpMemoryLeak(), + child: const Icon(Icons.memory), + ) + : null, ), ), ); diff --git a/frontend/appflowy_flutter/packages/appflowy_backend/lib/rust_stream.dart b/frontend/appflowy_flutter/packages/appflowy_backend/lib/rust_stream.dart index 02aa671ffa..d799ffd005 100644 --- a/frontend/appflowy_flutter/packages/appflowy_backend/lib/rust_stream.dart +++ b/frontend/appflowy_flutter/packages/appflowy_backend/lib/rust_stream.dart @@ -1,8 +1,10 @@ -import 'dart:isolate'; import 'dart:async'; -import 'dart:typed_data'; import 'dart:ffi'; +import 'dart:isolate'; +import 'dart:typed_data'; + import 'package:appflowy_backend/log.dart'; + import 'protobuf/flowy-notification/subject.pb.dart'; typedef ObserverCallback = void Function(SubscribeObject observable); diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index cfe77300a4..0335f2f77e 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -54,11 +54,11 @@ packages: dependency: "direct main" description: path: "." - ref: "4995d21" - resolved-ref: "4995d21ff49907c71286e668f938f830bf94ca0d" + ref: e6b1336 + resolved-ref: e6b1336041734b2858b1704f32236b2435ff5a74 url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git - version: "2.0.0" + version: "2.1.0" appflowy_popover: dependency: "direct main" description: diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index ed22ea103a..f5b1e51529 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -47,7 +47,7 @@ dependencies: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "4995d21" + ref: "e6b1336" appflowy_popover: path: packages/appflowy_popover