Skip to content

Commit

Permalink
pgdate: optimize bulk date parsing
Browse files Browse the repository at this point in the history
Add a "helper" to cache memory allocations to make repeated/bulk date
parsing more efficient.

```
name          old time/op    new time/op    delta
ParseDate-10    1.76µs ±12%    1.49µs ± 6%   -15.08%  (p=0.000 n=9+10)

name          old alloc/op   new alloc/op   delta
ParseDate-10    1.66kB ± 0%    0.00kB       -100.00%  (p=0.000 n=10+10)

name          old allocs/op  new allocs/op  delta
ParseDate-10      8.00 ± 0%      0.00       -100.00%  (p=0.000 n=10+10)
```

Fixes: #91834

Release note: None
  • Loading branch information
cucaroach committed Nov 28, 2022
1 parent e24d5b5 commit 048f833
Show file tree
Hide file tree
Showing 9 changed files with 137 additions and 69 deletions.
12 changes: 8 additions & 4 deletions pkg/sql/colexec/colexecbase/cast.eg.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion pkg/sql/colexec/execgen/cmd/execgen/cast_gen_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,8 @@ func stringToDate(to, from, evalCtx, _, _ string) string {
convStr := `
_now := %[3]s.GetRelativeParseTime()
_dateStyle := %[3]s.GetDateStyle()
_d, _, err := pgdate.ParseDate(_now, _dateStyle, string(%[2]s))
_ph := &%[3]s.ParseHelper
_d, _, err := pgdate.ParseDate(_now, _dateStyle, string(%[2]s), _ph)
if err != nil {
colexecerror.ExpectedError(err)
}
Expand Down
8 changes: 8 additions & 0 deletions pkg/sql/sem/eval/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,9 @@ type Context struct {

// ChangefeedState stores the state (progress) of core changefeeds.
ChangefeedState ChangefeedState

// ParseHelper makes date parsing more efficient.
ParseHelper pgdate.ParseHelper
}

// DescIDGenerator generates unique descriptor IDs.
Expand Down Expand Up @@ -564,6 +567,11 @@ func (ec *Context) GetRelativeParseTime() time.Time {
return ret.In(ec.GetLocation())
}

// GetDateHelper implements ParseTimeContext.
func (ec *Context) GetDateHelper() *pgdate.ParseHelper {
return &ec.ParseHelper
}

// GetTxnTimestamp retrieves the current transaction timestamp as per
// the evaluation context. The timestamp is guaranteed to be nonzero.
func (ec *Context) GetTxnTimestamp(precision time.Duration) *tree.DTimestampTZ {
Expand Down
17 changes: 16 additions & 1 deletion pkg/sql/sem/tree/datum.go
Original file line number Diff line number Diff line change
Expand Up @@ -2005,6 +2005,8 @@ type ParseTimeContext interface {
GetIntervalStyle() duration.IntervalStyle
// GetDateStyle returns the date style in the session.
GetDateStyle() pgdate.DateStyle
// GetParseHelper returns a helper to optmize date parsing.
GetDateHelper() *pgdate.ParseHelper
}

var _ ParseTimeContext = &simpleParseTimeContext{}
Expand Down Expand Up @@ -2037,6 +2039,7 @@ type simpleParseTimeContext struct {
RelativeParseTime time.Time
DateStyle pgdate.DateStyle
IntervalStyle duration.IntervalStyle
dateHelper pgdate.ParseHelper
}

// GetRelativeParseTime implements ParseTimeContext.
Expand All @@ -2054,6 +2057,11 @@ func (ctx simpleParseTimeContext) GetDateStyle() pgdate.DateStyle {
return ctx.DateStyle
}

// GetDateHelper implements ParseTimeContext.
func (ctx simpleParseTimeContext) GetDateHelper() *pgdate.ParseHelper {
return &ctx.dateHelper
}

// relativeParseTime chooses a reasonable "now" value for
// performing date parsing.
func relativeParseTime(ctx ParseTimeContext) time.Time {
Expand All @@ -2077,14 +2085,21 @@ func intervalStyle(ctx ParseTimeContext) duration.IntervalStyle {
return ctx.GetIntervalStyle()
}

func dateParseHelper(ctx ParseTimeContext) *pgdate.ParseHelper {
if ctx == nil {
return nil
}
return ctx.GetDateHelper()
}

// ParseDDate parses and returns the *DDate Datum value represented by the provided
// string in the provided location, or an error if parsing is unsuccessful.
//
// The dependsOnContext return value indicates if we had to consult the
// ParseTimeContext (either for the time or the local timezone).
func ParseDDate(ctx ParseTimeContext, s string) (_ *DDate, dependsOnContext bool, _ error) {
now := relativeParseTime(ctx)
t, dependsOnContext, err := pgdate.ParseDate(now, dateStyle(ctx), s)
t, dependsOnContext, err := pgdate.ParseDate(now, dateStyle(ctx), s, dateParseHelper(ctx))
return NewDDate(t), dependsOnContext, err
}

Expand Down
20 changes: 12 additions & 8 deletions pkg/util/timeutil/pgdate/field_extract.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,6 @@ type fieldExtract struct {
// Provides a time for evaluating relative dates as well as a
// timezone. Should only be used via the now() and location() accessors.
currentTime time.Time
// currentTimeUsed is set if we consulted currentTime (indicating if the
// result depends on the context).
currentTimeUsed bool

// location is set to the timezone specified by the timestamp (if any).
location *time.Location
Expand All @@ -66,15 +63,22 @@ type fieldExtract struct {
// Stores a reference to one of the sentinel values, to be returned
// by the makeDateTime() functions
sentinel *time.Time
// This indicates that the value in the year field was only
// two digits and should be adjusted to make it recent.
tweakYear bool
// Tracks the sign of the timezone offset. We need to track
// this separately from the sign of the tz1 value in case
// we're trying to store a (nonsensical) value like -0030.
tzSign int
// Tracks the fields that we want to extract.
wanted fieldSet

textChunksScratch [fieldMaximum]stringChunk
numbersScratch [fieldMaximum]numberChunk

// This indicates that the value in the year field was only
// two digits and should be adjusted to make it recent.
tweakYear bool
// currentTimeUsed is set if we consulted currentTime (indicating if the
// result depends on the context).
currentTimeUsed bool
// Tracks whether the current timestamp is of db2 format.
isDB2 bool
}
Expand All @@ -96,8 +100,8 @@ func (fe *fieldExtract) getLocation() *time.Location {
// string into a collection of date/time fields in order to populate a
// fieldExtract.
func (fe *fieldExtract) Extract(s string) error {
textChunks := fe.textChunksScratch[:fieldMaximum]
// Break the string into alphanumeric chunks.
textChunks := make([]stringChunk, fieldMaximum)
count, _ := chunk(s, textChunks)

if count < 0 {
Expand All @@ -107,7 +111,7 @@ func (fe *fieldExtract) Extract(s string) error {
}

// Create a place to store extracted numeric info.
numbers := make([]numberChunk, 0, fieldMaximum)
numbers := fe.numbersScratch[:0]

appendNumber := func(prefix, number string) error {
v, err := strconv.Atoi(number)
Expand Down
31 changes: 17 additions & 14 deletions pkg/util/timeutil/pgdate/field_extract_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
)

func TestExtractRelative(t *testing.T) {
var parseHelper ParseHelper
tests := []struct {
s string
rel int
Expand All @@ -42,20 +43,22 @@ func TestExtractRelative(t *testing.T) {
now := time.Date(2018, 10, 17, 0, 0, 0, 0, time.UTC)
for _, tc := range tests {
t.Run(tc.s, func(t *testing.T) {
d, depOnCtx, err := ParseDate(now, DateStyle{Order: Order_YMD}, tc.s)
if err != nil {
t.Fatal(err)
}
if !depOnCtx {
t.Fatalf("relative dates should depend on context")
}
ts, err := d.ToTime()
if err != nil {
t.Fatal(err)
}
exp := now.AddDate(0, 0, tc.rel)
if ts != exp {
t.Fatalf("expected %v, got %v", exp, ts)
for _, ph := range []*ParseHelper{nil, &parseHelper} {
d, depOnCtx, err := ParseDate(now, DateStyle{Order: Order_YMD}, tc.s, ph)
if err != nil {
t.Fatal(err)
}
if !depOnCtx {
t.Fatalf("relative dates should depend on context")
}
ts, err := d.ToTime()
if err != nil {
t.Fatal(err)
}
exp := now.AddDate(0, 0, tc.rel)
if ts != exp {
t.Fatalf("expected %v, got %v", exp, ts)
}
}
})
}
Expand Down
40 changes: 28 additions & 12 deletions pkg/util/timeutil/pgdate/parsing.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ var (
TimeNegativeInfinity = timeutil.Unix(-210866803200, 0)
)

type ParseHelper struct {
fe fieldExtract
}

// ParseDate converts a string into Date.
//
// Any specified timezone is inconsequential. Examples:
Expand All @@ -91,10 +95,16 @@ var (
//
// The dependsOnContext return value indicates if we had to consult the given
// `now` value (either for the time or the local timezone).
//
// Memory allocations can be avoided by passing ParseHelper which can be re-used
// across calls for batch parsing purposes, otherwise it can be nil.
func ParseDate(
now time.Time, dateStyle DateStyle, s string,
now time.Time, dateStyle DateStyle, s string, h *ParseHelper,
) (_ Date, dependsOnContext bool, _ error) {
fe := fieldExtract{
if h == nil {
h = &ParseHelper{}
}
h.fe = fieldExtract{
currentTime: now,
dateStyle: dateStyle,
required: dateRequiredFields,
Expand All @@ -104,42 +114,48 @@ func ParseDate(
wanted: dateTimeFields,
}

if err := fe.Extract(s); err != nil {
if err := h.fe.Extract(s); err != nil {
return Date{}, false, parseError(err, "date", s)
}
date, err := fe.MakeDate()
return date, fe.currentTimeUsed, err
date, err := h.fe.MakeDate()
return date, h.fe.currentTimeUsed, err
}

// ParseTime converts a string into a time value on the epoch day.
//
// The dependsOnContext return value indicates if we had to consult the given
// `now` value (either for the time or the local timezone).
//
// Memory allocations can be avoided by passing ParseHelper which can be re-used
// across calls for batch parsing purposes, otherwise it can be nil.
func ParseTime(
now time.Time, dateStyle DateStyle, s string,
now time.Time, dateStyle DateStyle, s string, h *ParseHelper,
) (_ time.Time, dependsOnContext bool, _ error) {
fe := fieldExtract{
if h == nil {
h = &ParseHelper{}
}
h.fe = fieldExtract{
currentTime: now,
required: timeRequiredFields,
wanted: timeFields,
}

if err := fe.Extract(s); err != nil {
if err := h.fe.Extract(s); err != nil {
// It's possible that the user has given us a complete
// timestamp string; let's try again, accepting more fields.
fe = fieldExtract{
h.fe = fieldExtract{
currentTime: now,
dateStyle: dateStyle,
required: timeRequiredFields,
wanted: dateTimeFields,
}

if err := fe.Extract(s); err != nil {
if err := h.fe.Extract(s); err != nil {
return TimeEpoch, false, parseError(err, "time", s)
}
}
res := fe.MakeTime()
return res, fe.currentTimeUsed, nil
res := h.fe.MakeTime()
return res, h.fe.currentTimeUsed, nil
}

// ParseTimeWithoutTimezone converts a string into a time value on the epoch
Expand Down
Loading

0 comments on commit 048f833

Please sign in to comment.