Go's time.Parse always allocates a FixedZone for time strings not in UTC (ending in "Z"). This avoids that allocation, at the cost of adding a cache. Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>main
parent
4e0ee141e8
commit
d503dee6f1
@ -0,0 +1,58 @@ |
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package tstime defines Tailscale-specific time utilities.
|
||||
package tstime |
||||
|
||||
import ( |
||||
"strings" |
||||
"sync" |
||||
"time" |
||||
) |
||||
|
||||
// zoneOf returns the RFC3339 zone suffix, or the empty string
|
||||
// if it's invalid or not something we want to cache.
|
||||
func zoneOf(s string) string { |
||||
if strings.HasSuffix(s, "Z") { |
||||
return "" |
||||
} |
||||
if len(s) < len("2020-04-05T15:56:00+08:00") { |
||||
// Too short, invalid? Let time.Parse fail on it.
|
||||
return "" |
||||
} |
||||
zone := s[len(s)-len("+08:00"):] |
||||
if c := zone[0]; c == '+' || c == '-' { |
||||
min := zone[len("+08:"):] |
||||
switch min { |
||||
case "00", "15", "30": |
||||
return zone |
||||
} |
||||
} |
||||
return "" |
||||
} |
||||
|
||||
// locCache maps from zone offset suffix string ("+08:00") =>
|
||||
// *time.Location (from FixedLocation).
|
||||
var locCache sync.Map |
||||
|
||||
// Parse3339 is a wrapper around time.Parse(time.RFC3339Nano, s) that caches
|
||||
// timezone Locations for future parses.
|
||||
func Parse3339(s string) (time.Time, error) { |
||||
zone := zoneOf(s) |
||||
if zone == "" { |
||||
return time.Parse(time.RFC3339Nano, s) |
||||
} |
||||
loci, ok := locCache.Load(zone) |
||||
if ok { |
||||
// TODO(bradfitz): just rewrite this do the trivial parsing by hand
|
||||
// which will be faster than Go's format-driven one. RFC3339 is trivial.
|
||||
return time.ParseInLocation(time.RFC3339Nano, s, loci.(*time.Location)) |
||||
} |
||||
t, err := time.Parse(time.RFC3339Nano, s) |
||||
if err != nil { |
||||
return time.Time{}, err |
||||
} |
||||
locCache.LoadOrStore(zone, t.Location()) |
||||
return t, nil |
||||
} |
||||
@ -0,0 +1,104 @@ |
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package tstime |
||||
|
||||
import ( |
||||
"testing" |
||||
"time" |
||||
) |
||||
|
||||
func TestZoneOf(t *testing.T) { |
||||
tests := []struct { |
||||
in, want string |
||||
}{ |
||||
{"2020-04-05T15:56:00+08:00", "+08:00"}, |
||||
{"2020-04-05T15:56:00-08:00", "-08:00"}, |
||||
{"2020-04-05T15:56:00.12345-08:00", "-08:00"}, |
||||
// don't cache weird offsets, only 00 15, 30:
|
||||
{"2020-04-05T15:56:00.12345-08:00", "-08:00"}, |
||||
{"2020-04-05T15:56:00.12345-08:30", "-08:30"}, |
||||
{"2020-04-05T15:56:00.12345-08:15", "-08:15"}, |
||||
{"2020-04-05T15:56:00.12345-08:17", ""}, |
||||
// don't cache UTC:
|
||||
{"2020-04-05T15:56:00.12345Z", ""}, |
||||
{"2020-04-05T15:56:00Z", ""}, |
||||
// too short:
|
||||
{"123+08:00", ""}, |
||||
{"+08:00", ""}, |
||||
} |
||||
for _, tt := range tests { |
||||
if got := zoneOf(tt.in); got != tt.want { |
||||
t.Errorf("zoneOf(%q) = %q; want %q", tt.in, got, tt.want) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func BenchmarkGoParse3339(b *testing.B) { |
||||
b.ReportAllocs() |
||||
const in = `2020-04-05T15:56:00.148487491+08:00` |
||||
for i := 0; i < b.N; i++ { |
||||
_, err := time.Parse(time.RFC3339Nano, in) |
||||
if err != nil { |
||||
b.Fatal(err) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func BenchmarkGoParse3339InLocation(b *testing.B) { |
||||
b.ReportAllocs() |
||||
const in = `2020-04-05T15:56:00.148487491+08:00` |
||||
|
||||
t, err := time.Parse(time.RFC3339Nano, in) |
||||
if err != nil { |
||||
b.Fatal(err) |
||||
} |
||||
loc := t.Location() |
||||
|
||||
t2, err := time.ParseInLocation(time.RFC3339Nano, in, loc) |
||||
if err != nil { |
||||
b.Fatal(err) |
||||
} |
||||
if !t.Equal(t2) { |
||||
b.Fatal("not equal") |
||||
} |
||||
if s1, s2 := t.Format(time.RFC3339Nano), t2.Format(time.RFC3339Nano); s1 != s2 { |
||||
b.Fatalf("times don't stringify the same: %q vs %q", s1, s2) |
||||
} |
||||
|
||||
for i := 0; i < b.N; i++ { |
||||
_, err := time.ParseInLocation(time.RFC3339Nano, in, loc) |
||||
if err != nil { |
||||
b.Fatal(err) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func BenchmarkParse3339(b *testing.B) { |
||||
b.ReportAllocs() |
||||
const in = `2020-04-05T15:56:00.148487491+08:00` |
||||
|
||||
t, err := time.Parse(time.RFC3339Nano, in) |
||||
if err != nil { |
||||
b.Fatal(err) |
||||
} |
||||
|
||||
t2, err := Parse3339(in) |
||||
if err != nil { |
||||
b.Fatal(err) |
||||
} |
||||
if !t.Equal(t2) { |
||||
b.Fatal("not equal") |
||||
} |
||||
if s1, s2 := t.Format(time.RFC3339Nano), t2.Format(time.RFC3339Nano); s1 != s2 { |
||||
b.Fatalf("times don't stringify the same: %q vs %q", s1, s2) |
||||
} |
||||
|
||||
for i := 0; i < b.N; i++ { |
||||
_, err := Parse3339(in) |
||||
if err != nil { |
||||
b.Fatal(err) |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue