Second hand stepping on a Rolex Submariner wrist watch at eight times a second, shown at 0.05x speed, courtesy of Horology House.

The time on a computer passes in small steps. If these values are stored or processed using a fixed size representation then care is needed to prevent issues. For example:

## CircuitPython time functions

CircuitPython provides a subset of functions from the time library. The monotonic in their name refers to a guarantee that time will not go backwards. For comparison, gettimeofday on Linux does not provide this guarantee and naive use for timing durations can result in negative values!

### time.monotonic()

Python and CircuitPython's `time.monotonic()` returns time in seconds as a `float` variable. This return value increases over time reducing the resolution available to represent the fractional part of the value. The effect is far more significant for the 30bit storage representation used by CircuitPython (based on single precision floating point) in combination with the epoch time of 0.0 at power-up. This lowers the precision of the millisecond portion as the program continues to run and increases the granularity. This effect can be very significant if the program runs for days or weeks.

### time.monotonic_ns()

Python (3.5+) and CircuitPython's `time.monotonic_ns()` (available on all boards bar Gemma M0, Trinket M0 and Feather M0 non-Express) is the equivalent of `time.monotonic()` with an  `int` return value in nanoseconds (one billionth of a second). This value is not subject to reduced precision over time as long as the value is not accidentally converted to a `float`. Examples of correct and incorrect usage are shown below.

```import time

# Good - int value preserved
start = time.monotonic_ns()
time.sleep(0.005)
ms_duration = (((time.monotonic_ns() - start) + 500000)
// 1000000)
print(ms_duration)

# Good - int subtraction then the small value
# duration value is ok to convert to a float
start = time.monotonic_ns()
time.sleep(0.005)
ms_duration = round((time.monotonic_ns() - start) / 1e6, 1)
print(ms_duration)

# BAD - accidental/premature conversion to
# float reducing accuracy of calculation
start_ms = time.monotonic_ns() / 1e6  # BAD
time.sleep(0.005)
ms_duration = round(time.monotonic_ns() / 1e6  # BAD
- start_ms, 1)
print(ms_duration)```

The first example will print `5`. The second example prints values like `4.6` or `4.7`. The third example will print `0.0` or infrequently `32.0` for a board that's been running for one day!

Care is needed when using the exponential notation like `1e6` for `1000000`. In Python, `1e6` is always a `float`. The example below from REPL shows how unintended type promotion to `float` spoils the accuracy of the addition.

```>>> t1 = time.monotonic_ns()
>>> t1
167333909366000
>>> t1 + 1e9
1.67335e+14
>>> int(t1 + 1e9)
167334878642176

>>> t1 + 1000000000
167334909366000```

Using the correct division operator for floor division (`//`) and ensuring it's used with `int` types is also critical to avoid trouble from `float`s.

```>>> smaller = 1034567890
>>> larger = smaller + 10

>>> if larger > smaller: print("all ok - integer calculation/comparison")
...
all ok - integer calculation/comparison
>>> if larger // 10 > smaller // 10: print("all ok - integer calculation/comparison")
...
all ok - integer calculation/comparison

>>> if larger / 10 > smaller // 10: print("not ok - float division")
...
>>> if larger // 1e1 > smaller // 10: print("not ok - integer division used with float")
...```

The `1e9` representation is useful for one billion because
the number 9 is easier to read and validate compared to visually counting the number of zeroes in `1000000000`. The previous section mentions the type of `1e9` is a `float`.

The language Ada introduced a representation of numeric literals (numbers) with optional underscores to allow a number to visually broken up. From Honeywell Bull's successful "GREEN" proposal for Ada, "Rationale for the Design of the GREEN Programming Language (1979)":

The underscore is permitted within a number to break up long sequences of digits, a requirement that has long been recognized by printers.

Java introduced this with V7 in 2011, Python introduced this in 3.6 in 2016. CircuitPython implements this, an example on REPL can be seen below showing how large `int` values can be more clearly represented.

```Adafruit CircuitPython 5.0.0 on 2020-03-02; Adafruit CLUE nRF52840 Express with nRF52840
>>>
>>> an_int = 1_000_000_000
>>> print(an_int,
...       an_int // 1000_000, an_int // 1_00_00_00,
...       int("-1_234_567"))
1000000000 1000 1000 -1234567```

## Performance Variability

In general, interpreted languages tend to offer less predictable performance than compiled languages so there may be some variation in this timing. CircuitPython performance will be occasionally affected by:

• garbage collection (a few ms),
• various other background tasks like USB activity including CIRCUITPY file system operations,
• automatic `displayio` screen refreshes (can be 100+ ms).
For benchmarking on boards with a screen, it is important to disable displayio automatic screen updates for serial console output.

## Demonstration of time.monotonic() Granularity

This is a short program which demonstrates the degradation in sub-second `time.monotonic()` resolution. The output will differ on boards which have been powered up for a long time showing how time becomes more granular.

```import board, time

# Disable updates to LCD screen which
# will occasionally introduce very large
# delays between statement execution
board.DISPLAY.auto_refresh = False

print(*[("{:" + str(hdr[0])
+ "s}").format(hdr[1])[:hdr[0]]
for hdr in ((3, "num"),
(15, "monotonic_ns()"),
(12, "monotonic()"),
(8, "fraction"),
(9, "offset"))
],
sep=" | ")

count = 1
previous = time.monotonic()

# Print the time when it changes according
# to time.monotonic() to inspect the
# sub-second precision
while True:
now = time.monotonic()
now_ns = time.monotonic_ns()
if now != previous:
now_ns_str = str(now_ns)
now_frac = now - int(now)
print("{:3d}".format(count),
now_ns_str[:-9] + "." + now_ns_str[-9:],
"{:.6f}".format(now),
"{:.6f}".format(now_frac),
"{:.6f}".format(int(now_ns_str[-9:-3])
/ 1e6 - now_frac),
sep=" | ")
previous = now
count = 1
else:
count += 1```

### Ten Minutes

A CLUE board which is approaching 10 minutes of uptime prints the time on every iteration of the loop and has sub-millisecond precision from `time.monotonic()`.

```num | monotonic_ns()  | monotonic()  | fraction | offset
1 | 563.305638000 | 563.304901 | 0.304932 | 0.000706
1 | 563.310520000 | 563.309813 | 0.309814 | 0.000705
1 | 563.315276000 | 563.314915 | 0.314941 | 0.000335
1 | 563.319893000 | 563.318825 | 0.318848 | 0.001045
1 | 563.324549000 | 563.323975 | 0.323975 | 0.000574
1 | 563.329326000 | 563.328838 | 0.328857 | 0.000468
1 | 563.333935000 | 563.332987 | 0.333008 | 0.000927
1 | 563.338627000 | 563.337898 | 0.337891 | 0.000736
1 | 563.343376000 | 563.343000 | 0.343018 | 0.000358
1 | 563.348121000 | 563.347864 | 0.347900 | 0.000221```

### Two Days

After almost two days the sub-second precision has dramatically dropped - the granularity is now 1/16th of a second (62.5ms).

```num | monotonic_ns()  | monotonic()  | fraction | offset
1 | 149621.908317000 | 149621.868134 | 0.875000 | 0.033317
179 | 149621.929066000 | 149621.939659 | 0.937500 | -0.008434

584 | 149621.993094000 | 149622.001648 | 0.000000 | 0.993094
663 | 149622.057075000 | 149622.058868 | 0.062500 | -0.005425
663 | 149622.121106000 | 149622.130394 | 0.125000 | -0.003894
662 | 149622.185034000 | 149622.192383 | 0.187500 | -0.002466
663 | 149622.249074000 | 149622.249603 | 0.250000 | -0.000926
663 | 149622.313108000 | 149622.306824 | 0.312500 | 0.000608
472 | 149622.360060000 | 149622.383118 | 0.375000 | -0.014940
581 | 149622.424027000 | 149622.430801 | 0.437500 | -0.013473
663 | 149622.488075000 | 149622.497559 | 0.500000 | -0.011925
663 | 149622.552114000 | 149622.559547 | 0.562500 | -0.010386
662 | 149622.616032000 | 149622.621536 | 0.625000 | -0.008968
663 | 149622.680073000 | 149622.688293 | 0.687500 | -0.007427
663 | 149622.744107000 | 149622.750282 | 0.750000 | -0.005893
660 | 149622.808095000 | 149622.812271 | 0.812500 | -0.004405
582 | 149622.872057000 | 149622.869492 | 0.875000 | -0.002943
663 | 149622.936033000 | 149622.941017 | 0.937500 | -0.001467

663 | 149623.000109000 | 149623.003006 | 0.000000 | 0.000109
495 | 149623.049088000 | 149623.060226 | 0.062500 | -0.013412```

The loop is typically running over 600 times before `time.monotonic()` changes. This is because only 4 bits are left for the fractional component of the time.

This guide was first published on Apr 01, 2020. It was last updated on Jun 19, 2024.

This page (Time in CircuitPython) was last updated on Mar 08, 2024.