Skip to main content
Version: 0.9.0

Time and Timers

This article has examples in the following target languages:

Logical Time​

A key property of Lingua Franca is logical time. All events occur at an instant in logical time. By default, the runtime system does its best to align logical time with physical time, which is some measurement of time on the execution platform. The lag is defined to be physical time minus logical time, and the goal of the runtime system is maintain a small non-negative lag.

The lag is allowed to go negative only if the fast target property or the --fast command-line argument is set to true. In that case, the program will execute as fast as possible with no regard to physical time.

In Lingua Franca, time is a data type. A parameter, state variable, port, or action may have type time.

In the C target, time values internally have type instant_t or interval_t, both of which are (usually) equivalent to the C type long long.

In the C++ target, time values internally have the type std::chrono::nanoseconds. For details, see the Target Language Details.

danger

In the Rust target, time values internally have type FIXME.

Time Values​

A time value is given with units (unless the value is 0, in which case the units can be omitted). The allowable units are:

  • For nanoseconds: ns, nsec, or nsecs
  • For microseconds: us, usec, or usecs
  • For milliseconds: ms, msec, or msecs
  • For seconds: s, sec, secs, second, or seconds
  • For minutes: min, minute, mins, or minutes
  • For hours: h, hour, or hours
  • For days: d, day, or days
  • For weeks: week or weeks

The following example illustrates using time values for parameters and state variables:

target C main reactor SlowingClock(start: time = 100 ms, incr: time = 100 ms) { state interval: time = start logical action a reaction(startup) -> a {= lf_schedule(a, self->start); =} reaction(a) -> a {= instant_t elapsed_logical_time = lf_time_logical_elapsed(); printf("Logical time since start: %lld nsec.\n", elapsed_logical_time ); self->interval += self->incr; lf_schedule(a, self->interval); =} }

This has two time parameters, start and incr, each with default value 100 ms and type time. This parameter is used to initialize the interval state variable, which also stores a time. The logical action a, explained in Actions, is used to schedule events to occur at time start after program startup and then at intervals that are increased each time by incr. The result of executing this program will look like this:

Logical time since start: 100000000 nsec.
Logical time since start: 300000000 nsec.
Logical time since start: 600000000 nsec.
Logical time since start: 1000000000 nsec.
...

Timers​

The simplest use of logical time in Lingua Franca is to invoke a reaction periodically. This is done by first declaring a timer using this syntax:

timer <name>(<offset>, <period>)

The <period>, which is optional, specifies the time interval between timer events. The <offset>, which is also optional, specifies the (logical) time interval between when the program starts executing and the first timer event. If no period is given, then the timer event occurs only once. If neither an offset nor a period is specified, then one timer event occurs at program start, simultaneous with the startup event.

The period and offset are given by a number and a units, for example, 10 ms. See the expressions documentation for allowable units. Consider the following example:

target C main reactor Timer { timer t(0, 1 sec) reaction(t) {= printf("Logical time is %lld.\n", lf_time_logical()); =} }

This specifies a timer named t that will first trigger at the start of execution and then repeatedly trigger at intervals of one second. Notice that the time units can be left off if the value is zero.

This target provides a built-in function for retrieving the logical time at which the reaction is invoked, lf_time_logical() get_logical_time() lf.time.logical() util.getCurrentLogicalTime() get_logical_time()

On most platforms (with the exception of some embedded platforms), the returned value is a 64-bit number representing the number of nanoseconds that have elapsed since January 1, 1970. Executing the above displays something like the following:

Logical time is 1648402121312985000.
Logical time is 1648402122312985000.
Logical time is 1648402123312985000.
...

The output lines appear at one second intervals unless the fast option has been specified.

Elapsed Time​

The times above are a bit hard to read, so, for convenience, each target provides a built-in function to retrieve the elapsed time. For example:

target C main reactor TimeElapsed { timer t(0, 1 s) reaction(t) {= printf( "Elapsed logical time is %lld.\n", lf_time_logical_elapsed() ); =} }

See the Target Language Details for the full set of functions provided for accessing time values.

Executing this program will produce something like this:

Elapsed logical time is 0.
Elapsed logical time is 1000000000.
Elapsed logical time is 2000000000.
...

Comparing Logical and Physical Times​

The following program compares logical and physical times:

target C main reactor TimeLag { timer t(0, 1 s) reaction(t) {= interval_t t = lf_time_logical_elapsed(); interval_t T = lf_time_physical_elapsed(); printf( "Elapsed logical time: %lld, physical time: %lld, lag: %lld\n", t, T, T-t ); =} }

Execution will show something like this:

Elapsed logical time: 0, physical time: 855000, lag: 855000
Elapsed logical time: 1000000000, physical time: 1004714000, lag: 4714000
Elapsed logical time: 2000000000, physical time: 2004663000, lag: 4663000
Elapsed logical time: 3000000000, physical time: 3000210000, lag: 210000
...

In this case, the lag varies from a few hundred microseconds to a small number of milliseconds. The amount of lag will depend on the execution platform.

Simultaneity and Instantaneity​

If two timers have the same offset and period, then their events are logically simultaneous. No observer will be able to see that one timer has triggered and the other has not.

A reaction is always invoked at a well-defined logical time, and logical time does not advance during its execution. Any output produced by the reaction will be logically simultaneous with the input. In other words, reactions are logically instantaneous (for an exception, see Logical Execution Time (FIXME: the original link is a FIXME)). Physical time, however, does elapse during execution of a reaction.

Timeout​

By default, a Lingua Franca program will terminate when there are no more events to process. If there is a timer with a non-zero period, then there will always be more events to process, so the default execution will be unbounded. To specify a finite execution horizon, you can either specify a timeout target property or a --timeout command-line option. For example, the following timeout property will cause the above timer with a period of one second to terminate after 11 events:

target C { timeout: 10 s }

Startup and Shutdown​

To cause a reaction to be invoked at the start of execution, a special startup trigger is provided:

reactor Foo { reaction(startup) {= ... perform initialization ... =} }

The startup trigger is equivalent to a timer with no offset or period.

To cause a reaction to be invoked at the end of execution, a special shutdown trigger is provided. Consider the following reactor, commonly used to build regression tests:

target C reactor TestCount(start: int = 0, stride: int = 1, num_inputs: int = 1) { state count: int = start state inputs_received: int = 0 input x: int reaction(x) {= printf("Received %d.\n", x->value); if (x->value != self->count) { printf("ERROR: Expected %d.\n", self->count); exit(1); } self->count += self->stride; self->inputs_received++; =} reaction(shutdown) {= printf("Shutdown invoked.\n"); if (self->inputs_received != self->num_inputs) { printf("ERROR: Expected to receive %d inputs, but got %d.\n", self->num_inputs, self->inputs_received ); exit(2); } =} }

This reactor tests its inputs against expected values, which are expected to start with the value given by the start parameter and increase by stride with each successive input. It expects to receive a total of num_inputs input events. It checks the total number of inputs received in its shutdown reaction.

The shutdown trigger typically occurs at microstep 0, but may occur at a larger microstep. See Superdense Time and Termination.