||package Tracer specification|
||package Tracer body|
||package Tracer_Messages. It is actually a renaming of Tracer_Messages_English or Tracer_Messages_French, allowing Tracer to speak your preferred language. Contributions for other languages welcome!|
||Package Tracer_Messages_English, specification (no body required). Contains all messages used by Tracer, English version.|
||Package Tracer_Messages_French, specification (no body required). Contains all messages used by Tracer, French version|
||package Tracer.Assert specification|
||package Tracer.Assert body|
||package Tracer.Timing specification.|
||package Tracer.Timing body.|
||(this file) package Tracer documentation|
||package Tracer implementation notes|
||test program and example for package Tracer|
||auxiliary package for test program (spec)|
||auxiliary package for test program (body)|
||package Protection specification|
||package Protection body|
||package Protection documentation|
||GNU General Public License|
Package Tracer makes use of package Protection, which is available separately from Adalog's software components page. As a convenience, we provide it with this distribution of Tracer, although it is an independent component.
Fully portable to any compiler supporting System-Programming
and Real-Time annexes.
This package uses no compiler specific feature; it uses features defined in the System-Programming and Real-Times annexes, and does not use any feature defined in other annexes.
The Tracer component (Packages Tracer, Tracer.Assert and Tracer.Timing and the associated documentation) is Copyright © 1997, 2021 ADALOG.
The Tracer component is free software; you can redistribute it and/or modify it under terms of the GNU General Public License as published by the Free Software Foundation; either version 2, or (at your option) any later version. This component is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License distributed with this program; see file COPYING. If not, write to the Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
As a special exception, if other files instantiate generics from the units of this component, or if you link units provided with this component with other files to produce an executable, these units do not by themselves cause the resulting executable to be covered by the GNU General Public License. This exception does not however invalidate any other reasons why the executable file might be covered by the GNU Public License.
This component is offered with this very liberal license as a demonstration of Adalog's know-how; please do not remove the reference to Adalog in the header comment of each unit.
Although we do not require it, you are very welcome, if you find this component useful, to send us a note at email@example.com. We also welcome critics, suggestions for improvement, etc.
ADALOG is providing training, consultancy, expertise and custom development in Ada and related software engineering techniques. For more information about our services:
|2 rue du Docteur Lombard
|Tel: +33 1 41 24 31 40
Fax: +33 1 41 24 07 36
This package provides a sophisticated trace facility,
especially valuable for multi-tasking programs. The child package
Tracer.Timing also provides
utilities for measuring execution time, and the child package Tracer.Assert provides utilities for checking that
certain properties of your program are actually met. To get an
idea, compile and run the test program (
The user interface of Tracer can be taylored to speak various
languages; currently, English and French are supported, and
English is the default. If you want Tracer to use another
language, please read the comments in package
We will gladly accept any voluntarily effort to support other
To use the package, you put calls to various
procedures into your code (don't forget to "with"
Tracer and/or package
Tracer.Assert). You can specify when you
enter or exit a procedure, so that trace messages are properly
indented. Indentation is kept on a per-task basis, and traces
from different tasks are clearly separated. You can even chose to
trace messages only from a given, or several, tasks (or except
from one or several tasks).
When you start your program, Tracer is automatically activated and pauses with the following message:
**Tracer** : Pause (Trace Mode: Enter=Normal, 'S'=Step, 'I'=Ignore Tracer) **Tracer** Command? ('H' for Help) :
trace. If the file exists, you are prompted for permission to overwrite it.
These are the most common answers at that point, but actually, any response allowed at a regular pause (see Controlling traces) is allowed.
Once your program is debugged, remove the trace calls and the
Tracer;" from your code. Remove the Tracer package
itself from your path, and recompile. If you left inadvertantly
some trace calls, the compiler will tell you... This is why it is
MUCH better to use this package rather than simple calls to
procedure Trace (Message : String);
This is the simplest trace procedure. It simply prints its
argument, according to the current indentation level. If the
message includes control characters, they are translated to
notation (i.e. a CR will be displayed as
can be handy in dectecting problems caused by control characters
type Trace_Flag is (Start, Stop); procedure Trace (Flag : Trace_Flag; Message : String);
This procedure is intended to trace subprograms calls. Put a
when you enter a subprogram, and a
when you exit it. The message can be anything, but generally you
will like to put the name of the entered subprogram here.
Entering and exiting subprograms is marked with arrows ("
<=") in the trace, and the
indentation level is changed accordingly.
type Tracer_String (<>) is limited private; type Tracer_String_Access is access Tracer_String; function "+" (Item : String) return Tracer_String_Access; type Auto_Tracer (Message : Tracer_String_Access) is limited private;
Alternatively, you can declare an object of type
initialized with the name of the subprogram, such as in:
Tracer: Auto_Tracer (+"My_Subprogram");
The initialization of this object will automatically call
(Start, ...), and its finalization will call
...). It is especially convenient for tracing functions
that return complex expressions, since the Finalization (and
therefore the call to
Trace (Stop...)) happens after
the evaluation of the return expression.
procedure Trace (Message : String; Value : Boolean);
This procedure allows you to trace boolean values. It prints
the message, followed by the image of
type Any_Int is range System.Min_Int .. System.Max_Int; procedure Trace (Message : String; Value : Any_Int);
This procedure allows you to trace integer values. It prints
the message, followed by the image of
Any_Int is compatible with any integer
type, you can call it as:
Trace("Value of X:", Any_Int(X));
whatever the type of X is.
type Any_Float is digits System.Max_Digits; procedure Trace (Message : String; Value : Any_Float);
This procedure allows you to trace float values. It prints the
message, followed by the image of
Value. Since type
is compatible with any floating point type, you can call it as:
Trace("Value of X:", Any_Float(X));
whatever the type of
procedure Trace (Message : String; Raised_Exception : Ada.Exceptions.Exception_Occurrence);
This procedure is useful to trace exceptions. It prints the
message, followed by the exception name of
It is typically put in an exception handler :
exception when Occ : others => Trace ("Unexpected exception raised : ", Occ); raise;
procedure Trace (Message : String; Value : Ada.Tags.Tag);
This procedure allows you to trace tags (it prints the expanded name of the tag). Typically, when you expect a given tagged type and the one you get is not the one you expected, you can write:
Trace("Tag of X:", X'Tag);
type Tracer_Stream_Access is access all Ada.Streams.Root_Stream_Type'Class; Trace_Stream : constant Tracer_Stream_Access;
This stream allows you to trace any type, by writing something like:
X : T; begin T'Write (Trace_Stream, X);
Output will be the hexadecimal representation of the stream elements and their interpretation as Ascii characters. Note that the representation can be compiler dependent, so check you compiler's documentation to interpret the values; especially note that on little endians (Intel) platforms, bytes may be swapped. Note also that for composite values, this will result to a call to 'Write for each elementary type (unless you have redefined the 'Write attribute), so you will obtain several trace messages. Any use of 'Read on this stream will raise Program_Error.
type Tracer_Counter is range 1..25; function Tracer_Count (Counter : Tracer_Counter) return String;
Tracer provides 25 counters. Each call to Tracer_Count increments the corresponding counter and returns its value as a string. This can be useful to trace where you are in a loop:
loop Trace ("Iteration" & Tracer_Count (1)); ...
procedure Pause; procedure Pause (Message : String);
These procedures simply pause (i.e. stop execution and wait
for input); this allows you to view the trace messages. At this
point, you are also given the opportunity to change trace
parameters; see Controlling traces for more
details. The form without a parameter prints a default message,
and the form with a
Message prints the message when
Pause is executed. Note that it is not
equivalent to calling
Trace followed by
Message, because in the latter case some
other task could insert its own messages inbetween.
The message is output to the current target, but the pause prompt is always printed on the console. In other words, if you are tracing to a file, the pause message will not appear on the terminal. However, if you issue a '?' command, it will show you the current pause message.
procedure Keep_Trace (...); procedure Flush_Trace;
Sometimes, you don't want Tracer output to be mixed with your
own output. In this case, use
Keep_Trace instead of
There is a
Keep_Trace procedure for each
with the same parameters (actually, there is an extra parameter,
but you can ignore it for now, the default is OK).
Keep_Trace, the message is saved, and will
be printed as soon as you call any regular
you can force output by calling
course, proper ordering of trace messages is preserved. If your
program terminates, whether normally or abnormally, for any
reason, all outstanding kept messages are printed.
Trace messages originate from source tasks and are directed to a target file. By default, all tasks in the program are sources, and the target file is the console, but this can be changed either by calling subprograms provided by package Tracer, or by giving commands when the program pauses. We give here the description of the commands; corresponding subprograms are described under Advanced features.
Whenever the program pauses, you are prompted with the following message :
Command? ('H' for help) :
At that point, you may simply type 'Enter', or a string
consisting of characters representing trace commands. Execution
will resume after all commands have been analyzed, if the string
includes any of the "Go" commands described below.
"Enter" alone is equivalent to repeating the previous
"Go" command. The key letters described here are for
the English version of Tracer; if you compiled Tracer for a
different language, please refer to the
package for your language.
Pause. By convention, 'G0' (zero) means "don't pause unless there is an explicit call to pause". 'G' alone is a shorthand for 'G0'.
Marked' and '
Unmarked'. The "selected task" is the one that originated the last message; if in doubt, issue the '?' command which will tell you which is the currently selected task.
trace. The first time this command is issued, the file is created, or overwritten (after asking for permission) if it exists.
Pauseis forced. All kept messages will be printed. This is useful if you suspect infinite loops or dead-locks. Once you have gotten control back, you may for example call the user watch procedure to inspect the state of your variables. You can also call one of your own procedures instead of
Pausewhen the timer expires; see Advanced features. By convention, 'D0' (zero) cancels the timer. 'D' alone is a shorthand for 'D0'.
You can use the previous facilities normally from several tasks. The package is completely protected for multi-tasking, so outputs (and pauses) from several tasks will be properly serialized, and no race condition can occur within Tracer nor can any deadlock occur due to Tracer.
When Tracer discovers a call that does not come from the main task, it enters multi-tasking mode. In this mode, trace messages are prefixed with the Task_ID (except for messages that originate directly from Tracer internal tasks). When it enters this mode, Tracer tells you the Task_ID of the main task, so you can identify it in the subsequent traces.
Whenever there is a task switch, traces are separated with a line of hyphens. Indentation levels are kept on a per-task basis, so they are meaningful even in multi-task mode. Note also that task IDs are at the beginning of the line: if you output the trace to a file, and then sort the file (taking as a key the text before the colon), you'll get a complete trace for each task independently. Under Unix, the magic formula is:
sort -s -t: +0 -1 trace >trace.sorted
Another use is to "grep" the trace file with the task_ID to extract all messages output by a given task.
There are special issues if you want to trace calls to protected operations.
Keep_Traceprocedures are not potentially blocking, so it is safe to call them. All other procedures are potentially blocking.
Current_Taskis not meaningful if called from within a entry body. Since
Current_Taskto know the currently executing task, you must tell it; that's why all
Keep_Traceprocedures have an extra parameter (
Caller) of type
Task_ID. The default value is
Current_Task, but from an entry body, you should provide it with the
'Callerfor the entry. In short, use it as:
entry body E when ... is Keep_Trace (Start, "Entering E", Caller => E'Caller); ... Keep_Trace (Stop, "Leaving E", Caller => E'Caller); end;
Note that if you forget to mention the
parameter in a
Keep_Trace in an entry body,
GNAT will warn you with the message
"Current_Task" should not be used in
entry body (RM C.7(17))
A task may be marked, and you can chose to trace only marked
tasks. In other words, when a
procedure is called, the request is ignored if the calling task
is not marked. Conversely, you can chose to trace all but the
marked tasks; see Controlling traces.
The mark status of a task can be toggled during a pause (see Controlling traces). Alternatively, it can be set (and retrieved) by program:
package ATI renames Ada.Task_Identification; procedure Mark (Target_Task : ATI.Task_ID := ATI.Current_Task) procedure Unmark (Target_Task : ATI.Task_ID := ATI.Current_Task) function Is_Marked (Target_Task : ATI.Task_ID := ATI.Current_Task) return Boolean;
Note that by default these procedures mark or unmark the
current task, but that you can mark or unmark any task. This
allows for example the main program to mark the tasks that are to
be traced, or a server to mark its clients (thanks to the
During its initialization, Tracer checks for a file named
in the current directory. If it exists, it reads its command from
this file, and returns to the console when the file is exhausted.
Blank lines and lines whose first character is '#' are ignored.
While reading from the file, all confirmation questions (whose
responses are Yes/No) allways default to Yes and are not read
from the file nor from the console. This means that for example,
if you are tracing to a file and the file already exists, Tracer
will automatically overwrite it without asking the question. This
is intended to allow the same script to work whether the trace
file exists or not.
As a special case, if the file
and is empty (like a symbolic link to
under Unix for example), Tracer will automatically enter "Ignore"
mode and will not print anything. This allows leaving calls to
Tracer in a completely silent way, in order, for example, to
provide "beta" versions to your testers. If something
goes wrong, you just need to delete the
file to reactivate Tracer.
type Procedure_Access is access procedure; procedure Set_Watch (To : Procedure_Access);
This procedure allows you to pass a pointer-to-procedure to Tracer. The procedure will be called during a pause when the 'W' command is issued (see Controlling traces). Such a procedure can be used to print the values of internal variables, thus providing a kind of "watch" facility. All regular Tracer procedures can be used from the watch procedure.
type Trace_Source is (All_Tasks, Marked_Only, Marked_Excepted); procedure Set_Source (To : Trace_Source); function Current_Source return Trace_Source;
These subprograms allow you to set or query the source tasks at any time.
type Trace_Target is (Console, File, None); procedure Set_Target (To : Trace_Target); function Current_Target return Trace_Target; File_Trace_Denied : exception;
These subprograms allow you to change or query the current
target. There is no restriction: you can switch freely between
Console for example. The first time you switch
File, if a file named
exists, the user is asked for permission to overwrite. If
overwriting is denied, then the
exception is raised.
This procedure unconditionnally terminates your program, including all tasks. All kept messages are printed. This is useful when you have reached the point where your program is having trouble, and you want to stop it immediately without caring for task terminations. See caveat however.
generic with procedure To_Do; Label : String := ""; package Last_Will is private ... end Last_Will;
When instantiated, this (empty) package registers the
procedure passed as
To_Do to execute it after the
main program is left. This is quite similar to a "watch"
procedure, except that the given procedure is automatically
called after program termination (for whatever reason, of course,
including general abort). Any Tracer service can be called from
To_Do procedure. This can be quite handy to
check, for example, that a counter of ressources has reached 0
when the program terminates. It is also possible to give Tracer's
procedure Pause as a
To_Do procedure, therefore
giving you a last chance to check your program after the
main program has been left.
This package can only be instantiated at accessibility level 0
(i.e. in a library package, but not in a procedure, not
even the main program). We strongly recommend to instantiate this
package at the very end of the containing package; this will
ensure that your
To_Do procedure is called before
anything in the package gets finalized.
type Procedure_Access is access procedure; Off : constant Duration := 0.0; procedure Set_Timer (How_Long : Duration; Processing : Procedure_Access := null);
This subprogram allows you to change the remaining time before
the timer is triggered, and optionaly the procedure called when
it is triggered. You can restart the timer at any time, or
disable it by calling
If you pass a pointer to a parameterless procedure as the
second argument (
Processing), the provided procedure
will be called instead of
Pause (the default). This
is useful if you want to print the value of variables, etc. once
you're deadlocked. Note however that given accessibility rules,
the provided procedure must be global. And YES,
Keep_Trace) procedures can safely be used
Processing procedure. You may also wish to
Set_Timer from your provided procedure in order
to restart the timer; this allows you to periodically sample the
state of your program. Another common usage is to pass
Processing procedure, in order to kill your
program once it is dead-locked. If you don't specify
Processing procedure is left unchanged.
function Remaining_Time return Duration;
This function returns the time remaining until the timer is triggered.
procedure Suspend_Timer; procedure Resume_Timer;
Suspend_Timer allows you to temporarily suspend
the timer; remaining time is saved. The timer will restart with
the saved value when you call
Note: The timer is disabled as soon as a general abort is in progress, as a consequence of a 'q' command or of a call to Abort_Main. Calls to timer-related subprograms are ignored.
Abort_Main, whether directly or by leaving
the program by answering 'q' to a pause, is very demanding on the
tasking implementation. It relies on a very precise semantic
abort and finalization which is difficult
for compiler-makers to implement.
As of currently, we have tried package Tracer on Gnat 3.15p for
Linux, Gnat 3.15p for Windows, and ObjectAda 7.2.1a for Windows,
as well as on earlier versions of these compilers. Some had
Abort_Main was used. Observed
behaviours were :
This is not too worrysome since all messages are printed in any case, but the exit may not be very clean, or, in the case of 3), you may have to kill the process manually. If you have the opportunity to try Tracer with other compilers, please keep us informed how it behaved by sending a note to firstname.lastname@example.org.
This package provides a simple timing facility for measuring execution time of code. We wanted the timing facility itself to be as short as possible, in order to minimize the execution time of the procedures and provide more accurate timings. As a consequence, this package is not protected against tasking, reentrancy, or even recursion.
The package provides 10 timers (this can be changed by
modifying the constant
Max_Timer in the
specification - no other change is required). Each timer can be
started and stopped at will, it keeps a record of the total time
and the number of times it was started. A
procedure displays these values, as well as the average execution
Max_Timer : constant := 10; type Timer_Index is range 1..Max_Timer; procedure Start (The_Timer : Timer_Index);
This procedure starts the given timer. If the timer is already started, the call is ignored.
procedure Stop (The_Timer : Timer_Index; With_Trace : Boolean := False);
This procedure stops the given timer. If the timer is not
started, the call is ignored. If
a trace message is printed giving the time elapsed between
Stop for this timer.
type Auto_Timer (The_Timer : Timer_Index; With_Trace : Boolean) is limited private;
Alternatively, you can declare an object of type
such as in:
Tracer: Auto_Timer (The_Timer => 5, With_Trace => False);
The initialization of this object will automatically call
on the given timer, and its finalization will call the
a message reporting the time spent in the subprogram is printed
for each call.
Using the auto-timer is especially convenient for timing
procedures with multiple returns, or functions that return
complex expressions, since the Finalization (and therefore the
Stop) happens after the evaluation of the
return expression. On the other hand, controlled types add some
overhead, therefore the measurement is less accurate.
procedure Reset (The_Timer : Timer_Index);
This procedure resets the timer to the initial state; all associated counters are cleared.
procedure Name (The_Timer : Timer_Index; As : String);
This procedure associates a name (the
argument) with the timer; if defined, the name will be printed by
Report procedure. This is useful if you don't
remember what you are timing!
If the timer is active or has never been activated, an appropriate message will tell you.
procedure Report (The_Timer : Timer_Index);
This procedure prints the timer number, its associated name if
any, the total time spent between calls to
Stop, the number of timing sessions, and the average
time (total time divided by number of calls). Note that printing
is done by calling
Trace; tracing conditions and
target are determined by the current state of
This procedure calls
Report for all counters
whose call count is not zero. It is automatically invoked when
the program terminates. This way, you don't need to bother about
reporting: just put
calls around the pieces of code you want to measure, you'll
automatically get the statistics when the program terminates.
Note also that the profile of
conformant with the type
Procedure_Access: you can
use it as a watch procedure.
This package supports run-time assertion, i.e. checking that certain things happen (or don't happen) in your program.
procedure Check (Assumption : Boolean; Message : String);
with the given message.
Max_Detector : constant := 10; type Detector_Index is range 1..Max_Detector; type R_Detector (Num : Detector_Index; Message : Tracer_String_Access) is limited private;
This is intended to help you discover reentrancy problems, i.e.
when two tasks call some subprograms at the same time when they
should not. If a task calls a subprogram which declares an object
R_Detector (shorthand for Reentrancy
Detector) while another task is executing a subprogram which also
declares an object of type
R_Detector with the same
Trace is called with a message identifying
the tasks, and both messages. For example, detecting plain
reentrancy in a subprogram just requires you to declare:
Detector : R_Detector (1, +"My_Subprogram_Name");
Tracer_String_Access, see "The basic trace procedures". If you need more different detectors, simply change the value of the constant
Max_Detector, no other change is necessary.
Q:How do I skip the next 100 messages?
A:From a pause, give the command "NG100": set target to none, pause after 100 messages. Then resume with "CS" for example: set target to console, step.
Q:What is the difference between the "N"
command (trace to None) and the "I" command (Ignore
A: "N" temporarily disables output, but Tracer is still active. All pauses are executed, giving you a chance to resume traces. When you issue an "I" command, Tracer is totally deactivated, there is no way to reestablish it (actually, it even stops its internal tasks and releases as much space as it can).
Q: How to identify which tasks corresponds to a task ID
A: Start the task with a trace like :
task is the controller"); Since the message shows the
task-id, you'll know the correspondance. Note that the task ID
image provided by Gnat includes the task's name.
Q: There is a
Trace function for floating-point
types, but not for fixed-points. Why?
A: It is more difficult to define an "Any_Fixed" type than an "Any_Float". And since fixed points can be converted to floating points, it is just as easy to trace fixed point values with the trace function for floating points.
Q: Why provide a fixed number of counters? It seems
simple to declare a Counter type for example...
A: Yes it is, but this would require the user to declare such objects. The idea of Tracer is that you just add calls to Trace, and as far as possible, you don't need to add anything else, especially declarations. 25 counters should be more than enough; normally, you don't leave many traces in your program, you add them when you have a problem, and you remove them when you have found the cause. Therefore, we favour ease of editing over generality. Of course, this is due to the particular context of Tracer, not applicable to general Ada programming!
Q: Why provide two different
functions, with and without message, rather than simply a
with a default value ?
A: The parameterless
Pause can be provided as
Processing action to the timer, but not the
Q: During a pause, I asked to trace only marked tasks,
and I had only one such marked task. However, after I typed 'G',
I got messages from other tasks.
A: Those messages were sent by other tasks while you were in the pause (don't forget that during a pause, other tasks continue to execute in the background). Of course, the messages were blocked until you typed 'G', and they appear as soon as you restart execution.
Q: After a 'Q' answer during a pause, the program
doesn't seem to stop, it even executes pauses...
A: As with the previous question, you still get messages which were sent during the the pause, and also messages that are sent by finalization routines executed as a consequence of the general abort. If you have some automatic pause (as in Step mode), they are normally executed. If you want to leave immediately and silently, type "IQ" (Ignore and Quit).
Q: I call my 'watch' procedure from a Pause, but
nothing is printed...
A: If you are currently tracing to a file, traces from your 'watch' procedure go to the file, like all other traces. Set traces back to console ('C'), call your watch procedure ('W'), and then return the trace to the file ('F').
Q: Can I trace finalizations ?
A: Yes, and no message will ever be lost, even if you finalize your own global variables due to an abort of the main task... Guaranteed!
Q: Are you really, REALLY sure that no message
can be lost ?
A: OK, if you insist :-)... There is one case where messages are lost: if you run short of memory, since Tracer uses allocators. However, in that case, you'll see a message telling you that messages have been lost. And of course, if you later release space, Tracer will resume its normal operation and the messages will appear at the right place. Note that the task-Id of this "Messages lost" message is the one of the first lost message.
Q: How can I trace deadlocks with protected types ?
A: Trace your protected operations (with
of course); In the main program, call
a duration long enough to allow the dead-lock to occur. The main
program will pause at that time, all kept messages will be
printed, and you'll be free to call a user watch procedure to get
Q: I gave a 'D' command with a delay value, but Tracer
still says that the timer value is infinite...
A: You are pausing while a general abort is going on ('q' command, or a call to
Abort_Main). The timer is
disabled in this case. If you issue a '?' command, Tracer will
tell you that a general abort is in progress.
If you are curious about how package Tracer works, you can view the implementation notes. But of course, this is not necessary to use the package.
If you found this package useful...
If you think that it would have taken an awful time to write it yourself...
If it showed you some usages of Ada that you didn't think about...
Maybe you should consider using Adalog's consulting and training services !