Repository navigation
fix(timestamp): eliminate f64 timebase and master clock drift in long recordings (#2144) #4152
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,187 +1,196 @@ | ||
| use std::time::{Duration, Instant, SystemTime}; | ||
| #[cfg(target_os = "macos")] | ||
| mod macos; | ||
|
|
||
| #[cfg(windows)] | ||
| #[cfg(target_os = "windows")] | ||
| mod win; | ||
| #[cfg(windows)] | ||
| pub use win::*; | ||
|
|
||
| #[cfg(target_os = "macos")] | ||
| mod macos; | ||
| mod master_clock; | ||
|
|
||
| use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; | ||
|
|
||
| use serde::{Deserialize, Serialize}; | ||
| use specta::Type; | ||
|
|
||
| #[cfg(target_os = "macos")] | ||
| pub use macos::*; | ||
|
|
||
| mod master_clock; | ||
| pub use master_clock::*; | ||
| #[cfg(target_os = "windows")] | ||
| pub use win::*; | ||
|
|
||
| /// Captures anchor timestamps at the same instant, allowing later | ||
| /// conversions from any timestamp type. | ||
| #[derive(Debug, Clone, Copy)] | ||
| pub struct Timestamps { | ||
| instant: Instant, | ||
| system_time: SystemTime, | ||
| #[cfg(target_os = "macos")] | ||
| mach_absolute_time: MachAbsoluteTimestamp, | ||
| #[cfg(target_os = "windows")] | ||
| performance_counter: PerformanceCounterTimestamp, | ||
| } | ||
|
|
||
| #[derive(Clone, Copy, Debug)] | ||
| impl Timestamps { | ||
| pub fn now() -> Self { | ||
| Self { | ||
| instant: Instant::now(), | ||
| system_time: SystemTime::now(), | ||
| #[cfg(target_os = "macos")] | ||
| mach_absolute_time: MachAbsoluteTimestamp::now(), | ||
| #[cfg(target_os = "windows")] | ||
| performance_counter: PerformanceCounterTimestamp::now(), | ||
| } | ||
| } | ||
|
|
||
| pub fn from_wall_and_instant(system_time: SystemTime, instant: Instant) -> Self { | ||
| Self { | ||
| instant, | ||
| system_time, | ||
| #[cfg(target_os = "macos")] | ||
| mach_absolute_time: MachAbsoluteTimestamp::now(), | ||
| #[cfg(target_os = "windows")] | ||
| performance_counter: PerformanceCounterTimestamp::now(), | ||
| } | ||
| } | ||
|
|
||
| pub fn instant(&self) -> Instant { | ||
| self.instant | ||
| } | ||
|
|
||
| pub fn system_time(&self) -> SystemTime { | ||
| self.system_time | ||
| } | ||
|
|
||
| #[cfg(target_os = "macos")] | ||
| pub fn mach_absolute_time(&self) -> MachAbsoluteTimestamp { | ||
| self.mach_absolute_time | ||
| } | ||
|
|
||
| #[cfg(target_os = "windows")] | ||
| pub fn performance_counter(&self) -> PerformanceCounterTimestamp { | ||
| self.performance_counter | ||
| } | ||
| } | ||
|
|
||
| #[derive(Serialize, Deserialize, Debug, Clone, Copy, Type)] | ||
| pub struct SystemTimeStamp { | ||
| value: Duration, | ||
| } | ||
|
|
||
| #[derive(Debug, Clone, Copy)] | ||
| pub enum Timestamp { | ||
| Instant(Instant), | ||
| SystemTime(SystemTime), | ||
| #[cfg(windows)] | ||
| #[cfg(target_os = "windows")] | ||
| PerformanceCounter(PerformanceCounterTimestamp), | ||
| #[cfg(target_os = "macos")] | ||
| MachAbsoluteTime(MachAbsoluteTimestamp), | ||
| } | ||
|
|
||
| impl Timestamp { | ||
| pub fn duration_since(&self, start: Timestamps) -> Duration { | ||
| match self { | ||
| Self::Instant(instant) => instant.duration_since(start.instant), | ||
| Self::SystemTime(time) => time.duration_since(start.system_time).unwrap(), | ||
| #[cfg(windows)] | ||
| Self::PerformanceCounter(counter) => counter.duration_since(start.performance_counter), | ||
| #[cfg(target_os = "macos")] | ||
| Self::MachAbsoluteTime(time) => time.duration_since(start.mach_absolute_time), | ||
| } | ||
| } | ||
|
|
||
| pub fn checked_duration_since(&self, start: Timestamps) -> Option<Duration> { | ||
| match self { | ||
| Self::Instant(instant) => instant.checked_duration_since(start.instant), | ||
| Self::SystemTime(time) => time.duration_since(start.system_time).ok(), | ||
| #[cfg(windows)] | ||
| Self::PerformanceCounter(counter) => { | ||
| counter.checked_duration_since(start.performance_counter) | ||
| pub fn duration_since(&self, start: Timestamps) -> Option<Duration> { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Preserve these APIs while adding nanosecond conversion, or update their callers together. Otherwise, the recording crate cannot compile. Prompt To Fix With AIThis is a comment left during a code review.
Path: crates/timestamp/src/lib.rs
Line: 90
Comment:
**Capture timestamp calls break**
`Timestamp::duration_since` now returns `Option<Duration>`, but existing callers still use its result as a `Duration`. For example, `AudioMixer::detect_laggard_source` calls `.checked_sub(...)` on it. The rewrite also removes `Timestamp::from_cpal`, `checked_duration_since`, timestamp arithmetic, and `PerformanceCounterTimestamp::from_100ns` while capture code still calls them.
Preserve these APIs while adding nanosecond conversion, or update their callers together. Otherwise, the recording crate cannot compile.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly. |
||
| Some(match self { | ||
| Timestamp::Instant(t) => t.checked_duration_since(start.instant())?, | ||
| Timestamp::SystemTime(t) => t.duration_since(start.system_time()).ok()?, | ||
| #[cfg(target_os = "windows")] | ||
| Timestamp::PerformanceCounter(t) => { | ||
| t.checked_duration_since(start.performance_counter())? | ||
| } | ||
| #[cfg(target_os = "macos")] | ||
| Self::MachAbsoluteTime(time) => time.checked_duration_since(start.mach_absolute_time), | ||
| } | ||
| Timestamp::MachAbsoluteTime(t) => { | ||
| t.checked_duration_since(start.mach_absolute_time())? | ||
| } | ||
| }) | ||
| } | ||
|
|
||
| pub fn signed_duration_since_secs(&self, start: Timestamps) -> f64 { | ||
| /// Returns the signed elapsed duration in nanoseconds since `start`. | ||
| /// Negative if `self` is earlier than `start`. | ||
|
Comment on lines
+105
to
+106
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The new documentation comments only restate the method name and return value. Context Used: AGENTS.md (source) Prompt To Fix With AIThis is a comment left during a code review.
Path: crates/timestamp/src/lib.rs
Line: 105-106
Comment:
**Comments repeat the code**
The new documentation comments only restate the method name and return value. `AGENTS.md` requires comments to capture non-obvious context and explicitly bans comments that paraphrase parameter names or narrate the code. Remove these comments or explain a non-obvious timing constraint instead. The same pattern appears above `signed_duration_since_secs` and elsewhere in the rewrite. This repository requirement must be satisfied before merging.
**Context Used:** AGENTS.md ([source](https://github.com/capsoftware/cap/blob/main/AGENTS.md))
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! |
||
| pub fn signed_duration_since_nanos(&self, start: Timestamps) -> i128 { | ||
| match self { | ||
| Self::Instant(instant) => { | ||
| if let Some(duration) = instant.checked_duration_since(start.instant) { | ||
| duration.as_secs_f64() | ||
| Timestamp::Instant(t) => { | ||
| if let Some(d) = t.checked_duration_since(start.instant()) { | ||
| d.as_nanos() as i128 | ||
| } else if let Some(d) = start.instant().checked_duration_since(*t) { | ||
| -(d.as_nanos() as i128) | ||
| } else { | ||
| let reverse = start.instant.duration_since(*instant); | ||
| -(reverse.as_secs_f64()) | ||
| 0 | ||
| } | ||
| } | ||
| Self::SystemTime(time) => match time.duration_since(start.system_time) { | ||
| Ok(duration) => duration.as_secs_f64(), | ||
| Err(e) => -(e.duration().as_secs_f64()), | ||
| Timestamp::SystemTime(t) => match t.duration_since(start.system_time()) { | ||
| Ok(d) => d.as_nanos() as i128, | ||
| Err(e) => -(e.duration().as_nanos() as i128), | ||
| }, | ||
| #[cfg(windows)] | ||
| Self::PerformanceCounter(counter) => { | ||
| counter.signed_duration_since_secs(start.performance_counter) | ||
| #[cfg(target_os = "windows")] | ||
| Timestamp::PerformanceCounter(t) => { | ||
| t.signed_duration_since_nanos(start.performance_counter()) | ||
| } | ||
| #[cfg(target_os = "macos")] | ||
| Self::MachAbsoluteTime(time) => { | ||
| time.signed_duration_since_secs(start.mach_absolute_time) | ||
| Timestamp::MachAbsoluteTime(t) => { | ||
| t.signed_duration_since_nanos(start.mach_absolute_time()) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| pub fn from_cpal(instant: cpal::StreamInstant) -> Self { | ||
| #[cfg(windows)] | ||
| { | ||
| Self::PerformanceCounter(PerformanceCounterTimestamp::from_cpal(instant)) | ||
| } | ||
| #[cfg(target_os = "macos")] | ||
| { | ||
| Self::MachAbsoluteTime(MachAbsoluteTimestamp::from_cpal(instant)) | ||
| } | ||
| #[cfg(not(any(windows, target_os = "macos")))] | ||
| { | ||
| let _ = instant; | ||
| Self::Instant(Instant::now()) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl std::ops::Add<Duration> for &Timestamp { | ||
| type Output = Timestamp; | ||
|
|
||
| fn add(self, rhs: Duration) -> Self::Output { | ||
| match *self { | ||
| Timestamp::Instant(i) => Timestamp::Instant(i + rhs), | ||
| Timestamp::SystemTime(t) => Timestamp::SystemTime(t + rhs), | ||
| #[cfg(windows)] | ||
| Timestamp::PerformanceCounter(c) => Timestamp::PerformanceCounter(c + rhs), | ||
| #[cfg(target_os = "macos")] | ||
| Timestamp::MachAbsoluteTime(c) => Timestamp::MachAbsoluteTime(c + rhs), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl std::ops::Add<Duration> for Timestamp { | ||
| type Output = Timestamp; | ||
|
|
||
| fn add(self, rhs: Duration) -> Self::Output { | ||
| /// Returns the signed elapsed duration in seconds since `start`. | ||
| /// Negative if `self` is earlier than `start`. | ||
| pub fn signed_duration_since_secs(&self, start: Timestamps) -> f64 { | ||
| match self { | ||
| Timestamp::Instant(i) => Timestamp::Instant(i + rhs), | ||
| Timestamp::SystemTime(t) => Timestamp::SystemTime(t + rhs), | ||
| #[cfg(windows)] | ||
| Timestamp::PerformanceCounter(c) => Timestamp::PerformanceCounter(c + rhs), | ||
| Timestamp::Instant(_) | Timestamp::SystemTime(_) => { | ||
| self.signed_duration_since_nanos(start) as f64 / 1_000_000_000.0 | ||
| } | ||
| #[cfg(target_os = "windows")] | ||
| Timestamp::PerformanceCounter(t) => { | ||
| t.signed_duration_since_secs(start.performance_counter()) | ||
| } | ||
| #[cfg(target_os = "macos")] | ||
| Timestamp::MachAbsoluteTime(c) => Timestamp::MachAbsoluteTime(c + rhs), | ||
| Timestamp::MachAbsoluteTime(t) => { | ||
| t.signed_duration_since_secs(start.mach_absolute_time()) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl std::ops::Sub<Duration> for Timestamp { | ||
| type Output = Timestamp; | ||
|
|
||
| fn sub(self, rhs: Duration) -> Self::Output { | ||
| pub fn duration_since_unix_epoch(&self) -> Duration { | ||
| match self { | ||
| Timestamp::Instant(i) => Timestamp::Instant(i.checked_sub(rhs).unwrap_or(i)), | ||
| Timestamp::SystemTime(t) => Timestamp::SystemTime(t - rhs), | ||
| #[cfg(windows)] | ||
| Timestamp::PerformanceCounter(c) => Timestamp::PerformanceCounter(c - rhs), | ||
| Timestamp::Instant(t) => { | ||
| let now = Timestamps::now(); | ||
| let duration_since = now.instant().duration_since(*t); | ||
| let system_time = now.system_time() - duration_since; | ||
| system_time | ||
| .duration_since(UNIX_EPOCH) | ||
| .expect("insight into the past is not supported") | ||
| } | ||
| Timestamp::SystemTime(t) => t | ||
| .duration_since(UNIX_EPOCH) | ||
| .expect("insight into the past is not supported"), | ||
| #[cfg(target_os = "windows")] | ||
| Timestamp::PerformanceCounter(t) => t.duration_since_unix_epoch(), | ||
| #[cfg(target_os = "macos")] | ||
| Timestamp::MachAbsoluteTime(c) => Timestamp::MachAbsoluteTime(c - rhs), | ||
| Timestamp::MachAbsoluteTime(t) => t.duration_since_unix_epoch(), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[derive(Clone, Copy, Debug)] | ||
| pub struct Timestamps { | ||
| instant: Instant, | ||
| system_time: SystemTime, | ||
| #[cfg(windows)] | ||
| performance_counter: PerformanceCounterTimestamp, | ||
| #[cfg(target_os = "macos")] | ||
| mach_absolute_time: MachAbsoluteTimestamp, | ||
| } | ||
|
|
||
| impl Timestamps { | ||
| pub fn now() -> Self { | ||
| Self { | ||
| instant: Instant::now(), | ||
| system_time: SystemTime::now(), | ||
| #[cfg(windows)] | ||
| performance_counter: PerformanceCounterTimestamp::now(), | ||
| #[cfg(target_os = "macos")] | ||
| mach_absolute_time: MachAbsoluteTimestamp::now(), | ||
| } | ||
| } | ||
|
|
||
| pub fn instant(&self) -> Instant { | ||
| self.instant | ||
| impl From<Instant> for Timestamp { | ||
| fn from(instant: Instant) -> Self { | ||
| Timestamp::Instant(instant) | ||
| } | ||
| } | ||
|
|
||
| pub fn system_time(&self) -> SystemTime { | ||
| self.system_time | ||
| impl From<SystemTime> for Timestamp { | ||
| fn from(system_time: SystemTime) -> Self { | ||
| Timestamp::SystemTime(system_time) | ||
| } | ||
| } | ||
|
|
||
| #[cfg(target_os = "macos")] | ||
| pub fn mach_absolute_time(&self) -> MachAbsoluteTimestamp { | ||
| self.mach_absolute_time | ||
| #[cfg(target_os = "windows")] | ||
| impl From<PerformanceCounterTimestamp> for Timestamp { | ||
| fn from(performance_counter: PerformanceCounterTimestamp) -> Self { | ||
| Timestamp::PerformanceCounter(performance_counter) | ||
| } | ||
| } | ||
|
|
||
| impl std::ops::Add<Duration> for Timestamps { | ||
| type Output = Timestamps; | ||
|
|
||
| fn add(self, rhs: Duration) -> Self::Output { | ||
| Self { | ||
| instant: self.instant + rhs, | ||
| system_time: self.system_time + rhs, | ||
| #[cfg(windows)] | ||
| performance_counter: self.performance_counter + rhs, | ||
| #[cfg(target_os = "macos")] | ||
| mach_absolute_time: self.mach_absolute_time + rhs, | ||
| } | ||
| #[cfg(target_os = "macos")] | ||
| impl From<MachAbsoluteTimestamp> for Timestamp { | ||
| fn from(mach_absolute_time: MachAbsoluteTimestamp) -> Self { | ||
| Timestamp::MachAbsoluteTime(mach_absolute_time) | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
serdeandspectaare imported without being declared incrates/timestamp/Cargo.toml. Neither is available as a direct dependency, socap-timestampfails to compile on every platform. Add both dependencies or remove the new imports and derives.Prompt To Fix With AI