Handle thousands of socket message in iOS - ios

I am working on a code where I need to display socket messages in the tableview and tableview get scroll to bottom with slow animation. I handle that but if the message load from socket is continues (consider 20/30 message per second) then the UI get freeze. I need to show the message like Facebook do on live screen one or two messages per iteration.
Here is my code
override func viewDidLoad() {
super.viewDidLoad()
Timer.scheduledTimer(timeInterval: 0.025, target: self, selector: #selector(self.scrollTableView(_:)), userInfo: nil, repeats: true)
self.designLayout()
}
#objc func scrollTableView(_ timer: Timer) {
guard messagesData.count > 0 else {
return
}
if tableView.contentSize.height > tableView.bounds.height {
tableView.contentInset.top = 0
}
tableView.scrollToRow(at: IndexPath(row: messagesData.count - 1, section: 0), at: UITableViewScrollPosition.bottom, animated: true)
}
self.socket.on(“key”) {data, ack in
print("data type is \(type(of: data))")
let arrayValue = data as Array<Any>
DispatchQueue.global(qos: .background).async {
self.handleCountsAndMessaging(data: arrayValue)
}
}
func handleCountsAndMessaging(data: Array<Any>) {
if let arrData = data[0] as? NSMutableDictionary {
if(arrData.object(forKey: "text") != nil) {
self.tableDataLoading(str: String(describing: arrData.object(forKey: "text")!))
}
}
}
func tableDataLoading() {
print ("sttttttt \(str)")
DispatchQueue.main.async {
self.messagesData.add(str)
self.tableView.reloadData()
}
}
Some times I get messages like 200 at time from socket and again 200 while processing previous messages, CPU consumption is showing like 120 and UI get freeze.
Thanks in advance.

Well, there is 'live' and 'live':)
This a phone we are talking about, so it's useless to update the screen every 0.025 seconds... you can easily slow down your fetch time but let's work one problem at a time :
1/ Don't use tableView.scrollToRow, but rather 'insertRows(at indexPaths: [IndexPath], with animation: UITableViewRowAnimation)' to insert a new row, this way if your user is reading, something, he won't be disturbed if you add 200 new item
https://developer.apple.com/documentation/uikit/uitableview/1614879-insertrows
2/ Slow down your UI updates, you are overusing your main thread... You can fetch every 25ms if you like, but I recommend using a throttling mechanism to update the UI only if you get new content, and only if last UI update wasn't less than 5 seconds ago.

Related

Looking for best practices when updating a tableview by monitoring for changes in DB

I'm somewhat new to this and this is my first question on stackoverflow. Thanks in advance for your help and bear with me if my formatting sucks
I've got multiple views within my app (all displaying data using tableview subviews) that need to update automatically when the data changes on the database (Firestore), i.e. another user updates the data.
I've found a way to do this which is working well, but I want to ask the community if there's a better way.
Currently, I am creating a Timer object with a timeInterval of 2. On the interval, the timer queries the database and checks a stored data sample against updated data. If the two values vary, I run viewDidLoad which contains my original query, tableView.reloadData(), etc..
Any suggestions or affirmations would be very useful.
var timer = Timer()
var oldChallengesArray = [String]()
var newChallengesArray = [String]()
override func viewDidLoad() {
super.viewDidLoad()
//set tableview delegate
mainTableView.delegate = self
mainTableView.dataSource = self
//set challengesmodel delegate
challengesModel.delegate = self
//get challenges
DispatchQueue.main.async {
self.challengesModel.getChallenges(accepted: true, challengeDenied: false, incomingChallenges: false, matchOver: false)
self.mainTableView.reloadData()
}
scheduledTimerWithTimeInterval()
}
func scheduledTimerWithTimeInterval(){
// Scheduling timer to Call the function "updateCounting" with the interval of 1 seconds
timer = Timer.scheduledTimer(timeInterval: 2, target: self, selector: #selector(self.updateTableView), userInfo: nil, repeats: true)
}
#objc func updateTableView(){
ChallengeService.getAllUserChallengeIDs(accepted: true, challengeDenied: false, matchOver: false) { (array) in
if array.isEmpty {
return
} else {
self.newChallengesArray = array
if self.oldChallengesArray != self.newChallengesArray {
self.oldChallengesArray = self.newChallengesArray
self.newChallengesArray.removeAll()
self.viewDidLoad()
}
}
}
}
Firestore is a "realtime database", that means that the database warns you when changes happen to the data. To achieve that the app needs to subscribe to relevant changes in the db. The sample code below can be found here:
db.collection("cities").document("SF")
.addSnapshotListener { documentSnapshot, error in
guard let document = documentSnapshot else {
print("Error fetching document: \(error!)")
return
}
guard let data = document.data() else {
print("Document data was empty.")
return
}
print("Current data: \(data)")
}
Also, I would like to point out that calling viewDidLoad is incorrect, you should never call viewDidLoad yourself, create an func to update the data. Something like this:
DispatchQueue.main.async {
self.mainTableView.reloadData()
}

Table View error on reloading sections - seems to be some race condition

I have some hard to solve crashes. They happen in application from time to time not in regular way. I think the problem maybe with some race conditions and synchronizations.
I am using such pattern.
1) reloadData()
2) reloadSection1(), reloadSection2(), reloadSection2(), etc.
3) I have tap events that can do reload like reloadData()
4) There are also Socket.IO messages that can cause reloadData(), reloadSectionN()
5) I try to use debouncers to execute only last reload of given type.
6) debouncers use Serial Queue to execute task serially one by one in case that request-response-reload last longer then new socket.io event arrives
7) section/table reloadin must happen on UI thread so at the end I moves to it using DispatchQueue.main.async { }
8) I have even tried to wrap data excesses/ reloads into semaphores to block modification by other threads.
But errors can happens and app can crash in spite of this. And I have no idea what may cause it.
Below I place the most important parts of code.
I have such instance properties:
private let debouncer = Debouncer()
private let debouncer1 = Debouncer()
private let debouncer2 = Debouncer()
private let serialQueue = DispatchQueue(label: "serialqueue")
private let semaphore = DispatchSemaphore(value: 1)
Here Debouncer.debounce instance method
func debounce(delay: DispatchTimeInterval, queue: DispatchQueue = .main, action: #escaping (() -> Void) ) -> () -> Void {
return { [weak self] in
guard let self = self else { return }
self.currentWorkItem?.cancel()
self.currentWorkItem = DispatchWorkItem {
action()
}
if let workItem = self.currentWorkItem {
queue.asyncAfter(deadline: .now() + delay, execute: workItem)
}
}
}
Here are debounced reloads of table view and its sections
func debounceReload() {
let debounceReload = debouncer.debounce(delay: .milliseconds(500), queue: serialQueue) {
self.reloadData()
}
debounceReload()
}
func debounceReloadOrders() {
let debounceReload = debouncer1.debounce(delay: .milliseconds(500), queue: serialQueue) {
self.reloadOrdersSection(animating: false)
}
debounceReload()
}
This debounce methods can be invoked on tap, pull to refresh screen navigation, or Socket.IO events (here possible multiple events at the same time if there are multiple users).
Each reload called by debounce reload start and ends with such methods (in between there are synchronous requests to remote api that may take some time (and are executed on this serialqueue). All debouncers reuse the same serial queue (so they should not conflict/race with each other and caused data inconsistency while reloading tableview or its sections).
private func startLoading() {
print("startLoading")
activityIndicator.startAnimating()
activityIndicator.isHidden = false
tableView.isHidden = true
// cancel section reload
debouncer1.currentWorkItem?.cancel()
debouncer2.currentWorkItem?.cancel()
}
private func stopLoading() {
guard debouncer.currentWorkItem?.isCancelled != true else { return }
self.semaphore.wait()
print("stopLoading")
tableView.reloadData()
activityIndicator.isHidden = true
refreshControl.endRefreshing()
tableView.isHidden = false
self.semaphore.signal()
}
Above are added this additional semaphores as additional check to ensure data consistency.
func startLoading(section: Int, animating: Bool = true) {
self.semaphore.wait()
print("startLoading section \(section)")
tableView.beginUpdates()
if animating {
self.data[section] = .loadingSpinner
}
tableView.reloadSections([section], with: .none)
tableView.endUpdates()
self.semaphore.signal()
}
func stopLoading(section: Int, model: Model) {
self.semaphore.wait()
print("stopLoading section \(section)")
if section == 0 {
guard debouncer1.currentWorkItem?.isCancelled != true else { return }
} else if section == 1 {
guard debouncer2.currentWorkItem?.isCancelled != true else { return }
}
tableView.beginUpdates()
self.data[section] = model
tableView.reloadSections([section], with: .none)
tableView.endUpdates()
self.semaphore.signal()
}
private func clearData() {
self.semaphore.wait()
print("clearData")
data.removeAll()
self.semaphore.signal()
}
I think this extra semaphores should not be required as this is using serial queue so all request-response-reload are executed on serial queue. Maybe some problem is that I need to switch from serial queue to main queue in order to clear/add spinner-reload and then fill-data/reload table or section. But I think it should last shorter then next data replacement happens. I consider moving self.data.append(model1) to semaphore critical section in stopLoading() for reloadData() using self.data = data assignment in this critical section.
Example error that I have encountered:
Fatal Exception: NSInternalInconsistencyException Invalid update:
invalid number of rows in section 0. The number of rows contained in
an existing section after the update (3) must be equal to the number
of rows contained in that section before the update (5), plus or minus
the number of rows inserted or deleted from that section (0 inserted,
0 deleted) and plus or minus the number of rows moved into or out of
0x195bef098 +[_CFXNotificationTokenRegistration keyCallbacks]
3 Foundation 0x1966b2b68 -[NSAssertionHandler handleFailureInMethod:object:file:lineNumber:description:]
4 UIKitCore 0x1c23ecc78 -[UITableView _endCellAnimationsWithContext:]
5 UIKitCore 0x1c24030c8 -[UITableView endUpdates]
6 MyApplication 0x104b39230 MyViewController.reload(section:) + 168
> that section (0 moved in, 0 moved out).
I have also seen errors on cellForRow function, this errors happen several times a week, app is rather used multiple times a day so it is hard to repeat this error. I have tried to send simple refreshing sockets from POSTman but they do not change underling data (row count) and I think is way everything works then ok.
UPDATE
I have updated stopLoading() to stopLoading(data:) to have datasource update and tableView.reload on main queue.
So all startLoading, stopLoading, reload methods are performed on DispatchQueue.main.async { }.
private func stopLoading(data: [Model]) {
guard debouncer.currentWorkItem?.isCancelled != true else { return }
self.semaphore.wait()
print("stopLoading")
self.data = data
tableView.reloadData()
activityIndicator.isHidden = true
refreshControl.endRefreshing()
tableView.isHidden = false
self.semaphore.signal()
}

large uitableview (1000 rows) freezes on reloadData()

I have a UITableView with about 1000 rows. I also have a timer running every 6 seconds that fetches data from a web service. Each time I call reloadData() there is a blip - my app freezes very noticeably for a brief moment. This is very evident when scrolling.
I tried fetching about 400 rows only and the blip disappears. Any tips how to get rid of this while still fetching the 1000 rows?
var items: [Item] = []
Timer.scheduledTimer(withTimeInterval: 6, repeats: true) { [weak self] _ in
guard let strongSelf = self else { return }
Alamofire.request(urlString, method: method, parameters: params) { response in
// parse the response here and save it in array called itemsFromResponse
OperationQueue.main.addOperation {
strongSelf.items = itemsFromResponse
strongSelf.itemsTableView.reloadData()
}
}
}
UITableViewDataSource code:
extension ItemViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "itemCell", for: indexPath)
cell.textLabel?.text = items[indexPath.row].name
return cell
}
}
The problem is being caused because you are storing the items from the response and then updating the table view from the same OperationQueue, meaning that the UI thread is being blocked while your array is being updated. Using an operation queue in itself is not an optimal way to schedule tasks if you do not need fine grain control over the task (such as cancelling and advanced scheduling, like you don't need here). You should instead be using a DispatchQueue, see here for more.
In order to fix your issue, you should update your array from the background completion handler, then update your table.
Timer.scheduledTimer(withTimeInterval: 6, repeats: true) { [weak self] _ in
guard let strongSelf = self else { return }
Alamofire.request(urlString, method: method, parameters: params) { response in
// parse the response here and save it in array called itemsFromResponse
strongSelf.items = itemsFromResponse
// update the table on the main (UI) thread
DispatchQueue.main.async {
strongSelf.itemsTableView.reloadData()
}
}
}
You should also maybe look into a more efficient way to fetch new data, because reloading the entire dataset every 6 seconds is not very efficient in terms of data or CPU on the user's phone.
The problem is you are reloading data every 6 seconds, so if the data is so big you're reloading 1000 rows every 6 seconds. I recommend you request the data and compare if there's new data so in that case you need to reload data or you simply ask to refresh once. For example:
var items: [Item] = []
Timer.scheduledTimer(withTimeInterval: 6, repeats: true) { [weak self] _ in
guard let strongSelf = self else { return }
Alamofire.request(urlString, method: method, parameters: params) { response in
// parse the response here and save it in array called itemsFromResponse
OperationQueue.main.addOperation {
if(strongSelf.items != itemsFromResponse){
strongSelf.items = itemsFromResponse
strongSelf.itemsTableView.reloadData()
}
}
}

Memory growth when using DispatchQueue. Instruments shows growth in non-object objects

The memory of my app (navigation app) keeps on growing until iOS kills it sooner or later. I was able to pinpoint the problem to the use of a custom DispatchQueue in a timer target function that fires every second. (i.e. while navigating a timer is running that fires every second and executes the updateUI function below.)
This is an extract of the viewcontroller:
class NavViewController: UIViewController, ... {
...
let processLocationUpdateQeue = DispatchQueue(label: "processLocationUpdateQeue", qos: .userInteractive)
var updateUITimer: Timer!
...
func startNav() {
...
DispatchQueue.main.async { [unowned self] (_) -> Void in
self.updateUITimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(NavViewController.updateUI), userInfo: nil, repeats: true)
}
...
}
func updateUI() {
let freeDrive = self.isFreeDrive ?? false
processLocationUpdateQeue.async { [unowned self] in
self.totalBreakTimeInclCurrentBreak = self.totalBreakTimeInSec
if !self.takingBreak {
if self.timeOfLastPositionUpdate != nil && self.timeOfLastPositionUpdate.secondsAgo() >= 90 {
// some code that's not being executed during my analyses of this problem
}
}
else {
self.totalBreakTimeInclCurrentBreak = self.totalBreakTimeInSec + Float((self.breakStartTime ?? Date()).secondsAgo())
if ((self.autoResumeSwitch != nil && self.autoResumeSwitch!.isOn) || self.autoResumeSwitch == nil) && self.breakStartCoordinate?.distance(from: lastKnownLocation.coordinate.toCLLocation()) > 100 {
// some code that's not being executed during my analyses of this problem
}
}
self.avgSpeedMperS = min(self.maxSpeedMperS,self.coveredDistanceInMeters/(Float(self.tripStartTime.secondsAgo())-self.totalBreakTimeInclCurrentBreak))
if freeDrive && UIApplication.shared.applicationState == .active {
// app is running in background for my analyses, so the code here doesn't get executed
}
}
}
}
When I run my app using Instruments allocations tool I see the memory growing. Most (in fact all) of the growth is in non-object and the stack trace points to the updateUI function. See this screenshot:
Note the trace step just after (above) the step where the function is called. It's something with DispatchQueue.
I've been analysing this for 2 days now, tried a bunch of things. Here's what I know at this point:
Memory does not grow if I take the code out of processLocationUpdateQeue.async {} and run it on the main queue.
Wrapping the code inside processLocationUpdateQeue.async {} into autoreleasepool {} doesn't help
Using processLocationUpdateQeue = DispatchQueue(label: "processLocationUpdateQeue", qos: .userInteractive, attributes: [], autoreleaseFrequency: .workItem, target: nil) (note the .workItem) doesn't help.
I'm using [unowned self] so retain cycle is not the problem (I think)??
I tried using [weak self], same problem.
I tried DispatchQueue(label: "processLocationUpdateQeue", qos: .userInteractive).async {} instead of using the class let processLocationUpdateQeue, same problem
Simulating a memory warning doesn't 'clean' the growth
The memory graph debugger doesn't show any leaks
What am I doing wrong?
UPDATE
So, this is interesting. I only see this when my app is running in the background. Also, when I bring my app to the foreground the related non-objects that were built up while backgrounded, disappear !!!

Swift/Parse - No Results Matched the Query when Updating Data

I have recently been playing around with parse,and yes, I know that it is closing soon:(. I have this error however that keeps on coming up. I am trying to update my textview.text to parse every single second. However, when the code runs a message comes up saying "no results matched the query", even though textview.text is not empty. I would really appreciate your help. Thanks.
override func viewDidLoad() {
super.viewDidLoad()
notes["Content"] = detailDescriptionLabel.text
notes.saveInBackgroundWithBlock { (succes, error) -> Void in
if error != nil {
print("unable to save objects")
}
}
scheduledTimerWithTimeInterval()
// Do any additional setup after loading the view, typically from a nib.
self.configureView()
}
func scheduledTimerWithTimeInterval(){
// Scheduling timer to Call the function **Countdown** with the interval of 1 seconds
timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: Selector("updateText"), userInfo: nil, repeats: true)
}
func updateText () {
let query = PFQuery(className: "Notes")
query.getObjectInBackgroundWithId("Content") { (notes, error) -> Void in
notes?["Content"] = self.detailDescriptionLabel.text
print("Updated")
}
}
Picture of error in Log
Image of Parse Dashboard

Resources