time: Improve Instant::now() perf with test-util (#5513)

The test-util feature flag is only intended to be used with tests.
However, it is possible to enable it in release mode accidentally. This
patch reduces the overhead of `Instant::now()` when the `test-util`
feature flag is enabled but `time::pause()` is not called.

The optimization is implemented by adding a static atomic flag that
tracks if `time::pause()` has ever been called. In `Instant::now()`, the
atomic flag is first checked before the thread-local and mutex are
accessed.
This commit is contained in:
Carl Lerche
2023-02-27 10:21:42 -08:00
committed by GitHub
parent 815d89a407
commit ee1c940709
4 changed files with 65 additions and 0 deletions
+8
View File
@@ -4,6 +4,9 @@ version = "0.0.0"
publish = false
edition = "2018"
[features]
test-util = ["tokio/test-util"]
[dependencies]
tokio = { version = "1.5.0", path = "../tokio", features = ["full"] }
bencher = "0.1.5"
@@ -67,3 +70,8 @@ harness = false
name = "copy"
path = "copy.rs"
harness = false
[[bench]]
name = "time_now"
path = "time_now.rs"
harness = false
+25
View File
@@ -0,0 +1,25 @@
//! Benchmark spawning a task onto the basic and threaded Tokio executors.
//! This essentially measure the time to enqueue a task in the local and remote
//! case.
#[macro_use]
extern crate bencher;
use bencher::{black_box, Bencher};
fn time_now_current_thread(bench: &mut Bencher) {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_time()
.build()
.unwrap();
bench.iter(|| {
rt.block_on(async {
black_box(tokio::time::Instant::now());
})
})
}
bencher::benchmark_group!(time_now, time_now_current_thread,);
bencher::benchmark_main!(time_now);
+18
View File
@@ -30,6 +30,8 @@ cfg_not_test_util! {
cfg_test_util! {
use crate::time::{Duration, Instant};
use crate::loom::sync::Mutex;
use crate::loom::sync::atomic::Ordering;
use std::sync::atomic::AtomicBool as StdAtomicBool;
cfg_rt! {
#[track_caller]
@@ -65,6 +67,15 @@ cfg_test_util! {
inner: Mutex<Inner>,
}
// Used to track if the clock was ever paused. This is an optimization to
// avoid touching the mutex if `test-util` was accidentally enabled in
// release mode.
//
// A static is used so we can avoid accessing the thread-local as well. The
// `std` AtomicBool is used directly because loom does not support static
// atomics.
static DID_PAUSE_CLOCK: StdAtomicBool = StdAtomicBool::new(false);
#[derive(Debug)]
struct Inner {
/// True if the ability to pause time is enabled.
@@ -199,6 +210,10 @@ cfg_test_util! {
/// Returns the current instant, factoring in frozen time.
pub(crate) fn now() -> Instant {
if !DID_PAUSE_CLOCK.load(Ordering::Acquire) {
return Instant::from_std(std::time::Instant::now());
}
with_clock(|maybe_clock| {
Ok(if let Some(clock) = maybe_clock {
clock.now()
@@ -241,6 +256,9 @@ cfg_test_util! {
This is the default Runtime used by `#[tokio::test].");
}
// Track that we paused the clock
DID_PAUSE_CLOCK.store(true, Ordering::Release);
let elapsed = match inner.unfrozen.as_ref() {
Some(v) => v.elapsed(),
None => return Err("time is already frozen")
+14
View File
@@ -0,0 +1,14 @@
#![cfg(all(feature = "full"))]
use tokio::time::{Duration, Instant};
#[tokio::test(start_paused = true)]
async fn test_start_paused() {
let now = Instant::now();
// Pause a few times w/ std sleep and ensure `now` stays the same
for _ in 0..5 {
std::thread::sleep(Duration::from_millis(1));
assert_eq!(now, Instant::now());
}
}