Improving UI Responsiveness Using Pre/Post-Initialization
Author: Tomas Pavek
$Revision: 1.7 $
$Date: 2003/02/07 19:30:48 $
Document history: available in
CVS
Problem
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.
Analysis
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 placed,
- 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
JavaKit and 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
ModuleActions.getDefault().getContextActions();
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).
There are also (still
experimental) tasks for editor and java modules
speeding up the
first file opening, context menu invocation and folder expansion: EditorWarmUpTask
and JavaWarmUpTask.
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.