Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 29 additions & 9 deletions frontend/app/components/trace_viewer_v2/animation.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,19 @@ class Animation {
inline static absl::NoDestructor<std::vector<Animation*>> finished_;
};

// Returns true if the two values are almost equal within the given tolerances.
// Default implementation uses abs() and works for scalar types. Complex types
// like TimeRange can overload this for specialized behavior.
//
// This fuzzy comparison is crucial for preventing infinite animation loops
// caused by precision loss during JS-WASM interop (e.g., URL state sync).
template <typename T>
bool AlmostEquals(const T& a, const T& b, double rel_tol = 0.0,
double abs_tol = 1e-4) {
using std::abs;
return abs(a - b) <= std::max(abs(b) * rel_tol, abs_tol);
}

// Animated<T> represents a value of type T that can be animated over time
// towards a target value.
// Type T must support copy/move construction, assignment, operators `==`, `!=`,
Expand All @@ -79,7 +92,9 @@ class Animated : public Animation {
explicit Animated(
T value = T(), T target = T(), OnFinished on_finished = [](const T&) {})
: current_(value), target_(target), on_finished_(on_finished) {
if (current_ != target_) Animation::Register(this);
if (!AlmostEquals(current_, target_, 0.0, kAbsoluteTolerance)) {
Animation::Register(this);
}
}

// Copy constructor: deliberately doesn't register copied instance for
Expand All @@ -95,7 +110,9 @@ class Animated : public Animation {
target_(other.target_),
on_finished_(std::move(other.on_finished_)) {
Animation::Unregister(&other);
if (current_ != target_) Animation::Register(this);
if (!AlmostEquals(current_, target_, 0.0, kAbsoluteTolerance)) {
Animation::Register(this);
}
}
~Animated() override { Animation::Unregister(this); }

Expand All @@ -106,10 +123,10 @@ class Animated : public Animation {
// Sets a new target value and starts animating towards it.
// The on_finished callback is reset to a no-op.
Animated& operator=(const T& new_value) {
if (target_ == new_value) return *this;
if (AlmostEquals(target_, new_value, 0.0, kAbsoluteTolerance)) return *this;
target_ = new_value;
on_finished_ = [](const T&) {};
if (current_ != target_) {
if (!AlmostEquals(current_, target_, 0.0, kAbsoluteTolerance)) {
Animation::Register(this);
}
return *this;
Expand All @@ -118,10 +135,10 @@ class Animated : public Animation {
// Sets a new target value and an on_finished callback, and starts
// animating towards it.
Animated& operator()(const T& new_value, OnFinished on_finished) {
if (target_ == new_value) return *this;
if (AlmostEquals(target_, new_value, 0.0, kAbsoluteTolerance)) return *this;
target_ = new_value;
on_finished_ = on_finished ? std::move(on_finished) : [](const T&) {};
if (current_ != target_) {
if (!AlmostEquals(current_, target_, 0.0, kAbsoluteTolerance)) {
Animation::Register(this);
}
return *this;
Expand All @@ -146,9 +163,12 @@ class Animated : public Animation {
void on_finished() override { on_finished_(target_); }

bool Converged() const {
using std::abs;
return abs(current_ - target_) <
std::max(abs(target_) * kRelativeTolerance, kAbsoluteTolerance);
// We use a combined relative and absolute tolerance check to determine if
// the value has arrived at its target. Overloading AlmostEquals for
// types like TimeRange allows using duration for scaling instead of
// absolute timestamps.
return AlmostEquals(current_, target_, kRelativeTolerance,
kAbsoluteTolerance);
}

bool Update(float delta_time) override {
Expand Down
22 changes: 20 additions & 2 deletions frontend/app/components/trace_viewer_v2/timeline/time_range.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@
namespace traceviewer {

// Represents a time interval [start, end].
class TimeRange;

// Returns true if the two TimeRanges are almost equal within the given
// tolerances.
inline bool AlmostEquals(const TimeRange& a, const TimeRange& b,
double rel_tol = 0.0, double abs_tol = 1e-4);

class TimeRange {
public:
TimeRange() = default;
Expand Down Expand Up @@ -105,7 +112,7 @@ class TimeRange {
}

bool operator==(const TimeRange& other) const {
return start_ == other.start_ && end_ == other.end_;
return AlmostEquals(*this, other);
}

bool operator!=(const TimeRange& other) const { return !(*this == other); }
Expand All @@ -116,6 +123,17 @@ class TimeRange {
static constexpr Microseconds kAbsoluteTolerance = 1e-4;
};

// Returns true if the two TimeRanges are almost equal within the given
// tolerances. We use the duration of the range for relative tolerance
// calculations, as it represents the scale of the visible window. This avoids
// the precision issues that occur with large absolute timestamps (e.g., 10^12).
inline bool AlmostEquals(const TimeRange& a, const TimeRange& b,
double rel_tol, double abs_tol) {
double tol = std::max(a.duration() * rel_tol, abs_tol);
return std::fabs(a.start() - b.start()) <= tol &&
std::fabs(a.end() - b.end()) <= tol;
}

// Defines an abs() operation for TimeRange. This is used by
// `Animated<TimeRange>::Update()` to check for convergence. The input `range`
// is typically the result of `current_ - target_`. The sum of the absolute
Expand All @@ -126,7 +144,7 @@ class TimeRange {
// Defined as inline in the header to allow template instantiation
// (e.g. Animated<TimeRange>) and prevent multiple definition errors.
inline Microseconds abs(const TimeRange& range) {
return std::fabs(range.start()) + std::fabs(range.end());
return std::max(std::fabs(range.start()), std::fabs(range.end()));
}

} // namespace traceviewer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,14 @@ TEST(TimeRangeTest, OperatorMinus) {
TEST(TimeRangeTest, Abs) {
TimeRange range(10.0, 20.0);

EXPECT_EQ(abs(range), 30.0);
EXPECT_EQ(abs(range), 20.0);
}

TEST(TimeRangeTest, FuzzyEquality) {
TimeRange range1(10.0, 20.0);
TimeRange range2(10.0 + 1e-12, 20.0 - 1e-12);

EXPECT_EQ(range1, range2);
}

TEST(TimeRangeTest, AnimatedTimeRangeBeforeUpdate) {
Expand Down
Loading