我们使用Firebase作为我们服务的后端。它有一个PeopleViewController
,用于显示当前用户半径范围内的用户。半径可以在1到40英里之间变化。还有一个检查阻止用户,年龄,性别。在项目开发的最初阶段,在应用程序中为此功能进行了临时实施。我创建了几个完成这些请求的经理,然后将结果提供给PeopleViewController。在项目开始时,由于截止日期紧迫,分页没有实施,但由于用户甚至在半径范围内都不到100,因此不需要分页。现在,在一个半径范围内可以有超过600个用户和我认为这些请求在应用程序内执行是不对的。我们将这些功能从应用程序端移动到云端功能,以便我们可以调整每个问题中应该有多少用户。
问题是,即使是一个预热的云功能也比应用程序内与此功能相关的所有功能都要慢。平均而言,半径为40英里的应用程序内的代码以及所有年龄组的包含在1.5秒内返回646个用户,而最佳结果是云函数3.6。下面我将给出应用程序端和云功能的时间和代码示例。
使用在应用程序中执行的代码获得结果:
1 number of users 646, 1.6775569915771484 seconds
2 number of users 646, 1.4022901058197021 seconds
3 number of users 646, 1.5957129001617432 seconds
云功能时间的结果以毫秒为单位指定。参数和位置与从应用程序运行的代码相同:
1 "timers": {
"afterVerify": 1,
"gotIdsFromGeofire": 1372,
"gotBlockedUsersIDs": 2463,
"gotBlockedByUsersIDs": 2467,
"gotUserSearchLocationModels": 5256,
"gotAllInitData": 5256,
"filtersAndSortsDone": 6168
},
2 "timers": {
"afterVerify": 6,
"gotIdsFromGeofire": 1209,
"gotBlockedUsersIDs": 1909,
"gotBlockedByUsersIDs": 1913,
"gotUserSearchLocationModels": 3800,
"gotAllInitData": 3800,
"filtersAndSortsDone": 4207
},
3 "timers": {
"afterVerify": 1,
"gotIdsFromGeofire": 812,
"gotBlockedUsersIDs": 1415,
"gotBlockedByUsersIDs": 1419,
"gotUserSearchLocationModels": 3912,
"gotAllInitData": 3913,
"filtersAndSortsDone": 4417
},
在请求用户列表并过滤应用程序中收到的数据的经理中。
extension PeopleListViewController {
@objc fileprivate func requestPeopleNearBy() {
let firPeopleSearchDatabaseManagerStart = Date().timeIntervalSince1970
firPeopleSearchDatabaseManager.searchPeople(success: { [weak self](userSearchLocationModels) in
let firPeopleSearchDatabaseManagerEnd = Date().timeIntervalSince1970
let resultTimeStamp = firPeopleSearchDatabaseManagerEnd - firPeopleSearchDatabaseManagerStart
debugPrint("firPeopleSearchDatabaseManager userSearchLocationModels", userSearchLocationModels.count, "resultTimeStamp", resultTimeStamp)
}) { (error) in
}
}
}
class FIRPeopleSearchDatabaseManager {
fileprivate enum MainGateways {
case userSearchLocationModel, userLocations
var description: String {
switch self {
case .userSearchLocationModel:
return "userSearchLocationModel"
case .userLocations:
return "userLocations"
}
}
}
func saveUserSearchLocationModel(_ userSearchLocationModel: UserSearchLocationModel, success: (() -> Void)?, fail: ((_ error: Error) -> Void)?) {
DispatchQueue.global(qos: .background).async {
let ref = Database.database().reference().child(MainGateways.userSearchLocationModel.description).child(userSearchLocationModel.userID)
let json = userSearchLocationModel.toJSON()
ref.updateChildValues(json, withCompletionBlock: { (error, ref) in
guard error == nil else {
fail?(error!)
return
}
success?()
})
}
}
private func downloadUserSearchLocationModel(_ userID: String, success: ((_ userSearchLocationModel: UserSearchLocationModel) -> Void)?, notExist: (() -> Void)?, fail: ((_ error: Error) -> Void)?) {
DispatchQueue.global(qos: .background).async {
let ref = Database.database().reference().child(MainGateways.userSearchLocationModel.description).child(userID)
ref.observeSingleEvent(of: .value, with: { (snapshot) in
if snapshot.value is NSNull {
notExist?()
return
}
guard let json = snapshot.value as? [String : Any] else {
debugPrint("doest exist", userID)
notExist?()
return
}
guard let userSearchLocationModel = Mapper<UserSearchLocationModel>().map(JSON: json) else {
debugPrint("doest exist", userID)
notExist?()
return
}
guard !userSearchLocationModel.userID.isEmpty else {
notExist?()
return
}
success?(userSearchLocationModel)
}, withCancel: { (error) in
fail?(error)
})
}
}
func searchPeople(success: ((_ searchLocationModels: [UserSearchLocationModel]) -> Void)?, fail: ((_ error: Error) -> Void)?) {
let realmManager = RealmManager()
guard let currentUserID = realmManager.getCurrentUser()?.id else { return }
guard let location = LocationManager.shared.currentLocation else { return }
let realmUserSettingsManager = RealmUserSettingsManager()
let miles = realmUserSettingsManager.getMaxDistanceInMiles() ?? 15
let distanceConverter = DistanceConvertor()
let radius = distanceConverter.convertedMilesToMetersForFIRRequest(miles)
DispatchQueue.global(qos: .background).async {
let usersGeofireRef = Database.database().reference().child(MainGateways.userLocations.description)
guard let geoFire = GeoFire(firebaseRef: usersGeofireRef) else { return }
guard let circleQuery = geoFire.query(at: location, withRadius: radius) else { return }
var usersListIDs = [String]()
var generalBlockModel = [BlockModel]()
let dispatchGroup = DispatchGroup()
let blockSystemManager = BlockSystemManager()
dispatchGroup.enter()
blockSystemManager.requestBlockingUserIDs(completion: { (blockModels, error) in
generalBlockModel = blockModels
dispatchGroup.leave()
})
dispatchGroup.enter()
circleQuery.observe(.keyEntered, with: { (key, location) in
if let _key = key {
if _key != currentUserID {
usersListIDs.append(_key)
}
}
})
circleQuery.observeReady({
circleQuery.removeAllObservers()
dispatchGroup.leave()
})
dispatchGroup.notify(queue: .global(qos: .background), execute: {
if usersListIDs.count == 0 {
circleQuery.removeAllObservers()
success?([])
return
}
var unblockedUserListIDs = [String]()
for usersListID in usersListIDs {
let isContains = generalBlockModel.contains(where: { $0.userID == usersListID })
if !isContains {
unblockedUserListIDs.append(usersListID)
}
}
self.downloadUsers(usersListIDs: unblockedUserListIDs, success: success)
})
}
}
private func downloadUsers(usersListIDs: [String], success: ((_ searchLocationModels: [UserSearchLocationModel]) -> Void)?) {
var userIDsCount = usersListIDs.count
var userSearchLocationModels = [UserSearchLocationModel]()
for userID in usersListIDs {
self.downloadUserSearchLocationModel(userID, success: { (userSearchLocationModel) in
userSearchLocationModels.append(userSearchLocationModel)
if userIDsCount == userSearchLocationModels.count {
self.filter(userSearchLocationModels, success: { (sortedUserSearchLocationModels) in
success?(sortedUserSearchLocationModels)
})
}
}, notExist: {
userIDsCount -= 1
if userIDsCount == userSearchLocationModels.count {
self.filter(userSearchLocationModels, success: { (sortedUserSearchLocationModels) in
success?(sortedUserSearchLocationModels)
})
}
}, fail: { (error) in
userIDsCount -= 1
if userIDsCount == userSearchLocationModels.count {
self.filter(userSearchLocationModels, success: { (sortedUserSearchLocationModels) in
success?(sortedUserSearchLocationModels)
})
}
})
}
}
private func filter(_ userSearchLocationModels: [UserSearchLocationModel], success: ((_ sortedUserSearchLocationModels: [UserSearchLocationModel]) -> Void)?) {
filterByUserSearchFilter(userSearchLocationModels) { (searchFilteredUserSearchLocationModels) in
self.filterByOnlineAndRecentLaunchApp(searchFilteredUserSearchLocationModels, success: { (onlineRecentFilteredUserSearchLocationModels) in
success?(onlineRecentFilteredUserSearchLocationModels)
})
}
}
private func filterByUserSearchFilter(_ userSearchLocationModels: [UserSearchLocationModel], success: ((_ sortedUserSearchLocationModels: [UserSearchLocationModel]) -> Void)?) {
DispatchQueue.main.async {
let timeManager = TimeManager()
let realmUserSettingsManager = RealmUserSettingsManager()
guard let cuPeopleFilterSettings = realmUserSettingsManager.getUserPeopleFilterSettings() else { return }
var sortedUserSearchLocationModels = [UserSearchLocationModel]()
for userSearchLocationModel in userSearchLocationModels {
let userYearAge = timeManager.getUserAgeFromBirthdayTimeStamp(userSearchLocationModel.birthdayTimeStamp)
if cuPeopleFilterSettings.filterGenderModeIndex != 2 {
// sorting with mode
if cuPeopleFilterSettings.maxAgeValue != UserPeopleFilterSettings.Standard.maxAge {
if userYearAge >= cuPeopleFilterSettings.minAgeValue && userYearAge <= cuPeopleFilterSettings.maxAgeValue && userSearchLocationModel.genderIndex == cuPeopleFilterSettings.filterGenderModeIndex {
sortedUserSearchLocationModels.append(userSearchLocationModel)
}
} else {
// max age settings
if userYearAge >= cuPeopleFilterSettings.minAgeValue && userSearchLocationModel.genderIndex == cuPeopleFilterSettings.filterGenderModeIndex {
sortedUserSearchLocationModels.append(userSearchLocationModel)
}
}
} else {
if cuPeopleFilterSettings.maxAgeValue != UserPeopleFilterSettings.Standard.maxAge {
if userYearAge >= cuPeopleFilterSettings.minAgeValue && userYearAge <= cuPeopleFilterSettings.maxAgeValue {
sortedUserSearchLocationModels.append(userSearchLocationModel)
}
} else {
// max age in settings
if userYearAge >= cuPeopleFilterSettings.minAgeValue {
sortedUserSearchLocationModels.append(userSearchLocationModel)
}
}
}
}
//sorting by online status and last seen timestamp
let onlineUsers = sortedUserSearchLocationModels.filter { $0.isOnline }
let otherUsersSortedByTimeStamp = sortedUserSearchLocationModels.filter { !$0.isOnline }.sorted { $0.lastSeenTimeStamp > $1.lastSeenTimeStamp }
var resultSortedArray = [UserSearchLocationModel]()
resultSortedArray.append(contentsOf: onlineUsers)
resultSortedArray.append(contentsOf: otherUsersSortedByTimeStamp)
success?(resultSortedArray)
}
}
/// 1. online users sorted by most recently active on the app 2. offline users sorted by most recently active on the app
private func filterByOnlineAndRecentLaunchApp(_ userSearchLocationModels: [UserSearchLocationModel], success: ((_ sortedUserSearchLocationModels: [UserSearchLocationModel]) -> Void)?) {
DispatchQueue.global(qos: .background).async {
let sortedUsers = userSearchLocationModels.sorted(by: { $0.recentActivityTimeStamp > $1.recentActivityTimeStamp })
success?(sortedUsers)
}
}
}
来自BlockSystemManager
extension BlockSystemManager {
func requestBlockingUserIDs(completion: ((_ blockModels: [BlockModel], _ error: Error?) -> Void)?) {
DispatchQueue.main.async {
guard let currentUserID = RealmManager().getCurrentUser()?.id else { return }
DispatchQueue.global(qos: .background).async {
let dispatchGroup = DispatchGroup()
var generalBlockModels = [BlockModel]()
let blockedUsersRef = Database.database().reference().child(MainPath.blockingInfo.description).child(currentUserID).child(SubPath.blockedUsers.description)
dispatchGroup.enter()
blockedUsersRef.observeSingleEvent(of: .value, with: { (snap) in
if snap.value is NSNull {
dispatchGroup.leave()
return
}
guard let dict = snap.value as? [String : [String : Any]] else {
dispatchGroup.leave()
return
}
guard let blockModelsDict = Mapper<BlockModel>().mapDictionary(JSON: dict)?.values else {
dispatchGroup.leave()
return
}
let blockModels = Array(blockModelsDict)
generalBlockModels += blockModels
dispatchGroup.leave()
}, withCancel: { (error) in
dispatchGroup.leave()
})
let blockedByUsersRef = Database.database().reference().child(MainPath.blockingInfo.description).child(currentUserID).child(SubPath.blockedByUsers.description)
dispatchGroup.enter()
blockedByUsersRef.observeSingleEvent(of: .value, with: { (snap) in
if snap.value is NSNull {
dispatchGroup.leave()
return
}
guard let dict = snap.value as? [String : [String : Any]] else {
dispatchGroup.leave()
return
}
guard let blockModelsDict = Mapper<BlockModel>().mapDictionary(JSON: dict)?.values else {
dispatchGroup.leave()
return
}
let blockModels = Array(blockModelsDict)
generalBlockModels += blockModels
dispatchGroup.leave()
}, withCancel: { (error) in
dispatchGroup.leave()
})
dispatchGroup.notify(queue: .global(qos: .background), execute: {
completion?(generalBlockModels, nil)
})
}
}
}
}
此功能位于PeopleSearchManager
并调用云功能
func searchPeopleFirstRequest(success: ((_ searchLocationModels: [UserSearchLocationModel]) -> Void)?, fail: ((_ error: Error) -> Void)?) {
DispatchQueue.main.async {
guard let location = LocationManager.shared.currentLocation else { return }
let realmUserSettingsManager = RealmUserSettingsManager()
let miles = realmUserSettingsManager.getMaxDistanceInMiles() ?? 15
let parameters = ["miles" : miles, "latitude" : location.coordinate.latitude, "longitude" : location.coordinate.longitude] as [String : Any]
DispatchQueue.global(qos: .background).async {
Auth.auth().currentUser?.getIDTokenForcingRefresh(true, completion: { (token, error) in
if let error = error {
// Handle error
fail?(error)
return
}
guard let _token = token else { return }
let headers = ["X-Auth-MyApp-Token" : _token]
guard let url = URL(string: BackendEndPoint.searchPeopleFirstRequest.path) else { return }
AlamofireMyAppManager.shared.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers).responseJSON(completionHandler: { (response) in
switch response.result {
case .success(let value):
guard let dict = value as? [String : Any] else {
success?([])
return
}
guard let result = dict["result"] as? [[String : Any]] else {
success?([])
return
}
let userSearchLocationModels = Mapper<UserSearchLocationModel>().mapArray(JSONArray: result)
success?(userSearchLocationModels)
// for second request
if let lastUserSearchLocationModelID = dict["lastUserSearchLocationModelID"] as? String {
debugPrint("lastUserSearchLocationModelID", lastUserSearchLocationModelID)
self.peopleSecondRequestModel = PeopleSecondRequestModel(lastUserSearchLocationModelID: lastUserSearchLocationModelID, location: location, radius: miles, offset: userSearchLocationModels.count)
}
case .failure(let error):
debugPrint(error)
fail?(error)
}
})
})
}
}
}
云功能
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const GeoFire = require('geofire');
const moment = require('moment');
const map = require('lodash').map;
const filter = require('lodash').filter;
const sortBy = require('lodash').sortBy;
const last = require('lodash').last;
module.exports = functions.https.onRequest((req, res) => {
const token = req.header('X-Auth-MyApp-Token');
const miles = req.query['miles'];
const latitude = req.query['latitude'];
const longitude = req.query['longitude'];
let currentUserID;
let outputUsers;
const startTime = moment();
const timers = {};
admin.auth().verifyIdToken(token)
.then((decodedToken) => {
timers.afterVerify = moment() - startTime;
currentUserID = decodedToken.uid;
console.log('userID', currentUserID);
const getUsersSearchModelsByLocationPromise = new Promise((resolve, reject) => {
try {
const userLocationRef = admin.database().ref().child('userLocations');
const geoFire = new GeoFire(userLocationRef);
const geoQuery = geoFire.query({
center: [Number(latitude), Number(longitude)],
radius: Number(miles) * 1.609
});
const usersIDs = [];
const keyListener = geoQuery.on('key_entered', (key) => usersIDs.push(key));
geoQuery.on('ready', () => {
keyListener.cancel();
geoQuery.cancel();
timers.gotIdsFromGeofire = moment() - startTime;
resolve(usersIDs)
});
} catch (error) {
reject(error)
}
})
.then((userIDs) => {
return Promise.all(map(userIDs, (id) => admin.database().ref().child('userSearchLocationModel').child(id)
.once('value')
.then((snapshot) => snapshot.val())
))
.then((users) => {
timers.gotUserSearchLocationModels = moment() - startTime;
return Promise.resolve(users);
})
});
const getBlockedUsersIDsPromise = Promise.all([
admin.database().ref().child('blockingInfo').child(currentUserID).child('blockedUsers')
.once('value')
.then((snapshot) => snapshot.val())
.then((users) => {
timers.gotBlockedUsersIDs = moment() - startTime;
return map(users, 'userID')
}),
admin.database().ref().child('blockingInfo').child(currentUserID).child('blockedByUsers')
.once('value')
.then((snapshot) => snapshot.val())
.then((users) => {
timers.gotBlockedByUsersIDs = moment() - startTime;
return map(users, 'userID')
}),
]);
const getFilterSettingsPromise = admin.database().ref().child('userPeopleFilterSettings').child(currentUserID)
.once('value')
.then((snapshot) => snapshot.val());
return Promise.all([
getUsersSearchModelsByLocationPromise,
getBlockedUsersIDsPromise,
getFilterSettingsPromise
]);
})
.then(([users, blockedUsersIds, filterSettings]) => {
timers.gotAllInitData = moment() - startTime;
outputUsers = users;
// filter nulls
outputUsers = filter(outputUsers, (user) => user);
// filter blocked
outputUsers = filter(outputUsers, (user) => blockedUsersIds.indexOf(user.userID) === -1);
// filter by gender
outputUsers = filterSettings.filterGenderModeIndex !== 2 ?
filter(outputUsers, (user) => user.genderIndex === filterSettings.filterGenderModeIndex) :
outputUsers;
// filter by age
outputUsers = filter(outputUsers, (user) => {
const userAge = moment().diff(user.birthdayTimeStamp * 1000, 'years');
if (filterSettings.maxAgeValue === 55 && userAge >= 55) return true;
return (userAge >= filterSettings.minAgeValue && userAge <= filterSettings.maxAgeValue);
});
// sort by recent activity timestamp
outputUsers = sortBy(outputUsers, 'recentActivityTimeStamp');
// slice
outputUsers = outputUsers.slice(0, 99);
timers.filtersAndSortsDone = moment() - startTime;
return res.status(200).send({
timers: timers,
result: outputUsers,
lastUserSearchLocationModelID: last(outputUsers)
});
})
.catch((error) => {
console.log('Error: ', error);
return res.status(500).send({
code: error.code,
message: error.message,
stack: error.stack
});
});
});
另外我想说我读过这个post,但正如我在开头写的那样,即使加热功能也是这样的。我还问了我的问题here。结果如此大的差异是什么原因?如何解决这个问题?或者解决这个问题的方法是什么?