Does iOS kill my app for high CPU usage?

Good Morning Everyone!


I am experiencing a strange phenomenon with my app.


Context:


My app needs to work completely offline. As a result, it needs to download massive amounts of data, as well as hundreds of thousands of images during the initial sync.


Once this content is downloaded the first time, the app performs delta updates, so only the first sync takes a long time (~6 hours).


The syncing is handled across many threads, each managed by NSOperationQueues and/or NSURLSession tasks.


The Problem:


The problem I keep running into is that after ~20 minutes of syncing the first time, the app crashes. The only report I can access from the device logs state that I am using too much CPU.


Specifically, the log I get is "exceeding limit of 50% cpu over 180 seconds."


The environment varies, but usually the app is running on an iPad Pro (various gens) running iOS 12 or iOS 13.


I am not 100% sure that this is a crash log and not simply an energy log, but the timing of receiving this message and the app crashing is too correlated to not be related.


Bottom Line:


Bottom line my question is this: will iOS kill my app for high CPU usage IN THE FOREGROUND? And if so, is there a way to either disable that, or appeal for a higher CPU usage limit?


I am aware that high CPU usage will get my app killed in the background, but mine is getting killed in the foreground repeatedly and consistently, whilst performing user-initiated work!


Strangely, my 2016 iPad Pro (1st gen 9.7"), running iOS 12, has not crashed at all! I'm not sure if this is a new limiter added in the newer hardware, or something added in iOS 13, or what. But my 2016 device has no problems with my app syncing!


I have tried to analyze the usage with Instruments, but physics is physics, I simply have so much content that I need to be using multiple threads to sync.


I am using between 10 and 20 threads to manage all of my syncing, broken up by content type.


I'm really at the end of my rope on this one. I have tried all the usual tricks:

  • Implement NSURLSessions
  • Implement NSOperationQueues
  • Perform the sync in batches
  • Between each batch, put the threads to sleep to allow the system to reallocate resources.


I don't know what else to try and am hoping someone here has a new trick to teach me!


Any help is greatly appreciated!


Thanks,
~Arash


EDIT 1 (08 OCT 2019 1325 EST):


I thought it worth clarifying that I have run the app with instruments and do not see unbounded memory usage, leaks, or zombies while the app is syncing.


While I do see the CPU is being used heavily (90%), this makes sense, as my app is performing foreground work and trying to do it as fast as possible on behalf of the user.


There are also no other crash logs from the devices relating to my app.


EDIT 2 (09 OCT 2019 1000):


It seems I have found a way to cure the CPU usage.


The way that my NSOperationQueue operations work is by firing up an NSURLSession task, and then using an NSCondition to wait for the task's asyncronous completion handler to fire.


In the completion handler for the task, if I call [NSThread sleepForTimeInterval:0.025], the CPU usage drops from being well above 100% (in some cases even above 200%) on the simulator, to being ~30%.


While this does keep the CPU usage low, it significantly reduces the speed with which the app is able to download it's content.


Another workaround, which I admit is not ideal, is to let the original process go at full bore, and simply set up a timer to suspend the queue every 2 minutes for 5 seconds. Using this approach, the CPU is NOT being used above 50% for 180 seconds, as the Duet CPU Activity Scheduler complains about, because the tasks never holds the CPU for more than 2 minutes. This works, but could be a moving target if Apple decides to change the limiter values.


I'm thinking that the better solution would be to use synchronous download tasks, and avoid using the NSCondition all together.


Anyway, hope this info helps someone!


Thanks,
~Arash

Replies

I just wanted to say thanks for sharing not only your problem but also approach to solution. I am currently experiencing something similar although on a smaller scale.

I am running in the background in the "heavier" BGProcessingTaskRequest. And get crashed like so:

Event:            cpu usage
Action taken:     Process killed
CPU:              48 seconds cpu time over 55 seconds (87% cpu average), exceeding limit of 80% cpu over 60 seconds

My current suspicion is that I am using DispatchQueue.global(qos: .background) which might look for such a limit but I am not sure about it. Also the docs specifically ask to use this queue in the background. Maybe it is something different but it would be odd to claim you can train a machine learning model in the background if you are not allowed to use more than 80% cpu over 60 seconds.