How to use Chronoscope



Preparing your application for profiling

Basic setup

The minimum you need to do to profile your application with Chronoscope is to add calls to ChronoInit() and ChronoTerm() to your code, and to link with the Chronoscope library. Both of these are described in the following sections.

Code changes

First, you need to tell CodeWarrior to generate profiling hooks in your code. You can do this by checking the 'Profiler information' checkbox in the PPC Processor panel of the project settings, or by using a #pragma:

#pragma profile on

You can test whether profiling is enabled in your code by doing:

#if __profile__

or

#if __option(profile)

Next, you have to add calls to the Chronoscope API in your code to set up the profiler, and tell it when to terminate.

The ChronoInit() call sets up the profiler, optionally supplying a location for the data file, and some parameters that determine how much memory Chronoscope reserves for data collection.

ChronoInit() must be called on the main application thread, must not be called in interrupt-level code, and should be called as early as possible. The call can move memory (it allocates some large data handles). It does not, however, require that any the Toolbox has been initialized.

Warning: The Chronoscope engine and viewer don't currently behave well when the stack becomes shallower than it was at the call to ChronoInit(). It is thus strongly recommended that you call ChronoInit() from your code's main() routine. You can call ChronoSetStatus(false) to turn off profiling until a later time if necessary.

The ChronoInit() function looks like this:

OSErr ChronoInit(void*          inMainThreadID,     /* Identifier for the main thread */
        const FSSpec*           inOutputFile,       /* FSSpec for the output file. May be nil */
        SInt32                  inNumFunctions,     /* Max num functions in profiled code */
        SInt32                  inStackDepth,       /* Max stack depth */
        SInt32                  inMaxSamples,       /* Max num of samples for which space is reserved */
        );

The parameters to this routine are as follows:

void* inMainThreadID
Identifier for the application's main thread. For single-threaded applications, just pass nil. For multi-threaded applications, this value must uniquely identify the main thread. For example, an application using the Thread Manager would call GetCurrentThread(), and pass the resulting ThreadID, cast to a void* to ChronoInit().
const FSSpec* inOutputFile
Location of the data output file. Pass nil to have Chronoscope create the data file in its default location, which is in the same folder as the application being profiled (using ProcessInfoRec.processAppSpec), with file name "{process name} data". In either case, Chronoscope will make a new file with a unique name ("Foo data", "Foo data.2", "Foo data.3") for each run.
SInt32 inNumFunctions
This value should be the number of unique functions that will be called in profiled code. It is used to set the size of an internal data stucture to hold function information. If you pass a value that is too small, Chronoscope will fail to collect data for some functions, and may give inaccurate results. You can see how many of the allocated slots were used in a run by looking at the data collection summary in the lower left of the viewer window.
SInt32 inStackDepth
This value should be the maximum depth of the stack during the run. This value is currently unused.
SInt32 inMaxSamples
This value should be the maximum number of profiler samples that will accumulate between calls that allow Chronoscope to flush data to disk (namely ChronoTask() and ChronoTerm()). A profiler sample is generated for each profiler 'event', such as a function entry, function exit, thread context switch, or entry or exit of interrupt-level code. Again, the viewer shows information on how many of the allocated samples were used. If this value is too low, then Chronoscope will lose data, and will produce inaccurate results.

ChronoInit() can return a number of Mac OS errors, most likely Memory Manager errors. Large values in the 'inNumFunctions' or 'inMaxSamples' parameters will require Chronoscope to make some large Memory Manager requests, mostly for temporary memory handles. If this call returns -108, ensure that you have maximised the amount of free memory by quitting other applications. You may be able to reduced the sample memory requirements by adding calls to ChronoTask() in your code.

Before you app quits, you need also to add a call to ChronoTerm(). This call writes remaining data to disk, and shuts down the Chronoscope profiler. You should call ChronoTerm() from your main thread in user-level code.

Linking with Chronoscope

First, make sure that you are not linking with any of the Metrowerks profiling libraries (ProfilerLib, Profiler PPC.Lib etc).

There are two options when it comes to linking with Chronoscope. The best is to add the ChronoscopeStubs file to your project, then put a copy of (or an alias to) the Chronoscope shared libray in the same folder as the application you are profiling, or into your Extensions folder. Alternatively, you could simply link your application with the Chronoscope.lib static library.

Giving the profiler time

Chronoscope logs all data to memory, to avoid skewing the profiling results by spending inpredicable amounts of time writing data to disk. However, because of this, it's easy for it to run out of space for sample data. To solve this problem the Chronoscope API contains a call ChronoTask(), which you should call at regular intervals from your code to allow Chronoscope to spool data to disk. It's recommended that you call ChronoTask() whenever you call WaitNextEvent() in your code.

Note: Chronoscope may some day be factored into a faceless background app that handles spooling of data to disk at task time, which would obviate the need for applications to call ChronoTask() under most circumstances.

If you have have large amounts of code that run between calls to WaitNextEvent(), or your application does not call WaitNextEvent(), then you'll have to put ChronoTask() calls elsewhere in your code. The main point here is that ChronoTask() is being called at well-determined times, so you can account for this when looking at profiling results.

Chronoscope can put up dialogs when your application calls ChronoTask() (for example if it runs out of memory), so you should only call this on your main thread.

Bear in mind also that the need to call ChronoTask() also depends on the the inMaxSamples paramter that you passed to ChronoInit(). A higher value for inMaxSamples means that you don't need to call ChronoTask() as often, or perhaps at all. The viewer tells you the largest number of the allocated samples that were used, in the 'Show Info' window.

The Chronoscope viewer currently removes time spent in ChronoTask() from the display, unless some interrupt-time code in your application (e.g. async file I/O callbacks or Open Transport notifiers) runs while a code>ChronoTask() call is in progress. In this case, the viewer displays this time with a shaded gray background, so that you can easily identify it.

Instrumenting threads

Chronoscope was written to be capable of profiling threaded applications, but requires some instrumentation of thread creation/deletion, and thread context switching, to be able to do this.

Chronoscope is agnostic of the particular threading model used, but for now assumes that threads are scheduled cooperatively, without concurrency. The various thread-related paramters in the Chronoscope API take void* thread identifiers, which are simply required to be unique identifiers for different threads. For Thread Manager threads, you can pass a ThreadID cast to a void*. These thread identifiers must be unique for all extant threads at any given time, but it is OK for a thread identifier to be reused after ChronoDeleteThread() has been called with that identifier.

To instrument threads, you need to use the following Chronoscope API calls:

ChronoCreateThread(void* inThreadID, SInt32 inThreadStackDepth)
This should be called for each thread that you create (except the main application thread). If you are using the Thread Manager, you'd call this just after your call to NewThread(). The inThreadStackDepth is the maximum depth of the stack for this thread.
ChronoDeleteThread(void* inThreadID)
Call this after exiting the last profiled function on a thread. Thread Manager users can do this via a thread termination proc, set via SetThreadTerminator().
ChronoEnterThread(void* inThreadID)
This should be called whenever the thread scheduler causes a context switch into the specified thread. Thread Manager users can call this via a thread switch-in proc set via SetThreadSwitcher().

Thread switches are shown in the Chonoscope viewer as vertical red lines in the detail pane.

When you collect statistics for a time period during which several threads ran, statistics are shown separately for each thread.

Interrupt-time code

Since Chronoscope uses only interrupt-safe calls, and does not move memory when profiling, it is possible and safe to have the compiler generate profiling hooks in interrupt-level code. Chronoscope does attempt to detect that functions are being called at interrupt time (using the OS call TaskLevel()), and will display this in the viewer with yellow vertical lines. However, it only detects this at the time of a function entry or exit, so may attribute time incorrectly. To get more accurate results, you should use the following API calls in interrupt-level code:

ChronoEnterInterrupt()
Call this at the start of a function that has been registered as an interrupt-time callback (e.g. an async file I/O callback, a VBL task, or an Open Transport notifier routine).
ChronoLeaveInterrupt()
Call this at the end of interrupt-time callback functions. Chronoscope expects that the next function exit it sees after this call is the end of the interrupt-level code.

Controlling Profiling

Chronoscope offers two ways to turn profiling on and off. Turning off profiling via either method causes time spent in unprofiled code to 'disappear' from the timeline view when looking at the data.

ChronoSuspend()/ChronoResume()
These calls can be used to turn off data collection for specific lines of code that you want to exclude from the profiling results. A call to ChronoSuspend() must be followed by a call to ChronoResume() from the same function; the suspend and resume calls must be paired. In addition, no other threads must run between the suspend and the resume call, so the contained code should not make any thread yield calls. These restriction allow suspend/resume to be efficient.
ChronoSetStatus(Boolean inStatus)
The second way to turn profiling on and off is to use ChronoSetStatus() with an argument of true (turn profiling on) or false (turn profiling off). ChronoSetStatus() may be called at any time, and from any function. The profiling state of other threads running when ChronoSetStatus() is called is also affected when those threads are next scheduled, so the profiling status controlled by ChronoSetStatus() affects all threads. ChronoSetStatus() is a much more expensive call than ChronoSuspend()/ChronoResume() because, internally, Chronoscope has to maintain stack states for every thread.

Your profiled application will run a little faster when profiling is turned off, but still nowhere near as fast as an unprofiled app would, because Chronoscope has to maintain internally stacks for each thread.

Intra-function profiling

Chronoscope is a fuction-level profiler, and, as such, doesn't show you where time is spent within a function that does not call other functions. To address this shortcoming, the Chronoscope API contains a pair of calls that allow you to attribute time to a user-specified event, or "user section".

ChronoEnterUserSection(const char* sectionName)
Indicate to Chronoscope that you are entering a section of code for which you want to see delimiters in the timeline view in the viewer. This will show up as a vertical line inside of the parent function call. The parameter supplies a string that will show up in the viewer to identify this user section. Must be matched by a call to ChronoLeaveUserSection() later in the same function.
ChronoLeaveUserSection()
Indicate the end of the section of code which you want to see delimited in the viewer. Must be matched by a preceding call to ChronoEnterUserSection() in the same function.

The user section API calls are useful to attribute time inside a function to a Toolbox call, or a call to some other non-profiled code. Think of user sections as 'fake' function calls that don't increase the depth of the stack in the viewer.

Profiling problems

The most common problem with profiling is running out of memory for sample data. If this happens, Chronoscope will stop collecting data, and put up a dialog the next time the profiled application calls ChronoTask(). To rectify this, either increase the inMaxSamples value you pass to ChronoInit(), or add more calls to ChronoTask() in your code.

Chronoscope can also run out of space for its table of unique functions, in which case you need to increase the value of the inNumFunctions parameter to ChronoInit(). Again, if this happens, Chronoscope will throw up a dialog in ChronoTask().

Large applications may slow to crawl when being profiled with Chronoscope. That may be partially alleviated by reducing the amount of code being profiled (with ProfilerSetStatus() calls), or only turning profiling on at build time.

Once you've run your profiled application and created a data file, you can use the Chronoscope viewer to look at the data.


SourceForge Logo
smfr@users.sourceforge.net
Last modified: 8-Apr-02