tailscale/tstime/tstime.go
Josh Bleecher Snyder aa9d7f4665 tstime: add Parse3339B, for byte slices
Use go4.org/mem for memory safety.
A slight performance hit, but a huge performance win
for clients who start with a []byte.
The perf hit is due largely to the MapHash call, which adds ~25ns.
That is necessary to keep the fast path allocation-free.

name                     old time/op    new time/op    delta
GoParse3339/Z-8             281ns ± 1%     283ns ± 2%     ~     (p=0.366 n=9+9)
GoParse3339/TZ-8            509ns ± 0%     510ns ± 1%     ~     (p=0.059 n=9+9)
GoParse3339InLocation-8     330ns ± 1%     330ns ± 0%     ~     (p=0.802 n=10+6)
Parse3339/Z-8              69.3ns ± 1%    74.4ns ± 1%   +7.45%  (p=0.000 n=9+10)
Parse3339/TZ-8              110ns ± 1%     140ns ± 3%  +27.42%  (p=0.000 n=9+10)
ParseInt-8                 8.20ns ± 1%    8.17ns ± 1%     ~     (p=0.452 n=9+9)

name                     old alloc/op   new alloc/op   delta
GoParse3339/Z-8             0.00B          0.00B          ~     (all equal)
GoParse3339/TZ-8             160B ± 0%      160B ± 0%     ~     (all equal)
GoParse3339InLocation-8     0.00B          0.00B          ~     (all equal)
Parse3339/Z-8               0.00B          0.00B          ~     (all equal)
Parse3339/TZ-8              0.00B          0.00B          ~     (all equal)

name                     old allocs/op  new allocs/op  delta
GoParse3339/Z-8              0.00           0.00          ~     (all equal)
GoParse3339/TZ-8             3.00 ± 0%      3.00 ± 0%     ~     (all equal)
GoParse3339InLocation-8      0.00           0.00          ~     (all equal)
Parse3339/Z-8                0.00           0.00          ~     (all equal)
Parse3339/TZ-8               0.00           0.00          ~     (all equal)


Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2020-11-19 14:47:11 -08:00

144 lines
3.7 KiB
Go

// 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 (
"errors"
"fmt"
"sync"
"time"
"go4.org/mem"
)
var memZ = mem.S("Z")
// zoneOf returns the RFC3339 zone suffix (either "Z" or like
// "+08:30"), or the empty string if it's invalid or not something we
// want to cache.
func zoneOf(s mem.RO) mem.RO {
if mem.HasSuffix(s, memZ) {
return memZ
}
if s.Len() < len("2020-04-05T15:56:00+08:00") {
// Too short, invalid? Let time.Parse fail on it.
return mem.S("")
}
zone := s.SliceFrom(s.Len() - len("+08:00"))
if c := zone.At(0); c == '+' || c == '-' {
min := zone.SliceFrom(len("+08:"))
if min.EqualString("00") || min.EqualString("15") || min.EqualString("30") {
return zone
}
}
return mem.S("")
}
// locCache maps from hash of zone offset suffix string ("+08:00") =>
// {zone string, *time.Location (from FixedLocation)}.
var locCache sync.Map
type locCacheEntry struct {
zone string
loc *time.Location
}
func getLocation(zone, timeValue mem.RO) (*time.Location, error) {
if zone.EqualString("Z") {
return time.UTC, nil
}
key := zone.MapHash()
if entry, ok := locCache.Load(key); ok {
// We're keying only on a hash; double-check zone to ensure no spurious collisions.
e := entry.(locCacheEntry)
if zone.EqualString(e.zone) {
return e.loc, nil
}
}
// TODO(bradfitz): just parse it and call time.FixedLocation.
// For now, just have time.Parse do it once:
t, err := time.Parse(time.RFC3339Nano, timeValue.StringCopy())
if err != nil {
return nil, err
}
loc := t.Location()
locCache.LoadOrStore(key, locCacheEntry{zone: zone.StringCopy(), loc: loc})
return loc, nil
}
func parse3339m(s mem.RO) (time.Time, error) {
zone := zoneOf(s)
if zone.Len() == 0 {
// Invalid or weird timezone offset. Use slow path,
// which'll probably return an error.
return time.Parse(time.RFC3339Nano, s.StringCopy())
}
loc, err := getLocation(zone, s)
if err != nil {
return time.Time{}, err
}
s = s.SliceTo(s.Len() - zone.Len()) // remove zone suffix
var year, mon, day, hr, min, sec, nsec int
const baseLen = len("2020-04-05T15:56:00")
if s.Len() < baseLen ||
!parseInt(s.SliceTo(4), &year) ||
s.At(4) != '-' ||
!parseInt(s.Slice(5, 7), &mon) ||
s.At(7) != '-' ||
!parseInt(s.Slice(8, 10), &day) ||
s.At(10) != 'T' ||
!parseInt(s.Slice(11, 13), &hr) ||
s.At(13) != ':' ||
!parseInt(s.Slice(14, 16), &min) ||
s.At(16) != ':' ||
!parseInt(s.Slice(17, 19), &sec) {
return time.Time{}, errors.New("invalid time")
}
nsStr := s.SliceFrom(baseLen)
if nsStr.Len() != 0 {
if nsStr.At(0) != '.' {
return time.Time{}, errors.New("invalid optional nanosecond prefix")
}
nsStr = nsStr.SliceFrom(1)
if !parseInt(nsStr, &nsec) {
return time.Time{}, fmt.Errorf("invalid optional nanosecond number %q", nsStr.StringCopy())
}
for i := 0; i < len("999999999")-nsStr.Len(); i++ {
nsec *= 10
}
}
return time.Date(year, time.Month(mon), day, hr, min, sec, nsec, loc), nil
}
func parseInt(s mem.RO, dst *int) bool {
if s.Len() == 0 || s.Len() > len("999999999") {
*dst = 0
return false
}
n := 0
for i := 0; i < s.Len(); i++ {
d := s.At(i) - '0'
if d > 9 {
*dst = 0
return false
}
n = n*10 + int(d)
}
*dst = n
return true
}
// Parse3339 is a wrapper around time.Parse(time.RFC3339Nano, s) that caches
// timezone Locations for future parses.
func Parse3339(s string) (time.Time, error) {
return parse3339m(mem.S(s))
}
// Parse3339B is Parse3339 but for byte slices.
func Parse3339B(b []byte) (time.Time, error) {
return parse3339m(mem.B(b))
}