TF-495 Show FPS when load more emails
This commit is contained in:
@@ -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';
|
||||
|
||||
@@ -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<FpsCallback> _fpsCallbacks = [];
|
||||
|
||||
/// Temporarily save 120 frames
|
||||
static const int _queue_capacity = 120;
|
||||
final ListQueue framesQueue = ListQueue<FrameTiming>(_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<FrameTiming> timings) async {
|
||||
if (_fpsCallbacks.isNotEmpty) {
|
||||
for (FrameTiming timing in timings) {
|
||||
framesQueue.addFirst(timing);
|
||||
}
|
||||
while (framesQueue.length > _queue_capacity) {
|
||||
framesQueue.removeLast();
|
||||
}
|
||||
|
||||
List<FrameTiming> 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}';
|
||||
}
|
||||
}
|
||||
@@ -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<Either<Failure, Success>>(Right(UIState.idle));
|
||||
final connectivityResult = Rxn<ConnectivityResult>();
|
||||
FpsCallback? fpsCallback;
|
||||
|
||||
void consumeState(Stream<Either<Failure, Success>> 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!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -243,7 +243,9 @@ class ThreadRepositoryImpl extends ThreadRepository {
|
||||
|
||||
@override
|
||||
Stream<EmailsResponse> 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;
|
||||
}
|
||||
|
||||
@@ -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}');
|
||||
|
||||
Reference in New Issue
Block a user