Kastri series continued…
In this post I wanted to talk about how Delphi apps run. As an app developer, especially when targeting mobile devices using Firemonkey (FMX) with Delphi, it’s important that you understand what happens within the framework, app code and components when the user taps on your app icon.
Getting this wrong, at best case, can result in slow app launch times which can frustrate users. Or worst case, cause the OS watcher service to think your app has failed to load because it’s taking too long and kill it before the user even sees it run.
Either way, not good and easy to avoid if you understand what actually happens during the app startup.
So where does Kastri fit into this? It includes some useful classes you can use to improve app load times and performance, but more on this later.
Let’s start at the beginning
In your project sources, you will notice a few files that impact app initialisation:
- <AppName>.dpr
- <AppName>.dproj
- <AppName>.deployproj
The .dpr file contains the app startup code (ObjectPascal). We’ll talk about this more shortly.
The .dproj file contains the project configuration. All the settings you configure in Project -> Options in the IDE are stored in here along with references to all the files to be deployed at build time.
The .deployproj file is generated when your app is built and contains references to all files deployed with your app (i.e. everything to be copied into the app bundle).
The app run flow
When a user taps on your app icon (or when you run the app through the IDE), the following happens:
For this example, let’s use this minimal app .dpr sources below for reference:
program MyApp;
uses
System.StartUpCopy,
FMX.Skia, //if using Skia
FMX.Forms,
Unit1 in 'Unit1.pas' {Form1};
{$R *.res}
begin
//If using Skia
GlobalUseMetal := True;
GlobalUseVulkan := True;
GlobalUseSkia := True;
GlobalUseSkiaRasterWhenAvailable := False;
//Applicable to Skia or FMX apps
Application.Initialize;
Application.CreateForm(TForm1, Form1);
Application.Run;
end.
- The app binary is loaded into memory.
- The Firemonkey framework is loaded into memory
- The .dpr file is “run”, starting with all the units listed at the top of this file.
- For each unit, if it has any code in the initialization section at the bottom of the unit, it will be run. This includes all the Delphi-provided units as well as your own.
TIP: if you must have code in the initialization section of your units, keep it to a minimum and make sure it’s very fast. - If you’re using Skia, one of the first unit will be for the Skia library. The initialization code for this loads the Skia library into memory.
- The first unit to initialize is System.StartupCopy.pas. This unit has only one purposes – it copies every file referenced in the Deployment Manager into local storage on the device. Some are added by default for all apps by Delphi, but you can also include any files you need for your app in here too.
If you have a lot of files or they are large, this can take some time.
TIP: Avoid deploying large files, or lots of files with your app.
If the app needs large files or lots of data, try getting these another way (e.g. download from an API after all startup), that may improve all load times.- It’s also important to realise that there’s a known bug in the System.StartupCopy.pas unit. If the file already exists in local storage it doesn’t replace it (even if you set it to do so in the Deployment Manager). This means you can’t deploy updated support files with your app.
Kastri has a great solution to this, to avoid having to import a custom copy of the StartupCopy.pas file into your project to work around this.
See DW.StartupCopy.pas for details.
- It’s also important to realise that there’s a known bug in the System.StartupCopy.pas unit. If the file already exists in local storage it doesn’t replace it (even if you set it to do so in the Deployment Manager). This means you can’t deploy updated support files with your app.
- After running through all the units, Application.Initialize; is called. This sets up the core app capabilities required for the target platform and configures the framework. This takes a fraction of a second in most cases.
- Next, each form listed as Auto-Create in the Project Options is created. FormCreate is called for each of these forms (which also runs in the main app thread).
- Finally, Application.Run is called which displays the first form and activates the app and context.
So all of this happens before the first form even appears on-screen. This is usually hidden by a splash screen, but the more complex the app is, the longer it will take.
The most important thing to realise is that all of this happens sequentially in the main app thread.
Why is this important?
All UI operations of an app must happen in the main thread, so while your app is doing anything else on the same (main) thread – such as all of the app startup processes, the UI can’t respond to user interaction.
This means your app appears frozen to the user until the main thread is freed up (after all the Forms are created and the Form events are finished).
Not so much of a problem when the app is covered with a splash screen, but if it takes too long, the OS will see it as frozen and crash the app (known as an ANR on Android (App Not Responding)).
The minimal app generated by Delphi is very fast. 1-2s on most devices, which is great. However, when you start to add lots of units and screens to your app, this increases.
If your have advanced UIs in your forms, it’s likely you’ll need UI setup code to run before it shows (usually in FormCreate) – all of that causes the app to take longer to load if this is your main form.
Add third party components or frameworks and this increases again.
Need custom app initialization code into the .dpr file? This all runs in sequence and increases the app load time again.
Before you know it, your app is taking 10-15s to load on older Android devices and your users start to grumble very loudly – if they stay with your app at all.
So what can you do to mitigate the risks of a long start-up time?
Performance Tips
1. Be careful what you run in the main thread
Concurrent programming is an art form and can be difficult to get right when you first start coding. However, for performant mobile apps, it’s an essential skill to understand and use correctly.
The key things are:
- Any time you add, update or interact with a UI component, it must be done in the main thread. If you accidentally update the UI in a background thread, the compiler won’t tell you but your app will crash. And not every time either – it will do so at random (not helpful).
- Delphi provides the TThread class for working with threads. You can create threads for background work, anonymous threads for one-off background work and TThread.Synchronize() or .Queue to jump into the main thread whenever you need to do UI updates. (This blog won’t go through these and when or how you’d use them, but here’s a Stack Overflow post which explains it)
Kastri provides a useful set of convenience methods to make working with threads easier.
DW.Classes.Helpers.pas
The class TDo contains several helper functions for threads:
TDo = record
public
/// <summary>
/// Determines whether the current execution is in the main thread
/// </summary>
class function IsMainThread: Boolean; static;
/// <summary>
/// Queues a method for execution after an optional delay
/// </summary>
class procedure Queue(const AProc: TThreadProcedure; const ADelay: Integer = 0); static;
/// <summary>
/// Queues a method in the main thread, if necessary
/// </summary>
class procedure QueueMain(const ARunProc: TThreadProcedure); static;
/// <summary>
/// Synchronizes a method for execution after an optional delay
/// </summary>
class procedure Sync(const AProc: TThreadProcedure; const ADelay: Integer = 0); static;
/// <summary>
/// Syncs a method in the main thread, if necessary
/// </summary>
class procedure SyncMain(const ARunProc: TThreadProcedure); static;
/// <summary>
/// Runs a method in a thread
/// </summary>
class procedure Run(const ARunProc: TThreadProcedure); static;
/// <summary>
/// Runs a method in a thread and queues/syncs a callback if supplied
/// </summary>
class procedure RunQueue(const ARunProc: TThreadProcedure; const ACallbackProc: TThreadProcedure = nil); static;
/// <summary>
/// Runs a method in a thread and queues/syncs a callback if supplied
/// </summary>
class procedure RunSync(const ARunProc: TThreadProcedure; const ACallbackProc: TThreadProcedure = nil); static;
end;
The most useful here are:
TDo.Run(procedure
var
LAVariable: String;
begin
//The code to run in a background thread
TDo.Sync(procedure
begin
//Any UI update code that will be run in the main thread. Note that
//it captures any variables from the thread such as LAVariable which
//can be very useful
end);
end);
It also provides functions to combine the above example for convenience:
TDo.RunSync(procedure
var
LAVariable: String;
begin
//The code to run in a background thread
end,
procedure
begin
//Any UI update code that will be run in the main thread. Note that
//this version DOESN'T captures any variables from the thread
//such as LAVariable unless it's declared in the outer function.
end));
2. Avoid blocking up the FormCreate event handler of the main form
This is a classic performance issue. The app startup sequence is all run in the main thread (as mentioned), including the form creation of any auto-created forms.
In fact, all event handlers are run in the main thread.
When you create and show a form, the following events fire (and handler code is run) – all in the main form:
- FormCreate
- FormShow
- FormActivate
If you put a lot of code in any of these handlers, the form can take a while to appear to the user and the app will look frozen while it does so.
A common problem is when you have something that needs to happen when the form is first created, or first shown. E.g. a call to get the data to be shown from an API or file.
For something that can run in a background thread, you have the TDo class mentioned above (e.g. an API call) but if it’s a UI-related operation that must be run in the main thread, this can be a problem as the form won’t appear until the code is run.
Kastri has another great tool to help with this.
DW.FMX.Helpers.pas
The TIdleCaller class allows you to run code later when the app isn’t doing something else.
How does this work? Under the hood, all apps run using a Run Loop. This means that the app flow, at a very low level is:
- Allocate memory, load the app code into memory
- OS-level initialisation stuff
- Start an event loop
– is there an event triggered (e.g. screen tap or key pressed?) -> yes? Run the hander
– has the app been terminated, or finished? Exit the loop
– keep looping!
It sounds very basic, but at a very basic and low level, this is how every app runs. The app sits in a loop. When an event occurs, the appropriate code is run but until it does, the loop continues indefinitely.
While the app has no events to run, it’s called Idle Time. Delphi apps can hook into it and run code when the app isn’t doing anything else – i.e. the least inconvenient time for your user as the app isn’t doing anything anyway.
If some code that needs to be run in the main thread can be delayed until “idle time” then the app performance can appear a lot better as nothing is being blocked at a time when the user is expecting something else to happen (e.g. for the form to appear).
TIdleCaller.CallOnNextIdle(procedure
begin
//Do something on the main thread at a later time
end);
What if the long-running code impacts the initial form UI? That’s up to you, but the usual solution is to have some “holding” UI (e.g. a “loading your data” message with TAniIndicator spinning away) which is hidden once the delayed code has been able to do it’s magic and update the UI.
3. Do as little custom work as possible in the .dpr file
We made this mistake. We had lots of core app data we needed to check before it was used (e.g. make a local backup, verify integrity etc). Logically we thought – we’ll do this in the .dpr before the main form creation so nothing will be using it…
A nice theory, but as the app was used more and the data files grew in size, the time these processes took increased, as did the app load times.
The lesson here is – just don’t do that. Keep any custom changes in the .dpr to be state-based (e.g. registering custom fonts for Skia, setting global app configurations etc) and find another way to call other code which doesn’t block up the app loading.
A quick reference back to StartupCopy.pas
The Kastri solution to the bug in System. StartupCopy.pas is to use their TStartupCopy.CopyDocuments() function in your .dpr file AFTER the System.StartupCopy.pas file has done it’s job and before Application.Initialise;
This allows you to manually force-copy certain files (or entire folders) from within the app bundle to the local file system to work around the bug.
uses DW.StartupCopy;
TStartupCopy.CopyDocuments(['whatsnew.html','changelog.log']);
It’s easy to do, but do so with caution. File copies can be slow, especially on cheap Android phones where they use slow and cheap memory components. Only copy files that you know have been changed and need to be updated.
And remember that this extra copy will be done in the app main thread before the first form is displayed so can make your app load times increase if over-used.
These are a few tips to help keep your app starting and running well, and I hope you found them useful.
For a lot more detail (and more suggestions) on writing high performance Delphi apps, I highly recommend the Delphi High Performance book by Primož Gabrijelčič.
Happy coding!