MUI Subclassing Tutorial: SciMark2 Port
From MorphOS Library
Grzegorz Kraszewski
Contents
The Application
Many programming tutorials tend to bore readers with some useless examples. In this one a "real world" application will be ported to MorphOS and "MUI-fied". The application is SciMark 2. SciMark is yet another CPU/memory benchmark. It performs some typical scientific calculations like Fast Fourier Transform, matrix LU decomposition, sparse matrix multiplication and so on. The benchmark measures mainly CPU speed at floating point calculations, cache efficiency and memory speed. Being written in Java initially, it has been rewritten in C (and in fact in many other languages). The C source is available on the project homepage.
The source uses only the pure ANSI C standard, so it is easily compilable on MorphOS using the provided Makefile. One has just to replace the $CC = cc line to $CC = gcc, to match the name of the MorphOS compiler. As a result, a typical shell-based application is obtained. Here are example results for a Pegasos 2 machine with G4 processor:
Not very impressive in fact. This is because no optimizaton flags are passed to the compiler in the makefile. They can be added by inserting the line $CFLAGS = -O3 below the $CC = gcc line. Let's also link with libnix (a statically linked unix environment emulation, see Standard C and C++ Libraries) by adding -noixemul to CFLAGS and LDFLAGS. After rebuilding the program and running it again the results are significantly improved (the program has been compiled with GCC 4.4.4 from the official SDK).
This shows how important optimization of the code is, especially for computationally intensive programs. Optimized code is more than 4 times faster!
Code Inspection
The original source code is well modularized. Five files: FFT.c, LU.c, MonteCarlo.c, SOR.c and SparseCompRow.c implement the five single benchmarks. Files array.c and Random.c contain auxiliary functions used in the benchmarks. The file Stopwatch.c implements time measurement. An important file kernel.c gathers all the above and provides timing for the five functions performing all the benchmarks. Finally scimark2.c contains the main() function and implements the shell interface.
The planned MUI interface should allow the user to run every benchmark separately or run all of them. There is also a -large option, which increases memory sizes for calculated problems, so they do not fit into the processor cache. A general rule of porting is that as few files as possible should be modified. The rule makes it easier to upgrade the port when a new version of the original program is released. In the case of SciMark, only one file, scimark2.c has to be replaced. An advanced port may also replace Stopwatch.c with code using timer.device directly for improved accuracy of time measurements however, this is out of scope of this tutorial.
A closer look at "scimark2.c" reveals that there is a Random object (a structure defined in "Random.h"), which is required for all the benchmarks. In the original code it is created with new_Random_seed() at the program start and disposed with delete_Random() at exit. The best place for it in the MUI-ified version is the instance data area of the subclassed Application class. Then it can be created in OM_NEW() of the class and deleted in OM_DISPOSE(). These two methods should then be overridden.
GUI Design
Of course there is no one and only proper GUI design for SciMark. A simple design, using a limited set of MUI classes is shown on the left. There are five buttons for individual benchmarks and one for running all of them. All these buttons are instances of the Text class. On the right there are gadgets for displaying benchmark results. These gadgets also belong to the Text class, just having different attributes. The "Large Data" button, of the Text class of course, is a toggle button. Surprisingly the status bar (displaying "Ready.") is not an instance of the Text class, but instead the Gauge class. Then it will be able to display a progress bar when running all five tests. Spacing horizontal bars above the "All Benchmarks" button are instances of the Rectangle class. There are also three invisible objects of the Group class. The first is a vertical, main group, being the root object of the window. It contains two sub-groups. The upper one is the table group with two columns and contains all the benchmark buttons and result display gadgets. The lower group contains the "Large Data" toggle button and the status bar.The simplest way to start with GUI design is just to copy the "Hello World" example. Then MUI objects may be added to the build_gui() function. The modified example is ready to compile and run. It is not a complete program of course, just a GUI model without any functionality added.
A quick view into the build_gui() function reveals that it does not contain all the GUI code. Code for some subobjects is placed in functions called from the main MUI_NewObject(). Splitting the GUI building function into many subfunctions has a few important advantages:
- Improved code readability and easier modifications. A single MUI_NewObject() call gets longer and longer quickly as the project evolves. Editing a large function spanning over a few screens is uncomfortable. Adding and removing GUI objects in such a function becomes a nightmare even with indentation used consequently. On the other hand the function can have 10 or more indentation levels, which makes it hard to read as well.
- Code size reduction. Instead of writing very similar code multiple times, for example buttons with different labels, a subroutine may be called with a label as an argument.
- Debugging. It happens sometimes that MUI refuses to create the application object because of some buggy tags or values passed to it. If the main MUI_NewObject() call is split into subfunctions, it is easy to isolate the buggy object by inserting some Printf()-s in subfunctions.
Methods and Attributes
The SciMark GUI just designed, defines six actions for the application. There are five actions for running individual benchmarks and the sixth one for running all the tests and calculating the global result. Actions will be directly mapped to Application subclass methods. There is also one attribute connected with the "Large Data" button, it determines the sizes of problems solved by benchmarks. Methods do not need any parameters, so there is no need to define method messages. An attribute may be applicable at initialisation time (in the object constructor), may be also settable (needs OM_SET() method overriding) and gettable (needs OM_GET() method overriding). Our new attribute, named APPA_LargeData in the code only needs to be settable. In the constructor it can be implicitly set to FALSE, as the button is switched off initially. GET-ability is not needed, because this attribute will be used only inside the Application subclass.
It is recommended that every subclass in the application is placed in a separate source file. It helps to keep code modularity and also allows for hiding class private data. This requires writing a makefile, but one is needed anyway, as the original SciMark code consists of multiple files. Implementing the design directions discussed above a class header file and class code can be written. The class still does nothing, just implements six empty methods and overrides OM_SET(), OM_NEW() and OM_DISPOSE(). In fact it is a boring template example and as such it has been generated with the ChocolateCastle template generator. Unfortunately ChocolateCastle is still beta, so files had to be tweaked manually after generation.
The next step in the application design is to connect methods and attributes with GUI elements using notifications. Notifications must of course be created after both source and target object are created. In the SciMark code they are just set up after executing build_gui(). All the six action buttons have very similar notifications, so only one is shown here:
DoMethod(findobj(OBJ_BUTTON_FFT, App), MUIM_Notify, MUIA_Pressed, FALSE, App, 1, APPM_FastFourierTransform);
The "Large Data" button has a notification setting the corresponding attribute:
DoMethod(findobj(OBJ_BUTTON_LDATA, App), MUIM_Notify, MUIA_Selected, MUIV_EveryTime, App, 3, MUIM_Set, APPA_LargeData, MUIV_TriggerValue);
Notified objects are accessed with dynamic search (the findobj() macro), which saves the programmer from defining global variables for all of them.
Implementing Functionality
The five methods implementing single SciMark benchmarks are very similar, so only one, running the Fast Fourier Transform has been shown:
IPTR ApplicationFastFourierTransform(Class *cl, Object *obj) { struct ApplicationData *d = INST_DATA(cl, obj); double result; LONG fft_size; if (d->LargeData) fft_size = LG_FFT_SIZE; else fft_size = FFT_SIZE; SetAttrs(findobj(OBJ_STATUS_BAR, obj), MUIA_Gauge_InfoText, (LONG)"Performing Fast Fourier Transform test...", MUIA_Gauge_Current, 0, TAG_END); set(findobj(OBJ_RESULT_FFT, obj), MUIA_Text_Contents, ""); set(obj, MUIA_Application_Sleep, TRUE); result = kernel_measureFFT(fft_size, RESOLUTION_DEFAULT, d->R); NewRawDoFmt("%.2f MFlops (N = %ld)", RAWFMTFUNC_STRING, d->Buf, result, fft_size); set(findobj(OBJ_RESULT_FFT, obj), MUIA_Text_Contents, d->Buf); set(obj, MUIA_Application_Sleep, FALSE); set(findobj(OBJ_STATUS_BAR, obj), MUIA_Gauge_InfoText, "Ready."); return 0; }
The code uses dynamic object tree search for accessing MUI objects.
The method sets the benchmark data size first, based on the d->LargeData switch variable. This variable is set with the APPA_LargeData attribute, which in turn is bound to the "Large Data" button via a notification. Then the status bar progress is cleared and some text is set to inform the user what is being done. The result textfield for the benchmark is cleared as well.
The next step is to put the application in the "busy" state. It should be always done, when the application may not be responding to user input for anything longer than, let's say half a second. Setting MUIA_Application_Sleep to TRUE locks the GUI and displays the busy mouse pointer when the application window is active. Of course offloading processor intensive tasks to a subprocess is a better solution in general cases, but for a benchmark it makes little sense. A user has to wait for the benchmark result anyway before doing anything else, like starting another benchmark. The only usability problem is that a benchmark can't be stopped before it finishes. Let's leave it as is for now, for a benchmark, where the computer is expected to use all its computing power for benchmarking, a few seconds of GUI being unresponsive is not such a big problem.
The next line of code runs the benchmark, by calling kernel_measureFFT() function from the original SciMark code. After the benchmark is done, the result is formatted and displayed in the result field using NewRawDoFmt(), which is a low-level string formatting function from exec.library and with the RAWFMTFUNC_STRING constant, it works just like sprintf(). It uses a fixed buffer of 128 characters (which is much more than needed, but adds a safety margin) located in the object instance data. Unsleeping the application and setting the status bar text to "Ready." ends the method.
The APPM_AllBenchmarks() method code is longer so it is not repeated here. The method is very similar to the single benchmark method anyway. The difference is it runs all 5 tests accumulating their results in a table. It also updates the progress bar after every benchmark. Finally it calculates a mean score and displays it.
Final Port
The complete source of SciMark2 MUI port
The program may be built by running make in the source directory.