I had tried this several weeks ago, but recent exploration in #373 prompted me to revisit it. Indeed, `cargo llvm-lines` reveals that Jiff is emitting a fair bit of code to LLVM just due to its error values. I don't know how much of this is inherent to Jiff's attention to good error messages or whether it's tied to how I've gone about it. With that said... This isn't a pareto distribution. It's not like Jiff's error handling is contributing to 80% of Jiff's LLVM lines or something. It's just the biggest chunk. But that chunk seems to be around 11% or so. This PR tries to reduce that chunk, mostly by being a little more careful about which error constructors are inlined and which aren't. Basically, we want the error *branch* to be inlined into the calling code (because that's likely a critical path), but we want everything within the branch to be a call to a function that is ideally not duplicated too much. I think there's a limit to how well we can do here, but this PR does seem to improve things *without* runtime regressions. Here's the first ~30% of where Jiff is emitting LLVM lines (as run from the root of Jiff's repository) on current `master`: ``` Lines Copies Function name ----- ------ ------------- 203564 5977 (TOTAL) 10182 (5.0%, 5.0%) 86 (1.4%, 1.4%) <jiff::error::Error as jiff::error::ErrorContext>::with_context 5688 (2.8%, 7.8%) 204 (3.4%, 4.9%) core::result::Result<T,E>::map_err 3801 (1.9%, 9.7%) 145 (2.4%, 7.3%) <core::result::Result<T,E> as core::ops::try_trait::Try>::branch 2837 (1.4%, 11.1%) 116 (1.9%, 9.2%) core::option::Option<T>::ok_or_else 2120 (1.0%, 12.1%) 2 (0.0%, 9.3%) jiff::fmt::friendly:🖨️:SpanPrinter::print_duration_designators 1822 (0.9%, 13.0%) 2 (0.0%, 9.3%) jiff::fmt::temporal:🖨️:SpanPrinter::print_span 1658 (0.8%, 13.8%) 1 (0.0%, 9.3%) jiff::fmt::strtime::parse::Parser::parse 1652 (0.8%, 14.6%) 1 (0.0%, 9.3%) jiff::fmt::strtime::format::Formatter<W,L>::format_one 1548 (0.8%, 15.4%) 6 (0.1%, 9.4%) jiff::fmt::temporal:🖨️:DateTimePrinter::print_time 1312 (0.6%, 16.0%) 110 (1.8%, 11.3%) <core::result::Result<T,F> as core::ops::try_trait::FromResidual<core::result::Result<core::convert::Infallible,E>>>::from_residual 1260 (0.6%, 16.6%) 6 (0.1%, 11.4%) jiff::fmt::temporal:🖨️:DateTimePrinter::print_date 1219 (0.6%, 17.2%) 47 (0.8%, 12.1%) core::option::Option<T>::map 1160 (0.6%, 17.8%) 22 (0.4%, 12.5%) core::option::Option<T>::map_or 1071 (0.5%, 18.3%) 28 (0.5%, 13.0%) core::result::Result<T,E>::unwrap 997 (0.5%, 18.8%) 21 (0.4%, 13.3%) core::option::Option<T>::or_else 986 (0.5%, 19.3%) 1 (0.0%, 13.4%) jiff::span::Span::from_invariant_nanoseconds 976 (0.5%, 19.8%) 16 (0.3%, 13.6%) <jiff::util::rangeint::RangedDebug<_,_> as core::fmt::Debug>::fmt 920 (0.5%, 20.2%) 10 (0.2%, 13.8%) <jiff::util::rangeint::ri32<_,_> as jiff::util::rangeint::RFrom<jiff::util::rangeint::ri64<_,_>>>::rfrom 920 (0.5%, 20.7%) 4 (0.1%, 13.9%) jiff::fmt::temporal:🖨️:DateTimePrinter::print_offset_rounded 912 (0.4%, 21.1%) 6 (0.1%, 14.0%) jiff::fmt::friendly:🖨️:DesignatorWriter<W>::write 890 (0.4%, 21.6%) 2 (0.0%, 14.0%) jiff::fmt::temporal:🖨️:SpanPrinter::print_duration 840 (0.4%, 22.0%) 12 (0.2%, 14.2%) jiff::util::rangeint::ri8<_,_>::get 828 (0.4%, 22.4%) 6 (0.1%, 14.3%) core::slice::<impl [T]>::binary_search_by 819 (0.4%, 22.8%) 32 (0.5%, 14.8%) core::result::Result<T,E>::unwrap_or_else 802 (0.4%, 23.2%) 2 (0.0%, 14.9%) jiff::fmt::friendly:🖨️:SpanPrinter::print_span_hms 777 (0.4%, 23.6%) 10 (0.2%, 15.0%) jiff::util::rangeint::Composite<T>::map 758 (0.4%, 24.0%) 2 (0.0%, 15.1%) jiff::fmt::friendly:🖨️:SpanPrinter::print_duration_hms 744 (0.4%, 24.3%) 8 (0.1%, 15.2%) core::array::try_from_fn_erased 744 (0.4%, 24.7%) 2 (0.0%, 15.2%) jiff::fmt::temporal:🖨️:DateTimePrinter::print_pieces 720 (0.4%, 25.0%) 2 (0.0%, 15.3%) jiff::fmt::friendly:🖨️:SpanPrinter::print_span_designators_non_fraction 700 (0.3%, 25.4%) 10 (0.2%, 15.4%) <core::iter::adapters::zip::Zip<A,B> as core::iter::adapters::zip::ZipImpl<A,B>>::next 700 (0.3%, 25.7%) 7 (0.1%, 15.5%) <jiff::util::rangeint::ri8<_,_> as jiff::util::rangeint::RFrom<jiff::util::rangeint::ri64<_,_>>>::rfrom 682 (0.3%, 26.1%) 6 (0.1%, 15.6%) jiff::fmt::strtime::format::<impl jiff::fmt::strtime::Extension>::write_int 660 (0.3%, 26.4%) 10 (0.2%, 15.8%) <core::iter::adapters::enumerate::Enumerate<I> as core::iter::traits::iterator::Iterator>::next 659 (0.3%, 26.7%) 1 (0.0%, 15.8%) jiff::fmt::rfc2822::DateTimePrinter::print_civil_with_offset 600 (0.3%, 27.0%) 6 (0.1%, 15.9%) <jiff::util::rangeint::ri8<_,_> as jiff::util::rangeint::RFrom<jiff::util::rangeint::ri32<_,_>>>::rfrom 588 (0.3%, 27.3%) 12 (0.2%, 16.1%) jiff::util::rangeint::ri8<_,_>::try_new 588 (0.3%, 27.6%) 1 (0.0%, 16.1%) jiff::tz::db::zoneinfo::inner::walk 584 (0.3%, 27.9%) 86 (1.4%, 17.6%) <core::result::Result<T,jiff::error::Error> as jiff::error::ErrorContext>::with_context::{{closure}} 583 (0.3%, 28.2%) 1 (0.0%, 17.6%) jiff::civil:📅:DateDifference::since_with_largest_unit 581 (0.3%, 28.4%) 11 (0.2%, 17.8%) jiff::error::RangeError::new 574 (0.3%, 28.7%) 2 (0.0%, 17.8%) core::slice::sort::stable::quicksort::stable_partition 552 (0.3%, 29.0%) 2 (0.0%, 17.9%) core::str::pattern::TwoWaySearcher::next_back 545 (0.3%, 29.3%) 97 (1.6%, 19.5%) <T as jiff::util::rangeint::RInto<U>>::rinto 544 (0.3%, 29.5%) 2 (0.0%, 19.5%) jiff::fmt::strtime::format::write_offset 536 (0.3%, 29.8%) 4 (0.1%, 19.6%) jiff::fmt::temporal:🖨️:DateTimePrinter::print_datetime ``` Other than error handling, it doesn't look like there is any one thing that is contributing the most here. That means that I think real improvements here are probably going to require combing through these and ensuring that the number of copies of each function is as small as it can be. This PR does decrease the number of LLVM lines, but only marginally: ``` Lines Copies Function name ----- ------ ------------- 192748 2457 (TOTAL) 3978 (2.1%, 2.1%) 1 (0.0%, 0.0%) jiff::fmt::strtime::format::Formatter<W,L>::format_one 3785 (2.0%, 4.0%) 1 (0.0%, 0.1%) jiff::fmt::strtime::parse::Parser::parse 3600 (1.9%, 5.9%) 2 (0.1%, 0.2%) jiff::fmt::temporal:🖨️:SpanPrinter::print_span 3340 (1.7%, 7.6%) 2 (0.1%, 0.2%) jiff::fmt::friendly:🖨️:SpanPrinter::print_duration_designators 3250 (1.7%, 9.3%) 86 (3.5%, 3.7%) <core::result::Result<T,jiff::error::Error> as jiff::error::ErrorContext>::with_context::{{closure}} 2910 (1.5%, 10.8%) 6 (0.2%, 4.0%) jiff::fmt::temporal:🖨️:DateTimePrinter::print_time 2190 (1.1%, 12.0%) 1 (0.0%, 4.0%) jiff::span::Span::from_invariant_nanoseconds 2166 (1.1%, 13.1%) 6 (0.2%, 4.3%) jiff::fmt::temporal:🖨️:DateTimePrinter::print_date 1760 (0.9%, 14.0%) 2 (0.1%, 4.4%) jiff::fmt::temporal:🖨️:SpanPrinter::print_duration 1588 (0.8%, 14.8%) 2 (0.1%, 4.4%) jiff::fmt::friendly:🖨️:SpanPrinter::print_span_hms 1450 (0.8%, 15.6%) 2 (0.1%, 4.5%) jiff::fmt::friendly:🖨️:SpanPrinter::print_span_designators_non_fraction 1432 (0.7%, 16.3%) 4 (0.2%, 4.7%) jiff::fmt::temporal:🖨️:DateTimePrinter::print_offset_rounded 1398 (0.7%, 17.0%) 2 (0.1%, 4.8%) jiff::fmt::temporal:🖨️:DateTimePrinter::print_pieces 1368 (0.7%, 17.8%) 6 (0.2%, 5.0%) jiff::fmt::friendly:🖨️:DesignatorWriter<W>::write 1364 (0.7%, 18.5%) 2 (0.1%, 5.1%) jiff::fmt::friendly:🖨️:SpanPrinter::print_duration_hms 1248 (0.6%, 19.1%) 16 (0.7%, 5.7%) <jiff::util::rangeint::RangedDebug<_,_> as core::fmt::Debug>::fmt 1243 (0.6%, 19.8%) 1 (0.0%, 5.8%) jiff::fmt::rfc2822::DateTimePrinter::print_civil_with_offset 1222 (0.6%, 20.4%) 13 (0.5%, 6.3%) jiff::util::rangeint::ri8<_,_>::new 1108 (0.6%, 21.0%) 28 (1.1%, 7.4%) core::result::Result<T,E>::unwrap 1097 (0.6%, 21.5%) 1 (0.0%, 7.5%) jiff::shared::tzif::Header::parse 1086 (0.6%, 22.1%) 22 (0.9%, 8.4%) core::option::Option<T>::map_or 975 (0.5%, 22.6%) 21 (0.9%, 9.2%) core::option::Option<T>::or_else 970 (0.5%, 23.1%) 1 (0.0%, 9.3%) jiff::fmt::rfc2822::DateTimePrinter::print_civil_always_utc 923 (0.5%, 23.6%) 1 (0.0%, 9.3%) jiff::fmt::offset::Parser::parse_numeric 919 (0.5%, 24.1%) 1 (0.0%, 9.4%) jiff::zoned::ZonedDifference::until_with_largest_unit 884 (0.5%, 24.5%) 2 (0.1%, 9.4%) jiff::tz::concatenated::Header::read 842 (0.4%, 25.0%) 2 (0.1%, 9.5%) jiff::fmt::strtime::format::write_offset 822 (0.4%, 25.4%) 6 (0.2%, 9.8%) core::slice::<impl [T]>::binary_search_by 816 (0.4%, 25.8%) 6 (0.2%, 10.0%) jiff::fmt::strtime::format::<impl jiff::fmt::strtime::Extension>::write_int 796 (0.4%, 26.2%) 1 (0.0%, 10.1%) jiff::shared::tzif::<impl jiff::shared::Tzif<alloc::string::String,jiff::shared::util::array_str::ArrayStr<30_usize>,alloc::vec::Vec<jiff::shared::TzifLocalTimeType>,alloc::vec::Vec<i64>,alloc::vec::Vec<jiff::shared::TzifDateTime>,alloc::vec::Vec<jiff::shared::TzifDateTime>,alloc::vec::Vec<jiff::shared::TzifTransitionInfo>>>::parse64 792 (0.4%, 26.6%) 22 (0.9%, 10.9%) <core::result::Result<T,jiff::error::Error> as jiff::error::ErrorContext>::context::{{closure}} 784 (0.4%, 27.0%) 4 (0.2%, 11.1%) jiff::fmt::temporal:🖨️:DateTimePrinter::print_datetime 773 (0.4%, 27.4%) 2 (0.1%, 11.2%) jiff::tz::offset::OffsetConflict::resolve_via_reject 758 (0.4%, 27.8%) 2 (0.1%, 11.3%) jiff::shared::posix::<impl jiff::shared::PosixTimeZone<ABBREV>>::to_ambiguous_kind 736 (0.4%, 28.2%) 4 (0.2%, 11.4%) jiff::fmt::friendly:🖨️:FractionalPrinter::print 687 (0.4%, 28.6%) 1 (0.0%, 11.5%) jiff::tz::db::zoneinfo::inner::walk 680 (0.4%, 28.9%) 1 (0.0%, 11.5%) jiff::shared::tzif::<impl jiff::shared::Tzif<alloc::string::String,jiff::shared::util::array_str::ArrayStr<30_usize>,alloc::vec::Vec<jiff::shared::TzifLocalTimeType>,alloc::vec::Vec<i64>,alloc::vec::Vec<jiff::shared::TzifDateTime>,alloc::vec::Vec<jiff::shared::TzifDateTime>,alloc::vec::Vec<jiff::shared::TzifTransitionInfo>>>::parse_time_zone_designations 654 (0.3%, 29.3%) 2 (0.1%, 11.6%) jiff::shared::posix::<impl jiff::shared::PosixTimeZone<ABBREV>>::previous_transition 652 (0.3%, 29.6%) 2 (0.1%, 11.7%) jiff::shared::posix::<impl jiff::shared::PosixTimeZone<ABBREV>>::next_transition 650 (0.3%, 29.9%) 1 (0.0%, 11.7%) jiff::tz::system::get_env_tz ``` I wonder if my approach to error handling with just using strings everywhere is hurting things. The main alternative would be structured errors everywhere. But there are _so many_ error messages that writing out structured definitions for each is terrifying to me (even if I was willing to use something like `thiserror` to reduce the boiler plate aspect of it). In terms of actual compile times, this _does_ seem to have a positive impact on compile times. My test setup here was to: 1. Add `[patch.crates-io]` with a `jiff` entry to [Biff's](https://github.com/BurntSushi/biff) `Cargo.toml`. 2. Run `touch path/to/jiff/src/lib.rs && touch src/main.rs && time cargo build --release` from the root of Biff's repository. The timings I get are wildly variable. But the minimum time I got with this PR was 5.8s while the minimum time I've seen for Jiff master is 6.3s. This is on an otherwise quiet machine with the CPU governor set to `performance`. Finally, running the benchmarks has some noise, but there are no obvious regressions: ``` $ critcmp base zchange3 -f '/jiff$' -t 5 group base zchange3 ----- ---- -------- civil_datetime/to_timestamp_tzdb_lookup/zoneinfo/jiff 1.00 43.5±0.27ns ? ?/sec 1.24 54.1±0.23ns ? ?/sec date/add_days/one/duration/jiff 1.00 5.2±0.05ns ? ?/sec 1.08 5.6±0.04ns ? ?/sec date/add_days/one/span/jiff 1.00 9.5±0.09ns ? ?/sec 1.06 10.0±0.08ns ? ?/sec date/add_years_months_days/jiff 1.08 24.7±0.20ns ? ?/sec 1.00 22.8±0.16ns ? ?/sec parse/friendly/long/span/jiff 1.06 122.8±0.18ns ? ?/sec 1.00 115.9±0.27ns ? ?/sec parse/friendly/longer/span/jiff 1.05 124.2±0.29ns ? ?/sec 1.00 118.0±0.79ns ? ?/sec parse/friendly/medium/span/jiff 1.05 67.8±0.22ns ? ?/sec 1.00 64.5±0.26ns ? ?/sec parse/friendly/short/duration/jiff 1.17 16.6±0.28ns ? ?/sec 1.00 14.2±0.34ns ? ?/sec parse/rfc2822/jiff 1.25 29.4±0.31ns ? ?/sec 1.00 23.5±0.26ns ? ?/sec parse/strptime/oneshot/jiff 1.11 74.3±0.63ns ? ?/sec 1.00 67.0±0.48ns ? ?/sec timestamp/every_hour_in_week/series/jiff 1.33 143.8±0.70ns ? ?/sec 1.00 107.7±0.61ns ? ?/sec tz/posix_datetime_to_offset/jiff 1.00 31.0±0.24ns ? ?/sec 1.05 32.5±0.20ns ? ?/sec zoned/fixed_offset_to_timestamp/jiff 1.25 0.4±0.00ns ? ?/sec 1.00 0.3±0.00ns ? ?/sec ``` Given the reduction in LLVM times, the seeming reduction in compile times and the lack of obvious runtime regressions, I think this PR is a strict improvement. Ref #373 |
||
|---|---|---|
| .devcontainer | ||
| .github | ||
| .vim | ||
| bench | ||
| crates | ||
| examples | ||
| scripts | ||
| src | ||
| testprograms/invalid-tz-environment-variable | ||
| tests | ||
| .gitignore | ||
| .rgignore | ||
| Cargo.toml | ||
| CHANGELOG.md | ||
| COMPARE.md | ||
| COPYING | ||
| Cross.toml | ||
| DESIGN.md | ||
| LICENSE-MIT | ||
| PLATFORM.md | ||
| README.md | ||
| rustfmt.toml | ||
| UNLICENSE | ||
Jiff
Jiff is a datetime library for Rust that encourages you to jump into the pit of success. The focus of this library is providing high level datetime primitives that are difficult to misuse and have reasonable performance. Jiff supports automatic and seamless integration with the Time Zone Database, DST aware arithmetic and rounding, formatting and parsing zone aware datetimes losslessly, opt-in Serde support and a whole lot more.
Jiff takes enormous inspiration from Temporal, which is a TC39 proposal to improve datetime handling in JavaScript.
Dual-licensed under MIT or the UNLICENSE.
Documentation
- API documentation on docs.rs
- Comparison with
chrono,time,hifitimeandicu - The API design rationale for Jiff
- Platform support
- CHANGELOG
Example
Here is a quick example that shows how to parse a typical RFC 3339 instant, convert it to a zone aware datetime, add a span of time and losslessly print it:
use jiff::{Timestamp, ToSpan};
fn main() -> Result<(), jiff::Error> {
let time: Timestamp = "2024-07-11T01:14:00Z".parse()?;
let zoned = time.in_tz("America/New_York")?.checked_add(1.month().hours(2))?;
assert_eq!(zoned.to_string(), "2024-08-10T23:14:00-04:00[America/New_York]");
// Or, if you want an RFC3339 formatted string:
assert_eq!(zoned.timestamp().to_string(), "2024-08-11T03:14:00Z");
Ok(())
}
There are many more examples in the documentation.
Usage
Jiff is on crates.io and can be
used by adding jiff to your dependencies in your project's Cargo.toml.
Or more simply, just run cargo add jiff.
Here is a complete example that creates a new Rust project, adds a dependency
on jiff, creates the source code for a simple datetime program and then runs
it.
First, create the project in a new directory:
$ cargo new jiff-example
$ cd jiff-example
Second, add a dependency on jiff:
$ cargo add jiff
Third, edit src/main.rs. Delete what's there and replace it with this:
use jiff::{Unit, Zoned};
fn main() -> Result<(), jiff::Error> {
let now = Zoned::now().round(Unit::Second)?;
println!("{now}");
Ok(())
}
Fourth, run it with cargo run:
$ cargo run
Compiling jiff v0.2.0 (/home/andrew/rust/jiff)
Compiling jiff-play v0.2.0 (/home/andrew/tmp/scratch/rust/jiff-play)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.37s
Running `target/debug/jiff-play`
2024-07-10T19:54:20-04:00[America/New_York]
The first time you run the program will show more output like above. But subsequent runs shouldn't have to re-compile the dependencies.
Crate features
Jiff has several crate features for customizing support for Rust's standard
library, serde support and whether to embed a copy of the Time Zone Database
into your binary.
The "crate features" section of the documentation lists the full set of supported features.
Future plans
With jiff 0.2 out about 6 months after the jiff 0.1 initial release, my
plan remains roughly the same as it started. That is, I'd still like to get a
jiff 1.0 release out this summer 2025 (in about 6 months) and then commit to
it indefinitely. This plan may change if something critically wrong is found
with the current API.
The purpose of this plan is to get Jiff to a 1.0 stable state as quickly as possible. The reason is so that others feel comfortable relying on Jiff as a public dependency that won't cause ecosystem churn.
Performance
The most important design goal of Jiff is to be a high level datetime library
that makes it hard to do the wrong thing. Second to that is performance. Jiff
should have reasonable performance, but there are likely areas in which it
could improve. See the bench directory for benchmarks.
Note that performance is still an important goal. Some aspects of Jiff have had optimization attention paid to them, but many still have not. It is a goal to improve where we can, but performance will generally come second to API comprehension and correctness.
Platform support
The question of platform support in the context of datetime libraries comes up primarily in relation to time zone support. Specifically:
- How should Jiff determine the time zone transitions for an IANA time zone
identifier like
Antarctica/Troll? - How should Jiff determine the default time zone for the current system?
Both of these require some level of platform interaction.
For discovering time zone transition data, Jiff relies on the
IANA Time Zone Database. On Unix systems, this is usually found at
/usr/share/zoneinfo, although it can be configured via the TZDIR
environment variable (which Jiff respects). On Windows, Jiff will automatically
embed a copy of the time zone database into the compiled library.
For discovering the system time zone, Jiff reads /etc/localtime on Unix. On
Windows, Jiff reads the Windows-specific time zone identifier via
GetDynamicTimeZoneInformation and then maps it to an IANA time zone
identifier via Unicode's CLDR XML data.
I expect Jiff to grow more support for other platforms over time. Please file issues, although I will likely be reliant on contributor pull requests for more obscure platforms that aren't easy for me to test.
For more on platform support, see PLATFORM.md.
Dependencies
At time of writing, it is no accident that Jiff has zero dependencies on Unix. In general, my philosophy on adding new dependencies in an ecosystem crate like Jiff is very conservative. I consider there to be two primary use cases for adding new dependencies:
- When a dependency is practically required in order to interact with a
platform. For example,
windows-sysfor discovering the system time zone on Windows. - When a dependency is necessary for inter-operability. For example,
serde. But even here, I expect to be conservative, where I'm generally only willing to depend on things that have fewer breaking change releases than Jiff.
A secondary use case for new dependencies is if Jiff gets split into multiple
crates. I did a similar thing for the regex crate for very compelling
reasons. It is possible that will happen with Jiff as well, although there are
no plans for that. And in general, I expect the number of crates to stay small,
if only to make keep maintenance lightweight. (Managing lots of semver API
boundaries has a lot of overhead in my experience.)
Minimum Rust version policy
This crate's minimum supported rustc version is 1.70.0.
The policy is that the minimum Rust version required to use this crate can be
increased in minor version updates. For example, if jiff 1.0 requires Rust
1.20.0, then jiff 1.0.z for all values of z will also require Rust 1.20.0 or
newer. However, jiff 1.y for y > 0 may require a newer minimum version of
Rust.