From 3f13351b1c613abdf5d459a3c878a0a454b7314a Mon Sep 17 00:00:00 2001 From: Igor Zhukov Date: Tue, 14 Apr 2015 21:10:17 +0300 Subject: [PATCH] Improved large getDiff perfomance Now grouping most UI updates Closes #705 --- app/js/controllers.js | 157 +++++++++++++++++++++++++++++++++++++---- app/js/lib/tl_utils.js | 68 ++++++++++-------- app/js/services.js | 88 +++++++++++++++++------ 3 files changed, 252 insertions(+), 61 deletions(-) diff --git a/app/js/controllers.js b/app/js/controllers.js index f1bb39a7..f38b12c1 100644 --- a/app/js/controllers.js +++ b/app/js/controllers.js @@ -631,24 +631,37 @@ angular.module('myApp.controllers', ['myApp.i18n']) }); }); - $scope.$on('dialogs_update', function (e, dialog) { + $scope.$on('dialogs_multiupdate', function (e, dialogsUpdated) { if ($scope.search.query !== undefined && $scope.search.query.length) { return false; } - var pos = false; - angular.forEach($scope.dialogs, function(curDialog, curPos) { - if (curDialog.peerID == dialog.peerID) { - pos = curPos; - } + var topMessages = []; + var topToDialogs = {}; + angular.forEach(dialogsUpdated, function (dialog, peerID) { + topToDialogs[dialog.top_message] = dialog; + topMessages.push(dialog.top_message); }); + topMessages.sort(); - var wrappedDialog = AppMessagesManager.wrapForDialog(dialog.top_message, dialog.unread_count); - if (pos !== false) { - var prev = $scope.dialogs.splice(pos, 1); - safeReplaceObject(prev, wrappedDialog); + var i, dialog; + var len = $scope.dialogs.length; + for (i = 0; i < len; i++) { + dialog = $scope.dialogs[i]; + if (dialogsUpdated[dialog.peerID]) { + $scope.dialogs.splice(i, 1); + i--; + len--; + } } - $scope.dialogs.unshift(wrappedDialog); + len = topMessages.length; + for (i = 0; i < len; i++) { + dialog = topToDialogs[topMessages[i]]; + $scope.dialogs.unshift( + AppMessagesManager.wrapForDialog(dialog.top_message, dialog.unread_count) + ); + } + delete $scope.isEmpty.dialogs; if (!peersInDialogs[dialog.peerID]) { @@ -1055,6 +1068,17 @@ angular.module('myApp.controllers', ['myApp.i18n']) return false; } + function historiesQueuePop (peerID) { + var i; + for (i = 0; i < $scope.peerHistories.length; i++) { + if ($scope.peerHistories[i].peerID == peerID) { + $scope.peerHistories.splice(i, 1); + return true; + } + } + return false; + } + function updateHistoryPeer(preload) { var peerData = AppPeersManager.getPeer(peerID); // console.log('update', preload, peerData); @@ -1460,15 +1484,29 @@ angular.module('myApp.controllers', ['myApp.i18n']) $scope.$on('history_update', angular.noop); + var loadAfterSync = false; + $scope.$on('stateSynchronized', function () { + if (!loadAfterSync) { + return; + } + if (loadAfterSync == $scope.curDialog.peerID) { + loadHistory(); + } + loadAfterSync = false; + }); + + var typingTimeouts = {}; $scope.$on('history_append', function (e, addedMessage) { var history = historiesQueueFind(addedMessage.peerID); + // var history = historiesQueuePush(addedMessage.peerID); if (!history) { return; } var curPeer = addedMessage.peerID == $scope.curDialog.peerID; if (curPeer) { - if ($scope.historyFilter.mediaType || $scope.historyState.skipped) { + if ($scope.historyFilter.mediaType || + $scope.historyState.skipped) { if (addedMessage.my) { returnToRecent(); } else { @@ -1518,6 +1556,101 @@ angular.module('myApp.controllers', ['myApp.i18n']) } }); + $scope.$on('history_multiappend', function (e, historyMultiAdded) { + // console.log(dT(), 'multiappend', historyMultiAdded); + var regroupped = false; + var unreadAfterChanged = false; + var isIDLE = $rootScope.idle.isIDLE; + angular.forEach(historyMultiAdded, function (msgs, peerID) { + var history = historiesQueueFind(peerID); + // var history = historiesQueuePush(peerID); + if (!history) { + return; + } + var curPeer = peerID == $scope.curDialog.peerID; + var len = msgs.length; + + if (curPeer) { + if ($scope.historyFilter.mediaType || + $scope.historyState.skipped) { + $scope.historyState.missedCount += len; + return; + } + delete $scope.state.empty; + } + if (len > 10) { + if (curPeer) { + var exlen = history.messages.length; + if (exlen > 10) { + minID = history.messages[exlen - 1].id; + $scope.historyState.skipped = hasLess = minID > 0; + if (hasLess) { + loadAfterSync = peerID; + $scope.$broadcast('ui_history_append'); + return; + } + } + } else { + historiesQueuePop(peerID); + return; + } + } + var messageID, historyMessage, i; + var hasOut = false; + var unreadAfterNew = false; + var lastIsRead = !(history.messages[history.messages.length - 1] || {}).unread; + for (i = 0; i < len; i++) { + messageID = msgs[i]; + historyMessage = AppMessagesManager.wrapForHistory(messageID); + history.messages.push(historyMessage); + if (!unreadAfterNew && isIDLE) { + if (historyMessage.unread && + !historyMessage.out && + lastIsRead) { + unreadAfterNew = messageID; + } else { + lastIsRead = !historyMessage.unread; + } + } + if (!hasOut && historyMessage.out) { + hasOut = true; + } + } + + if (AppMessagesManager.regroupWrappedHistory(history.messages, -len - 2)) { + regroupped = true; + } + + if (curPeer) { + if ($scope.historyState.typing.length) { + $scope.historyState.typing.splice(0, $scope.historyState.typing.length); + } + $scope.$broadcast('ui_history_append_new', { + idleScroll: unreadAfterIdle && !hasOut && unreadAfterNew + }); + + if (isIDLE) { + if (unreadAfterNew) { + $scope.historyUnreadAfter = unreadAfterNew; + unreadAfterIdle = true; + unreadAfterChanged = true; + } + } else { + $timeout(function () { + AppMessagesManager.readHistory($scope.curDialog.inputPeer); + }); + } + } + }); + + if (regroupped) { + $scope.$broadcast('messages_regroup'); + } + if (unreadAfterChanged) { + $scope.$broadcast('messages_unread_after'); + } + }); + $scope.$on('history_delete', function (e, historyUpdate) { var history = historiesQueueFind(historyUpdate.peerID); if (!history) { diff --git a/app/js/lib/tl_utils.js b/app/js/lib/tl_utils.js index 6b49f775..856f13b1 100644 --- a/app/js/lib/tl_utils.js +++ b/app/js/lib/tl_utils.js @@ -226,20 +226,22 @@ TLSerialization.prototype.storeMethod = function (methodName, params) { this.storeInt(intToUint(methodData.id), methodName + '[id]'); - var self = this; - angular.forEach(methodData.params, function (param) { - var type = param.type; + var param, type, i, condType, fieldBit; + var len = methodData.params.length; + for (i = 0; i < len; i++) { + param = methodData.params[i]; + type = param.type; if (type.indexOf('?') !== -1) { - var condType = type.split('?'); - var fieldBit = condType[0].split('.'); + condType = type.split('?'); + fieldBit = condType[0].split('.'); if (!(params[fieldBit[0]] & (1 << fieldBit[1]))) { - return; + continue; } type = condType[1]; } - self.storeObject(params[param.name], type, methodName + '[' + param.name + ']'); - }); + this.storeObject(params[param.name], type, methodName + '[' + param.name + ']'); + } return methodData.type; }; @@ -308,20 +310,22 @@ TLSerialization.prototype.storeObject = function (obj, type, field) { this.writeInt(intToUint(constructorData.id), field + '[' + predicate + '][id]'); } - var self = this; - angular.forEach(constructorData.params, function (param) { - var type = param.type; + var param, type, i, condType, fieldBit; + var len = constructorData.params.length; + for (i = 0; i < len; i++) { + param = constructorData.params[i]; + type = param.type; if (type.indexOf('?') !== -1) { - var condType = type.split('?'); - var fieldBit = condType[0].split('.'); + condType = type.split('?'); + fieldBit = condType[0].split('.'); if (!(obj[fieldBit[0]] & (1 << fieldBit[1]))) { - return; + continue; } type = condType[1]; } - self.storeObject(obj[param.name], type, field + '[' + predicate + '][' + param.name + ']'); - }); + this.storeObject(obj[param.name], type, field + '[' + predicate + '][' + param.name + ']'); + } return constructorData.type; }; @@ -532,7 +536,7 @@ TLDeserialization.prototype.fetchObject = function (type, field) { if (type.charAt(0) == '%') { var checkType = type.substr(1); - for (i = 0; i < schema.constructors.length; i++) { + for (var i = 0; i < schema.constructors.length; i++) { if (schema.constructors[i].type == checkType) { constructorData = schema.constructors[i]; break @@ -543,7 +547,7 @@ TLDeserialization.prototype.fetchObject = function (type, field) { } } else if (type.charAt(0) >= 97 && type.charAt(0) <= 122) { - for (i = 0; i < schema.constructors.length; i++) { + for (var i = 0; i < schema.constructors.length; i++) { if (schema.constructors[i].predicate == type) { constructorData = schema.constructors[i]; break @@ -566,12 +570,17 @@ TLDeserialization.prototype.fetchObject = function (type, field) { return newDeserializer.fetchObject(type, field); } - for (i = 0; i < schema.constructors.length; i++) { - if (schema.constructors[i].id == constructorCmp) { - constructorData = schema.constructors[i]; - break; + var index = schema.constructorsIndex; + if (!index) { + schema.constructorsIndex = index = {}; + for (var i = 0; i < schema.constructors.length; i++) { + index[schema.constructors[i].id] = i; } } + var i = index[constructorCmp]; + if (i) { + constructorData = schema.constructors[i]; + } var fallback = false; if (!constructorData && this.mtproto) { @@ -601,19 +610,22 @@ TLDeserialization.prototype.fetchObject = function (type, field) { if (this.override[overrideKey]) { this.override[overrideKey].apply(this, [result, field + '[' + predicate + ']']); } else { - angular.forEach(constructorData.params, function (param) { - var type = param.type; + var i, param, type, condType, fieldBit; + var len = constructorData.params.length; + for (i = 0; i < len; i++) { + param = constructorData.params[i]; + type = param.type; if (type.indexOf('?') !== -1) { - var condType = type.split('?'); - var fieldBit = condType[0].split('.'); + condType = type.split('?'); + fieldBit = condType[0].split('.'); if (!(result[fieldBit[0]] & (1 << fieldBit[1]))) { - return; + continue; } type = condType[1]; } result[param.name] = self.fetchObject(type, field + '[' + predicate + '][' + param.name + ']'); - }); + } } if (fallback) { diff --git a/app/js/services.js b/app/js/services.js index 65771893..faff57fa 100755 --- a/app/js/services.js +++ b/app/js/services.js @@ -792,7 +792,7 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils']) } }) -.service('AppMessagesManager', function ($q, $rootScope, $location, $filter, ApiUpdatesManager, AppUsersManager, AppChatsManager, AppPeersManager, AppPhotosManager, AppVideoManager, AppDocsManager, AppAudioManager, AppWebPagesManager, MtpApiManager, MtpApiFileManager, RichTextProcessor, NotificationsManager, PeersSelectService, Storage, FileManager, TelegramMeWebService, StatusManager, _) { +.service('AppMessagesManager', function ($q, $rootScope, $location, $filter, $timeout, ApiUpdatesManager, AppUsersManager, AppChatsManager, AppPeersManager, AppPhotosManager, AppVideoManager, AppDocsManager, AppAudioManager, AppWebPagesManager, MtpApiManager, MtpApiFileManager, RichTextProcessor, NotificationsManager, PeersSelectService, Storage, FileManager, TelegramMeWebService, StatusManager, _) { var messagesStorage = {}; var messagesForHistory = {}; @@ -895,6 +895,12 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils']) AppUsersManager.saveApiUsers(dialogsResult.users); AppChatsManager.saveApiChats(dialogsResult.chats); + + // return { + // count: 0, + // dialogs: [] + // }; + saveMessages(dialogsResult.messages); if (maxID > 0) { @@ -2270,6 +2276,25 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils']) }); } + var newMessagesHandlePromise = false; + var newMessagesToHandle = {}; + var newDialogsHandlePromise = false; + var newDialogsToHandle = {}; + + function handleNewMessages () { + $timeout.cancel(newMessagesHandlePromise); + newMessagesHandlePromise = false; + $rootScope.$broadcast('history_multiappend', newMessagesToHandle); + newMessagesToHandle = {}; + } + + function handleNewDialogs () { + $timeout.cancel(newDialogsHandlePromise); + newDialogsHandlePromise = false; + $rootScope.$broadcast('dialogs_multiupdate', newDialogsToHandle); + newDialogsToHandle = {}; + } + $rootScope.$on('apiUpdate', function (e, update) { // if (update._ != 'updateUserStatus') { // console.log('on apiUpdate', update); @@ -2320,34 +2345,45 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils']) } if (!pendingMessage) { - $rootScope.$broadcast('history_append', {peerID: peerID, messageID: message.id}); + if (newMessagesToHandle[peerID] === undefined) { + newMessagesToHandle[peerID] = []; + } + newMessagesToHandle[peerID].push(message.id); + if (!newMessagesHandlePromise) { + newMessagesHandlePromise = $timeout(handleNewMessages, 0); + } } - var foundDialog = getDialogByPeerID(peerID), - dialog; + var foundDialog = getDialogByPeerID(peerID); + var dialog; + var inboxUnread = !message.out && message.unread; if (foundDialog.length) { dialog = foundDialog[0]; - dialogsStorage.dialogs.splice(foundDialog[1], 1); + if (foundDialog[1] > 0) { + dialogsStorage.dialogs.splice(foundDialog[1], 1); + dialogsStorage.dialogs.unshift(dialog); + } + dialog.top_message = message.id; + if (inboxUnread) { + dialog.unread_count++; + } } else { - dialog = {peerID: peerID, unread_count: 0, top_message: false} + SearchIndexManager.indexObject(peerID, AppPeersManager.getPeerSearchText(peerID), dialogsIndex); + + dialog = { + peerID: peerID, + unread_count: inboxUnread ? 1 : 0, + top_message: message.id + }; + dialogsStorage.dialogs.unshift(dialog); } - if (!message.out && message.unread) { - // console.log('inc unread count', dialog.unread_count); - dialog.unread_count++; + newDialogsToHandle[peerID] = dialog; + if (!newDialogsHandlePromise) { + newDialogsHandlePromise = $timeout(handleNewDialogs, 0); } - dialog.top_message = message.id; - // console.log('new message', message, peerID, historyStorage, foundDialog, dialog); - - SearchIndexManager.indexObject(peerID, AppPeersManager.getPeerSearchText(peerID), dialogsIndex); - - dialogsStorage.dialogs.unshift(dialog); - $rootScope.$broadcast('dialogs_update', dialog); - - if (($rootScope.selectedPeerID != peerID || $rootScope.idle.isIDLE) && - !message.out && - message.unread) { + if (inboxUnread && ($rootScope.selectedPeerID != peerID || $rootScope.idle.isIDLE)) { var notifyPeer = message.flags & 16 ? message.from_id : peerID; var isMutedPromise = NotificationsManager.getPeerMuted(notifyPeer); @@ -3716,12 +3752,13 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils']) syncPending = false; } - MtpApiManager.invokeApi('updates.getDifference', {pts: curState.pts, date: curState.date, qts: 0}).then(function (differenceResult) { + MtpApiManager.invokeApi('updates.getDifference', {pts: curState.pts, date: curState.date, qts: -1}).then(function (differenceResult) { if (differenceResult._ == 'updates.differenceEmpty') { console.log(dT(), 'apply empty diff', differenceResult.seq); curState.date = differenceResult.date; curState.seq = differenceResult.seq; syncLoading = false; + $rootScope.$broadcast('stateSynchronized'); return false; } @@ -3729,10 +3766,12 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils']) AppChatsManager.saveApiChats(differenceResult.chats); // Should be first because of updateMessageID + // console.log(dT(), 'applying', differenceResult.other_updates.length, 'other updates'); angular.forEach(differenceResult.other_updates, function(update){ saveUpdate(update); }); + // console.log(dT(), 'applying', differenceResult.new_messages.length, 'new messages'); angular.forEach(differenceResult.new_messages, function (apiMessage) { saveUpdate({ _: 'updateNewMessage', @@ -3752,6 +3791,8 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils']) if (differenceResult._ == 'updates.differenceSlice') { getDifference(); } else { + // console.log(dT(), 'finished get diff'); + $rootScope.$broadcast('stateSynchronized'); syncLoading = false; } }); @@ -3855,6 +3896,11 @@ angular.module('myApp.services', ['myApp.i18n', 'izhukov.utils']) setTimeout(function () { syncLoading = false; }, 1000); + + // curState.seq = 1; + // curState.pts = stateResult.pts - 5000; + // curState.date = 1; + // getDifference(); }) }