| /* |
| * Copyright (C) 2019 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| #ifndef SRC_TRACE_PROCESSOR_UTIL_CLOCK_SYNCHRONIZER_H_ |
| #define SRC_TRACE_PROCESSOR_UTIL_CLOCK_SYNCHRONIZER_H_ |
| |
| #include <algorithm> |
| #include <array> |
| #include <cinttypes> |
| #include <cstddef> |
| #include <cstdint> |
| #include <limits> |
| #include <map> |
| #include <memory> |
| #include <optional> |
| #include <random> |
| #include <set> |
| #include <tuple> |
| #include <utility> |
| #include <vector> |
| |
| #include "perfetto/base/logging.h" |
| #include "perfetto/base/status.h" |
| #include "perfetto/ext/base/circular_queue.h" |
| #include "perfetto/ext/base/flat_hash_map.h" |
| #include "perfetto/ext/base/murmur_hash.h" |
| #include "perfetto/ext/base/status_or.h" |
| #include "perfetto/public/compiler.h" |
| |
| #include "protos/perfetto/common/builtin_clock.pbzero.h" |
| |
| namespace perfetto::trace_processor { |
| |
| class ClockTrackerTest; |
| class TraceProcessorContext; |
| |
| // This class handles synchronization of timestamps across different clock |
| // domains. This includes multi-hop conversions from two clocks A and D, e.g. |
| // A->B -> B->C -> C->D, even if we never saw a snapshot that contains A and D |
| // at the same time. |
| // The API is fairly simple (but the inner operation is not): |
| // - AddSnapshot(map<clock_id, timestamp>): pushes a set of clocks that have |
| // been snapshotted at the same time (within technical limits). |
| // - ToTraceTime(src_clock_id, src_timestamp): |
| // converts a timestamp between clock domain and TraceTime. |
| // |
| // Concepts: |
| // - Snapshot hash: |
| // As new snapshots are pushed via AddSnapshot() we compute a snapshot hash. |
| // Such hash is the hash(clock_ids) (only IDs, not their timestamps) and is |
| // used to find other snapshots that involve the same clock domains. |
| // Two clock snapshots have the same hash iff they snapshot the same set of |
| // clocks (the order of clocks is irrelevant). |
| // This hash is used to efficiently go from the clock graph pathfinder to the |
| // time-series obtained by appending the various snapshots. |
| // - Snapshot id: |
| // A simple monotonic counter that is incremented on each AddSnapshot() call. |
| // |
| // Data structures: |
| // - For each clock domain: |
| // - For each snapshot hash: |
| // - A logic vector of (snapshot_id, timestamp) tuples (physically stored |
| // as two vectors of the same length instead of a vector of pairs). |
| // This allows to efficiently binary search timestamps within a clock domain |
| // that were obtained through a particular snapshot. |
| // |
| // - A graph of edges (source_clock, target_clock) -> snapshot hash. |
| // |
| // Operation: |
| // Upon each AddSnapshot() call, we incrementally build an unweighted, directed |
| // graph, which has clock domains as nodes. |
| // The graph is timestamp-oblivious. As long as we see one snapshot that |
| // connects two clocks, we assume we'll always be able to convert between them. |
| // This graph is queried by the Convert() function to figure out the shortest |
| // path between clock domain, possibly involving hopping through snapshots of |
| // different type (i.e. different hash). |
| // |
| // Example: |
| |
| // We see a snapshot, with hash S1, for clocks (A,B,C). We build the edges in |
| // the graph: A->B, B->C, A->C (and the symmetrical ones). In other words we |
| // keep track of the fact that we can convert between any of them using S1. |
| // Later we get another snapshot containing (C,E), this snapshot will have a |
| // different hash (S2, because Hash(C,E) != Hash(A,B,C)) and will add the edges |
| // C->E, E->C [via S2] to the graph. |
| // At this point when we are asked to convert a timestamp from A to E, or |
| // viceversa, we use a simple BFS to figure out a conversion path that is: |
| // A->C [via S1] + C->E [via S2]. |
| // |
| // Visually: |
| // Assume we make the following calls: |
| // - AddSnapshot(A:10, B:100) |
| // - AddSnapshot(A:20, C:2000) |
| // - AddSnapshot(B:400, C:5000) |
| // - AddSnapshot(A:30, B:300) |
| |
| // And assume Hash(A,B) = S1, H(A,C) = S2, H(B,C) = S3 |
| // The vectors in the tracker will look as follows: |
| // Clock A: |
| // S1 {t:10, id:1} {t:30, id:4} |
| // S2 | {t:20, id:2} | |
| // | | | |
| // Clock B: | | | |
| // S1 {t:100, id:1} | {t:300, id:4} |
| // S3 | {t:400, id:3} |
| // | | |
| // Clock C: | | |
| // S2 {t: 2000, id: 2} | |
| // S3 {t:5000, id:3} |
| |
| // Base class for ClockSynchronizer containing non-templated types. |
| // This allows listener implementations to reference these types without |
| // depending on the template instantiation. |
| class ClockSynchronizerBase { |
| public: |
| using ClockId = int64_t; |
| |
| // Error type when clock conversion fails (used internally for listener) |
| enum class ErrorType { |
| kOk = 0, |
| kUnknownSourceClock, // Source clock never seen in any snapshot |
| kUnknownTargetClock, // Target clock never seen in any snapshot |
| kNoPath, // No snapshot path connects source to target |
| }; |
| }; |
| |
| template <typename TClockEventListener> |
| class ClockSynchronizer : public ClockSynchronizerBase { |
| public: |
| explicit ClockSynchronizer( |
| std::unique_ptr<TClockEventListener> clock_event_listener) |
| : trace_time_clock_id_(protos::pbzero::BUILTIN_CLOCK_BOOTTIME), |
| clock_event_listener_(std::move(clock_event_listener)) {} |
| |
| // Clock description. |
| struct Clock { |
| explicit Clock(ClockId clock_id) : id(clock_id) {} |
| Clock(ClockId clock_id, int64_t unit, bool incremental) |
| : id(clock_id), unit_multiplier_ns(unit), is_incremental(incremental) {} |
| |
| ClockId id; |
| int64_t unit_multiplier_ns = 1; |
| bool is_incremental = false; |
| }; |
| |
| // Timestamp with clock. |
| struct ClockTimestamp { |
| ClockTimestamp(ClockId id, int64_t ts) : clock(id), timestamp(ts) {} |
| ClockTimestamp(ClockId id, int64_t ts, int64_t unit, bool incremental) |
| : clock(id, unit, incremental), timestamp(ts) {} |
| |
| Clock clock; |
| int64_t timestamp; |
| }; |
| |
| // IDs in the range [64, 128) are reserved for sequence-scoped clock ids. |
| // They can't be passed directly in ClockSynchronizer calls and must be |
| // resolved to 64-bit global clock ids by calling SeqScopedClockIdToGlobal(). |
| static bool IsSequenceClock(ClockId clock_id) { |
| return clock_id >= 64 && clock_id < 128; |
| } |
| |
| // Converts a sequence-scoped clock ids to a global clock id that can be |
| // passed as argument to ClockSynchronizer functions. |
| static ClockId SequenceToGlobalClock(uint32_t seq_id, uint32_t clock_id) { |
| PERFETTO_DCHECK(IsSequenceClock(clock_id)); |
| return (static_cast<int64_t>(seq_id) << 32) | clock_id; |
| } |
| |
| // Extracts the sequence ID and raw clock ID from a converted sequence-scoped |
| // clock (i.e., one created via SequenceToGlobalClock). |
| // Returns {sequence_id, raw_clock_id}. |
| static std::pair<uint32_t, uint32_t> ExtractSequenceClockId( |
| ClockId global_clock_id) { |
| PERFETTO_DCHECK(!IsSequenceClock(global_clock_id)); // Must be converted |
| auto raw_clock_id = static_cast<uint32_t>(global_clock_id & 0xFFFFFFFF); |
| PERFETTO_DCHECK( |
| IsSequenceClock(raw_clock_id)); // Raw ID must be sequence-scoped |
| auto seq_id = static_cast<uint32_t>(global_clock_id >> 32); |
| return {seq_id, raw_clock_id}; |
| } |
| |
| // Converts a timestamp from an arbitrary clock domain to the trace time. |
| // On the first call, it also "locks" the trace time clock, preventing it |
| // from being changed later. |
| // If byte_offset is provided and conversion fails, records the error via |
| // the listener. |
| PERFETTO_ALWAYS_INLINE std::optional<int64_t> ToTraceTime( |
| ClockId clock_id, |
| int64_t timestamp, |
| std::optional<size_t> byte_offset = std::nullopt) { |
| if (PERFETTO_UNLIKELY(!trace_time_clock_id_used_for_conversion_)) { |
| clock_event_listener_->OnTraceTimeClockIdChanged(trace_time_clock_id_); |
| } |
| trace_time_clock_id_used_for_conversion_ = true; |
| if (clock_id == trace_time_clock_id_) { |
| return ToHostTraceTime(timestamp); |
| } |
| std::optional<int64_t> ts = |
| Convert(clock_id, timestamp, trace_time_clock_id_, byte_offset); |
| if (ts) { |
| return ToHostTraceTime(*ts); |
| } |
| return ts; |
| } |
| |
| // Appends a new snapshot for the given clock domains. |
| // This is typically called by the code that reads the ClockSnapshot packet. |
| // Returns the internal snapshot id of this set of clocks. |
| base::StatusOr<uint32_t> AddSnapshot( |
| const std::vector<ClockTimestamp>& clock_timestamps) { |
| const auto snapshot_id = cur_snapshot_id_++; |
| |
| // Clear the cache |
| cache_.fill({}); |
| |
| // Compute the fingerprint of the snapshot by hashing all clock ids. This is |
| // used by the clock pathfinding logic. |
| base::MurmurHashCombiner hasher; |
| for (const auto& clock_ts : clock_timestamps) |
| hasher.Combine(clock_ts.clock.id); |
| const auto snapshot_hash = static_cast<SnapshotHash>(hasher.digest()); |
| |
| // Add a new entry in each clock's snapshot vector. |
| for (const auto& clock_ts : clock_timestamps) { |
| ClockId clock_id = clock_ts.clock.id; |
| ClockDomain& domain = clocks_[clock_id]; |
| |
| if (domain.snapshots.empty()) { |
| if (clock_ts.clock.is_incremental && |
| !IsConvertedSequenceClock(clock_id)) { |
| clock_event_listener_->OnInvalidClockSnapshot(); |
| return base::ErrStatus( |
| "Clock sync error: the global clock with id=%" PRId64 |
| " cannot use incremental encoding; this is only " |
| "supported for sequence-scoped clocks.", |
| clock_id); |
| } |
| domain.unit_multiplier_ns = clock_ts.clock.unit_multiplier_ns; |
| domain.is_incremental = clock_ts.clock.is_incremental; |
| } else if (PERFETTO_UNLIKELY(domain.unit_multiplier_ns != |
| clock_ts.clock.unit_multiplier_ns || |
| domain.is_incremental != |
| clock_ts.clock.is_incremental)) { |
| clock_event_listener_->OnInvalidClockSnapshot(); |
| return base::ErrStatus( |
| "Clock sync error: the clock domain with id=%" PRId64 |
| " (unit=%" PRId64 |
| ", incremental=%d), was previously registered with " |
| "different properties (unit=%" PRId64 ", incremental=%d).", |
| clock_id, clock_ts.clock.unit_multiplier_ns, |
| clock_ts.clock.is_incremental, domain.unit_multiplier_ns, |
| domain.is_incremental); |
| } |
| if (PERFETTO_UNLIKELY(clock_id == trace_time_clock_id_ && |
| domain.unit_multiplier_ns != 1)) { |
| // The trace time clock must always be in nanoseconds. |
| clock_event_listener_->OnInvalidClockSnapshot(); |
| return base::ErrStatus( |
| "Clock sync error: the trace clock (id=%" PRId64 |
| ") must always use nanoseconds as unit multiplier.", |
| clock_id); |
| } |
| const int64_t timestamp_ns = |
| clock_ts.timestamp * domain.unit_multiplier_ns; |
| domain.last_timestamp_ns = timestamp_ns; |
| |
| ClockSnapshots& vect = domain.snapshots[snapshot_hash]; |
| if (!vect.snapshot_ids.empty() && |
| PERFETTO_UNLIKELY(vect.snapshot_ids.back() == snapshot_id)) { |
| clock_event_listener_->OnInvalidClockSnapshot(); |
| return base::ErrStatus( |
| "Clock sync error: duplicate clock domain with id=%" PRId64 |
| " at snapshot %" PRIu32 ".", |
| clock_id, snapshot_id); |
| } |
| |
| // Clock ids in the range [64, 128) are sequence-scoped and must be |
| // translated to global ids via SeqScopedClockIdToGlobal() before calling |
| // this function. |
| PERFETTO_DCHECK(!IsSequenceClock(clock_id)); |
| |
| // Snapshot IDs must be always monotonic. |
| PERFETTO_DCHECK(vect.snapshot_ids.empty() || |
| vect.snapshot_ids.back() < snapshot_id); |
| |
| if (!vect.timestamps_ns.empty() && |
| timestamp_ns < vect.timestamps_ns.back()) { |
| // Clock is not monotonic. |
| |
| if (clock_id == trace_time_clock_id_) { |
| clock_event_listener_->OnInvalidClockSnapshot(); |
| // The trace clock cannot be non-monotonic. |
| return base::ErrStatus( |
| "Clock sync error: the trace clock (id=%" PRId64 |
| ") is not monotonic at snapshot %" PRIu32 ". %" PRId64 |
| " not >= %" PRId64 ".", |
| clock_id, snapshot_id, timestamp_ns, vect.timestamps_ns.back()); |
| } |
| |
| PERFETTO_DLOG("Detected non-monotonic clock with ID %" PRId64, |
| clock_id); |
| |
| // For the other clocks the best thing we can do is mark it as |
| // non-monotonic and refuse to use it as a source clock in the |
| // resolution graph. We can still use it as a target clock, but not |
| // viceversa. The concrete example is the CLOCK_REALTIME going 1h |
| // backwards during daylight saving. We can still answer the question |
| // "what was the REALTIME timestamp when BOOTTIME was X?" but we can't |
| // answer the opposite question because there can be two valid |
| // BOOTTIME(s) for the same REALTIME instant because of the 1:many |
| // relationship. |
| non_monotonic_clocks_.insert(clock_id); |
| |
| // Erase all edges from the graph that start from this clock (but keep |
| // the ones that end on this clock). |
| auto begin = graph_.lower_bound(ClockGraphEdge{clock_id, 0, 0}); |
| auto end = graph_.lower_bound(ClockGraphEdge{clock_id + 1, 0, 0}); |
| graph_.erase(begin, end); |
| } |
| vect.snapshot_ids.emplace_back(snapshot_id); |
| vect.timestamps_ns.emplace_back(timestamp_ns); |
| } |
| // Create graph edges for all the possible tuples of clocks in this |
| // snapshot. If the snapshot contains clock a, b, c, d create edges [ab, ac, |
| // ad, bc, bd, cd] and the symmetrical ones [ba, ca, da, bc, db, dc]. This |
| // is to store the information: Clock A is syncable to Clock B via the |
| // snapshots of type (hash). |
| // Clocks that were previously marked as non-monotonic won't be added as |
| // valid sources. |
| for (auto it1 = clock_timestamps.begin(); it1 != clock_timestamps.end(); |
| ++it1) { |
| auto it2 = it1; |
| ++it2; |
| for (; it2 != clock_timestamps.end(); ++it2) { |
| if (!non_monotonic_clocks_.count(it1->clock.id)) |
| graph_.emplace(it1->clock.id, it2->clock.id, snapshot_hash); |
| |
| if (!non_monotonic_clocks_.count(it2->clock.id)) |
| graph_.emplace(it2->clock.id, it1->clock.id, snapshot_hash); |
| } |
| } |
| |
| return snapshot_id; |
| } |
| |
| // If trace clock and source clock are available in the snapshot will return |
| // the trace clock time in snapshot. |
| std::optional<int64_t> ToTraceTimeFromSnapshot( |
| const std::vector<ClockTimestamp>& snapshot) { |
| auto maybe_found_trace_time_clock = std::find_if( |
| snapshot.begin(), snapshot.end(), |
| [this](const ClockTimestamp& clock_timestamp) { |
| return clock_timestamp.clock.id == this->trace_time_clock_id_; |
| }); |
| |
| if (maybe_found_trace_time_clock == snapshot.end()) |
| return std::nullopt; |
| |
| return maybe_found_trace_time_clock->timestamp; |
| } |
| |
| // Sets the offset for a given clock to convert timestamps from a remote |
| // machine to the host's trace time. This is typically called by the code |
| // that reads the RemoteClockSync packet. Typically only the offset of |
| // |trace_time_clock_id_| (which is CLOCK_BOOTTIME) is used. |
| void SetRemoteClockOffset(ClockId clock_id, int64_t offset) { |
| remote_clock_offsets_[clock_id] = offset; |
| } |
| |
| // Sets the clock domain to be used as the trace time. |
| // Can be called multiple times with the same clock_id, but will log an error |
| // and do nothing if called with a different clock_id after a timestamp |
| // conversion has already occurred. |
| base::Status SetTraceTimeClock(ClockId clock_id) { |
| PERFETTO_DCHECK(!IsSequenceClock(clock_id)); |
| if (trace_time_clock_id_used_for_conversion_ && |
| trace_time_clock_id_ != clock_id) { |
| return base::ErrStatus( |
| "Not updating trace time clock from %" PRId64 " to %" PRId64 |
| " because the old clock was already used for timestamp " |
| "conversion - ClockSnapshot too late in trace?", |
| trace_time_clock_id_, clock_id); |
| } |
| trace_time_clock_id_ = clock_id; |
| clock_event_listener_->OnSetTraceTimeClock(clock_id); |
| |
| return base::OkStatus(); |
| } |
| |
| // Returns the timezone offset in seconds from UTC, if one has been set. |
| std::optional<int64_t> timezone_offset() const { return timezone_offset_; } |
| |
| // Sets the timezone offset in seconds from UTC. |
| void set_timezone_offset(int64_t offset) { timezone_offset_ = offset; } |
| |
| // For testing: |
| void set_cache_lookups_disabled_for_testing(bool v) { |
| cache_lookups_disabled_for_testing_ = v; |
| } |
| |
| const base::FlatHashMap<ClockId, int64_t>& |
| remote_clock_offsets_for_testing() { |
| return remote_clock_offsets_; |
| } |
| |
| uint32_t cache_hits_for_testing() const { return cache_hits_for_testing_; } |
| |
| private: |
| using SnapshotHash = uint32_t; |
| |
| // 0th argument is the source clock, 1st argument is the target clock. |
| using ClockGraphEdge = std::tuple<ClockId, ClockId, SnapshotHash>; |
| |
| // TODO(b/273263113): Remove in the next stages. |
| friend class ClockTrackerTest; |
| |
| // A value-type object that carries the information about the path between |
| // two clock domains. It's used by the BFS algorithm. |
| struct ClockPath { |
| static constexpr size_t kMaxLen = 4; |
| ClockPath() = default; |
| ClockPath(const ClockPath&) = default; |
| |
| // Constructs an invalid path with just a source node. |
| explicit ClockPath(ClockId clock_id) : last(clock_id) {} |
| |
| // Constructs a path by appending a node to |prefix|. |
| // If |prefix| = [A,B] and clock_id = C, then |this| = [A,B,C]. |
| ClockPath(const ClockPath& prefix, ClockId clock_id, SnapshotHash hash) { |
| PERFETTO_DCHECK(prefix.len < kMaxLen); |
| len = prefix.len + 1; |
| path = prefix.path; |
| path[prefix.len] = ClockGraphEdge{prefix.last, clock_id, hash}; |
| last = clock_id; |
| } |
| |
| bool valid() const { return len > 0; } |
| const ClockGraphEdge& at(uint32_t i) const { |
| PERFETTO_DCHECK(i < len); |
| return path[i]; |
| } |
| |
| uint32_t len = 0; |
| ClockId last = 0; |
| std::array<ClockGraphEdge, kMaxLen> path; // Deliberately uninitialized. |
| }; |
| |
| struct ClockSnapshots { |
| // Invariant: both vectors have the same length. |
| std::vector<uint32_t> snapshot_ids; |
| std::vector<int64_t> timestamps_ns; |
| }; |
| |
| struct ClockDomain { |
| // One time-series for each hash. |
| std::map<SnapshotHash, ClockSnapshots> snapshots; |
| |
| // Multiplier for timestamps given in this domain. |
| int64_t unit_multiplier_ns = 1; |
| |
| // Whether this clock domain encodes timestamps as deltas. This is only |
| // supported on sequence-local domains. |
| bool is_incremental = false; |
| |
| // If |is_incremental| is true, this stores the most recent absolute |
| // timestamp in nanoseconds. |
| int64_t last_timestamp_ns = 0; |
| |
| // Treats |timestamp| as delta timestamp if the clock uses incremental |
| // encoding, and as absolute timestamp otherwise. |
| int64_t ToNs(int64_t timestamp) { |
| if (!is_incremental) |
| return timestamp * unit_multiplier_ns; |
| |
| int64_t delta_ns = timestamp * unit_multiplier_ns; |
| last_timestamp_ns += delta_ns; |
| return last_timestamp_ns; |
| } |
| |
| const ClockSnapshots& GetSnapshot(uint32_t hash) const { |
| auto it = snapshots.find(hash); |
| PERFETTO_DCHECK(it != snapshots.end()); |
| return it->second; |
| } |
| }; |
| |
| // Holds data for cached entries. At the moment only single-path resolution |
| // are cached. |
| struct CachedClockPath { |
| ClockId src; |
| ClockId target; |
| ClockDomain* src_domain; |
| int64_t min_ts_ns; |
| int64_t max_ts_ns; |
| int64_t translation_ns; |
| }; |
| |
| ClockSynchronizer(const ClockSynchronizer&) = delete; |
| ClockSynchronizer& operator=(const ClockSynchronizer&) = delete; |
| |
| std::optional<int64_t> ConvertSlowpath( |
| ClockId src_clock_id, |
| int64_t src_timestamp, |
| std::optional<int64_t> src_timestamp_ns, |
| ClockId target_clock_id, |
| std::optional<size_t> byte_offset) { |
| PERFETTO_DCHECK(!IsSequenceClock(src_clock_id)); |
| PERFETTO_DCHECK(!IsSequenceClock(target_clock_id)); |
| clock_event_listener_->OnClockSyncCacheMiss(); |
| |
| ClockPath path = FindPath(src_clock_id, target_clock_id); |
| if (!path.valid()) { |
| // Determine which clock(s) are unknown and record error |
| ErrorType error; |
| if (clocks_.find(src_clock_id) == clocks_.end()) { |
| error = ErrorType::kUnknownSourceClock; |
| } else if (clocks_.find(target_clock_id) == clocks_.end()) { |
| error = ErrorType::kUnknownTargetClock; |
| } else { |
| error = ErrorType::kNoPath; |
| } |
| clock_event_listener_->RecordConversionError( |
| error, src_clock_id, target_clock_id, src_timestamp, byte_offset); |
| return std::nullopt; |
| } |
| |
| // Iterate trough the path found and translate timestamps onto the new clock |
| // domain on each step, until the target domain is reached. |
| ClockDomain* src_domain = GetClock(src_clock_id); |
| int64_t ns = |
| src_timestamp_ns ? *src_timestamp_ns : src_domain->ToNs(src_timestamp); |
| |
| // These will track the overall translation and valid range for the whole |
| // path. |
| int64_t total_translation_ns = 0; |
| int64_t path_min_ts_ns = std::numeric_limits<int64_t>::min(); |
| int64_t path_max_ts_ns = std::numeric_limits<int64_t>::max(); |
| |
| for (uint32_t i = 0; i < path.len; ++i) { |
| const ClockGraphEdge edge = path.at(i); |
| ClockDomain* cur_clock = GetClock(std::get<0>(edge)); |
| ClockDomain* next_clock = GetClock(std::get<1>(edge)); |
| const SnapshotHash hash = std::get<2>(edge); |
| |
| // Find the closest timestamp within the snapshots of the source clock. |
| const ClockSnapshots& cur_snap = cur_clock->GetSnapshot(hash); |
| const auto& ts_vec = cur_snap.timestamps_ns; |
| auto it = std::upper_bound(ts_vec.begin(), ts_vec.end(), ns); |
| if (it != ts_vec.begin()) |
| --it; |
| |
| // Now lookup the snapshot id that matches the closest timestamp. |
| size_t index = static_cast<size_t>(std::distance(ts_vec.begin(), it)); |
| PERFETTO_DCHECK(index < ts_vec.size()); |
| PERFETTO_DCHECK(cur_snap.snapshot_ids.size() == ts_vec.size()); |
| uint32_t snapshot_id = cur_snap.snapshot_ids[index]; |
| |
| // And use that to retrieve the corresponding time in the next clock |
| // domain. The snapshot id must exist in the target clock domain. If it |
| // doesn't either the hash logic or the pathfinding logic are bugged. This |
| // can also happen if the checks in AddSnapshot fail and we skip part of |
| // the snapshot. |
| const ClockSnapshots& next_snap = next_clock->GetSnapshot(hash); |
| |
| // Using std::lower_bound because snapshot_ids is sorted, so we can do |
| // a binary search. std::find would do a linear scan. |
| auto next_it = |
| std::lower_bound(next_snap.snapshot_ids.begin(), |
| next_snap.snapshot_ids.end(), snapshot_id); |
| if (next_it == next_snap.snapshot_ids.end() || *next_it != snapshot_id) { |
| PERFETTO_DFATAL("Snapshot does not exist in clock domain."); |
| continue; |
| } |
| size_t next_index = static_cast<size_t>( |
| std::distance(next_snap.snapshot_ids.begin(), next_it)); |
| PERFETTO_DCHECK(next_index < next_snap.snapshot_ids.size()); |
| int64_t next_timestamp_ns = next_snap.timestamps_ns[next_index]; |
| |
| // The translated timestamp is the relative delta of the source timestamp |
| // from the closest snapshot found (ns - *it), plus the timestamp in |
| // the new clock domain for the same snapshot id. |
| const int64_t hop_translation_ns = next_timestamp_ns - *it; |
| ns += hop_translation_ns; |
| |
| // Now, calculate the valid range for this specific hop and intersect it |
| // with the accumulated valid range for the whole path. |
| // The range for this hop needs to be translated back to the source |
| // clock's coordinate system. |
| const int64_t kInt64Min = std::numeric_limits<int64_t>::min(); |
| const int64_t kInt64Max = std::numeric_limits<int64_t>::max(); |
| |
| int64_t hop_min_ts_ns = (it == ts_vec.begin()) ? kInt64Min : *it; |
| auto ubound = it + 1; |
| int64_t hop_max_ts_ns = (ubound == ts_vec.end()) ? kInt64Max : *ubound; |
| |
| // Translate the hop's valid range back to the original source clock's |
| // domain. `total_translation_ns` is the translation from the *start* of |
| // the path to the *start* of the current hop. |
| int64_t hop_min_in_src_domain_ns = |
| (hop_min_ts_ns == kInt64Min) ? kInt64Min |
| : hop_min_ts_ns - total_translation_ns; |
| int64_t hop_max_in_src_domain_ns = |
| (hop_max_ts_ns == kInt64Max) ? kInt64Max |
| : hop_max_ts_ns - total_translation_ns; |
| |
| // Intersect with the path's current valid range. |
| path_min_ts_ns = std::max(path_min_ts_ns, hop_min_in_src_domain_ns); |
| path_max_ts_ns = std::min(path_max_ts_ns, hop_max_in_src_domain_ns); |
| |
| // Accumulate the translation. |
| total_translation_ns += hop_translation_ns; |
| |
| // The last clock in the path must be the target clock. |
| PERFETTO_DCHECK(i < path.len - 1 || std::get<1>(edge) == target_clock_id); |
| } |
| |
| // After the loop, we have the final converted timestamp `ns`, and the |
| // total translation and valid range for the entire path. |
| // We can now cache this result. |
| CachedClockPath cache_entry{}; |
| cache_entry.src = src_clock_id; |
| cache_entry.target = target_clock_id; |
| cache_entry.src_domain = src_domain; |
| cache_entry.min_ts_ns = path_min_ts_ns; |
| cache_entry.max_ts_ns = path_max_ts_ns; |
| cache_entry.translation_ns = total_translation_ns; |
| cache_[rnd_() % cache_.size()] = cache_entry; |
| |
| return ns; |
| } |
| |
| // Converts a timestamp between two clock domains. Tries to use the cache |
| // first (only for single-path resolutions), then falls back on path finding |
| // as described in the header. |
| std::optional<int64_t> Convert(ClockId src_clock_id, |
| int64_t src_timestamp, |
| ClockId target_clock_id, |
| std::optional<size_t> byte_offset) { |
| std::optional<int64_t> ns; |
| if (PERFETTO_LIKELY(!cache_lookups_disabled_for_testing_)) { |
| for (const auto& cached_clock_path : cache_) { |
| if (cached_clock_path.src != src_clock_id || |
| cached_clock_path.target != target_clock_id) { |
| continue; |
| } |
| if (!ns) { |
| ns = cached_clock_path.src_domain->ToNs(src_timestamp); |
| } |
| if (*ns >= cached_clock_path.min_ts_ns && |
| *ns < cached_clock_path.max_ts_ns) { |
| cache_hits_for_testing_++; |
| return *ns + cached_clock_path.translation_ns; |
| } |
| } |
| } |
| return ConvertSlowpath(src_clock_id, src_timestamp, ns, target_clock_id, |
| byte_offset); |
| } |
| |
| // Returns whether |global_clock_id| represents a sequence-scoped clock, i.e. |
| // a ClockId returned by SeqScopedClockIdToGlobal(). |
| static bool IsConvertedSequenceClock(ClockId global_clock_id) { |
| // If the id is > 2**32, this is a sequence-scoped clock id translated into |
| // the global namespace. |
| return (global_clock_id >> 32) > 0; |
| } |
| |
| // Finds the shortest clock resolution path in the graph that allows to |
| // translate a timestamp from |src| to |target| clocks. |
| // The return value looks like the following: "If you want to convert a |
| // timestamp from clock C1 to C2 you need to first convert C1 -> C3 using the |
| // snapshot hash A, then convert C3 -> C2 via snapshot hash B". |
| ClockPath FindPath(ClockId src, ClockId target) { |
| PERFETTO_CHECK(src != target); |
| |
| // If we've never heard of the clock before there is no hope: |
| if (clocks_.find(target) == clocks_.end()) { |
| return ClockPath(); |
| } |
| if (clocks_.find(src) == clocks_.end()) { |
| return ClockPath(); |
| } |
| |
| // This is a classic breadth-first search. Each node in the queue holds also |
| // the full path to reach that node. |
| // We assume the graph is acyclic, if it isn't the ClockPath::kMaxLen will |
| // stop the search anyways. |
| queue_find_path_cache_.clear(); |
| queue_find_path_cache_.emplace_back(src); |
| |
| while (!queue_find_path_cache_.empty()) { |
| ClockPath cur_path = queue_find_path_cache_.front(); |
| queue_find_path_cache_.pop_front(); |
| |
| const ClockId cur_clock_id = cur_path.last; |
| if (cur_path.len >= ClockPath::kMaxLen) |
| continue; |
| |
| // Expore all the adjacent clocks. |
| // The lower_bound() below returns an iterator to the first edge that |
| // starts on |cur_clock_id|. The edges are sorted by (src, target, hash). |
| for (auto it = graph_.lower_bound(ClockGraphEdge(cur_clock_id, 0, 0)); |
| it != graph_.end() && std::get<0>(*it) == cur_clock_id; ++it) { |
| ClockId next_clock_id = std::get<1>(*it); |
| SnapshotHash hash = std::get<2>(*it); |
| if (next_clock_id == target) |
| return ClockPath(cur_path, next_clock_id, hash); |
| queue_find_path_cache_.emplace_back( |
| ClockPath(cur_path, next_clock_id, hash)); |
| } |
| } |
| return ClockPath(); // invalid path. |
| } |
| |
| ClockDomain* GetClock(ClockId clock_id) { |
| auto it = clocks_.find(clock_id); |
| PERFETTO_DCHECK(it != clocks_.end()); |
| return &it->second; |
| } |
| |
| // Apply the clock offset to convert remote trace times to host trace time. |
| PERFETTO_ALWAYS_INLINE int64_t ToHostTraceTime(int64_t timestamp) { |
| if (PERFETTO_LIKELY(clock_event_listener_->IsLocalHost())) { |
| // No need to convert host timestamps. |
| return timestamp; |
| } |
| |
| // Find the offset for |trace_time_clock_id_| and apply the offset, or |
| // default offset 0 if not offset is found for |trace_time_clock_id_|. |
| int64_t clock_offset = remote_clock_offsets_[trace_time_clock_id_]; |
| return timestamp - clock_offset; |
| } |
| |
| ClockId trace_time_clock_id_ = 0; |
| std::map<ClockId, ClockDomain> clocks_; |
| std::set<ClockGraphEdge> graph_; |
| std::set<ClockId> non_monotonic_clocks_; |
| std::array<CachedClockPath, 8> cache_{}; |
| bool cache_lookups_disabled_for_testing_ = false; |
| uint32_t cache_hits_for_testing_ = 0; |
| std::minstd_rand rnd_; // For cache eviction. |
| uint32_t cur_snapshot_id_ = 0; |
| bool trace_time_clock_id_used_for_conversion_ = false; |
| base::FlatHashMap<ClockId, int64_t> remote_clock_offsets_; |
| std::optional<int64_t> timezone_offset_; |
| std::unique_ptr<TClockEventListener> clock_event_listener_; |
| |
| // A queue of paths to explore. Stored as a field to reduce allocations |
| // on every call to FindPath(). |
| base::CircularQueue<ClockPath> queue_find_path_cache_; |
| }; |
| |
| } // namespace perfetto::trace_processor |
| |
| #endif // SRC_TRACE_PROCESSOR_UTIL_CLOCK_SYNCHRONIZER_H_ |