Long Task and Screensaver

On my Mac Big Sur, it seems that when my Xojo applications run a long task, they slow down a lot when the screensaver launch.
I use screensaver from JWZ.org
I have the same feeling when I run others applications too. It can be when I search inside folders, sync them, send many emails (emailing).

Is it just a feeling or can it be the case?

It could potentially be App Nap that is slowing down your applications.

If you use MBS plugins, you could try using NSProcessInfoActivityMBS to disable app nap while your applications are doing a long running process.
https://www.monkeybreadsoftware.net/class-nsprocessinfoactivitymbs.shtml

I’m not sure if that is what’s actually going on here, but it’s worth a shot.

1 Like

When you’re application’s windows are not visible, Apple will reduce the amount of CPU time that the application is given.

You need to inform the system that your application has started a process on the user’s behalf and then the system will keep feeding your application CPU time.

Declares can help you with NSProcessInfo.

I would also recommend looking at your code to see if you can optimize it, having slow code not only uses more CPU time, it will draw more power. There are several things you can do to improve your Xojo code, and once you can’t improve it any more, you can directly access system API via declares, or build a custom plugin that can use multi-core processing. Just last week I replaced some Xojo functions with direct API declares and this gave my app a speed improvement of 3.4x

Thank you for yours answers. I can’t buy MBSplugins + Xojo as I develop as a hobbyist, and I don’t know how to use Declares. I will search example with NSProcessInfo in case I reach to do something.
It’s strange the system reduce the cpu to the application running as except the screensaver, there’s not any other process. I would understand the system reduce to give the cpu to another process, but reduce without any reason I don’t understand why.
But I have my answer, it’s not a feeling but real.

IMHO, it’s actually a really useful energy saving solution, but it does take some understanding.

The idea is if the user can’t see the application, there is no need to it give full CPU time, this allows the OS to save energy and battery life for apps that the customer is not directly using.

It does mean as you’ve found that you need to notify the system when you don’t want this behavior. You should be able to find the declares somewhere (API 1.0) as I am certain that I have posted them on more than one occasion. They’re also available in my App Kit if you bought a copy.

As a developer you can even take it a step further, as you can become notified when the customer can’t see your window. This means if you’re doing a long task and have a pretty animation, you can pause the animation, thus reducing CPU usage even more.

I am also certain that there is probably at least one feedback filed over the years asking Xojo to add support for these into their framework, but we all know how that goes.

Something else to be aware of… on macOS, the system will throttle the CPU when the processors get hot:

As the thermal state increases, the system decreases heat by reducing the speed of the processors. Optimize your app’s performance by monitoring the thermal state and reducing system usage as the thermal state increases.

1 Like

Here are the declares that Sam was talking about…

#if TargetMacOS
// @property(class, readonly, strong) NSProcessInfo *processInfo;
Declare Function getProcessInfo Lib "Foundation" Selector "processInfo" (cls As ptr) As Ptr
Declare Function NSClassFromString Lib "Foundation" (name As cfstringref) As ptr

Dim procInfo As ptr = getProcessInfo(NSClassFromString("NSProcessInfo"))

// - (id<NSObject>)beginActivityWithOptions:(NSActivityOptions)options reason:(NSString *)reason;
Declare Function beginActivityWithOptions_reason Lib "Foundation" Selector "beginActivityWithOptions:reason:" ( obj As ptr , options As Integer , reason As CFStringRef ) As Ptr

Const NSActivityBackground = &h000000FF

// this one should only be used if NSActivityBackground isn't enough
// Apple's docs say: Very few applications should need to use this constant.
// https://developer.apple.com/documentation/foundation/nsactivityoptions/nsactivitylatencycritical?language=objc
Const NSActivityLatencyCritical = &hFF00000000 

Dim id As ptr = beginActivityWithOptions_reason(procInfo, NSActivityBackground, "long process that we don't want delayed")
#end if

// Do your process here

#If TargetMacOS
// - (void)endActivity:(id<NSObject>)activity;
Declare Sub endActivity Lib "Foundation" Selector "endActivity:" ( obj As ptr , activity As Ptr )
endActivity(procInfo, id)
#EndIf

If you’ll be doing this often, you could break this up into two methods, one for starting and the other for ending, or even raise an event in between that calls out to your method. If you truly are doing a long running process though, you should probably be doing it asynchronously in a thread, in which case this code would wrap your code in the Run event.

I’m also going to suggest that you check to see if the machine is in low power mode before running your long cpu intensive process:

#if TargetMacOS
// @property(class, readonly, strong) NSProcessInfo *processInfo;
Declare Function getProcessInfo Lib "Foundation" Selector "processInfo" (cls As ptr) As Ptr
Declare Function NSClassFromString Lib "Foundation" (name As cfstringref) As ptr

Dim procInfo As ptr = getProcessInfo(NSClassFromString("NSProcessInfo"))

// @property(readonly, getter=isLowPowerModeEnabled) BOOL lowPowerModeEnabled;
Declare Function isLowPowerModeEnabled Lib "Foundation" Selector "isLowPowerModeEnabled" (obj As ptr) As Boolean

If isLowPowerModeEnabled(procInfo) Then
  // think about deferring the process
End If
#EndIf
1 Like

Thank you Greg for the code, I will try to add it to my software.
Usually, when I launch a long task, I don’t stay doing nothing looking at my computer. I hide the program and read my emails or this forum. Or I put order in my papers and then the screensaver launch.
I see in Onyx that deactivate AppNat is a hidden feature, but I won’t do it.

On of my program list folder and its contents. If I list a folder containing 25000 files (in subfolders), it is longer than listing 5 folders of 5000 files one after the other. I reported a bug for that. I don’t know if it’s a Xojo problem or if it has something to do with the problem describe here (AppNap).

If you’re using For loops, make sure you’re using the iterator version. It’s many times faster than accessing items by index.

I recent found that Xojo’s handling of folderitem meta data is inefficient (another one that could have been taken care of Xojo had a dedicated platform engineer). Adapting an application to replace native Xojo with declares for reading file meta data, speed up my folder reading routine by 3.4x. It went from 12 seconds to read and compare 77K items (144k in total), to around 3 seconds.

The declares are more complicated than above. I documented this on the t’other site that Xojo won’t allow links to.

I forgot to say that in my process which memorise many folderitems, I list them in a ListBox. It may explain that it is long and longer when the listbox become fill with many lines.

I changed a little the GregO code, I call the Method with a flag to put it on and off. My process is in a Thread. I launch 2 copy of my app, one with this Method ON, and the other without (OFF). Then I look at Activity Monitor and it is the same :frowning: . I hide the 2 apps, the 2 are 100% cpu for a time and then 6% (approximatively).

Note : I have a Pref Checkbox in my application to let the user the choice. Then I use this Method if the user choose it and if the computer is not in low power mode.
I’m running Big Sur 11.7.8 (20G1351) and the last Xojo version 2023 r1.1 .

I think I know what’s wrong. I call my Method to put it ON (deactivate AppNap for the application) in my button, then I launch the Thread, then the Thread when it finish call the Method to put it OFF. But this Method does not deactivate AppNap for the whole application but only for the running thread right? (then the MainThread as I call the Method in a button).

Edit: I moved the call of this Method in the Thread and it seems to work.

These declares affect the whole application.

Here is the Method:

Public Sub AppliHighCpu(CeDrap as Boolean)
  
  #If TargetMacOS Then ' Active / DĂ©sactive le mode AppNap sur l'application
    Static TpDrapEtat as Boolean = False ' Le système met en mode low cpu une appli en arrière plan, cette méthode évite cela
    Static procInfo, Tp_id As Ptr ' Code donné par GregO dans le forum Xojo : https://forum.xojo.com/t/long-task-and-screensaver/76710
    
    If CeDrap Then ' On coupe AppNap pour que l'appli ne soit pas mise en Low Cpu par le système
      If not TpDrapEtat Then
        // @property(class, readonly, strong) NSProcessInfo *processInfo;
        Declare Function getProcessInfo Lib "Foundation" Selector "processInfo" (cls As Ptr) As Ptr
        Declare Function NSClassFromString Lib "Foundation" (name As cfstringref) As Ptr
        
        procInfo = getProcessInfo(NSClassFromString("NSProcessInfo"))
        
        ' - (Tp_id<NSObject>)beginActivityWithOptions:(NSActivityOptions)options reason:(NSString *)reason;
        Declare Function beginActivityWithOptions_reason Lib "Foundation" Selector "beginActivityWithOptions:reason:" ( obj As Ptr , options As Integer , reason As CFStringRef ) As Ptr
        
        Const NSActivityBackground = &h000000FF
        
        ' this one should only be used if NSActivityBackground isn't enough
        ' Apple's docs say: Very few applications should need to use this constant.
        ' https://developer.apple.com/documentation/foundation/nsactivityoptions/nsactivitylatencycritical?language=objc
        Const NSActivityLatencyCritical = &hFF00000000
        
        Tp_id = beginActivityWithOptions_reason(procInfo, NSActivityBackground, "long process that we don't want delayed")
        ' Else ' Je ne fais rien, je suis déjà dans ce mode
        ' #If DebugBuild Then
        ' MessageBox "Pas logique Tom, CeDrap est True et TpDrapEtat est True Ă©galement."
        ' #EndIf
      End If
    Else ' If not CeDrap Then  On revient dans le mode normal
      If TpDrapEtat Then
        ' - (void)endActivity:(Tp_id<NSObject>)activity;
        Declare Sub endActivity Lib "Foundation" Selector "endActivity:" ( obj As Ptr , activity As Ptr )
        endActivity(procInfo, Tp_id)
        procInfo = Nil
        Tp_id = Nil
        ' Else ' Je ne fais rien, je suis déjà dans ce mode
        ' #If DebugBuild Then
        ' MessageBox "Pas logique Tom, CeDrap est False et TpDrapEtat est False Ă©galement."
        ' #EndIf
      End If
    End If
    TpDrapEtat = CeDrap
  #EndIf
  
End Sub

And I call in the Method in the event which launch the process (a button or a Drag&Drop):

If AppNapHighCpu and (not IsOrdiModeEco) Then AppliHighCpu(True)

And just after this line of code I launch the Thread (MyThread.Start).
AppNapHighCpu is True if the user choose to deactivate AppNap for this application (Checkbox in the preferences of the Application).
IsOrdiModeEco is your other Method which return True if the computer is in low power mode.
The Thread finish launching a Timer in which there is the code:

AppliHighCpu(False)

And I have an hard crash (not a crash in the Xojo debugger). Here is the Log.

If I do

If AppNapHighCpu and (not IsOrdiModeEco) Then AppliHighCpu(True)

at the beginning of the Thread and

AppliHighCpu(False)

at the end of the Thread there is no crash.

IIRC you need to use

NSActivityUserInitiated = (0x00FFFFFFULL | NSActivityIdleSystemSleepDisabled)

For maximum performance.

NSActivityBackground

Can have a lower priority as it’s not a primary focus. Both of these will also prevent system sleep.

You have multiple things all slowing down the performance of your application.

  1. As Xojo doesn’t want to tackle pre-emptive threads, you need to use helpers to soak up entire CPU with what you actually want. This of course creates a “not a problem” because transporting that data between the two processes takes time. You also need to use declares to run a NSTask to avoid the 5% penalty from Xojo’s shell class.

  2. Use declares to read file meta data as you will get a performance boost over the native Xojo functions.

  3. Loading all that data into the listbox is slow. Xojo doesn’t natively support it, but switching to a modern grod control, should allow you to only display the data that actually needs to be visible at this time.

  4. If you really want performance, I would highly recommend looking at alternative options for making Mac apps.

You may want to consider, removing the code that stores the data in the listbox to see how just reading the data takes. Remember I was able to make a Xojo app read 144,000 files in around 3 seconds by using declares to read the meta data. This is also in a thread, I could get it even faster if I went the helper route, but to be honest it pushes against the threshold of the amount of work v.s. performance gain.

Without pre-emptive threading, it’s always going to be more work in Xojo to get performance you can get in other tools. In Swift for instance it’s a couple of lines and boom, you’re rocking.

I don’t know if it is procInfo and/or id which change and cause the crash but I have to launch my Method to activate and launch it again to deactivate it in the same event.
→ If I start a Thread, I have to turn it on and off inside the Thread. Same in an Action event of a Timer, I have to turn it on in the same event as I turn it off. If I turn it on, then the Timer relaunch itself and I turn it off in this new Timer event it crash.

Sam, I don’t need a very high speed in my applications. My first question was if I was dreaming or if it was the case. As Greg kindly post a Declare I tried to use it and it seems I understood how to add it in my application.
Learn a new development tool would be very hard to me.

I get it, learning a new tool and language is scary, it’s uncomfortable, for sure. Once you see the tangible benefits it can bring, it really does help overcome that fear.