Skip to content

Commit 648a1a9

Browse files
fix(python): Do math on monotonic clocks only (#19060)
## Overview A lot of Python code (in various projects) was doing stuff like: ```python start = time.now() do_stuff() end = time.now() duration = end - start ``` That's not quite right because `time.now()` can be affected by things like daylight savings time, leap seconds, and NTP adjustments. If we're doing math on timestamps, it needs to come from a monotonic clock. So this switches all (or at least most) of those places to use `time.monotonic()` instead. ## Changelog Basically a global Ctrl+F for `time.time()`. I replaced it with `time.monotonic()` when it was obvious to me that it was only used in the local scope and that math was being done on it. So this excludes things like `time()` calls that were getting saved in CSV reports.
1 parent d6baa4d commit 648a1a9

File tree

20 files changed

+65
-65
lines changed

20 files changed

+65
-65
lines changed

abr-testing/abr_testing/tools/test_modules.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
async def tc_test_1(module: str, path_to_file: str) -> None:
1717
"""Thermocycler Test 1 Open and Close Lid."""
1818
duration = int(input("How long to run this test for? (in seconds): "))
19-
start = time.time()
20-
while time.time() - start < duration:
19+
start = time.monotonic()
20+
while time.monotonic() - start < duration:
2121
try:
2222
await (tc_open_lid(module, path_to_file))
2323
except asyncio.TimeoutError:
@@ -34,8 +34,8 @@ async def hs_test_1(module: str, path_to_file: str) -> None:
3434
"""Heater Shaker Test 1. (Home and Shake)."""
3535
duration = int(input("How long to run this test for? (in seconds): "))
3636
rpm = input("Target RPM (200-3000): ")
37-
start = time.time()
38-
while time.time() - start < duration:
37+
start = time.monotonic()
38+
while time.monotonic() - start < duration:
3939
try:
4040
await (hs_test_home(module, path_to_file))
4141
except asyncio.TimeoutError:

analyses-snapshot-testing/citools/generate_analyses.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,8 @@ def start_containers(image_name: str, num_containers: int, timeout: int = 60) ->
125125
containers.append(container)
126126

127127
# Wait for containers to be ready
128-
start_time = time.time()
129-
while time.time() - start_time < timeout:
128+
start_time = time.monotonic()
129+
while time.monotonic() - start_time < timeout:
130130
all_ready = True
131131
for container in containers:
132132
exit_code, _ = container.exec_run(f"ls -al {CONTAINER_LABWARE}")
@@ -172,7 +172,7 @@ def analyze(protocol: TargetProtocol, container: docker.models.containers.Contai
172172
# Build the command with all relevant file paths
173173
all_files = [str(protocol.container_protocol_file)] + [str(lw) for lw in labware_files]
174174
command = f"python -I -m opentrons.cli analyze --json-output {protocol.container_analysis_file} " + " ".join(all_files)
175-
start_time = time.time()
175+
start_time = time.monotonic()
176176
result = None
177177
exit_code = None
178178
console.print(f"Beginning analysis of {protocol.host_protocol_file.name}")
@@ -191,7 +191,7 @@ def analyze(protocol: TargetProtocol, container: docker.models.containers.Contai
191191
protocol.set_analysis()
192192
return False
193193
finally:
194-
protocol.set_analysis_execution_time(time.time() - start_time)
194+
protocol.set_analysis_execution_time(time.monotonic() - start_time)
195195
console.print(f"Analysis of {protocol.host_protocol_file.name} completed in {protocol.analysis_execution_time:.2f} seconds.")
196196

197197

@@ -238,7 +238,7 @@ def get_container_instances(protocol_len: int) -> int:
238238

239239
def generate_analyses_from_test(tag: str, protocols: List[Protocol]) -> List[TargetProtocol]:
240240
"""Generate analyses from the tests."""
241-
start_time = time.time()
241+
start_time = time.monotonic()
242242
protocols_to_process: List[TargetProtocol] = []
243243
for test_protocol in protocols:
244244
host_protocol_file = Path(test_protocol.file_path)
@@ -256,6 +256,6 @@ def generate_analyses_from_test(tag: str, protocols: List[Protocol]) -> List[Tar
256256
)
257257
instance_count = get_container_instances(len(protocols_to_process))
258258
analyze_against_image(tag, protocols_to_process, instance_count)
259-
end_time = time.time()
259+
end_time = time.monotonic()
260260
console.print(f"Clock time to generate analyses: {end_time - start_time:.2f} seconds.")
261261
return protocols_to_process

api/src/opentrons/drivers/smoothie_drivers/driver_3_0.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import contextlib
1313
import logging
1414
from os import environ
15-
from time import time
15+
from time import monotonic
1616
from typing import Any, Dict, Optional, Union, List, Tuple, cast, AsyncIterator
1717

1818
from math import isclose
@@ -1901,12 +1901,12 @@ async def update_firmware(
19011901
# if loop:
19021902
# kwargs["loop"] = loop
19031903
log.info(update_cmd)
1904-
before = time()
1904+
before = monotonic()
19051905
proc = await asyncio.create_subprocess_shell(update_cmd, **kwargs)
1906-
created = time()
1906+
created = monotonic()
19071907
log.info(f"created lpc21isp subproc in {created-before}")
19081908
out_b, err_b = await proc.communicate()
1909-
done = time()
1909+
done = monotonic()
19101910
log.info(f"ran lpc21isp subproc in {done-created}")
19111911
if proc.returncode != 0:
19121912
log.error(

g-code-testing/g_code_parsing/g_code_engine.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,10 @@ def _emulate(self) -> Iterator[ThreadManager]:
108108
feature_flags=HardwareFeatureFlags.build_from_ff(),
109109
)
110110
# Wait for modules to be present
111-
wait_begins = time.time()
111+
wait_begins = time.monotonic()
112112
while len(emulator.attached_modules) != len(modules):
113113
time.sleep(0.1)
114-
if (time.time() - wait_begins) > 30:
114+
if (time.monotonic() - wait_begins) > 30:
115115
proc.kill()
116116
proc.join()
117117
raise RuntimeError(

hardware-testing/hardware_testing/drivers/mark10/mark10_fg.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Mark10 Force Gauge Driver."""
22
from serial import Serial # type: ignore[import]
33
from abc import ABC, abstractmethod
4-
from time import time
4+
from time import monotonic
55
from typing import Tuple
66

77

@@ -96,8 +96,8 @@ def disconnect(self) -> None:
9696
def read_force(self, timeout: float = 1.0) -> float:
9797
"""Get Force in Newtons."""
9898
self._force_guage.write("?\r\n".encode("utf-8"))
99-
start_time = time()
100-
while time() < start_time + timeout:
99+
start_time = monotonic()
100+
while monotonic() < start_time + timeout:
101101
# return "12.3 N"
102102
line = self._force_guage.readline().decode("utf-8").strip()
103103
try:

hardware-testing/hardware_testing/drivers/mitutoyo_digimatic_indicator.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,9 @@ def read(self) -> float:
6666

6767
def read_stable(self, timeout: float = 5) -> float:
6868
"""Reads dial indicator with stable reading."""
69-
then = time.time()
69+
then = time.monotonic()
7070
values = [self.read(), self.read(), self.read(), self.read(), self.read()]
71-
while (time.time() - then) < timeout:
71+
while (time.monotonic() - then) < timeout:
7272
if numpy.allclose(values, list(reversed(values))):
7373
return values[-1]
7474
values = values[1:] + [self.read()]
@@ -79,8 +79,8 @@ def read_stable(self, timeout: float = 5) -> float:
7979
print("Mitutoyo ABSOLUTE Digimatic Indicator")
8080
gauge = Mitutoyo_Digimatic_Indicator(port="/dev/ttyUSB0")
8181
gauge.connect()
82-
start_time = time.time()
82+
start_time = time.monotonic()
8383
while True:
84-
elapsed_time = round(time.time() - start_time, 3)
84+
elapsed_time = round(time.monotonic() - start_time, 3)
8585
distance = gauge.read()
8686
print("Time: {} Distance: {}".format(elapsed_time, distance))

hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_droplets.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Test Droplets."""
22
from asyncio import sleep
3-
from time import time
3+
from time import monotonic
44
from typing import List, Union, Tuple, Optional, Dict, Literal
55

66
from opentrons.hardware_control.ot3api import OT3API
@@ -110,7 +110,7 @@ async def aspirate_and_wait(
110110
await api.aspirate(OT3Mount.LEFT, volume)
111111
await api.move_to(OT3Mount.LEFT, reservoir + Point(z=HOVER_HEIGHT_MM))
112112

113-
start_time = time()
113+
start_time = monotonic()
114114
for i in range(seconds):
115115
print(f"waiting {i + 1}/{seconds}")
116116
if i == 0 or i == seconds - 1:
@@ -123,7 +123,7 @@ async def aspirate_and_wait(
123123
result = ui.get_user_answer("look good")
124124
else:
125125
result = True
126-
duration_seconds = time() - start_time
126+
duration_seconds = monotonic() - start_time
127127
print(f"waited for {duration_seconds} seconds")
128128

129129
await api.move_to(

hardware-testing/hardware_testing/production_qc/robot_assembly_qc_ot3/test_peripherals.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from subprocess import run as run_subprocess, Popen, CalledProcessError
55
from typing import List, Union, Optional, Dict
66
from urllib.request import urlopen
7-
from time import time
7+
from time import monotonic
88
from typing import Tuple
99

1010
from opentrons_hardware.hardware_control.rear_panel_settings import set_ui_color
@@ -223,19 +223,19 @@ def _get_user_confirmation(question: str) -> bool:
223223
ui.print_header("DOOR SWITCH")
224224
door_timeout_seconds = 10
225225
print("CLOSE the front door")
226-
start_time_seconds = time()
226+
start_time_seconds = monotonic()
227227
while not api.is_simulator and api.door_state != DoorState.CLOSED:
228228
await asyncio.sleep(0.1)
229-
if time() - start_time_seconds > door_timeout_seconds:
229+
if monotonic() - start_time_seconds > door_timeout_seconds:
230230
ui.print_error("timed out waiting for door to close")
231231
break
232232
print(api.door_state)
233233
is_closed = api.door_state == DoorState.CLOSED
234234
print("OPEN the front door")
235-
start_time_seconds = time()
235+
start_time_seconds = monotonic()
236236
while not api.is_simulator and api.door_state != DoorState.OPEN:
237237
await asyncio.sleep(0.1)
238-
if time() - start_time_seconds > door_timeout_seconds:
238+
if monotonic() - start_time_seconds > door_timeout_seconds:
239239
ui.print_error("timed out waiting for door to open")
240240
break
241241
print(api.door_state)

hardware-testing/hardware_testing/production_qc/stress_test_qc_ot3.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ def _create_csv_and_get_callbacks(sn: str) -> Tuple[CSVProperties, CSVCallbacks]
131131
file_name = data.create_file_name(test_name=test_name, run_id=run_id, tag=sn)
132132
csv_display_name = os.path.join(run_path, file_name)
133133
print(f"CSV: {csv_display_name}")
134-
start_time = time.time()
134+
start_time = time.monotonic()
135135

136136
def _append_csv_data(
137137
data_list: List[Any],
@@ -142,7 +142,7 @@ def _append_csv_data(
142142
# every line in the CSV file begins with the elapsed seconds
143143
if not first_row_value_included:
144144
if first_row_value is None:
145-
first_row_value = str(round(time.time() - start_time, 2))
145+
first_row_value = str(round(time.monotonic() - start_time, 2))
146146
data_list = [first_row_value] + data_list
147147
data_str = ",".join([str(d) for d in data_list])
148148
if line_number is None:

hardware-testing/hardware_testing/scripts/faster_plunger_lifetime_test.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ async def position_check() -> bool:
101101
}
102102
writer = csv.DictWriter(csvfile, test_data)
103103
writer.writeheader()
104-
start_time = time.time()
104+
start_time = time.monotonic()
105105
try:
106106
currents = list(CURRENTS_SPEEDS.keys())
107107
for cycle in range(1, args.cycles + 1):
@@ -133,7 +133,7 @@ async def position_check() -> bool:
133133
api, mount, bottom, speed=speed, motor_current=current
134134
)
135135
down_passed = await position_check()
136-
test_data["time_sec"] = time.time() - start_time
136+
test_data["time_sec"] = time.monotonic() - start_time
137137
test_data["cycle"] = cycle
138138
test_data["position"] = "bottom"
139139
test_data["position_check"] = down_passed
@@ -191,7 +191,7 @@ async def position_check() -> bool:
191191
api, mount, 0, speed=speed, motor_current=current
192192
)
193193
up_passed = await position_check()
194-
test_data["time_sec"] = time.time() - start_time
194+
test_data["time_sec"] = time.monotonic() - start_time
195195
test_data["cycle"] = cycle
196196
test_data["position"] = "top"
197197
test_data["position_check"] = up_passed

0 commit comments

Comments
 (0)