time: improve ParseDuration performance for invalid input

Add "parseDurationError" to reduce memory allocation in the error path of
"ParseDuration" and delay the generation of error messages. This improves
the performance when dealing with invalid input.

The format of the error message remains unchanged.

Benchmarks:

                      │     old      │                 new                 │
                      │    sec/op    │   sec/op     vs base                │
ParseDurationError-10   132.10n ± 4%   45.93n ± 2%  -65.23% (p=0.000 n=10)

                      │     old     │                new                 │
                      │    B/op     │    B/op     vs base                │
ParseDurationError-10   192.00 ± 0%   64.00 ± 0%  -66.67% (p=0.000 n=10)

                      │    old     │                new                 │
                      │ allocs/op  │ allocs/op   vs base                │
ParseDurationError-10   6.000 ± 0%   2.000 ± 0%  -66.67% (p=0.000 n=10)

Fixes #75521

Change-Id: I0dc9f28c9601b6be07b70d0a98613757d76e2c97
GitHub-Last-Rev: 737273936a
GitHub-Pull-Request: golang/go#75531
Reviewed-on: https://go-review.googlesource.com/c/go/+/705195
Reviewed-by: Michael Knyszek <mknyszek@google.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
Auto-Submit: Alan Donovan <adonovan@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
This commit is contained in:
apocelipes 2025-09-19 10:34:09 +00:00 committed by Gopher Robot
parent f9e61a9a32
commit ee7bf06cb3
2 changed files with 27 additions and 10 deletions

View file

@ -1602,6 +1602,16 @@ func leadingFraction(s string) (x uint64, scale float64, rem string) {
return x, scale, s[i:] return x, scale, s[i:]
} }
// parseDurationError describes a problem parsing a duration string.
type parseDurationError struct {
message string
value string
}
func (e *parseDurationError) Error() string {
return "time: " + e.message + " " + quote(e.value)
}
var unitMap = map[string]uint64{ var unitMap = map[string]uint64{
"ns": uint64(Nanosecond), "ns": uint64(Nanosecond),
"us": uint64(Microsecond), "us": uint64(Microsecond),
@ -1637,7 +1647,7 @@ func ParseDuration(s string) (Duration, error) {
return 0, nil return 0, nil
} }
if s == "" { if s == "" {
return 0, errors.New("time: invalid duration " + quote(orig)) return 0, &parseDurationError{"invalid duration", orig}
} }
for s != "" { for s != "" {
var ( var (
@ -1649,13 +1659,13 @@ func ParseDuration(s string) (Duration, error) {
// The next character must be [0-9.] // The next character must be [0-9.]
if !(s[0] == '.' || '0' <= s[0] && s[0] <= '9') { if !(s[0] == '.' || '0' <= s[0] && s[0] <= '9') {
return 0, errors.New("time: invalid duration " + quote(orig)) return 0, &parseDurationError{"invalid duration", orig}
} }
// Consume [0-9]* // Consume [0-9]*
pl := len(s) pl := len(s)
v, s, err = leadingInt(s) v, s, err = leadingInt(s)
if err != nil { if err != nil {
return 0, errors.New("time: invalid duration " + quote(orig)) return 0, &parseDurationError{"invalid duration", orig}
} }
pre := pl != len(s) // whether we consumed anything before a period pre := pl != len(s) // whether we consumed anything before a period
@ -1669,7 +1679,7 @@ func ParseDuration(s string) (Duration, error) {
} }
if !pre && !post { if !pre && !post {
// no digits (e.g. ".s" or "-.s") // no digits (e.g. ".s" or "-.s")
return 0, errors.New("time: invalid duration " + quote(orig)) return 0, &parseDurationError{"invalid duration", orig}
} }
// Consume unit. // Consume unit.
@ -1681,17 +1691,17 @@ func ParseDuration(s string) (Duration, error) {
} }
} }
if i == 0 { if i == 0 {
return 0, errors.New("time: missing unit in duration " + quote(orig)) return 0, &parseDurationError{"missing unit in duration", orig}
} }
u := s[:i] u := s[:i]
s = s[i:] s = s[i:]
unit, ok := unitMap[u] unit, ok := unitMap[u]
if !ok { if !ok {
return 0, errors.New("time: unknown unit " + quote(u) + " in duration " + quote(orig)) return 0, &parseDurationError{"unknown unit " + quote(u) + " in duration", orig}
} }
if v > 1<<63/unit { if v > 1<<63/unit {
// overflow // overflow
return 0, errors.New("time: invalid duration " + quote(orig)) return 0, &parseDurationError{"invalid duration", orig}
} }
v *= unit v *= unit
if f > 0 { if f > 0 {
@ -1700,19 +1710,19 @@ func ParseDuration(s string) (Duration, error) {
v += uint64(float64(f) * (float64(unit) / scale)) v += uint64(float64(f) * (float64(unit) / scale))
if v > 1<<63 { if v > 1<<63 {
// overflow // overflow
return 0, errors.New("time: invalid duration " + quote(orig)) return 0, &parseDurationError{"invalid duration", orig}
} }
} }
d += v d += v
if d > 1<<63 { if d > 1<<63 {
return 0, errors.New("time: invalid duration " + quote(orig)) return 0, &parseDurationError{"invalid duration", orig}
} }
} }
if neg { if neg {
return -Duration(d), nil return -Duration(d), nil
} }
if d > 1<<63-1 { if d > 1<<63-1 {
return 0, errors.New("time: invalid duration " + quote(orig)) return 0, &parseDurationError{"invalid duration", orig}
} }
return Duration(d), nil return Duration(d), nil
} }

View file

@ -1620,6 +1620,13 @@ func BenchmarkParseDuration(b *testing.B) {
} }
} }
func BenchmarkParseDurationError(b *testing.B) {
for i := 0; i < b.N; i++ {
ParseDuration("9223372036854775810ns") // overflow
ParseDuration("9007199254.740993") // missing unit
}
}
func BenchmarkHour(b *testing.B) { func BenchmarkHour(b *testing.B) {
t := Now() t := Now()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {