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.
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
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 ChronoInit()
.
const FSSpec* inOutputFile
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
SInt32 inStackDepth
SInt32 inMaxSamples
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.
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.
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.
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)
NewThread()
.
The inThreadStackDepth
is the maximum depth of the stack for this thread.
ChronoDeleteThread(void* inThreadID)
SetThreadTerminator()
.
ChronoEnterThread(void* inThreadID)
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.
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()
ChronoLeaveInterrupt()
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()
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)
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.
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)
ChronoLeaveUserSection()
later in the same function.
ChronoLeaveUserSection()
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.
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.