Skip to content
Open
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: 1 addition & 1 deletion .formatter.exs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
inputs: ["{mix,.formatter,benchmark}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
91 changes: 70 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,19 @@ This repository allows to perform time zone operations on a huge set of predefin

## Generate the Java result set

Generate the result for Java by executing `GenerateTzData.java` in `/java`.
Generate the result for Java by executing `java java/GenerateTzData.java`.

The result is written in `/files/output`.

## Generate the Elixir libraries result set

Execute the mix task to generate the result for the Elixir libraries:
Execute the mix task to generate the result for the Elixir libraries (you can change the second argument to "files/input_far_future" if you want to switch the dataset):

```bash
mix tzdb.run tz
mix tzdb.run time_zone_info
mix tzdb.run zoneinfo
mix tzdb.run tzdata
mix tzdb.run tz "files/input"
mix tzdb.run time_zone_info "files/input"
mix tzdb.run zoneinfo "files/input"
mix tzdb.run tzdata "files/input"
```

The result is written in `/files/output`.
Expand All @@ -44,21 +44,70 @@ At the time of writing this,

### Performance

Time spent generating the result is logged in the console, giving some idea of the difference in terms of performance.

The time taken for each library to generate the output on my system are:
* ~50 seconds for `tz`
* ~50 seconds for `time_zone_info`
* ~80 seconds for `zoneinfo`
* ~160 seconds for `tzdata`

System used:
* Operating system: macOS
* CPU: Apple M2
* Available cores: 8
* Available memory: 16 GB
* Elixir version: 1.14.1
* Erlang version: 25.1.2
The results from `mix run benchmark.exs` (removed Logging output):

```
Generated tzdb_test app
Operating System: macOS
CPU Information: Apple M3 Pro
Number of Available Cores: 11
Available memory: 36 GB
Elixir 1.17.2
Erlang 27.0.1
JIT enabled: true

Benchmark suite executing with the following configuration:
warmup: 2 s
time: 5 s
memory time: 0 ns
reduction time: 0 ns
parallel: 1
inputs: far future, standard
Estimated total run time: 1 min 10 s

Benchmarking java with input far future ...
Benchmarking java with input standard ...
Benchmarking time_zone_info with input far future ...
Benchmarking time_zone_info with input standard ...
Benchmarking tz with input far future ...
Benchmarking tz with input standard ...
Benchmarking tzdata with input far future ...
Benchmarking tzdata with input standard ...
Benchmarking zoneinfo with input far future ...
Benchmarking zoneinfo with input standard ...
Calculating statistics...
Formatting results...

##### With input far future #####
Name ips average deviation median 99th %
java 0.42 2.40 s ±1.91% 2.39 s 2.45 s
time_zone_info 0.0695 14.39 s ±0.00% 14.39 s 14.39 s
tz 0.0259 38.61 s ±0.00% 38.61 s 38.61 s
zoneinfo 0.0252 39.71 s ±0.00% 39.71 s 39.71 s
tzdata 0.0220 45.41 s ±0.00% 45.41 s 45.41 s

Comparison:
java 0.42
time_zone_info 0.0695 - 6.00x slower +11.99 s
tz 0.0259 - 16.10x slower +36.21 s
zoneinfo 0.0252 - 16.56x slower +37.31 s
tzdata 0.0220 - 18.94x slower +43.02 s

##### With input standard #####
Name ips average deviation median 99th %
java 0.183 5.47 s ±0.00% 5.47 s 5.47 s
zoneinfo 0.0416 24.04 s ±0.00% 24.04 s 24.04 s
tz 0.0408 24.51 s ±0.00% 24.51 s 24.51 s
time_zone_info 0.0405 24.67 s ±0.00% 24.67 s 24.67 s
tzdata 0.00771 129.72 s ±0.00% 129.72 s 129.72 s

Comparison:
java 0.183
zoneinfo 0.0416 - 4.40x slower +18.57 s
tz 0.0408 - 4.48x slower +19.04 s
time_zone_info 0.0405 - 4.51x slower +19.20 s
tzdata 0.00771 - 23.72x slower +124.25 s
```

## How does it work?

Expand Down
62 changes: 62 additions & 0 deletions benchmark.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
benchmarks = %{
"time_zone_info" => {
fn input ->
Benchmark.run(:time_zone_info, TimeZoneInfo.iana_version(), input)
end,
before_scenario: fn input ->
Benchmark.start!(:time_zone_info)
input
end,
after_scenario: fn _ ->
Benchmark.stop!(:time_zone_info)
end
},
"tz" => {
fn input -> Benchmark.run(:tz, Tz.iana_version(), input) end,
before_scenario: fn input ->
Benchmark.start!(:tz)
input
end,
after_scenario: fn _ ->
Benchmark.stop!(:tz)
end
},
"zoneinfo" => {
fn input -> Benchmark.run(:tz, Benchmark.zoneifo_version(), input) end,
before_scenario: fn input ->
Benchmark.start!(:zoneinfo)
input
end,
after_scenario: fn _ ->
Benchmark.stop!(:zoneinfo)
end
},
"tzdata" => {
fn input -> Benchmark.run(:tzdata, Tzdata.tzdata_version(), input) end,
before_scenario: fn input ->
Benchmark.start!(:tzdata)
input
end,
after_scenario: fn _ ->
Benchmark.stop!(:tzdata)
end
}
}

benchmarks =
if System.find_executable("java") do
Map.merge(benchmarks, %{
"java" => fn input ->
System.cmd("java", ["java/GenerateTzData.java", input])
end
})
else
benchmarks
end

Benchee.run(benchmarks,
inputs: %{
"standard" => "files/input",
"far future" => "files/input_far_future"
}
)
2 changes: 2 additions & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import Config
config :tzdata, :autoupdate, :disabled
6 changes: 5 additions & 1 deletion java/GenerateTzData.java
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ private static String buildEntry(String timezone, LocalDateTime localDateTime) {
}

public static void main(String[] args) throws IOException {
if (args.length < 1) {
throw new IllegalArgumentException("Missing command line argument: input directory path.");
}
String ianaTzVersion = java.time.zone.ZoneRulesProvider
.getVersions("UTC")
.lastEntry()
Expand All @@ -75,7 +78,8 @@ public static void main(String[] args) throws IOException {
System.out.println("IANA tz version: " + ianaTzVersion);

String basePath = new File("").getAbsolutePath();
String inputDir = Paths.get(basePath, "files/input/").toString();
String input = args[0];
String inputDir = Paths.get(basePath, input).toString();

String outputDir = Files.createDirectories(Paths.get(basePath, "files/output/", ianaTzVersion, "java")).toString();

Expand Down
133 changes: 133 additions & 0 deletions lib/benchmark.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
defmodule Benchmark do
# @input "files/input"
# @input "files/input_far_future"
@with_zone_abbr true

def run(lib, version, input) do
module = lib |> Atom.to_string() |> Macro.camelize()
time_zone_db = Module.concat(module, TimeZoneDatabase)

generate_files(
time_zone_db,
version,
input,
Path.join([File.cwd!(), "files/output", version, to_string(lib)])
)
end

def start!(lib) do
{:ok, _} = Application.ensure_all_started(lib)
:ok
end

def stop!(lib) do
:ok = Application.stop(lib)
:ok
end

def zoneifo_version() do
{:ok, version} = File.read("/usr/share/zoneinfo/+VERSION")
String.trim(version)
end

defp date_time_to_string(dt) do
dt_string = Calendar.strftime(dt, "%c")
dt_string <> offset_to_string(dt.utc_offset + dt.std_offset)
end

defp offset_to_string(seconds) do
is_negative = if(seconds < 0, do: true, else: false)
seconds = if(is_negative, do: -1 * seconds, else: seconds)

string =
seconds
|> :calendar.seconds_to_time()
|> do_offset_to_string()
|> List.to_string()

if(is_negative, do: "-" <> string, else: "+" <> string)
end

defp do_offset_to_string({h, m, 0}), do: :io_lib.format("~2..0B:~2..0B", [h, m])
defp do_offset_to_string({h, m, s}), do: :io_lib.format("~2..0B:~2..0B:~2..0B", [h, m, s])

defp generate_files(time_zone_database, version, input, outputDir) do
inputDir = Path.join([File.cwd!(), input])

File.rm_rf!(outputDir)
File.mkdir_p!(outputDir)

File.ls!(inputDir)
|> Enum.each(fn filename ->
File.write!(Path.join(outputDir, filename), version <> "\n")

File.stream!(Path.join(inputDir, filename))
|> Stream.map(fn line ->
[timezone, date] = String.split(line, ";", trim: true)

timezone = String.trim(timezone)
date = String.trim(date)

{:ok, date} = Date.from_iso8601(date)
{:ok, time} = Time.new(0, 0, 0)

write_data(timezone, date, time, time_zone_database)
end)
|> Stream.into(File.stream!(Path.join(outputDir, filename), [:append]))
|> Stream.run()
end)
end

defp write_data(timezone, date, time, time_zone_database) do
{:ok, naive_date_time} =
NaiveDateTime.new(date.year, date.month, date.day, time.hour, time.minute, 0)

output = [timezone, NaiveDateTime.to_iso8601(naive_date_time)]

output =
output ++
try do
case DateTime.from_naive(naive_date_time, timezone, time_zone_database) do
{:ambiguous, dt1, dt2} ->
["ambiguous", date_time_to_string(dt1)] ++
if(@with_zone_abbr, do: [dt1.zone_abbr], else: []) ++
[date_time_to_string(dt2)] ++
if(@with_zone_abbr, do: [dt2.zone_abbr], else: [])

{:gap, dt1, dt2} ->
["gap", date_time_to_string(dt1)] ++
if(@with_zone_abbr, do: [dt1.zone_abbr], else: []) ++
[date_time_to_string(dt2)] ++
if(@with_zone_abbr, do: [dt2.zone_abbr], else: [])

{:ok, dt} ->
["ok", date_time_to_string(dt)] ++
if(@with_zone_abbr, do: [dt.zone_abbr], else: [])
end
rescue
error -> ["error", error.__struct__]
end

output =
output ++
try do
{:ok, dt} =
DateTime.from_naive!(naive_date_time, "Etc/UTC", time_zone_database)
|> DateTime.shift_zone(timezone, time_zone_database)

[date_time_to_string(dt)] ++
if(@with_zone_abbr, do: [dt.zone_abbr], else: [])
rescue
error -> ["error", error.__struct__]
end

output = Enum.join(output, ";") <> "\n"

if time.hour != 23 || time.minute != 45 do
time = Time.add(time, 900)
[output | write_data(timezone, date, time, time_zone_database)]
else
[output]
end
end
end
Loading