diff --git a/core/lib/core.dart b/core/lib/core.dart index 372f7c165..81e3558d7 100644 --- a/core/lib/core.dart +++ b/core/lib/core.dart @@ -27,6 +27,7 @@ export 'presentation/utils/html_transformer/dom/add_tooltip_link_transformers.da export 'data/utils/device_manager.dart'; export 'utils/app_logger.dart'; export 'utils/benchmark.dart'; +export 'utils/fps_manager.dart'; // Views export 'presentation/views/text/slogan_builder.dart'; diff --git a/core/lib/utils/fps_manager.dart b/core/lib/utils/fps_manager.dart new file mode 100644 index 000000000..5c9a1da83 --- /dev/null +++ b/core/lib/utils/fps_manager.dart @@ -0,0 +1,128 @@ +import 'dart:collection'; +import 'dart:ui'; + +import 'package:flutter/scheduler.dart'; + +/// FpsManager callback. +typedef FpsCallback = void Function(FpsInfo fpsInfo); + +class FpsManager { + + FpsManager._(); + + factory FpsManager() => _instance ??= FpsManager._(); + + static FpsManager? _instance; + + /// Threshold time consuming per frame + /// 1000/60hz ≈ 16.6ms 1000/120hz ≈ 8.3ms + Duration _thresholdPerFrame = Duration(microseconds: Duration.microsecondsPerSecond ~/ 60); + + /// Refresh rate, default 60 + double _refreshRate = 60; + + /// Set refresh rate + set refreshRate(double rate) { + if (rate != _refreshRate && rate >= 60) { + _refreshRate = rate; + _thresholdPerFrame = Duration(microseconds: Duration.microsecondsPerSecond ~/ _refreshRate); + } + } + + bool _started = false; + List _fpsCallbacks = []; + + /// Temporarily save 120 frames + static const int _queue_capacity = 120; + final ListQueue framesQueue = ListQueue(_queue_capacity); + + void addFpsCallback(FpsCallback callback) { + _fpsCallbacks.add(callback); + } + + void removeFpsCallback(FpsCallback callback) { + assert(_fpsCallbacks.contains(callback)); + _fpsCallbacks.remove(callback); + } + + void start() async { + if (!_started) { + SchedulerBinding.instance?.addTimingsCallback(_onTimingsCallback); + _started = true; + } + } + + void stop() { + if (_started) { + SchedulerBinding.instance?.removeTimingsCallback(_onTimingsCallback); + _started = false; + } + } + + _onTimingsCallback(List timings) async { + if (_fpsCallbacks.isNotEmpty) { + for (FrameTiming timing in timings) { + framesQueue.addFirst(timing); + } + while (framesQueue.length > _queue_capacity) { + framesQueue.removeLast(); + } + + List drawFrames = []; + for (FrameTiming timing in framesQueue) { + if (drawFrames.isEmpty) { + drawFrames.add(timing); + } else { + int lastStart = + drawFrames.last.timestampInMicroseconds(FramePhase.vsyncStart); + int interval = lastStart - + timing.timestampInMicroseconds(FramePhase.rasterFinish); + if (interval > (_thresholdPerFrame.inMicroseconds * 2)) { + // maybe in different set + break; + } + drawFrames.add(timing); + } + } + framesQueue.clear(); + + // compute total frames count. + int totalCount = drawFrames.map((frame) { + // If droppedCount > 0, + int droppedCount = frame.totalSpan.inMicroseconds ~/ _thresholdPerFrame.inMicroseconds; + return droppedCount + 1; + }).fold(0, (a, b) => a + b); + + int drawFramesCount = drawFrames.length; + int droppedCount = totalCount - drawFramesCount; + double fps = drawFramesCount / totalCount * _refreshRate; + FpsInfo fpsInfo = FpsInfo(fps, totalCount, droppedCount, drawFramesCount); + _fpsCallbacks.forEach((callBack) { + callBack(fpsInfo); + }); + } + } +} + +class FpsInfo { + final double fps; + final int totalFramesCount; + final int droppedFramesCount; + final int drawFramesCount; + + FpsInfo( + this.fps, + this.totalFramesCount, + this.droppedFramesCount, + this.drawFramesCount + ); + + @override + String toString() { + return 'FpsInfo{' + 'fps: $fps, ' + 'totalFramesCount: $totalFramesCount, ' + 'droppedFramesCount: $droppedFramesCount, ' + 'drawFramesCount: $drawFramesCount}'; + } +} \ No newline at end of file diff --git a/lib/features/base/base_controller.dart b/lib/features/base/base_controller.dart index b6cddb738..2c8486526 100644 --- a/lib/features/base/base_controller.dart +++ b/lib/features/base/base_controller.dart @@ -8,6 +8,7 @@ import 'package:tmail_ui_user/features/base/mixin/popup_context_menu_action_mixi abstract class BaseController extends GetxController with MessageDialogActionMixin, PopupContextMenuActionMixin { final viewState = Rx>(Right(UIState.idle)); final connectivityResult = Rxn(); + FpsCallback? fpsCallback; void consumeState(Stream> newStateStream) async { newStateStream.listen( @@ -48,4 +49,21 @@ abstract class BaseController extends GetxController with MessageDialogActionMix void onError(dynamic error); void onDone(); + + void startFpsMeter() { + FpsManager().start(); + fpsCallback = (fpsInfo) { + log('BaseController::startFpsMeter(): $fpsInfo'); + }; + if (fpsCallback != null) { + FpsManager().addFpsCallback(fpsCallback!); + } + } + + void stopFpsMeter() { + FpsManager().stop(); + if (fpsCallback != null) { + FpsManager().removeFpsCallback(fpsCallback!); + } + } } \ No newline at end of file diff --git a/lib/features/thread/data/repository/thread_repository_impl.dart b/lib/features/thread/data/repository/thread_repository_impl.dart index 2131d0868..0026d7ca5 100644 --- a/lib/features/thread/data/repository/thread_repository_impl.dart +++ b/lib/features/thread/data/repository/thread_repository_impl.dart @@ -243,7 +243,9 @@ class ThreadRepositoryImpl extends ThreadRepository { @override Stream loadMoreEmails(GetEmailRequest getEmailRequest) async* { + bench.start('loadMoreEmails in computed'); final response = await compute(_getAllEmailsWithoutLastEmailId, getEmailRequest); + bench.end('loadMoreEmails in computed'); await _updateEmailCache(newCreated: response.emailList); yield response; } diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index 60a0b316c..da1b430e2 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -222,6 +222,8 @@ class ThreadController extends BaseController { _markAsStarMultipleEmailFailure(failure); } else if (failure is EmptyTrashFolderFailure) { _emptyTrashFolderFailure(failure); + } else if (failure is LoadMoreEmailsFailure) { + stopFpsMeter(); } }, (success) { @@ -245,6 +247,8 @@ class ThreadController extends BaseController { _markAsEmailReadSuccess(success); } else if (success is MoveToMailboxSuccess) { _moveToMailboxSuccess(success); + } else if (success is LoadMoreEmailsSuccess) { + stopFpsMeter(); } } ); @@ -383,7 +387,7 @@ class ThreadController extends BaseController { void loadMoreEmails() { if (canLoadMore && _accountId != null) { log('ThreadController::loadMoreEmails(): latest: ${emailList.last.receivedAt}'); - bench.start('loadMoreEmails'); + startFpsMeter(); consumeState(_loadMoreEmailsInMailboxInteractor.execute( GetEmailRequest( _accountId!, @@ -397,7 +401,6 @@ class ThreadController extends BaseController { } void _loadMoreEmailsSuccess(LoadMoreEmailsSuccess success) { - bench.end('loadMoreEmails'); log('ThreadController::_loadMoreEmailsSuccess(): [BEFORE] totalEmailList = ${emailList.length}'); if (success.emailList.isNotEmpty) { log('ThreadController::_loadMoreEmailsSuccess(): add success: ${success.emailList.length}');