Skip to content

Commit 20d13a2

Browse files
authored
Merge pull request #759 from czj/perf/text-timestamp-without-timegm
Decode text timestamps ~40x faster on macOS, ~15x on Windows and ~1.4x on Linux, without timegm()
2 parents 4ce839e + 048a742 commit 20d13a2

3 files changed

Lines changed: 70 additions & 51 deletions

File tree

‎ext/extconf.rb‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,6 @@ module PG
309309
"or upgrade your database to at least PostgreSQL-10."
310310
# optional headers/functions
311311
have_func 'PQresultMemorySize', 'libpq-fe.h' # since PostgreSQL-12
312-
have_func 'timegm'
313312
have_func 'rb_hash_new_capa' # since ruby-3.2
314313

315314
have_header 'inttypes.h'

‎ext/pg_text_decoder.c‎

Lines changed: 56 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
*
2929
*/
3030

31-
#include "ruby/version.h"
3231
#include "pg.h"
3332
#include "pg_util.h"
3433
#ifdef HAVE_INTTYPES_H
@@ -621,6 +620,25 @@ static int parse_year(const char **str) {
621620
return year;
622621
}
623622

623+
/*
624+
* Seconds since 1970-01-01 00:00:00 UTC in the proleptic Gregorian calendar, like timegm() but much faster on macOS.
625+
* Days are counted per http://howardhinnant.github.io/date_algorithms.html#days_from_civil
626+
*/
627+
static int64_t seconds_since_epoch(int year, int mon, int day, int hour, int min, int sec)
628+
{
629+
int64_t era, days;
630+
int year_of_era, day_of_year, day_of_era;
631+
632+
year -= mon <= 2;
633+
era = (year >= 0 ? year : year - 399) / 400;
634+
year_of_era = (int)(year - era * 400);
635+
day_of_year = (153 * (mon > 2 ? mon - 3 : mon + 9) + 2) / 5 + day - 1;
636+
day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
637+
days = era * 146097 + day_of_era - 719468;
638+
639+
return days * 86400 + hour * 3600 + min * 60 + sec;
640+
}
641+
624642
#define TZ_NEG 1
625643
#define TZ_POS 2
626644

@@ -708,71 +726,59 @@ static VALUE pg_text_dec_timestamp(t_pg_coder *conv, const char *val, int len, i
708726
VALUE sec_value;
709727
VALUE gmt_offset_value;
710728
VALUE res;
711-
712-
#if (RUBY_API_VERSION_MAJOR > 2 || (RUBY_API_VERSION_MAJOR == 2 && RUBY_API_VERSION_MINOR >= 3)) && defined(HAVE_TIMEGM)
713-
/* Fast path for time conversion */
714-
struct tm tm;
715729
struct timespec ts;
716-
tm.tm_year = year - 1900;
717-
tm.tm_mon = mon - 1;
718-
tm.tm_mday = day;
719-
tm.tm_hour = hour;
720-
tm.tm_min = min;
721-
tm.tm_sec = sec;
722-
tm.tm_isdst = -1;
730+
int gmt_offset = tz_hour * 3600 + tz_min * 60 + tz_sec;
723731

724-
if (tz_given) {
725-
/* with timezone */
726-
time_t time = timegm(&tm);
727-
if (time != -1){
728-
int gmt_offset;
732+
if (tz_given == TZ_NEG)
733+
{
734+
gmt_offset = - gmt_offset;
735+
}
729736

730-
gmt_offset = tz_hour * 3600 + tz_min * 60 + tz_sec;
731-
if (tz_given == TZ_NEG)
732-
{
733-
gmt_offset = - gmt_offset;
734-
}
735-
ts.tv_sec = time - gmt_offset;
736-
ts.tv_nsec = nsec;
737-
return rb_time_timespec_new(&ts, gmt_offset);
737+
/* Fast path for time conversion */
738+
ts.tv_nsec = nsec;
739+
if (!tz_given && (conv->flags & PG_CODER_TIMESTAMP_DB_LOCAL)) {
740+
/* without timezone, interpreted as local time */
741+
#ifndef _WIN32
742+
/* Windows' mktime() fails on certain dates, so it takes the slow path */
743+
struct tm tm;
744+
tm.tm_year = year - 1900;
745+
tm.tm_mon = mon - 1;
746+
tm.tm_mday = day;
747+
tm.tm_hour = hour;
748+
tm.tm_min = min;
749+
tm.tm_sec = sec;
750+
tm.tm_isdst = -1;
751+
752+
ts.tv_sec = mktime(&tm);
753+
if (ts.tv_sec != -1) {
754+
return rb_time_timespec_new(&ts, conv->flags & PG_CODER_TIMESTAMP_APP_LOCAL ? INT_MAX : INT_MAX-1);
738755
}
756+
#endif
739757
} else {
740-
/* without timezone */
741-
time_t time;
742-
743-
if( conv->flags & PG_CODER_TIMESTAMP_DB_LOCAL ) {
744-
time = mktime(&tm);
745-
} else {
746-
time = timegm(&tm);
747-
}
748-
if (time != -1){
749-
ts.tv_sec = time;
750-
ts.tv_nsec = nsec;
751-
return rb_time_timespec_new(&ts, conv->flags & PG_CODER_TIMESTAMP_APP_LOCAL ? INT_MAX : INT_MAX-1);
758+
/* with timezone, or without timezone interpreted as UTC */
759+
int64_t time = seconds_since_epoch(year, mon, day, hour, min, sec) - gmt_offset;
760+
ts.tv_sec = (time_t)time;
761+
if (ts.tv_sec == time) {
762+
return rb_time_timespec_new(&ts, tz_given ? gmt_offset : conv->flags & PG_CODER_TIMESTAMP_APP_LOCAL ? INT_MAX : INT_MAX-1);
752763
}
753764
}
754-
/* Some libc implementations fail to convert certain values,
765+
/* mktime() fails on some values with some libc implementations,
766+
* and a 32 bit time_t can't hold every PostgreSQL timestamp,
755767
* so that we fall through to the slow path.
756768
*/
757-
#endif
758-
if (nsec) {
759-
int sec_numerator = sec * 1000000 + nsec / 1000;
760-
int sec_denominator = 1000000;
769+
if (nsec % 1000) {
770+
sec_value = rb_funcall(Qnil, s_id_Rational, 2,
771+
LL2NUM((LONG_LONG)sec * 1000000000 + nsec), INT2NUM(1000000000));
772+
} else if (nsec) {
773+
/* Counted in microseconds, the numerator stays a Fixnum where long is 32 bit, like on Windows */
761774
sec_value = rb_funcall(Qnil, s_id_Rational, 2,
762-
INT2NUM(sec_numerator), INT2NUM(sec_denominator));
775+
INT2NUM(sec * 1000000 + nsec / 1000), INT2NUM(1000000));
763776
} else {
764777
sec_value = INT2NUM(sec);
765778
}
766779

767780
if (tz_given) {
768781
/* with timezone */
769-
int gmt_offset;
770-
771-
gmt_offset = tz_hour * 3600 + tz_min * 60 + tz_sec;
772-
if (tz_given == TZ_NEG)
773-
{
774-
gmt_offset = - gmt_offset;
775-
}
776782
gmt_offset_value = INT2NUM(gmt_offset);
777783
} else {
778784
/* without timezone */

‎spec/pg/type_spec.rb‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,20 @@ def encode(value)
188188
expect( textdec_timestamp.decode('1998-12-31 23:59:60.1234') ).
189189
to be_within(0.000001).of( Time.new(1998,12,31, 23, 59, 60.1234) )
190190
end
191+
it 'decodes nanoseconds regardless of the year' do
192+
expect( textdec_timestamputc.decode('1000-03-15 12:00:00.123456789').nsec ).to eq( 123456789 )
193+
expect( textdec_timestamp.decode('1000-03-15 12:00:00.123456789').nsec ).to eq( 123456789 )
194+
expect( textdec_timestamputc.decode('0044-03-15 12:00:00.123456789 BC').nsec ).to eq( 123456789 )
195+
expect( textdec_timestamptz.decode('0044-03-15 12:00:00.123456789+02 BC').nsec ).to eq( 123456789 )
196+
expect( textdec_timestamptz.decode('294276-12-31 23:58:59.123456789+03').nsec ).to eq( 123456789 )
197+
end
198+
it 'decodes timestamps around february, leap years and centuries' do
199+
[[2000, 2, 29], [2016, 2, 15], [2024, 1, 31], [2024, 3, 1], [1900, 2, 28], [1900, 3, 1], [2100, 3, 1], [1600, 2, 29], [-43, 2, 28], [-43, 3, 1]].each do |year, month, day|
200+
text = year > 0 ? format("%04d-%02d-%02d 12:34:56", year, month, day) : format("%04d-%02d-%02d 12:34:56 BC", 1 - year, month, day)
201+
expect( textdec_timestamputc.decode(text) ).to eq( Time.utc(year, month, day, 12, 34, 56) )
202+
expect( textdec_timestamptz.decode("#{text.delete_suffix(" BC")}+05#{" BC" if year <= 0}") ).to eq( Time.new(year, month, day, 12, 34, 56, "+05:00") )
203+
end
204+
end
191205

192206
def textdec_timestamptz_decode_should_fail(str)
193207
expect(textdec_timestamptz.decode(str)).to eq(str)

0 commit comments

Comments
 (0)