diff --git a/tstime/tstime.go b/tstime/tstime.go new file mode 100644 index 000000000..4867b7ff7 --- /dev/null +++ b/tstime/tstime.go @@ -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 +} diff --git a/tstime/tstime_test.go b/tstime/tstime_test.go new file mode 100644 index 000000000..a598cec42 --- /dev/null +++ b/tstime/tstime_test.go @@ -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) + } + } +}