Boot time, wall time, arrival time: one incident, three clocks

A PX4 flight log counts from boot, the companion journal keeps wall time and the ground station logs arrival. Give each an explicit error bound, and you know which events you can put in order and which you cannot.

The first question in any incident review is what failed first. In INC-0142, obstacle data into PX4 stopped on a quad’s return leg, and the companion’s kernel killed the process that sent it. On one merged timeline the two events sit 39 ms apart, and the logs cannot say which came first.

INC-0142 is an example from the Foxborne example dataset, not a field report. Its logs keep three kinds of time: boot time, wall time and arrival time. Each needs an anchor to reach UTC, and every anchor comes with an error you can write down.

Write the errors down, and the ordering question gets a precise answer. Two events on different clocks have a supported order only when they sit farther apart than their bounds combined.

Boot time: the flight log

Every logged topic in a ULog must carry a timestamp field: a uint64_t in microseconds.1 In PX4’s messages that field is time since system start, as the sensor_gps definition states.2 The last obstacle_distance sample before the stall reads 1,132,793,000 µs, which is 1,132.793 s after boot and says nothing about when boot happened.

The anchor is sensor_gps. Each message carries both clocks: the boot timestamp, and time_utc_usec, the UTC time from the GPS module.2 A third field places that UTC time on the boot clock:3

msg/SensorGps.msgv1.16.0, lines 3 and 53 to 54uORB
uint64 timestamp		# time since system start (microseconds)
...
int32 timestamp_time_relative	# timestamp + timestamp_time_relative = Time of the UTC timestamp since system start, (microseconds)
uint64 time_utc_usec		# Timestamp (microseconds, UTC), this is the timestamp which comes from the gps module. It might be unavailable right after cold start, indicated by a value of 0

So each fix is a pair of readings of one instant: timestamp + timestamp_time_relative on the boot clock, and time_utc_usec in UTC. Drop the zeros the comment warns about. In the example, 1,081 fixes put the flight controller’s boot at 14:13:11.286, so the sample lands at 14:13:11.286 + 1,132.793 s = 14:32:04.079.

Their difference is the offset from boot to UTC. Its spread across the flight’s fixes is a direct check on the bound you claim. The log’s time_ref_utc entry is no substitute: the format defines it as a UTC time offset in seconds, with -3600 as its example.1

The anchor is not exact: PX4’s documentation says GPS time sent over serial carries “latency and jitter from serial communication”.4 A PPS signal captured by a hardware timer does much better.4 The example dataset carries ±40 ms on its flight logs.

Wall time: the companion journal

systemd’s journal stamps each entry with __REALTIME_TIMESTAMP: the wall clock at the point the journal received the entry, in microseconds since the epoch.5 The man page adds that it is “usually a bit later” than _SOURCE_REALTIME_TIMESTAMP, the earliest trusted timestamp of the message.5 This is the kill, from the example’s journal export:

uas04-orin-journal.jsonexample dataset, line 18,314, abridgedJSON
{
  "__REALTIME_TIMESTAMP": "1790260324118000",
  "__MONOTONIC_TIMESTAMP": "1152904000",
  "_BOOT_ID": "9b2e4c1d0f8a4e57b3c2a1d9e8f7c6b5",
  "_HOSTNAME": "uas04-orin",
  ...
  "MESSAGE": "Out of memory: Killed process 2213 (perception_node) total-vm:11873248kB, anon-rss:6823516kB, file-rss:10240kB, shmem-rss:0kB, UID:1001 pgtables:14720kB oom_score_adj:0"
}

1790260324118000 µs after the epoch is 14:32:04.118 UTC. The same entry carries __MONOTONIC_TIMESTAMP, 1,152.904 s on the companion’s monotonic clock, which the man page says to combine with _BOOT_ID.5 On the flight controller’s clock, the same instant is boot + 1,132.832 s.

Wall time is only as good as the clock under it, and on the companion chrony disciplines that clock. chronyc tracking displays the clock’s performance, and its Leap status reads Normal, Insert second, Delete second or Not synchronised.6 If it reads Not synchronised, the journal has no anchor and belongs on its own clock.

chronyc trackingexample output in the chrony 4.6 manual, abridgedtext
System time     : 0.000006523 seconds slow of NTP time
...
Root delay      : 0.013639022 seconds
Root dispersion : 0.001100737 seconds
...
Leap status     : Normal

The same manual gives an absolute bound on the clock error, assuming the stratum-1 computer at the top of the chain is correct:6

chronyc(1), trackingchrony 4.6text
clock_error <= |system_time_offset| + root_dispersion + (0.5 * root_delay)

On the manual’s example output, that is 0.0000065 + 0.0011007 + 0.5 × 0.0136390 s, about 7.9 ms.6 In INC-0142, chrony was synchronized with an offset of −1.8 ms, and the dataset carries ±6 ms on the journal.

Arrival time: the ground receive log

The receiver at range control stamps each MAVLink message when it arrives, on a clock kept by NTP. In the example that clock is good to ±15 ms, but only for arrival. Departure is earlier by the link latency, which on this run was 96 ms at the median and 412 ms at the 95th percentile.

Each record says what arrived, from which system, and when it arrived. This is the last heartbeat from UAS-04 before the gap:

rc1-receive.jsonlexample dataset, line 1,118JSON
{
  "recv_utc": "2026-09-24T14:32:03.912Z",
  "mono_ns": 12805145901122,
  "link": "mesh0",
  "sysid": 4,
  "compid": 1,
  "msgid": 0,
  "msg": "HEARTBEAT",
  "seq": 204,
  "len": 9
}

It arrived at 14:32:03.912. Allow 412 ms of latency and 15 ms of clock error, and it left the vehicle between 14:32:03.485 and its arrival. The bracket is lopsided, because latency only ever moves departure earlier.

When the order is unknown

Put two events from different sources on one axis, and give each its source’s bound. They have a supported order only when they sit farther apart than the two bounds added together. Closer than that, either could have come first.

All three clocks around the kill, 14:32:03.4 to 04.25 UTC03.503.603.703.803.904.004.104.2Ground receivesent 03.485 to arrival: 412 ms latency + 15 msarrived 03.912Flight loglast obstacle_distance sample 04.079, ±40 msCompanion journalkernel kills perception_node 04.118, ±6 msEnlarged: 04.03 to 04.1704.0404.0604.0804.1004.1204.1404.167 ms where either order fits39 ms apart, 46 ms combined bound: order unknownFlight log04.079 ±40 msCompanion journal04.118 ±6 ms
Example incident INC-0142 from the Foxborne example dataset. Top: each source’s error on one axis, with the ground receive bracket running back from arrival by up to 412 ms. Bottom: the last obstacle_distance sample and the kernel kill are 39 ms apart, inside their 46 ms combined bound, so either order fits.

The stall and the kill fail that test. They are 39 ms apart, and their combined bound is 40 + 6 = 46 ms. The flight log sample could be as late as 04.119 and the kill as early as 04.112, so for 7 ms either order fits.

The order matters. If the stream stalled first, something stopped it while perception_node was still alive; if the kill came first, the kill explains the stall. The example’s report takes neither side: “Their order is not claimed.”

The same test puts other pairs in order. mavlink-router.service stopped at 14:32:04.152 on the journal, and the next heartbeat was due at range control at 14:32:04.912. That is 760 ms apart against a bound of 6 + 15 + 412 = 433 ms, so the order holds.

Carry the bound with every event

None of this needs new sensors. It needs every event stored with its clock, its anchor and its bound, instead of a bare UTC time.

  1. Anchor the flight log from sensor_gps. Drop fixes where time_utc_usec is 0. With no fix at all, the log has boot time only.
  2. Record chronyc tracking on the companion. Capture it before and after each run. If Leap status reads Not synchronised, keep the journal on its own clock.
  3. Treat receive times as arrival. Carry latency as a one-sided term, from a percentile you measured on that link.
  4. Compare before you write “then”. If two events sit inside their combined bound, the report says “order unknown”.

This is how Foxborne aligns sources: each gets a clock, an anchor and an error bound. A source that cannot be anchored stays on its own clock, and the timeline shows exactly where the evidence stops.

What the evidence shows

  • The last obstacle_distance sample is at 14:32:04.079, ±40 ms, on the GPS-anchored flight log.
  • The kernel killed perception_node at 14:32:04.118, ±6 ms, on the chrony-disciplined journal.
  • mavlink-router.service stopped 0.76 s before the next heartbeat was due, outside the 0.43 s bound.

What it does not

  • Which came first, the stall or the kill. They are 39 ms apart, inside a 46 ms combined bound.
  • The moment the last heartbeat left the vehicle. The log gives a window, 14:32:03.485 to its arrival.
  • Why perception_node ran out of memory. The clocks order the events; they do not explain them.

Sources

  1. 1ULog file formatPX4 User Guide. Accessed September 26, 2026.
  2. 2SensorGps (uORB message)PX4 User Guide. Accessed September 26, 2026.
  3. 3SensorGps.msgPX4 Autopilot v1.16.0. Accessed September 26, 2026.
  4. 4PPS time synchronizationPX4 User Guide. Accessed September 26, 2026.
  5. 5systemd.journal-fields, man page sourcesystemd v262. Accessed September 26, 2026.
  6. 6chronyc manual: trackingchrony 4.6. Accessed September 26, 2026.

More field notes

Companion computersReading an OOM kill in a journalctl exportThe kernel’s out-of-memory report names the thread that asked, the process it killed and every page it counted. systemd then records the unit’s result, and two settings decide what happens next.Companion computersWhen BindsTo= takes your MAVLink router down with itA dependency in the unit graph can silence telemetry with no radio fault at all. Here is how to spot one in the journal, and how to prove it on the bench before anyone swaps a radio.Ground linksA telemetry gap is not a radio failureA hole in the ground receive log proves only that nothing arrived. A stopped router, a radio that left the USB bus and a berm all look the same there, and other logs tell them apart.

A pilot on your own data

Bring your hardest incident.

Send one failure you have already investigated. We rebuild it on your data, beside your current tools, and show where the evidence agrees with your conclusion and where it doesn’t.

  1. 1
    Send one incidentA failure you have already investigated, with the flight log and whatever companion or ground evidence you kept.
  2. 2
    We reconstruct itBeside your current tools, on your data, with every claim traced to its source.
  3. 3
    Compare the answersWhere the evidence agrees with your conclusion, where it does not, and what it cannot decide.