Was this page helpful?

Time and Timers

This page is showing examples in the target language CC++PythonTypeScriptRust. You can change the target language in the left sidebar.

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. 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);
  =}
}
target Cpp
main reactor SlowingClock(start: time(100 ms), incr: time(100 ms)) {
  state interval: time(start)
  logical action a
  reaction(startup) -> a {=
    a.schedule(start);
  =}
  reaction(a) -> a {=
    auto elapsed_logical_time = get_elapsed_logical_time();
    std::cout << "Logical time since start: " << elapsed_logical_time << " nsec" << std::endl;
    interval += incr;
    a.schedule(interval);
  =}
}
target Python
main reactor SlowingClock(start = 100 ms, incr = 100 ms) {
  state interval = start
  logical action a
  reaction(startup) -> a {=
    a.schedule(self.start)
  =}
  reaction(a) -> a {=
    elapsed_logical_time = lf.time.logical_elapsed()
    print(
        f"Logical time since start: {elapsed_logical_time} nsec."
    )
    self.interval += self.incr
    a.schedule(self.interval)
  =}
}
target TypeScript
main reactor SlowingClock(start: time = 100 ms, incr: time = 100 ms) {
  state interval: time = start
  logical action a
  reaction(startup) -> a {=
    actions.a.schedule(start, null);
  =}
  reaction(a) -> a {=
    console.log(`Logical time since start: ${util.getElapsedLogicalTime()}`)
    interval = interval.add(incr)
    actions.a.schedule(interval, null)
  =}
}
target Rust
main reactor SlowingClock(start: time = 100 ms, incr: time = 100 ms) {
  state start = start
  state incr = incr
  state interval: time = start
  state expected_time: time()
  logical action a
  reaction(startup) -> a {=
    ctx.schedule(a, After(self.start));
  =}
  reaction(a) -> a {=
    println!(
        "Logical time since start: {} nsec.",
        ctx.get_elapsed_logical_time().as_nanos(),
    );
    self.interval += self.incr;
    ctx.schedule(a, After(self.interval));
    self.expected_time += 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());
  =}
}
target Cpp
main reactor Timer {
  timer t(0, 1 s)
  reaction(t) {=
    std::cout << "Logical time is: " << get_logical_time() << std::endl;
  =}
}
target Python
main reactor Timer {
  timer t(0, 1 sec)
  reaction(t) {=
    print(f"Logical time is {lf.time.logical()}.")
  =}
}
target TypeScript
main reactor Timer {
  timer t(0, 1 sec)
  reaction(t) {=
    console.log(`Logical time is ${util.getCurrentLogicalTime()}.`)
  =}
}
target Rust
main reactor Timer {
  timer t(0, 1 sec)
  reaction(t) {=
    println!(
        "Logical time is {}.",
        ctx.get_elapsed_logical_time().as_nanos(),
    );
  =}
}

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, get_logical_time() FIXME lf.time.logical() util.getCurrentLogicalTime() FIXME. 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()
    );
  =}
}
target Cpp
main reactor TimeElapsed {
  timer t(0, 1 s)
  reaction(t) {=
    std::cout << "Elapsed logical time is " << get_elapsed_logical_time() << std::endl;
  =}
}
target Python
main reactor TimeElapsed {
  timer t(0, 1 s)
  reaction(t) {=
    print(
        f"Elapsed logical time is {lf.time.logical_elapsed()}."
    )
  =}
}
target TypeScript
main reactor TimeElapsed {
  timer t(0, 1 s)
  reaction(t) {=
    console.log(`Elapsed logical time is ${util.getElapsedLogicalTime()}`)
  =}
}
target Rust
main reactor TimeElapsed {
  timer t(0, 1 s)
  reaction(t) {=
    println!(
        "Elapsed logical time is {}.",
        ctx.get_elapsed_logical_time().as_nanos(),
    );
  =}
}

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
    );
  =}
}
target Cpp
main reactor TimeLag {
  timer t(0, 1 s)
  reaction(t) {=
    auto logical_time = get_elapsed_logical_time();
    auto physical_time = get_elapsed_physical_time();
    std::cout << "Elapsed logical time: " << logical_time
        << " physical time: " << physical_time
        << " lag: " << physical_time - logical_time <<  std::endl;
  =}
}
target Python
main reactor TimeLag {
  timer t(0, 1 s)
  reaction(t) {=
    t = lf.time.logical_elapsed()
    T = lf.time.physical_elapsed()
    print(
        f"Elapsed logical time: {t}, physical time: {T}, lag: {T-t}"
    )
  =}
}
target TypeScript
main reactor TimeLag {
  timer t(0, 1 s)
  reaction(t) {=
    const t = util.getElapsedLogicalTime()
    const T = util.getElapsedPhysicalTime()
    console.log(`Elapsed logical time: ${t}, physical time: ${T}, lag: ${T.subtract(t)}`)
  =}
}
target Rust
main reactor TimeLag {
  timer t(0, 1 s)
  reaction(t) {=
    let t = ctx.get_elapsed_logical_time();
    let T = ctx.get_elapsed_physical_time();
    println!(
      "Elapsed logical time: {}, physical time: {}, lag: {}",
      t.as_nanos(),
      T.as_nanos(),
      (T-t).as_nanos(),
    );
  =}
}

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). 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
}
target Cpp {
  timeout: 10 s
}
target Python {
  timeout: 10 s
}
target TypeScript {
  timeout: 10 s
}
target Rust {
  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);
    }
  =}
}
target Cpp
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) {=
    auto value = *x.get();
    std::cout << "Received " <<  value << std::endl;
    if (value != count) {
      std::cerr << "ERROR: Expected: "<< count << std::endl;
      exit(1);
    }
    count += stride;
    inputs_received++;
  =}
  reaction(shutdown) {=
    std::cout << "Shutdown invoked." << std::endl;
    if (inputs_received != num_inputs) {
      std::cerr << "ERROR: Expected to receive " << num_inputs
          << " inputs, but got " << inputs_received << std::endl;
      exit(2);
    }
  =}
}
target Python
reactor TestCount(start=0, stride=1, num_inputs=1) {
  state count = start
  state inputs_received = 0
  input x
  reaction(x) {=
    print(f"Received {x.value}.")
    if x.value != self.count:
      sys.stderr.write(f"ERROR: Expected {self.count}.\n")
      exit(1)
    self.count += self.stride
    self.inputs_received += 1
  =}
  reaction(shutdown) {=
    print("Shutdown invoked.")
    if self.inputs_received != self.num_inputs:
      sys.stderr.write(
          f"ERROR: Expected to receive {self.num_inputs} inputs, but got {self.inputs_received}.\n"
      )
      exit(2)
  =}
}
target TypeScript
reactor TestCount(start: number = 0, stride: number = 1, numInputs: number = 1) {
  state count: number = start
  state inputsReceived: number = 0
  input x: number
  reaction(x) {=
    console.log(`Received ${x}`)
    if (x != count) {
      console.error(`ERROR: Expected ${count}.`)
      process.exit(1)
    }
    count += stride;
    inputsReceived++
  =}
  reaction(shutdown) {=
    console.log("Shutdown invoked.")
    if (inputsReceived != numInputs) {
      console.error(`ERROR: Expected to receive ${numInputs}, but got ${inputsReceived}.`)
      process.exit(2)
    }
  =}
}
target Rust
reactor TestCount(start: u32 = 0, stride: u32 = 1, num_inputs: u32 = 1) {
  state stride = stride
  state num_inputs = num_inputs
  state count: u32 = start
  state inputs_received: u32 = 0
  input x: u32
  reaction(x) {=
    let x = ctx.get(x).unwrap();
    println!("Received {}.", x);
    if x != self.count {
      println!("ERROR: Expected {}.", self.count);
      std::process::exit(1);
    }
    self.count += self.stride;
    self.inputs_received += 1;
  =}
  reaction(shutdown) {=
    println!("Shutdown invoked.");
    if self.inputs_received != self.num_inputs {
      println!(
          "ERROR: Expected to receive {} inputs, but got {}.",
          self.num_inputs,
          self.inputs_received
      );
      std::process::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.

Lingua Franca is an open source project. Help us improve these pages by sending a Pull Request

Contributors to this page:
Eeal  (15)
SBSoroush Bateni  (8)
EALEdward A. Lee  (3)
PDPeter Donovan  (2)
HKHokeun Kim  (1)
Rrevol-xut  (1)

Last updated: Nov 10, 2023