It is possible to have the I/O Event Manager module look after your background processing for you. This is where you can do something like load a file or perform a long calculation while still letting the user select menus, or do other things in your application.
What you do is create an event handler that will perform your background processing and then ensure it is called regularly.
So how do you ensure it is called regularly? Well, whenever an I/O request completes it generates a signal. The I/O Event Manager then checks the status byte of all outstanding I/O requests. If it finds a status byte is not -46 (the code for pending I/O), then the I/O is no longer pending and the handler for it is called.
There are two ways to make the I/O Event Manager call your routine:
This method does not require an IOOPEN
, and hence there is no
need for keeping track of I/O handles. However, you will still need to call
ioAdd
and ioRem
as you would for regular I/O
handling.
Set the status byte to any value other than -46 and call
IOSIGNAL
. Even if other I/O requests complete, the fact you have
called IOSIGNAL
means that there is an extra signal and your
handler will eventually be called. Conveniently, when you create an I/O handler
via ioAdd
, the status byte is already set to a non -46 value
(zero).
To keep the I/O Event Manager calling your
background routine, you must always call IOSIGNAL
before returning
from the I/O handler to generate a new event signal, otherwise your background
routine will never be called again.
IMPORTANT NOTE: This method of background processing puts the Psion into what is called a 'busy-wait' mode. This means that while your background processing is running, the Psion will never enter its low-power idle mode. If your programs gets stuck in their background processing, your batteries will be drained quite quickly!
Summary: This type of background processing is best for things that must be completed as soon as possible. This includes calculations (such as the PI calculation in the major example below), print jobs, or loading (big) files.
Set up a timer (device TIM:). Each time the timer request completes, your handler is called. Typically you would set it up to complete fairly regularly, otherwise your background processing would take a long time to complete.
Each time the timer request completes (and your handling procedure is called), you must remember to make a new timer request otherwise your background processing will never be called again.
Summary: This type of background processing is best for things that do not need to be completed quickly, such as monitoring a 'watched' directory for files (to be processed - such as converted to a different format, printed, or faxed), or checking for external power and making sure the backlight is switched off.
For example:
Description | Without an I/O Request | With an I/O Request (assume tmrdly& is a
GLOBAL ) |
---|---|---|
Set up I/O device | n/a | LOCAL tmrhand%
IOOPEN( tmrhand%, "TIM:", -1 ) |
Register with I/O Manager | slot% = ioAdd%:( "bgproc", 127 ) | |
Make I/O Request | IOSIGNAL |
ioShand:( slot%, tmrhand% )
tmrdly& = 50 :REM in tenths of a second, 50 = 5 seconds
IOC( ioGhand%:( slot% ), 1, #ioGstat%:( slot% ), tmrdly&, #0 ) |
Set Up I/O Handler | PROC bgproc%:( slot% )
REM ...processing...
IF processing complete
ioRem:( slot% )
ELSE
IOSIGNAL
ENDIF
ENDP |
PROC bgproc%:( slot% )
REM ...processing...
IF processing complete
IOCLOSE( ioGhand%:( slot% ) )
ioRem:( slot% )
ELSE
IOC( ioGhand%:( slot% ), 1, #ioGstat%:( slot% ), tmrdly&, #0 )
ENDIF
ENDP |
If you're worried that the background processing will 'take over' and no other event handling take place, don't worry. The I/O Event Manager has a lower priority than all other system events (keyboard, background/foreground notification, date change, etc). So if your user presses a key, it will get handled before any I/O handling.
The entire Application Manager is designed around event-handling. Events are generated from either inside or outside your program and then handled. To maintain the responsiveness of your program, event handlers must perform their work quickly then return, so that other events (which could occur at any time) can be handled. Event handlers that take too long to perform their processing give the application the feeling that it is 'pausing' for short periods of time and ignoring keypresses or other events.
Background processes, therefore, must be broken up into small chunks of processing (in terms of execution time) that can be called repeatedly. For example, if you were writing an assembler you might develop it in a loop like:
This is easily transferred to a background process. For each call to the background process it would read one line from the source file and assemble it before returning to the Application Manager.
The biggest problem with this method of processing is keeping track of your state. For the assembler this might include:
Every time the background process was called it would have to get access to this information. The method I have adopted for my programs is to use one or both of the two long integer (4-byte) 'user variables' available to I/O processes to store a pointer to a state structure. The procedures to access these variables are:
ioGusr1%:( hdl% )
ioGusr2%:( hdl% )
These return the address of the respective user variable.
When I create the I/O process to act as the background process I create a
data structure (a STRUCT
in
OPP) in dynamic memory (using
ALLOC
), initialise it, then store the pointer in one of the I/O
user variables (or sometimes in half of it):
POKEW ioGusr1%:( slot% ), ptr%
POKEW UADD( ioGusr1%:( slot% ), 2 ), ptr%
The data structure contains all of the state information I need, and is automatically available to the I/O event handler via the I/O slot number. For example:
... PROC handler%:( slot% ) LOCAL ptr% ... ptr% = PEEKW( ioGusr1%:( slot% ) ) ... ENDP ...
When my background process is done, I simply FREEALLOC
the
pointer, remove the I/O handler (ioRem:
) and don't call
IOSIGNAL
.
For a concrete example of all this, I will develop a program that will calculate PI to N decimal places in the background. Since it is a calculation that is best completed as quickly as possible, it follows the 'Without an I/O Request' method.
The steps to do this would be similar for any background process:
Develop a working non-background process, preferably as a separate program.
Re-designing an algorithm so that it can be called in chunks is likely to introduce bugs. To maintain your sanity, you are advised to start off from as bug-free a position as possible.
To better illustrate point 3, the sort of thing to do is to set up a 'function harness'. The code that needs to be changed to test each background process is highlighted below.
PROC main: GLOBAL signal% :REM to 'fake' an I/O signal LOCAL ptr% ptr% = ALLOC( *** size of state structure *** ) REM *** initialise state structure *** signal% = 1 WHILE signal% signal% = 0 bghndlr%:( ptr% ) ENDWH ENDP PROC bghndlr%:( ptr% ) REM *** background processing here *** IF done% FREEALLOC ptr% ELSE signal% = 1 ENDIF ENDP
The example program is one that was originally written in QuickBASIC. I got it from jasonp's Pi Programs Page. The original source code can be downloaded from his site or from my site. I verified that it worked before porting it to OPL.
It calculates [ 16 × atan( 1 / 5 ) - 4 × atan( 1 / 239 ) ] (atan arguments are in radians). It is capable of calculating PI to around 150,000 digits, although the Psion can only allocate space for 32747 digits (they require 49146 bytes of memory).
This step involved porting the original QuickBASIC program to OPL. It is slightly complicated by the usage of dynamically-sized arrays (which OPL doesn't support). I implemented a dynamic memory allocation scheme, using procedure wrappers to allow access to the memory almost as if it were an array.
The idea of the state structure is to allow the background process to keep track of whatever it is doing between calls. This depends on how you intend to split up your process.
I started by analysing how much I needed to split up the program. I added a
PRINT ".";
to the inside of all the
loops in the mainline. This showed that the calls to atan5
and
atan239
were very slow. I clearly had to split even the
atanXXX
procedures. This made things a lot more complicated.
States are generally bounded by loops. The inside of each loop can be considered a state. The outside of each loop is also a state.
16*atan(1/5)
loop.'cruch out 1st term'
loop.-4*atan(1/239)
loop.'finish up'
loop.print out
loop.print out
loop.The atan
procedures have internal states of their own:
atan5
atan239
These add up to a grand total of 9 + 9 + 3 = 21 states. Any variables whose value needs to be maintained between states must be put into the state structure. The state structure I came up with was:
STRUCT pi_state REM Old global variables, plus a state number state% REM main state a5state% REM state of atan5 calculation a239state% REM state of atan239 calculation ptrsum% ptrterm% words% first% last% denom& REM Selected old local variables - not all local variables need to be REM kept between calls x% digits& rem1& rem2& rem3& rem4& d2& ENDS
Now that I had a state structure, I converted the code. The design uses
VECTOR
tables and a state number to direct the execution to the
appropriate places. As one state completes, it increments the state number which
allows the execution to move on to the next state.
To make my life easier, I wrote the new version using OPP. You can download PI2.OPP from here. There is also a pre-processed OPL version, PI2.OPL available here for those of you who don't have OPP. Be warned, though, the code isn't pretty! Consider this a hint that you really should register yourself a copy of OPP. It makes stuff like this so much easier.
Optimisation Note: Code like this:
ELSE p%->a5state% = p%->a5state% + 1 ENDIF GOTO done s7:: p%->x% = p%->last% + 2 p%->a5state% = p%->a5state% + 1 GOTO done s8::
could have been written like this:
ELSE p%->a5state% = p%->a5state% + 1 p%->x% = p%->last% + 2 ENDIF GOTO done s7::
so that fewer states were used, but I decided for clarity to keep just loop code in its own state, and give non-loop code (code between loops) its own state.
You might also notice that there is now just one GLOBAL
variable
- for IOSIGNAL
emulation! All the variables that were
GLOBAL
are now in the state structure. This helps to cut down on
the GLOBAL
namespace pollution that you might otherwise have.
This is where the good stuff happens - integrating the new process into your application. Download PI3A.OPP and translate it. I haven't supplied an equivalent OPL file as this version requires both the Asynchronous Application Manager and I/O Event Manager modules. The resulting OPL file would be too big for the editor to load.
Take particular note of the revised cleanup code in bgpi%:
. This
ensures all the memory and the I/O Event Manager
slot are freed.
When you run the program, press the Menu
key. Select the
'Calculate' option. Enter a number of decimal places (say 100) and press
Enter
. While it is calculating you can press Menu
and
select any of the options (even 'Exit'!). The calculation will continue in the
background and display its results when complete.
For a benchmarking comparison, on my 3mx PI3A.OPP calculates to 96 places in
33 seconds, PI2.OPP in 17 seconds. What that indicates is that I've probably
made bgpi%:
work too quickly. The
OPL interpreter
imposes a 'hit' on procedure calls and the execution times of
bgpi%:
are short compared to the overhead of the
I/O Event Manager and
OPL procedure calls.
If this was going to be a problem, I would look at performing some of the loops
inside bgpi%:
(i.e. change the IF
tests back to the
original WHILE
loops).
Another solution is to manually loop inside bgpi%:
. That is what
I have done with PI3B.OPP. By looping inside
bgpi%:
three times before releasing control back to the
I/O Event Manager, the execution time to calculate
96 places was reduced to 20 seconds. Execution time could be further improved by
increasing the number of loops per call, however, response to keypresses and
other events may be impaired.