[Skip top navbar]

Andrew Gregory's Web Pages

View from Mt Beadell, 25°32'1"S 125°16'37"E

-

Background Processing


Introduction

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:

1. Without an I/O Request

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.

2. With an I/O Request

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:

Background Processing Skeleton Code
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.


Theory

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:

  1. Read one line from source file
  2. Assemble source line

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:

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):

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.


Implementation

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:

  1. 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.

  2. Develop a state structure for your process and re-write your process to use it.
  3. Re-design your process so it can be called in chunks. This is the most complicated part. It must be designed so that repeated calls to a single procedure will perform your process.
  4. Set up the I/O Event Manager in your main application to call your new procedure.

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


Example

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).

Develop a working non-background process

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.

My ported OPL program.

Develop a state structure

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.

  1. Initialisation - This includes allocating and initialising the sum and term arrays.
  2. The 16*atan(1/5) loop.
  3. The initialising for the 'crunch out 1st term' loop.
  4. The 'cruch out 1st term' loop.
  5. The -4*atan(1/239) loop.
  6. The initialising for 'finish up' loop.
  7. The 'finish up' loop.
  8. The initialising for the print out loop.
  9. The print out loop.

The atan procedures have internal states of their own:

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

Re-design the code so it can be called in chunks

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.

Setup the I/O Manager

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.

Pi calculation screen shot
Pi calculation screen shot

-