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
9 changes: 2 additions & 7 deletions crates/cachet/src/fallback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ use tick::Clock;
use crate::Error;
use crate::cache::CacheName;
use crate::refresh::TimeToRefresh;
use crate::telemetry::ext::ClockExt;
use crate::telemetry::{CacheActivity, CacheOperation, CacheTelemetry};

/// Type alias for promotion predicate functions.
Expand Down Expand Up @@ -212,7 +211,7 @@ where
///
/// Separated from [`get`](Self::get) to keep the hot path (primary hits) small.
async fn get_from_fallback(&self, key: &K) -> Result<Option<CacheEntry<V>>, Error> {
let timed = self.inner.clock.timed_async(self.inner.fallback.get(key)).await;
let timed = self.inner.clock.timed(self.inner.fallback.get(key)).await;
self.inner
.telemetry
.record(self.inner.name, CacheOperation::Get, CacheActivity::Fallback, timed.duration);
Expand All @@ -223,11 +222,7 @@ where
if let Some(ref v) = fallback_value
&& self.inner.policy.should_promote(v)
{
let timed_insert = self
.inner
.clock
.timed_async(self.inner.primary.insert(key.clone(), v.clone()))
.await;
let timed_insert = self.inner.clock.timed(self.inner.primary.insert(key.clone(), v.clone())).await;
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This statement is formatted as a single very long line. Please run rustfmt (or break the call across lines) to match the project's formatting expectations and avoid rustfmt CI failures.

Suggested change
let timed_insert = self.inner.clock.timed(self.inner.primary.insert(key.clone(), v.clone())).await;
let timed_insert = self
.inner
.clock
.timed(self.inner.primary.insert(key.clone(), v.clone()))
.await;

Copilot uses AI. Check for mistakes.
// Insert errors are intentionally swallowed - a failed promotion should not
// fail the overall get. The CacheWrapper around the primary tier already
// records an Error activity on insert failure.
Expand Down
5 changes: 2 additions & 3 deletions crates/cachet/src/refresh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ use cachet_tier::{CacheEntry, CacheTier};
use parking_lot::Mutex;

use crate::fallback::{FallbackCache, FallbackCacheInner};
use crate::telemetry::ext::ClockExt;
use crate::telemetry::{CacheActivity, CacheOperation};

/// Configuration for background cache refresh.
Expand Down Expand Up @@ -146,7 +145,7 @@ where
F: CacheTier<K, V> + Send + Sync + 'static,
{
pub(crate) async fn fetch_and_promote(&self, key: K) {
let timed = self.clock.timed_async(self.fallback.get(&key)).await;
let timed = self.clock.timed(self.fallback.get(&key)).await;

match timed.result {
Ok(Some(value)) => self.handle_fallback_hit(key, value, timed.duration).await,
Expand All @@ -164,7 +163,7 @@ where
}

async fn promote_to_primary(&self, key: K, value: CacheEntry<V>) {
let timed = self.clock.timed_async(self.primary.insert(key, value)).await;
let timed = self.clock.timed(self.primary.insert(key, value)).await;

match timed.result {
Ok(()) => {
Expand Down
130 changes: 0 additions & 130 deletions crates/cachet/src/telemetry/ext.rs

This file was deleted.

1 change: 0 additions & 1 deletion crates/cachet/src/telemetry/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
pub(crate) mod attributes;
pub(crate) mod cache;
pub(crate) mod config;
pub(crate) mod ext;
#[cfg(any(feature = "metrics", test))]
pub(crate) mod metrics;

Expand Down
9 changes: 4 additions & 5 deletions crates/cachet/src/wrapper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ use cachet_tier::CacheTier;
use tick::Clock;

use crate::cache::CacheName;
use crate::telemetry::ext::ClockExt;
use crate::telemetry::{CacheActivity, CacheOperation, CacheTelemetry};
use crate::{CacheEntry, Error};

Expand Down Expand Up @@ -124,7 +123,7 @@ where
CT: CacheTier<K, V> + Send + Sync,
{
async fn get(&self, key: &K) -> Result<Option<CacheEntry<V>>, Error> {
let timed = self.clock.timed_async(self.inner.get(key)).await;
let timed = self.clock.timed(self.inner.get(key)).await;
match timed.result {
Ok(value) => Ok(self.handle_get_result(value, timed.duration)),
Err(e) => {
Expand All @@ -137,7 +136,7 @@ where

async fn insert(&self, key: K, mut entry: CacheEntry<V>) -> Result<(), Error> {
entry.ensure_cached_at(self.clock.system_time());
let timed = self.clock.timed_async(self.inner.insert(key, entry)).await;
let timed = self.clock.timed(self.inner.insert(key, entry)).await;
match &timed.result {
Ok(()) => {
self.telemetry
Expand All @@ -155,7 +154,7 @@ where
}

async fn invalidate(&self, key: &K) -> Result<(), Error> {
let timed = self.clock.timed_async(self.inner.invalidate(key)).await;
let timed = self.clock.timed(self.inner.invalidate(key)).await;
match &timed.result {
Ok(()) => {
self.telemetry
Expand All @@ -173,7 +172,7 @@ where
}

async fn clear(&self) -> Result<(), Error> {
let timed = self.clock.timed_async(self.inner.clear()).await;
let timed = self.clock.timed(self.inner.clear()).await;
match &timed.result {
Ok(()) => {
self.telemetry
Expand Down
5 changes: 5 additions & 0 deletions crates/tick/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ thread_aware.workspace = true
tokio = { workspace = true, optional = true, features = ["time", "rt"] }

[dev-dependencies]
#internal
ohno = { workspace = true, features = ["app-err"] }

#external
Expand Down Expand Up @@ -96,6 +97,10 @@ required-features = ["test-util"]
name = "interop_jiff"
required-features = ["test-util"]

[[example]]
name = "timed_result"
required-features = ["tokio"]

[[bench]]
name = "clock_bench"
harness = false
25 changes: 25 additions & 0 deletions crates/tick/examples/timed_result.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

//! This example demonstrates how to measure the execution time of an async
//! operation using [`Clock::timed`] and [`TimedResult`].

use tick::{Clock, TimedResult};

#[tokio::main]
async fn main() {
// Create a clock for the Tokio runtime.
let clock = Clock::new_tokio();

// Start some background work that returns a result after a delay.
let background_job = async {
clock.delay(std::time::Duration::from_millis(10)).await;
"Background job result"
};

// Use `Timed` to measure the time taken by the background job and capture its result.
let TimedResult { result, duration } = clock.timed(background_job).await;

// Print the result and the elapsed time.
println!("Result: {}, Elapsed time: {:?}", result, duration);

Check failure on line 24 in crates/tick/examples/timed_result.rs

View workflow job for this annotation

GitHub Actions / static-analysis (ubuntu-24.04-arm)

variables can be used directly in the `format!` string

Check failure on line 24 in crates/tick/examples/timed_result.rs

View workflow job for this annotation

GitHub Actions / static-analysis (ubuntu-latest)

variables can be used directly in the `format!` string

Check failure on line 24 in crates/tick/examples/timed_result.rs

View workflow job for this annotation

GitHub Actions / static-analysis (windows-11-arm)

variables can be used directly in the `format!` string

Check failure on line 24 in crates/tick/examples/timed_result.rs

View workflow job for this annotation

GitHub Actions / static-analysis (windows-latest)

variables can be used directly in the `format!` string
}
84 changes: 84 additions & 0 deletions crates/tick/src/clock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use std::time::{Duration, Instant, SystemTime};
use thread_aware::ThreadAware;
use thread_aware::affinity::{MemoryAffinity, PinnedAffinity};

use crate::Timed;
use crate::state::ClockState;
use crate::timers::TimerKey;

Expand Down Expand Up @@ -459,6 +460,35 @@ impl Clock {
crate::Stopwatch::new(self)
}

/// Wraps a future so that its execution time is measured.
///
/// Returns a [`Timed`] future whose output is a [`TimedResult`][crate::TimedResult]
/// containing both the inner future's result and the elapsed duration.
///
/// The measurement uses the same clock as the [`Stopwatch`][crate::Stopwatch],
/// so time can be controlled in tests via [`ClockControl`][crate::ClockControl].
///
/// # Examples
///
/// ```
/// use tick::{Clock, TimedResult};
///
/// # async fn timed_example(clock: &Clock) {
/// let TimedResult { result, duration } = clock.timed(async { 42 }).await;
/// println!("Result: {}, Duration: {:?}", result, duration);
/// assert_eq!(result, 42);
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The doctest example introduces unused bindings/imports (Duration import and the duration binding from TimedResult). This can produce warnings in doctest builds; consider removing the unused import and/or using _duration (or asserting on duration) to keep the example warning-free.

Suggested change
/// assert_eq!(result, 42);
/// assert_eq!(result, 42);
/// assert!(duration >= Duration::from_millis(0));

Copilot uses AI. Check for mistakes.
/// # }
/// ```
pub fn timed<F>(&self, f: F) -> Timed<F>
where
F: Future,
{
Comment on lines +482 to +485
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clock::timed uses F: Future, but Future isn’t in scope in this module (no use std::future::Future; and it’s not in the prelude). This will not compile as-is; import std::future::Future or fully qualify the bound.

Copilot uses AI. Check for mistakes.
Timed {
inner: f,
watch: self.stopwatch(),
}
}

pub(super) fn register_timer(&self, when: Instant, waker: Waker) -> TimerKey {
match self.clock_state() {
#[cfg(any(feature = "test-util", test))]
Expand Down Expand Up @@ -781,4 +811,58 @@ mod tests {

insta::assert_debug_snapshot!(clock);
}

#[tokio::test]
#[cfg_attr(miri, ignore)]
async fn timed_measures_duration() {
let control = ClockControl::new();
let clock = control.to_clock();

let timed = clock
.timed(async {
control.advance(Duration::from_millis(100));
42
})
.await;

assert_eq!(timed.result, 42);
assert_eq!(timed.duration, Duration::from_millis(100));
}

#[tokio::test]
#[cfg_attr(miri, ignore)]
async fn timed_handles_pending() {
use std::pin::Pin;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::task::{Context, Poll};

/// A future that returns Pending on the first poll, then Ready on the second.
struct YieldOnce {
yielded: Arc<AtomicBool>,
}

impl std::future::Future for YieldOnce {
type Output = i32;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<i32> {
if self.yielded.swap(true, Ordering::SeqCst) {
Poll::Ready(99)
} else {
cx.waker().wake_by_ref();
Poll::Pending
}
}
}

let control = ClockControl::new();
let clock = control.to_clock();

let timed = clock
.timed(YieldOnce {
yielded: Arc::new(AtomicBool::new(false)),
})
.await;

assert_eq!(timed.result, 99);
}
}
Loading
Loading