DateTime-Lite
view release on metacpan or search on metacpan
mature, feature-complete, and battle-tested library, but to make the
trade-offs explicit so you can choose the right tool for your context.
Module footprint
The number of files loaded into %INC, directly and indirectly through
dependencies, when "use DateTime" or "use DateTime::Lite" is evaluated:
DateTime 1.66 DateTime::Lite
------- ------------- --------------
use Module 137 67
TimeZone class alone 105 47
Runtime prereqs (META) 23 11
"DateTime" depends on Specio, Params::ValidationCompiler,
namespace::autoclean, and several supporting modules that collectively
account for the extra overhead. "DateTime::Lite" replaces this
validation layer with lightweight hand-written checks and uses
DateTime::Locale::FromCLDR instead of the heavier DateTime::Locale
stack.
"DateTime::TimeZone" loads 105 modules because it ships one ".pm" file
per IANA zone, such as "DateTime::TimeZone::America::New_York", all
loaded on the first "new()" call. "DateTime::Lite::TimeZone" loads,
directly and indirectly, 47 modules and stores all zone data in a single
SQLite file instead.
Load time
Measured as "time()" around a cold "require" (modules not yet in %INC):
DateTime 1.66 DateTime::Lite
------- ------------- --------------
require Module 48 ms 32 ms
require TimeZone standalone 180 ms 100 ms
Startup time matters in short-lived scripts (cron jobs, CLI tools, CGI)
where the process initialisation is a significant fraction of total
runtime. For a long-running Apache2/mod_perl2, Plack, or Mojolicious
service, this cost is paid once and amortised over millions of requests.
Memory (RSS after loading)
Measured in a clean Perl process immediately after "use Module":
DateTime 1.66 DateTime::Lite
------- ------------- --------------
use Module (~28 MB) ~28 MB ~37 MB
TimeZone class only ~19 MB ~16 MB
The "use Module" row is somewhat misleading on its own: "DateTime::Lite"
loads "DBD::SQLite", which embeds a complete compiled SQLite engine (~14
MB of native code) regardless of how many timezone objects you create.
When measuring the "TimeZone" class in isolation, the component that
actually handles date arithmetic, "DateTime::Lite::TimeZone" is lighter
(~16 MB vs ~19 MB) because it does not pre-load all Olson zone data into
RAM.
"DateTime::TimeZone" pre-loads all IANA Olson definitions into memory on
the first "new()" call (roughly 3-4 MB of compiled Perl structures on
top of the module overhead). "DateTime::Lite::TimeZone" queries a
compact SQLite database on demand and keeps those structures on disk.
CPU throughput (10,000 iterations, µs per call)
DateTime 1.66 DateTime::Lite
------- ------------- --------------
new( UTC ) ~13 µs ~10 µs
new( named zone, string ) ~25 µs ~64 µs (*)
new( named zone, all caches enabled ) ~25 µs ~14 µs
now( UTC ) ~11 µs ~10 µs
year + month + day + epoch ~0.5 µs ~0.4 µs
clone + add( days + hours ) ~35 µs ~25 µs
strftime ~3.5 µs ~3.6 µs
TimeZone->new (warm, no mem cache) ~2 µs ~19 µs (*)
TimeZone->new (mem cache enabled) ~2 µs ~0.4 µs
Rows marked "(*)" reflect the default behaviour without the memory
cache. With "DateTime::Lite::TimeZone->enable_mem_cache" active,
"TimeZone-"new> drops to ~0.4 µs and "new(named zone)" drops to ~14 µs,
which is faster than "DateTime" (~25 µs). See "TimeZone caching model"
for the full explanation.
For UTC construction, "now()", accessors, arithmetic, and formatting,
"DateTime::Lite" is equivalent or faster. The XS-accelerated clone and
the lighter validation layer account for the gain in arithmetic.
TimeZone caching model
This is the single most important trade-off to understand.
DateTime::TimeZone loads the complete set of IANA time zone rules into
RAM the first time any named zone is constructed (~180 ms startup, ~4 MB
of in-memory hash structures). Every subsequent
"DateTime::TimeZone->new( name => $name )" call is served from that hash
in about 4 µs. If you construct thousands of "DateTime" objects per
second in a long-lived process, this model is very fast after the
initial warm-up.
DateTime::Lite::TimeZone stores the same IANA data in a compact SQLite
database ("tz.sqlite3", included in the distribution). The first call
for a given zone name runs a query (~22 ms) and populates a per-instance
cache; subsequent calls for the same zone use a cached "DBD::SQLite"
prepared statement and return in ~130 µs. There is no process-wide
singleton by default, so two calls with the same name each incur the 130
µs cost.
Optional memory cache: "DateTime::Lite::TimeZone" also provides an
opt-in process-level memory cache that matches or beats
"DateTime::TimeZone" on per-call speed:
# Enable once at application start-up:
DateTime::Lite::TimeZone->enable_mem_cache;
# Or per call:
my $tz = DateTime::Lite::TimeZone->new(
name => 'America/New_York',
use_cache_mem => 1,
);
With the memory cache active, repeated "new()" calls for the same zone
return the cached object from a plain hash lookup in about 0.8 µs:
DateTime::TimeZone DateTime::Lite::TimeZone
------ ----------------- ------------------------
Cold first call ~225 ms ~22 ms
Warm (no mem cache) ~2 µs ~19 µs
Warm (mem cache only) ~2 µs ~0.4 µs
Warm (mem+span+footer cache) ~2 µs ~0.4 µs
new(named zone, all caches) ~25 µs ~14 µs
Practical guidance:
* For long-lived services constructing datetime objects with named
zones, call "DateTime::Lite::TimeZone->enable_mem_cache" once at
startup. This activates three layers of caching:
1. the object cache (avoids SQLite construction);
2. the span cache (avoids the UTC offset query); and
3. the footer cache (avoids the POSIX DST rule calculation).
With all layers warm, "new(named zone)" costs ~14 µs, which is
faster than "DateTime" (~25 µs).
* If you prefer explicit control, pass "use_cache_mem => 1" on each
individual "new()" call, or construct one "TimeZone" object and
reuse it:
my $tz = DateTime::Lite::TimeZone->new( name => 'America/New_York' );
my $dt = DateTime::Lite->new( ..., time_zone => $tz );
* For batch processing (log parsing, ETL, report generation) where
timezone construction is a small fraction of total I/O time, the
difference is imperceptible regardless of which option you choose.
* For short-lived scripts and command-line tools, "DateTime::Lite"
wins on both startup time (~120 ms vs ~320 ms) and memory (~19 MB vs
~28 MB).
Running the benchmark
A self-contained benchmark script is included in the distribution:
cd DateTime-Lite-vX.X.X
perl Makefile.PL && make # make sure the XS code is compiled
perl -Iblib/lib -Iblib/arch scripts/benchmark.pl
# More iterations for stable numbers:
perl -Iblib/lib -Iblib/arch scripts/benchmark.pl --iterations 50000
# Machine-readable CSV output:
perl -Iblib/lib -Iblib/arch scripts/benchmark.pl --csv > results.csv
USAGE
0-based Versus 1-based Numbers
"DateTime::Lite" follows a simple rule for 0-based vs. 1-based numbers.
Month, day of month, day of week, and day of year are 1-based. Every
1-based method also has a "_0" variant. For example, "day_of_week"
returns 1 (Monday) through 7 (Sunday), while "day_of_week_0" returns 0
through 6.
All *time*-related values (hour, minute, second) are 0-based.
Years are neither, as they can be positive or negative. There is a year
0.
There is no "quarter_0" method.
Floating DateTimes
The default time zone for new "DateTime::Lite" objects (except where
stated otherwise) is the "floating" time zone. This concept comes from
the iCal standard. A floating datetime is not anchored to any particular
time zone and does not include leap seconds, since those require a real
time zone to apply.
Date math and comparison between a floating datetime and one with a real
time zone produce results of limited validity, because one includes leap
seconds and the other does not.
If you plan to use objects with a real time zone, it is strongly
recommended that you do not mix them with floating datetimes.
Determining the Local Time Zone Can Be Slow
If $ENV{TZ} is not set, looking up the local time zone may involve
reading several files in /etc. If you know the local time zone will not
change during your program's lifetime and you need many objects for that
zone, cache it once:
my $local_tz = DateTime::Lite::TimeZone->new( name => 'local' );
my $dt = DateTime::Lite->new( ..., time_zone => $local_tz );
"DateTime::Lite::TimeZone" also provides a process-level cache that
eliminates this cost entirely:
DateTime::Lite::TimeZone->enable_mem_cache;
my $dt = DateTime::Lite->new( ..., time_zone => 'local' );
Far Future DST
For dates very far in the future (thousands of years from now),
"DateTime" with named time zones can consume large amounts of memory
because "DateTime::TimeZone" pre-computes all DST transitions from the
present to that date.
"DateTime::Lite" is not affected by this problem.
"DateTime::Lite::TimeZone" uses a compact SQLite database and a POSIX
footer TZ string to derive the correct offset for any future date
without expanding the full transition table.
( run in 1.328 second using v1.01-cache-2.11-cpan-71847e10f99 )