Rationale for Ada 2005

John Barnes
Contents   Index   References   Search   Previous   Next 

5.6 CPU clocks and timers

Ada 2005 introduces three different kinds of timers. Two are concerned with monitoring the CPU time of tasks – one applies to a single task and the other to groups of tasks. The third timer measures real time rather than execution time and can be used to trigger events at specific real times. We will look first at the CPU timers because that introduces more new concepts.
The execution time of one or more tasks can be monitored and controlled by the new package Ada.Execution_Time plus two child packages. 
Ada.Execution_Time

This is the root package and enables the monitoring of execution time of individual tasks.
Ada.Execution_Time.Timers

This provides facilities for defining and enabling timers and for establishing a handler which is called by the run time system when the execution time of the task reaches a given value.
Ada.Execution_Time.Group_Budgets

This enables several tasks to share a budget and provides means whereby action can be taken when the budget expires. 
The execution time of a task, or CPU time as it is commonly called, is the time spent by the system executing the task and services on its behalf. CPU times are represented by the private type CPU_Time. This type and various subprograms are declared in the root package Ada.Execution_Time whose specification is as follows (as before we have added some use clauses in order to ease the presentation)
with Ada.Task_Identification;  use Ada.Task_Identification;
with Ada.Real_Time;  use Ada.Real_Time;
package Ada.Execution_Time is
   type CPU_Time is private;
   CPU_Time_First: constant CPU_Time;
   CPU_Time_Last: constant CPU_Time;
   CPU_Time_Unit: constant := implementation-defined-real-number;
   CPU_Tick: constant Time_Span;
   function Clock(T: Task_Id := Current_Task) return CPU_Time;
   function "+" (Left: CPU_Time; Right: Time_Span) return CPU_Time;
   function "+" (Left: Time_Span; Right: CPU_Time) return CPU_Time;
   function "–" (Left: CPU_Time; Right: Time_Span) return CPU_Time;
   function "–" (Left: CPU_Time; Right: CPU_Time) return Time_Span;
   function "<" (Left, Right: CPU_Time) return Boolean;
   function "<=" (Left, Right: CPU_Time) return Boolean;
   function ">" (Left, Right: CPU_Time) return Boolean;
   function ">=" (Left, Right: CPU_Time) return Boolean;
   procedure Split(T: in CPU_Time; SC: out Seconds_Count; TS: out Time_Span);
   function Time_Of(SC: Seconds_Count; TS: Time_Span := Time_Span_Zero)
                        return CPU_Time;
private
   ... -- not specified by the language
end Ada.Execution_Time;
The CPU time of a particular task is obtained by calling the function Clock with the task as parameter. It is set to zero at task creation.
The constants CPU_Time_First and CPU_Time_Last give the range of values of CPU_Time. CPU_Tick gives the average interval during which successive calls of Clock give the same value and thus is a measure of the accuracy whereas CPU_Time_Unit gives the unit of time measured in seconds. We are assured that CPU_Tick is no greater than one millisecond and that the range of values of CPU_Time is at least 50 years (provided always of course that the implementation can cope).
The various subprograms perform obvious operations on the type CPU_Time and the type Time_Span of the package Ada.Real_Time.
A value of type CPU_Time can be converted to a Seconds_Count plus residual Time_Span by the function Split which is similar to that in the package Ada.Real_Time. The function Time_Of similarly works in the opposite direction. Note the default value of Time_Span_Zero for the second parameter – this enables times of exact numbers of seconds to be given more conveniently thus 
Four_Secs: CPU_Time := Time_Of(4);
In order to find out when a task reaches a particular CPU time we can use the facilities of the child package Ada.Execution_Time.Timers whose specification is
with System;  use System;
package Ada.Execution_Time.Timers is
   type Timer(T: not null access constant Task_Id) is tagged limited private;
   type Timer_Handler is access protected procedure (TM: in out Timer);
   Min_Handler_Ceiling: constant Any_Priority := implementation-defined;
   procedure Set_Handler(TM: in out Timer; In_Time: Time_Span; Handler: Timer_Handler);
   procedure Set_Handler(TM: in out Timer; At_Time: CPU_Time; Handler: Timer_Handler);
   function Current_Handler(TM: Timer) return Timer_Handler;
   procedure Cancel_Handler(TM: in out Timer; Cancelled: out Boolean);
   function Time_Remaining(TM: Timer) return Time_Span;
   Timer_Resource_Error: exception;
private
   ...  -- not specified by the language
end Ada.Execution_Time.Timers;
The general idea is that we declare an object of type Timer whose discriminant identifies the task to be monitored – note the use of not null and constant in the discriminant. We also declare a protected procedure which takes the timer as its parameter and which performs the actions required when the CPU_Time of the task reaches some value. Thus to take some action (perhaps abort for example although that would be ruthless) when the CPU_Time of the task My_Task reaches 2.5 seconds we might first declare 
My_Timer: Timer(My_Task'Identity'Access);
Time_Max: CPU_Time := Time_Of(2, Milliseconds(500));
and then 
protected Control is
   procedure Alarm(TM: in out Timer);
end;
protected body Control is
   procedure Alarm(TM: in out Timer) is
   begin
      -- abort the task
      Abort_Task(TM.T.all);
   end Alarm;
end Control;
Finally we set the timer in motion by calling the procedure Set_Handler which takes the timer, the time value and (an access to) the protected procedure thus 
Set_Handler(My_Timer, Time_Max, Control.Alarm'Access);
and then when the CPU time of the task reaches Time_Max, the protected procedure Control.Alarm is executed. Note how the timer object incorporates the information regarding the task concerned using an access discriminant T and that this is passed to the handler via its parameter TM.
Aborting the task is perhaps a little violent. Another possibility is simply to reduce its priority so that it is no longer troublesome, thus 
      -- cool that task
      Set_Priority(Priority'First, TM.T.all);
Another version of Set_Handler enables the timer to be set for a given interval (of type Time_Span).
The handler associated with a timer can be found by calling the function Current_Handler. This returns null if the timer is not set in which case we say that the timer is clear.
When the timer expires, and just before calling the protected procedure, the timer is set to the clear state. One possible action of the handler, having perhaps made a note of the expiration of the timer, it to set the handler again or perhaps another handler. So we might have 
protected body Control is
   procedure Alarm(TM: in out Timer) is
   begin
      Log_Overflow(TM);     -- note that timer had expired
      -- and then reset it for another 500 milliseconds
      Set_Handler(TM, Milliseconds(500), Kill'Access);
   end Alarm;
   procedure Kill(TM: in out Timer) is
   begin
      -- expired again so kill it
      Abort_Task(TM.T.all);
   end Kill;
end Control;
In this scenario we make a note of the fact that the task has overrun and then give it another 500 milliseconds but with the handler Control.Kill so that the second time is the last chance.
Setting the value of 500 milliseconds directly in the call is a bit crude. It might be better to parameterize the protected type thus 
protected type Control(MS: Integer) is ...
...
My_Control: Control(500);
and then the call of Set_Handler in the protected procedure Alarm would be 
Set_Handler(TM, Milliseconds(MS), Kill'Access);
Observe that overload resolution neatly distinguishes whether we are calling Set_Handler with an absolute time or a relative time.
The procedure Cancel_Handler can be used to clear a timer. The out parameter Cancelled is set to True if the timer was in fact set and False if it was clear. The function Time_Remaining returns Time_Span_Zero if the timer is not set and otherwise the time remaining.
Note also the constant Min_Handler_Ceiling. This is the minimum ceiling priority that the protected procedure should have to ensure that ceiling violation cannot occur.
This timer facility might be implemented on top of a POSIX system. There might be a limit on the number of timers that can be supported and an attempt to exceed this limit will raise Timer_Resource_Error.
We conclude by summarizing the general principles. A timer can be set or clear. If it is set then it has an associated (non-null) handler which will be called after the appropriate time. The key subprograms are Set_Handler, Cancel_Handler and Current_Handler. The protected procedure has a parameter which identifies the event for which it has been called. The same protected procedure can be the handler for many events. The same general structure applies to other kinds of timers which will now be described.
In order to program various so-called aperiodic servers it is necessary for tasks to share a CPU budget.
This can be done using the child package Ada.Execution_Time.Group_Budgets whose specification is
with System;  use System;
package Ada.Execution_Time.Group_Budgets is
   type Group_Budget is tagged limited private;
   type Group_Budget_Handler is access protected procedure (GB: in out Group_Budget);
   type Task_Array is array (Positive range <>) of Task_Id;
   Min_Handler_Ceiling: constant Any_Priority := implementation-defined;
   procedure Add_Task(GB: in out Group_Budget; T: in Task_Id);
   procedure Remove_Task(GB: in out Group_Budget; T: in Task_Id);
   function Is_Member(GB: Group_Budget; T: Task_Id) return Boolean;
   function Is_A_Group_Member(T: Task_Id) return Boolean;
   function Members(GB: Group_Budget) return Task_Array;
   procedure Replenish(GB: in out Group_Budget; To: in Time_Span);
   procedure Add(GB: in out Group_Budget; Interval: in Time_Span);
   function Budget_Has_Expired(GB: Group_Budget) return Boolean;
   function Budget_Remaining(GB: Group_Budget) return Time_Span;
   procedure Set_Handler(GB: in out Group_Budget; Handler: in Group_Budget_Handler);
   function Current_Handler(GB: Group_Budget) return Group_Budget_Handler;
   procedure Cancel_Handler(GB: in out Group_Budget; Cancelled: out Boolean);
   Group_Budget_Error: exception;
private
   ...  -- not specified by the language
end Ada.Execution_Time.Group_Budgets;
This has much in common with its sibling package Timers but there are a number of important differences.
The first difference is that we are here considering a CPU budget shared among several tasks. The type Group_Budget both identifies the group of tasks it covers and the size of the budget.
Various subprograms enable tasks in a group to be manipulated. The procedures Add_Task and Remove_Task add or remove a task. The function Is_Member identifies whether a task belongs to a specific group whereas Is_A_Group_Member identifies whether a task belongs to any group. A task cannot be a member of more than one group. An attempt to add a task to more than one group or remove it from the wrong group and so on raises Group_Budget_Error. Finally the function Members returns all the members of a group as an array.
The value of the budget (initially Time_Span_Zero) can be loaded by the procedure Replenish and increased by the procedure Add. Whenever a budget is non-zero it is counted down as the tasks in the group execute and so consume CPU time. Whenever a budget goes to Time_Span_Zero it is said to have become exhausted and is not reduced further. Note that Add with a negative argument can reduce a budget – it can even cause it to become exhausted but not make it negative.
The function Budget_Remaining simply returns the amount left and Budget_Has_Expired returns True if the budget is exhausted and so has value Time_Span_Zero.
Whenever a budget becomes exhausted (that is when the value transitions to zero) a hander is called if one has been set. A handler is a protected procedure as before and procedures Set_Handler, Cancel_Handler, and function Current_Handler are much as expected. But a major difference is that Set_Handler does not set the time value of the budget since that is done by Replenish and Add. The setting of the budget and the setting of the handler are decoupled in this package. Indeed a handler can be set even though the budget is exhausted and the budget can be counting down even though no handler is set. The reason for the different approach simply reflects the usage paradigm for the feature.
So we could set up a mechanism to monitor the CPU time usage of a group of three tasks TA, TB, and TC by first declaring an object of type Group_Budget, adding the three tasks to the group and then setting an appropriate handler. Finally we call Replenish which sets the counting mechanism going. So we might write 
ABC: Group_Budget;
...
Add_Task(ABC, TA'Identity);
Add_Task(ABC, TB'Identity);
Add_Task(ABC, TC'Identity);
Set_Handler(ABC, Control.Monitor'Access);
Replenish(ABC, Seconds(10));
Remember that functions Seconds and Minutes have been added to the package Ada.Real_Time.
The protected procedure might be 
protected body Control is
   procedure Monitor(GB: in out Group_Budget) is
   begin
      Log_Budget;
      Add(GB, Seconds(10));    -- add more time
   end Monitor;
end Control;
The procedure Monitor logs the fact that the budget was exhausted and then adds a further 10 seconds to it. Remember that the handler remains set all the time in the case of group budgets whereas in the case of the single task timers it automatically becomes cleared and has to be set again if required.
If a task terminates then it is removed from the group as part of the finalization process.
Note that again there is the constant Min_Handler_Ceiling.
The final kind of timer concerns real time rather than CPU time and so is provided by a child package of Ada.Real_Time whereas the timers we have seen so far were provided by child packages of Ada.Execution_Time. The specification of the package Ada.Real_Time.Timing_Events is
package Ada.Real_Time.Timing_Events is
   type Timing_Event is tagged limited private;
   type Timing_Event_Handler is access protected procedure (Event: in out Timing_Event);
   procedure Set_Handler(Event: in out Timing_Event; At_Time: Time;
                               Handler: Timing_Event_Handler);
   procedure Set_Handler(Event: in out Timing_Event; In_Time: Time_Span;
                               Handler: Timing_Event_Handler);
   function Current_Handler(Event: Timing_Event) return Timing_Event_Handler;
   procedure Cancel_Handler(Event: in out Timing_Event; Cancelled: out Boolean);
   function Time_Of_Event(Event: Timing_Event) return Time;
private
   ...  -- not specified by the language
end Ada.Real_Time.Timing_Events;
This package provides a very low level facility and does not involve Ada tasks at all. It has a very similar pattern to the package Execution_Time.Timers. A handler can be set by Set_Handler and again there are two versions one for a relative time and one for absolute time. There are also subprograms Current_Handler and Cancel_Handler. If no handler is set then Current_Handler returns null.
Set_Handler also specifies the protected procedure to be called when the time is reached. Times are of course specified using the type Real_Time rather than CPU_Time.
A minor difference is that this package has a function Time_Of_Event rather than Time_Remaining.
A simple example was given in the introductory chapter. We repeat it here for convenience. The idea is that we wish to ring a pinger when our egg is boiled after four minutes. The protected procedure might be 
protected body Egg is
   procedure Is_Done(Event: in out Timing_Event) is
   begin
      Ring_The_Pinger;
   end Is_Done;
end Egg;
and then 
Egg_Done: Timing_Event;
Four_Min: Time_Span := Minutes(4);
...
Put_Egg_In_Water;
Set_Handler(Event => Egg_Done, In_Time => Four_Min, Handler => Egg.Is_Done'Access);
-- now read newspaper whilst waiting for egg
This is unreliable because if we are interrupted between the calls of Put_Egg_In_Water and Set_Handler then the egg will be boiled for too long. We can overcome this by adding a further protected procedure Boil to the protected object and placing Is_Done in the private part so that it becomes 
protected Egg is
   procedure Boil(For_Time: in Time_Span);
private
   procedure Is_Done(Event: in out Timing_Event);
   Egg_Done: Timing_Event;
end Egg;
protected body Egg is
   procedure Boil(For_Time: in Time_Span) is
   begin
      Put_Egg_In_Water;
      Set_Handler(Egg_Done, For_Time, Is_Done'Access);
   end Boil;
   procedure Is_Done(Event: in out Timing_Event) is
   begin
      Ring_The_Pinger;
   end Is_Done;
end Egg;
This is much better. The timing mechanism is now completely encapsulated in the protected object and the procedure Is_Done is no longer visible outside. So all we have to do is 
Egg.Boil(Minutes(4));
-- now read newspaper whilst waiting for egg
Of course if the telephone rings as the pinger goes off and before we have a chance to eat the egg then it still gets overdone. One solution is to eat the egg within the protected procedure Is_Done as well. A gentleman would never let a telephone call disturb his breakfast.
One protected procedure could be used to respond to several events. In the case of the CPU timer the discriminant of the parameter identifies the task; in the case of the group and real-time timers, the parameter identifies the event.
If we want to use the same timer for several events then various techniques are possible. Note that the timers are limited so we cannot test for them directly. However, they are tagged and so can be extended. Moreover, we know that they are passed by reference and that the parameters are considered aliased.
Suppose we are boiling six eggs in one of those French breakfast things with a different coloured holder for each egg. We can write 
type Colour is (Black, Blue, Red, Green, Yellow, Purple);
Eggs_Done: array (Colour) of aliased Timing_Event;
We can then set the handler for the egg in the red holder by something like 
Set_Handler(Eggs_Done(Red), For_Time, Is_Done'Access);
and then the protected procedure might be 
procedure Is_Done(E: in out Timing_Event) is
begin
   for C in Colour loop
      if E'Access = Eggs_Done(C)'Access then
               -- egg in holder colour C is ready
         ...
         return;
      end if;
   end loop;
               -- falls out of loop – unknown event!
   raise Not_An_Egg ;
end Is_Done;
Although this does work it is more than a little distasteful to compare access values in this way and moreover requires a loop to see which event occurred.
A much better approach is to use type extension and view conversions. First we extend the type Timing_Event to include additional information about the event (in this case the colour) so that we can identify the particular event from within the handler 
type Egg_Event is new Timing_Event with
   record
      Event_Colour: Colour;
   end record;
We then declare an array of these extended events (they need not be aliased) 
Eggs_Done: array (Colour) of Egg_Event;
We can now call Set_Handler for the egg in the red holder 
Set_Handler(Eggs_Done(Red), For_Time, Is_Done'Access);
This is actually a call on the Set_Handler for the type Egg_Event inherited from Timing_Event. But it is the same code anyway.
Remember that values of tagged types are always passed by reference. This means that from within the procedure Is_Done we can recover the underlying type and so discover the information in the extension. This is done by using view conversions.
In fact we have to use two view conversions, first we convert to the class wide type Timing_Event'Class and then to the specific type Egg_Event. And then we can select the component Event_Colour. In fact we can do these operations in one statement thus 
procedure Is_Done(E: in out Timing_Event) is
   C: constant Colour := Egg_Event(Timing_Event'Class(E)).Event_Colour;
begin
               -- egg in holder colour C is ready
   ...
end Is_Done;
Note that there is a check on the conversion from the class wide type Timing_Event'Class to the specific type Egg_Event to ensure that the object passed as parameter is indeed of the type Egg_Event (or a further extension of it). If this fails then Tag_Error is raised. In order to avoid this possibility we can use a membership test. For example 
procedure Is_Done(E: in out Timing_Event) is
   C: Colour;
begin
   if Timing_Event'Class(E) in Egg_Event then
      C := Egg_Event(Timing_Event'Class(E)).Event_Colour;
               -- egg in holder colour C is ready
      ...
   else
               -- unknown event – not an egg event!
      raise Not_An_Egg;
   end if;
end Is_Done;
The membership test ensures that the event is of the specific type Egg_Event. We could avoid the double conversion to the class wide type by introducing an intermediate variable.
It is important to appreciate that no dispatching is involved in these operations at all – everything is static apart from the membership test.
Of course, it would have been a little more flexible if the various subprograms took a parameter of type Timing_Event'Class but this would have conflicted with the Restrictions identifier No_Dispatch. Note that Ravenscar itself does not impose No_Dispatch but the restriction is in the High-Integrity annex and thus might be imposed on some high-integrity applications which might nevertheless wish to use timers in a simple manner.
A few minor points of difference between the timers are worth summarizing.
The two CPU timers have a constant Min_Handler_Ceiling. This prevents ceiling violation. It is not necessary for the real-time timer because the call of the protected procedure is treated like an interrupt and thus is at interrupt ceiling level.
The group budget timer and the real-time timer do not have an exception corresponding to Timer_Resource_Error for the single task CPU timer. As mentioned above, it is anticipated that the single timer might be implemented on top of a POSIX system in which case there might be a limit to the number of timers especially since each task could be using several timers. In the group case, a task can only be in one group so the number of group timers is necessarily less than the number of tasks and no limit is likely to be exceeded. In the real-time case the events are simply placed on the delay queue and no other resources are required anyway.
It should also be noted that the group timer could be used to monitor the execution time of a single task. However, a task can only be in one group and so only one timer could be applied to a task that way whereas, as just mentioned, the single CPU timer is quite different since a given task could have several timers set for it to expire at different times. Thus both kinds of timers have their own distinct usage patterns.

Contents   Index   References   Search   Previous   Next 
© 2005, 2006, 2007 John Barnes Informatics.
Sponsored in part by:
The Ada Resource Association and its member companies: ARA Members AdaCore Polyspace Technologies Praxis Critical Systems IBM Rational Sofcheck and   Ada-Europe:
Ada-Europe