Improving UI Responsiveness Using Pre/Post-InitializationAuthor: Tomas Pavek
$Revision: 126.96.36.199 $
$Date: 2009/10/29 16:50:16 $
Document history: available in CVS
Our measurements of UI responsiveness in NetBeans IDE show general problem: many user operations perform reasonably well, except the first invocation. When executed for the first time, many things must be loaded and initialized, so taking significantly more time than subsequent executions. For example, the first opening of simple java file in source editor might take 4 seconds, while the second just about 0.5 second. This is result of postponing initialization of most of the functionality until it is needed ("lazy initialization"). It is generally good approach, at least for the system startup and overall effectiveness, but it hurts initial UI responsiveness of the IDE.
So in the first line, we wanted to find some way to improve the initial UI responsiveness, still not affecting the startup, and without the need to do major redesign of the IDE. We developed the "warm-up" technique for this purpose, as described below. But the first usage problem is not the whole thing, just a typical case. In general, the presented problem lies in operations that take long time leaving the IDE unresponsive. This document tries to describe possible way how to shorten such operations using the pre/post-initialization techniques.
Note: We reduce the problem only to performance aspect here, but UI responsiveness is more than that. At least, adequate UI feedback is quite essential, so the user always knows, when the invoked operation starts, what is being done at the moment, and when it is finished. So for example, in the case with opening java file, there could be a message like "Initializing editor for the first use..." shown during the time the file is being opened.
The suggested idea is to move parts of an operation to separate subtasks performed before and after on background; the operation itself then would take less time and look more responsive. The following figure illustrates how user operations are performed in a program sequentially. The scenario is: user invokes an action, an operation is started, user is waiting, operation is finished, user wakes up and invokes another action, etc.
A B C D +-------------------+ +-------------------+ time --------->| operation 1 |--------------->| operation 2 |-------> +-------------------+ +-------------------+ A, C - moments when user invokes an action B, D - moments when operation is finished, results are visible, program is responsive, the user may continue A-B, C-D - user waits for computer, computer is working (operation time) -A, B-C, D- - computer waits for user, user is working (idle time)
As mentioned above, we might try to move some work being done in operation time (when the user is waiting) to the idle time (when the computer is waiting). This can be done by:
- preparing some things before the operation is invoked,
- postponing some parts of the operation -- doing them after the operation is finished.
We call these two separated tasks as "pre-initialization" and "post-initialization". The result is that the main operation itself looks performing faster.
+----+ +-----+----+ +-----+ |pre1| A B |post1|pre2| C D |post2| +----+ +---------+ +-----+----+ +---------+ +-----+ --------->| op. 1 |--------------->| op. 2 |-------> time +---------+ +---------+ Note: - the user waits less for individual operations (A-B, C-D are shorter), - the whole task time is shorter, - user time B-C is the same, - there is no change in performance of the operations (same amount of work is done).
Theoretically, pre-init and post-init tasks can be very similar in nature. For example, a post-init task of one operation can be also a pre-init task of another (subsequent) operation. But typically they are used differently:
Pre-initialization prepares something in advance, so when an operation is invoked, some work is already done. To use this technique, there must be suitable moment(s) when we are adequately sure the pre-initialization work would be done sooner or later anyway, so the time spent on it is not wasted. (Example: After expanding a folder with files, some file will be likely opened in the source editor soon.) There is often lack of context information, so the pre-initialization might be done not for a particular operation, but for a set of possible operations. (Example: we can prepare some static parts of the editor although we don't know which concrete file will be opened.) The way how the pre-initialization is invoked (only in certain situation, typically in a different thread) implies that the main operation cannot rely on it was performed and must consider synchronization issues.
Post-initialization does some additional work just after an operation is finished and then visually updates the results. There are in fact two parts of post-initialization requiring different threading implementation: one is the post-init work itself running on background, the second one is the update of results which must be done in AWT event queue thread.
Using in the IDE
Not all actions/operations in the IDE are suitable for this approach. Some are suitable only for one of the techniques. Generally, it must be possible to extract pre-init and post-init tasks effectively leaving a reasonably compact main operation. As for behavior, the pre/post tasks must be "smart" -- they should not affect responsiveness (running on background with low priority), should not take much time, should not interact with the main operation improperly, etc.
The general instructions for implementation might look as follows:
- identify the operation(s) that need(s) to be improved (taking much time),
- use a profiler tool for analyzing the execution profile of the operation,
- analyze implementation of the operation to see whether there are some parts that could be pre- or post-initialized,
- create a separate pre-init task:
- pre-init task usually initializes something that is to be used soon,
- place the pre-init task -- find suitable preceding operation after which
the task can
- be aware of synchronization with the main operation,
- create a separate post-init task:
- post-init task typically computes some additional things for updating (completing) some part of the UI,
- place the post-init task -- invoke it in the end of the main operation; pre-init tasks for other subsequent operations may follow here.
Illustrational example: source editor
- operation: opening java file takes too long for the first time,
- profiling shows there's lot of time spent in loading and resolving classes, reading editor settings, preparing editor actions, initializing editor toolbar, etc,
- looks like the basic editor infrastructure can be initialized statically (without having concrete file to open),
- pre-init: by pre-creating
JavaKitand calling some method on it we may force loading many classes and reading the settings,
- we may do this just after the IDE starts as the editor will be used almost for sure sooner or later,
- post-init: let's do the toolbar initialization (incl. the navigation view) later after the file is displayed in the editor,
- further pre-init: as pre-init task for subsequent operations, we may also prepare code completion database and pre-create context menu -- both will be most probably used soon after the file is opened.
The pre-initialization technique can be used to deal with the first usage problem mentioned at the beginning of this document. It helps to reduce the initialization overhead of the first invocation of various functional parts. Here, we call the technique warm-up, it is just a special case of pre-initialization executed just after the start of the IDE. As such, it is suitable for initializing the most commonly used things which are worth to be pre-initialized, but which have no convenient moment or situation (i.e. no context) to do so, only the start-up. The editor pre-init example mentioned above is a typical example.
To address this in the NetBeans
IDE, we have implemented an
infrastructure allowing modules to run their own warm-up tasks. The
infrastructure looks in "WarmUp" folder for individual tasks and
executes them. The tasks are executed just after the IDE starts (main window is
shown) in a low priority thread. The tasks must implement
java.lang.Runnable interface and
can be specified in module layer like:
<folder name="WarmUp"> <file name="org-netbeans-modules-editor-EditorWarmUpTask.instance"/> </folder>
The infrastructure is placed in
core, you may look at
org.netbeans.core.WarmUpSupport if interested in details.
The infrastructure for running the tasks:
- provides central place for running individual warm-up tasks, so it can be easily managed (turned on/off, logged, optimized etc),
- runs the tasks in a low priority thread sequentially in background just after the main window is displayed (this should not affect responsiveness of the IDE, but it depends on how low priority threads behave on given platform),
- does not introduce any special API (just the XML layer registration),
- is robust enough to handle potential problems/failures of warm-up tasks transparently to the user (i.e. don't bother).
Individual warm-up tasks provided by modules should follow these requirements:
- don't rely on that the warm-up task is always run (execution is not guaranteed),
- take into account that the task may still be running when some functionality of the module is required (careful synchronization is needed),
- the task should run quickly if not needed (best with zero time; e.g. if the initialization is done during IDE startup when restoring something),
- try to avoid manipulation with live AWT/Swing components requiring to post something to AWT event queue thread,
- the warm-up should not be too long, so the warm-up is finished in a reasonable time; this means also that only strictly selected modules should provide their tasks.
An example of warm-up task can be found in
org.netbeans.core.ui.ContextMenuWarmUpTaks. There is code like
which is nice example of suitable warm-up operation -- it takes little time (~300ms), but speeds up the first invocation of any context menu significantly (300ms plays role here).
Post-Initialization Example: Dialogs
Good candidates for post-initialization techniques are wizards and dialogs which must be displayed quickly although doing non-trivial work when showing up. A separate document Optimization of Component's initialization is published about this topic.
Other documents (original posts):
Please send comments to nbdev.