Firemonkey is one of the most fascinating additions to RAD Studio ever. In terms of cross-platform developments, it ranks up there in a surprisingly small group of technologies that can span the gap of mobile and desktop, Windows, Android and Apple with a single code base. And it continues to evolve every 6 months...
I've been working with Delphi for a very long time - back from the Turbo Pascal days in fact - so been through all the pain of the Inprise years, saw the crumble of their IDE and the amazing work from Embarcadero to get things back to what they are today. XE 7 is incredible and by far the most stable, feature rich development environment I've seen in years from Borland/Inprise/Codegear/Embarcadero (did I miss any? :) )
Firemonkey was introduced in XE2 and was intriguing... Mac and Windows support, UI rendered on the graphics card to allow some very cool effects and animations out-of-the-box (have any of your tried animating a control in the VCL... if not, just don't!) - it was fascinating and very promising. Given Codegear/Embarcadero's reputation at the time for quick and shoddy releases and not-very-useful updates, I left well alone but continued watching from the sidelines to see what would happen.
A few months ago I decided it was time to take Firemonkey for a run in XE 7 to see what it could do...
The goal of my journey was to re-create my popular myShiftPlanner iOS app using Firemonkey to allow me to release it to Android and have a single code-base I could evolve the app from.
This blog follows that journey and allows me to share the knowledge gained throughout. I intend to use it to provide the answers I couldn't find on the Internet to others who may be following a similar journey, and hope you find it useful.
But please note - I'm learning as I go so don't claim that the advice in here is perfect or perhaps the best approach. If you know of a better way then please include a comment to let me and the readers know!
So, let's start with my biggest challenge... performance!
This is part 1 of a series, with each focusing on different elements of getting the best performance from your Firemonkey app, so please come back regularly for the follow-up parts!
Part 1 - The Challenge of Performant Control Use
Challenge #1 - Why does it take ages to render my control when I update or change it in code?
Coming from a VCL background, it wasn't unusual to have such as:
for i:=0 to sl.count-1 do
listbox.items.add(sl[i]);
Where SL is probably a TStringList with some useful data in. This will create a new item in the list box and render it to the screen. A FOR loop is optimised so the paint event for the list box isn't likely to fire until after the loop has finished, plus with off-screen buffering built into the wrapped Windows control and other optimisations going on, you're unlikely to see anything.
And for small lists you can get away with the above as-is, and that's what I started doing with Firemonkey too... until I hit a big performance problem.
Rule #1... Remember that Firemonkey renders controls differently to the VCL
Unlike the VCL, all UI controls in Firemonkey are hand-rendered by the Firemonkey framework straight onto the canvas of the window. Regardless of platform, a native window context (OpenGL or DirectX) is created in OS X/Windows/Android/iOS through the native APIs and Firemonkey draws your controls onto that - one by one in the order specified by the UI control hierarchy. That means NO help from the native platform - it's all drawn bit by bit by Firemonkey until the whole UI is rendered.
That means one thing - the more controls it has to render, the long it will take to render, and the more often it has to re-render the UI, the slower your app will become!
Useful side note:
This is also why mixing native controls (such as the TMS iOS iCL controls pack) with Firemonkey controls you get some very big challenges! The native controls ALWAYS draw on top of the Firemonkey ones - even if they're not on the active tab of a TabControl... TMS posted a very interesting article here that I'd recommend reading to understand this: http://www.tmssoftware.com/site/fmxicl.asp
So the rule is that we need to reduce the number of times the UI has to re-render itself - especially if we have lots of controls on it - which we generally will if it's a complex app - though I have tips on how to deal with this later in the article...
Rule #2 - Before performing an update on a control, BeginUpdate!
BeginUpdate can be called on any FMX.Controls.TControl and signals the framework to ignore all Paint messages from that control - or it's child controls. This means that if you have a loop in which you're adding new child controls (TListViewItem for example), it won't attempt to render the control.
When you're done, remember to call EndUpdate to signal that you're finished and it will raise one single Paint event to render the control.
I actually measured this as part of my performance tweaking exercises and it makes a massive difference!
The calendar control I include in myShiftPlanner is effectively a TListBox with styled cells - 7 columns and 5-6 rows (tip - for uniform grids, this is a great way to do it since Firemonkey doesn't have a classic 2D grid control!).
There is a method on the control which clears all the cells out and re-creates them when the month changes.
I originally didn't have BeginUpdate and EndUpdate surrounding this code and it measured at approx 200ms. After BeginUpdate and EndUpdate were added, it reduced to 20ms. It really CAN make a difference!
However - be wary of BeginUpdate too high up in your hierarchy!
So it's natural to think - why not just do a form.BeginUpdate, render everything and form.EndUpdate to stop all paint events while you're rendering things?
In theory you could. Firemonkey UI rendering is not inherently multi-threaded (it all runs from the Main thread) so you might think this would work, but remember - if you update a TListBox, only the TListBox (or anything on top of it) needs re-painting if you change it. By signalling form.BeginUpdate, you tell the framework that the WHOLE form is being updated so it has to re-render everything when you call EndUpdate.
Rule #3 - The trick to performant Firemonkey apps is have Firemonkey do as little as possible at all times!
Using TForm in the above example was an extreme example but just think carefully about how and where you use BeginUpdate and EndUpdate to make sure you only have Firemonkey do the minimum necessary at all times. The more it has to do, the slower it will be.
And don't think you can multi-thread you way out of this! Seasoned developers used to multi-threading will laugh that I even mentioned this, as they'll know this stuff inside-out but wanted to briefly cover it - especially if you're fairly new to Delphi or parallel programming.
XE7 introduced a fantastic Parallel Programming LIbrary (PPL) which makes dealing with multi-threading in apps a lot easier. However, there is one immovable truth with Firemonkey multi-threading when it comes to the UI - rendering to the UI always uses the Main Thread. You can't multi-thread this stuff. Sure, you can spur a thread which grabs or generates the data you want to render in a control, but the minute you want to interact with the control at all to update it, you must use Queue() or Synchronize() which operates on the main thread, along with all other UI rendering tasks - so it has to get into line and wait.
Challenge #2 - The trials of dynamically changing or building a UI
The second challenge I faced was what to do about changing the UI to suit the different stages of the user journey through my app, as I soon hit problems with my usual approach to this.
myShiftPlanner's UI structure and model is pretty standard. It's a navigation-based UI with master TabControl, one tab per "screen" and a common toolbar along the top which hosts the navigation buttons (usually "Back" or "Done") plus some feature buttons specific to certain "pages" of the UI.
My approach was to keep it simple - just put all the buttons I'd need on the toolbar (there aren't that many in total) and just hide and show them as relevant. I've done this a million times in VCL apps and it works well, but I soon hit a performance problem which wasn't what I expected. And it wasn't in the place I expected either!
Hiding and showing the buttons seemed fast enough - a slight delay of a fraction of a second but nothing I couldn't cover up through the usual UI trickery (that's a whole other blog entry!) However, my page transitions slowed down really badly. TabControl page-switching comes with free animations, so they slide away beautifully when you change them - really nice! But mine started to judder, and pause mid-animation... I couldn't get the animations to run smoothly and in some cases there would be a visible split-second delay after the page switched before the page contents painted - leaving a blank page and visible refresh!
Being the perfectionist I am, it was far from good enough so I spent a total of 2 days analysing, tweaking and investigating to find out what was going on. I finally figured it out, which leads to my 3 rules below...
Rule #4 - Do as little as possible in the TTabControl.OnChange event
The first problem I had was that all the "if tab X, show button Y and hide button Z" code was in the OnChange event. Rule #5 covers why this was a problem for me in detail but this rule explains why I got the effect I did with the animations as a result.
When you call SetActiveTabWithTransition() it creates the new page and renders it off-screen, then sets up an animation to move it in and over the top of the current page. This animation is done in the main thread, so needs to have enough CPU cycles to run 24 frames per second to get a smooth animation. The OnChange() event is called BEFORE the animation completes, so if you do anything within this event that grabs the main thread for enough cycles it will impact the animation.
I will cover smooth animations in a later part of this series as it's another challenge I faced throughout the app, but suffice to say, doing all the control visibility logic in the OnChange event caused a long enough delay to impacted the TabControl transition events.
So, my advice is to be very wary of this event. VCL apps didn't allow cool things like animating page changes so it didn't really impact in the same way, but Firemonkey lives and breathes like this so needs extra consideration.
Rule #5 - Hiding and showing controls can be slow!
Due to the way Firemonkey renders its controls, changing anything visible about a control will impact the performance of the app and that includes hiding, showing or moving controls around. I knew there would be an impact due to the repainting but didn't expect it to be so large. Even using BeginUpdate and EndUpdate didn't reduce the cost enough to stop it impacting the TTabControl.OnChange() event's effect on the animations.
So be wary of hiding and showing controls and the cost of doing so. If you need to do this (and I have no doubt you will), try to do it as infrequently as you can (during app start-up, or during other user-distracting activities) and certainly not within event handlers that are called mid-animation as the experience for the user is dreadful!
How did I solve this in the end? Well the only option I was left with was to add a toolbar onto each page of the page control with the buttons on I needed. This means I have duplicate Back buttons and Done buttons in the app (all sharing code where it can) but at least I don't have to hide or show them when switching pages - the framework deals with the whole page render at once.
Note: I can't help but think there MUST be a better way, so if you know of one please let me know in the comments of this article!
Rule #6 - Reparenting and creating UI controls dynamically is even slower so just don't unless you have to!
Trying to re-parent controls to move them around a UI has always been slow even in the VCL so I wasn't surprised that Firemonkey suffered a similar issue.
However, creating and adding TFrame's to a VCL TForm was generally performant so I've done it a lot in the past to build dynamic UIs or for in-page "dialog boxes" (fake dialogs).
Due to Firemonkey's lack of modal dialog box support (due to Android and iOS not allowing it) I tried a similar trick with a TFrame but it literally took up to 2 seconds from the user requesting to the TFrame appearing! After analysing this, I found that just creating the TFrame in the first place took up to a second, and then parenting it onto the form and rendering it another second.
So, I would suggest avoiding this. Firemonkey now supports non-modal dialogs if that suits what you need to do, and you can create and open full-screen TForm if you can get away with switching the visual context in this way for the user.
Again, maybe there is a better way but I didn't find one so please suggest other options in the comments as I'm sure the other readers would love to know too!
I hope you enjoyed this part of my journey! Please let me know what you thoughts on the article, share your own useful performance tips and if you disagree with my suggestions and have a better solution I'd love to hear about those too!
Please come back soon for FireMonkey Performance - Part 2
Article by Chris Pimlott, Managing Director of MyBuzz Technologies Ltd - specialists in Apps that make your life Simpler
Feb 25th 2015