Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Time.zone.at 10 times slower then Time.at

I have huge amount of data, and I have instantiate Time 30_000 times during 1 request. I've inspected performance and seen that querying data from db took 0.020 sec and instantiate Time object with

Time.zone.at(seconds_with_fraction)

took 0.5 second.

I've benchmarked difference between Time.zone.at and Time.at and got:

puts Benchmark.measure { 30_000.times { Time.zone.at(1439135459.6) } }
  0.510000   0.010000   0.520000 (  0.519298)

and

puts Benchmark.measure { 30_000.times { Time.at(1439135459.6) } }
  0.060000   0.000000   0.060000 (  0.068141)

Is there any way to reduce time that needs Time class to instantiate object with time in UTC zone?

like image 449
Aleksandr K. Avatar asked Sep 26 '22 17:09

Aleksandr K.


2 Answers

It isn't just the time zone change that is taking so long. If you just use Time.now you won't see the same slowdown:

irb(main):022:0> puts Benchmark.measure { 30_000.times { Time.now } }
  0.030000   0.000000   0.030000 (  0.028038)
=> nil
irb(main):023:0> puts Benchmark.measure { 30_000.times { Time.now.utc } }
  0.040000   0.010000   0.050000 (  0.076438)
=> nil

The best way to solve your problem is to define your Time with a different method than giving the number of seconds since unix epoch. However, if you have no way around that then a somewhat faster solution would be to use DateTime like so:

irb(main):038:0*  puts Benchmark.measure { 30_000.times { DateTime.strptime("1439135459.6",'%s') } }
  0.170000   0.030000   0.200000 (  0.217340)
=> nil
like image 75
Mike S Avatar answered Sep 30 '22 07:09

Mike S


TL;DR

You have a performance problem because of the way you are attempting to solve your timestamp issue. You would be well-served by handling your timestamp conversions in another way.

Your post doesn't explain where you're getting time zone information from. As a result, it's not clear why (or how) you are actually handling time zones. There are doubtless a number of ways to avoid doing time zone gymnastics, but they are outside the scope of your posted code. Nevertheless, the final example in this post shows one way to append the session time zone to each row in your query results.

In regards to your Ruby on Rails speed issue, you're not really instantiating Time, you're actually instantiating ActiveSupport::TimeZone from ActiveSupport::TimeZone::ThreadSafe, and both ActiveSupport and threadsafe libraries are bound to exact a performance penalty. You can't make Ruby instantiate objects faster, but you can avoid giving yourself headaches about it by using your database to perform the conversions instead.

In addition, while SQL-99 and PostgreSQL define timestamps with time zones as actual column types, neither MySQL nor MariaDB do. So, it's really unclear how you were planning to store that converted data, but it seems likely that you need to rethink your approach for both speed and data-type handling.

Regardless of what you're really trying to do, in almost every case the correct answer is to push more of the work out of Rails and into the database. Some MySQL/MariaDB examples follow below.

Database Timestamps from Epoch Seconds

You can simply pass the data as epoch-seconds into MySQL or MariaDB and have the database perform the conversion for you with the FROM_UNIXTIME function, rather than trying to do the conversion in Ruby. For example:

> SELECT FROM_UNIXTIME(1439135459.6);
+-----------------------------+
| FROM_UNIXTIME(1439135459.6) |
+-----------------------------+
| 2015-08-09 10:50:59.6       |
+-----------------------------+
1 row in set (0.00 sec)

It's up to you whether to store the epoch-seconds as numbers or as a timestamp, and thus whether to convert the data on storage or retrieval. The answer will likely depend more on how you plan to search and join on your data, so your mileage will vary.

Converting Time Zones

If you just want to use a single time zone and append it to your timestamp, that's trivial if you use the system or session time zones. For example:

> SELECT CONCAT(FROM_UNIXTIME(1439135459.6), " ", @@session.time_zone);
+---------------------------------------------------------------+
| CONCAT(FROM_UNIXTIME(1439135459.6), " ", @@session.time_zone) |
+---------------------------------------------------------------+
| 2015-08-09 10:50:59.6 -05:00                                  |
+---------------------------------------------------------------+
1 row in set (0.00 sec)

If you want to do more complex things, you need to first ensure that your time zone tables are populated if you plan to use zone names rather than numeric offsets. See mysql_tzinfo_to_sql for details.

Whether you use named zones or numeric offsets, call CONVERT_TZ on your arguments. For example:

> SELECT CONVERT_TZ('2004-01-01 12:00:00','+00:00','+10:00');
+-----------------------------------------------------+
| CONVERT_TZ('2004-01-01 12:00:00','+00:00','+10:00') |
+-----------------------------------------------------+
| 2004-01-01 22:00:00                                 |
+-----------------------------------------------------+
1 row in set (0.02 sec)

Putting It All Together

The following works on MariaDB, and uses MariaDB's handy sequence engine to avoid having to populate a sequence table or actually enter any values to make the example work. However, the principle (and the speed boost!) should be similar on MySQL, but you'll actually have to take the trouble to load the epoch-seconds or timestamp data into a table first.

-- Set a session time zone to concatenate with query results.
SET time_zone = '-5:00';

-- Load sequence engine on MariaDB.
-- Skip on MySQL, which does not currently have a sequence engine.
INSTALL SONAME "ha_sequence";

-- Select from the sequence on MariaDB.
-- Change query to SELECT...FROM a table on MySQL.
SELECT CONCAT(
    FROM_UNIXTIME(seq), @@session.time_zone
) FROM seq_1439135459_to_1439165458;

This returned a set as follows:

+---------------------------------------------------------------+
| CONCAT(         FROM_UNIXTIME(seq), @@session.time_zone     ) |
+---------------------------------------------------------------+
| 2015-08-09 10:50:59-05:00                                     |
| 2015-08-09 10:51:00-05:00                                     |
| 2015-08-09 10:51:01-05:00                                     |
| ...                                                           |
| 2015-08-09 19:10:57-05:00                                     |
| 2015-08-09 19:10:58-05:00                                     |
+---------------------------------------------------------------+
30000 rows in set (0.02 sec)

On my oldest, slowest laptop this returned 30,000 rows in 0.02 seconds or less. Server-class hardware should barely even notice the query, but of course your mileage may vary.

like image 25
Todd A. Jacobs Avatar answered Sep 30 '22 07:09

Todd A. Jacobs