Skip to content
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

use threads to update default time zone cache asynchronously #97

Closed
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
2 changes: 0 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,13 @@ keywords = ["date", "time", "temporal", "zone", "iana"]
edition = "2021"
exclude = ["/.github", "/tmp"]
autotests = false
autoexamples = false
rust-version = "1.70"

[workspace]
members = [
"jiff-cli",
"jiff-tzdb",
"jiff-tzdb-platform",
"examples/*",
]

# Features are documented in the "Crate features" section of the crate docs:
Expand Down
5 changes: 5 additions & 0 deletions bench/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ".." }
Expand Down
35 changes: 35 additions & 0 deletions bench/src/default_time_zone_benchmark.rs
Original file line number Diff line number Diff line change
@@ -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();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

After this PR, this benchmark is improved from 61.665 ns to 25.429 ns on my Mac.

})
});

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<usize> = 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);
36 changes: 36 additions & 0 deletions examples/default_time_zone_demo/main.rs
Original file line number Diff line number Diff line change
@@ -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}");
}
}
93 changes: 82 additions & 11 deletions src/tz/system/mod.rs
Original file line number Diff line number Diff line change
@@ -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"]
Expand Down Expand Up @@ -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.
///
Expand Down Expand Up @@ -76,13 +78,63 @@ static CACHE: RwLock<Cache> = RwLock::new(Cache::empty());
/// a way to reset this cache and force a re-creation of the time zone.
struct Cache {
tz: Option<TimeZone>,
expiration: Expiration,
ttl: Duration,
// if the cache isn't used after updating, the updating thread will be stopped.
in_use: bool,
handle: Option<JoinHandle<()>>,
}

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);
}
}

Expand All @@ -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<TimeZone, Error> {
pub(crate) fn get(db: &'static TimeZoneDatabase) -> Result<TimeZone, Error> {
let mut result: Option<TimeZone> = 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
Expand All @@ -117,7 +188,7 @@ pub(crate) fn get(db: &TimeZoneDatabase) -> Result<TimeZone, Error> {
// 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)
}
Expand Down
Loading