diff --git a/Cargo.toml b/Cargo.toml index 026f0ba..d4436a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,6 @@ keywords = ["date", "time", "temporal", "zone", "iana"] edition = "2021" exclude = ["/.github", "/tmp"] autotests = false -autoexamples = false rust-version = "1.70" [workspace] @@ -23,7 +22,6 @@ members = [ "jiff-cli", "jiff-tzdb", "jiff-tzdb-platform", - "examples/*", ] # Features are documented in the "Crate features" section of the crate docs: diff --git a/bench/Cargo.toml b/bench/Cargo.toml index 50b3e05..b28d26c 100644 --- a/bench/Cargo.toml +++ b/bench/Cargo.toml @@ -19,6 +19,11 @@ name = "jiff-bench" harness = false path = "src/bench.rs" +[[bench]] +name = "default_time_zone_benchmark" +harness = false +path = "src/default_time_zone_benchmark.rs" + [dependencies] criterion = "0.5.1" jiff = { version = "0.1.0", path = ".." } diff --git a/bench/src/default_time_zone_benchmark.rs b/bench/src/default_time_zone_benchmark.rs new file mode 100644 index 0000000..681d5e0 --- /dev/null +++ b/bench/src/default_time_zone_benchmark.rs @@ -0,0 +1,35 @@ +use criterion::{criterion_group, criterion_main, Criterion}; +use jiff::tz::TimeZone; +use std::sync::RwLock; +use std::time::Instant; + +fn default_time_zone_benchmark(c: &mut Criterion) { + c.bench_function("Get default TimeZone::system()", |b| { + b.iter(|| { + TimeZone::system(); + }) + }); + + c.bench_function("Clone a TimeZone", |b| { + let tz = TimeZone::system(); + b.iter(|| { + let _ = tz.clone(); + }) + }); + + c.bench_function("Read lock", |b| { + let cache: RwLock = RwLock::new(0); + b.iter(|| { + let a = cache.read().unwrap(); + }) + }); + + c.bench_function("Instant::now", |b| { + b.iter(|| { + let _ = Instant::now(); + }) + }); +} + +criterion_group!(benches, default_time_zone_benchmark); +criterion_main!(benches); diff --git a/examples/default_time_zone_demo/main.rs b/examples/default_time_zone_demo/main.rs new file mode 100644 index 0000000..7c76749 --- /dev/null +++ b/examples/default_time_zone_demo/main.rs @@ -0,0 +1,36 @@ +use jiff::tz::TimeZone; +use std::thread::sleep; +use std::time::Duration; + +fn main() { + // The time zone is updated by async thread each 20 seconds. + // And we get the default time zone continuously, + // so we can see the log: `cache is still using, so update the tz.` + for i in 1..=50 { + sleep(Duration::from_secs(1)); + TimeZone::system(); + println!("round 1------{i}"); + } + + // Stop the get the default time zone for a while, so we can see the log : + // `cache is not used so far, so stop this thread.` + for i in 1..=35 { + sleep(Duration::from_secs(1)); + println!("round 2------{i}"); + } + + // Get the default time zone again, we can see + // `try_update_time_zone` log to start the thread again. + // And see the `cache is still using, so update the tz.` log later. + for i in 1..=50 { + sleep(Duration::from_secs(1)); + TimeZone::system(); + println!("round 3------{i}"); + } + + // See the `cache is not used so far, so stop this thread.` log later. + for i in 1..=50 { + sleep(Duration::from_secs(1)); + println!("round 4------{i}"); + } +} diff --git a/src/tz/system/mod.rs b/src/tz/system/mod.rs index 2ea19a3..a7a7976 100644 --- a/src/tz/system/mod.rs +++ b/src/tz/system/mod.rs @@ -1,14 +1,14 @@ #![allow(dead_code)] // REMOVE ME -use std::{sync::RwLock, time::Duration}; - -use alloc::{string::ToString, sync::Arc}; +use std::{format, println, sync::RwLock, thread, time::Duration}; use crate::{ error::{err, Error, ErrorContext}, tz::{posix::PosixTz, TimeZone, TimeZoneDatabase}, - util::cache::Expiration, }; +use alloc::{string::ToString, sync::Arc}; +use std::prelude::v1::String; +use std::thread::{sleep, JoinHandle}; #[cfg(unix)] #[path = "unix.rs"] @@ -45,7 +45,9 @@ mod sys { } /// The duration of time that a cached time zone should be considered valid. -static TTL: Duration = Duration::new(5 * 60, 0); +const TTL: Duration = Duration::new(20, 0); + +const THREAD_NAME: &str = "jiff_time_zone_fetcher"; /// A cached time zone. /// @@ -76,13 +78,63 @@ static CACHE: RwLock = RwLock::new(Cache::empty()); /// a way to reset this cache and force a re-creation of the time zone. struct Cache { tz: Option, - expiration: Expiration, + ttl: Duration, + // if the cache isn't used after updating, the updating thread will be stopped. + in_use: bool, + handle: Option>, } impl Cache { /// Create an empty cache. The default state. const fn empty() -> Cache { - Cache { tz: None, expiration: Expiration::expired() } + Cache { tz: None, ttl: TTL, in_use: false, handle: None } + } + + fn try_schedule_update_time_zone( + &mut self, + db: &'static TimeZoneDatabase, + ) { + println!("try_update_time_zone"); + if self.handle.is_some() { + // Thread race happens here, other thread has already + // started the thread, so return directly. + return; + } + let ttl = self.ttl.clone(); + let handle = thread::Builder::new() + .name(THREAD_NAME.to_string()) + .spawn(move || { + loop { + sleep(ttl); + + let tz_result = get_force(db); + let mut cache = CACHE.write().unwrap(); + match tz_result { + Ok(tz) => { + if cache.in_use { + println!( + "cache is still using, so update the tz." + ); + cache.tz = Some(tz); + cache.in_use = false; + } else { + println!("cache is not used so far, so stop this thread."); + cache.handle = None; + cache.tz = None; + break; + } + } + Err(_) => { + // Get tz fails, so stop this updating thread. + cache.handle = None; + cache.tz = None; + break; + } + }; + } + }) + .expect(&format!("failed to spawn {} thread", THREAD_NAME)); + self.handle = Some(handle); } } @@ -99,15 +151,34 @@ impl Cache { /// it is just impractical to determine the time zone name. For example, when /// `/etc/localtime` is a hard link to a TZif file instead of a symlink and /// when the time zone name isn't recorded in any of the other obvious places. -pub(crate) fn get(db: &TimeZoneDatabase) -> Result { +pub(crate) fn get(db: &'static TimeZoneDatabase) -> Result { + let mut result: Option = None; + { let cache = CACHE.read().unwrap(); if let Some(ref tz) = cache.tz { - if !cache.expiration.is_expired() { - return Ok(tz.clone()); + let tz = tz.clone(); + if cache.in_use { + // Most of the case: It's already in use, so don't need to update. + return Ok(tz); + } else { + // The first call after updating the timezone. + // update the in_use to true in the next code block(needs write lock). + result = Some(tz); } } } + + { + // Cache has tz, but it's not used so far. We need update in_use to true. + if let Some(tz) = result { + let mut cache = CACHE.write().unwrap(); + cache.in_use = true; + return Ok(tz); + } + } + + // The cache is empty, update the tz and try_schedule_update_time_zone. let tz = get_force(db)?; { // It's okay that we race here. We basically assume that any @@ -117,7 +188,7 @@ pub(crate) fn get(db: &TimeZoneDatabase) -> Result { // will eventually be true in any sane environment. let mut cache = CACHE.write().unwrap(); cache.tz = Some(tz.clone()); - cache.expiration = Expiration::after(TTL); + cache.try_schedule_update_time_zone(db); } Ok(tz) }