Skip to content

Commit e79256b

Browse files
authored
backend: improve parse function performance in gtime package (#1238)
* have unit tests for parse function * define benchmark test for parse function * add one more test casae * Improve the code * fast return when the input is empty
1 parent e15f502 commit e79256b

File tree

3 files changed

+180
-3
lines changed

3 files changed

+180
-3
lines changed

backend/gtime/gtime.go

+33-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package gtime
22

33
import (
4+
"errors"
45
"fmt"
56
"regexp"
67
"strconv"
@@ -74,18 +75,47 @@ func ParseDuration(inp string) (time.Duration, error) {
7475
}
7576

7677
func parse(inp string) (time.Duration, string, error) {
77-
result := dateUnitPattern.FindSubmatch([]byte(inp))
78+
if inp == "" {
79+
return 0, "", backend.DownstreamError(errors.New("empty input"))
80+
}
81+
82+
// Fast path for simple duration formats (no date units)
83+
lastChar := inp[len(inp)-1]
84+
if lastChar != 'd' && lastChar != 'w' && lastChar != 'M' && lastChar != 'y' {
85+
dur, err := time.ParseDuration(inp)
86+
return dur, "", err
87+
}
88+
89+
// Check if the rest is a number for date units
90+
numPart := inp[:len(inp)-1]
91+
isNum := true
92+
for _, c := range numPart {
93+
if c < '0' || c > '9' {
94+
isNum = false
95+
break
96+
}
97+
}
98+
if isNum {
99+
num, err := strconv.Atoi(numPart)
100+
if err != nil {
101+
return 0, "", err
102+
}
103+
return time.Duration(num), string(lastChar), nil
104+
}
105+
106+
// Fallback to regex for complex cases
107+
result := dateUnitPattern.FindStringSubmatch(inp)
78108
if len(result) != 3 {
79109
dur, err := time.ParseDuration(inp)
80110
return dur, "", err
81111
}
82112

83-
num, err := strconv.Atoi(string(result[1]))
113+
num, err := strconv.Atoi(result[1])
84114
if err != nil {
85115
return 0, "", err
86116
}
87117

88-
return time.Duration(num), string(result[2]), nil
118+
return time.Duration(num), result[2], nil
89119
}
90120

91121
// FormatInterval converts a duration into the units that Grafana uses

backend/gtime/gtime_bench_test.go

+25
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,30 @@ import (
44
"testing"
55
)
66

7+
// go test -benchmem -run=^$ -bench=BenchmarkParse$ github.com/grafana/grafana-plugin-sdk-go/backend/gtime/ -memprofile p_mem.out -count 6 | tee pmem.0.txt
8+
func BenchmarkParse(b *testing.B) {
9+
testCases := []struct {
10+
name string
11+
input string
12+
}{
13+
{"PureNumber", "30"},
14+
{"SimpleUnit", "5s"},
15+
{"ComplexUnit", "1h30m"},
16+
{"DateUnit", "7d"},
17+
{"MonthUnit", "3M"},
18+
{"YearUnit", "1y"},
19+
}
20+
21+
for _, tc := range testCases {
22+
b.Run(tc.name, func(b *testing.B) {
23+
b.ResetTimer()
24+
for i := 0; i < b.N; i++ {
25+
_, _, _ = parse(tc.input)
26+
}
27+
})
28+
}
29+
}
30+
731
// go test -benchmem -run=^$ -bench=BenchmarkParseIntervalStringToTimeDuration$ github.
832
// com/grafana/grafana-plugin-sdk-go/backend/gtime/ -memprofile p_mem.out -count 6 | tee p_mem.txt
933
func BenchmarkParseIntervalStringToTimeDuration(b *testing.B) {
@@ -26,6 +50,7 @@ func BenchmarkParseIntervalStringToTimeDuration(b *testing.B) {
2650
for _, tc := range testCases {
2751
b.Run(tc.name, func(b *testing.B) {
2852
for i := 0; i < b.N; i++ {
53+
b.ResetTimer()
2954
_, err := ParseIntervalStringToTimeDuration(tc.interval)
3055
if err != nil {
3156
b.Fatal(err)

backend/gtime/gtime_test.go

+122
Original file line numberDiff line numberDiff line change
@@ -212,3 +212,125 @@ func TestRoundInterval(t *testing.T) {
212212
})
213213
}
214214
}
215+
216+
func TestParse(t *testing.T) {
217+
tests := []struct {
218+
name string
219+
input string
220+
wantDur time.Duration
221+
wantPeriod string
222+
wantErrRegex *regexp.Regexp
223+
}{
224+
{
225+
name: "simple duration seconds",
226+
input: "30s",
227+
wantDur: 30 * time.Second,
228+
wantPeriod: "",
229+
},
230+
{
231+
name: "simple duration minutes",
232+
input: "5m",
233+
wantDur: 5 * time.Minute,
234+
wantPeriod: "",
235+
},
236+
{
237+
name: "simple duration minutes and seconds",
238+
input: "1m30s",
239+
wantDur: 90 * time.Second,
240+
wantPeriod: "",
241+
},
242+
{
243+
name: "complex duration",
244+
input: "1h30m",
245+
wantDur: 90 * time.Minute,
246+
wantPeriod: "",
247+
},
248+
{
249+
name: "days unit",
250+
input: "7d",
251+
wantDur: 7,
252+
wantPeriod: "d",
253+
},
254+
{
255+
name: "weeks unit",
256+
input: "2w",
257+
wantDur: 2,
258+
wantPeriod: "w",
259+
},
260+
{
261+
name: "months unit",
262+
input: "3M",
263+
wantDur: 3,
264+
wantPeriod: "M",
265+
},
266+
{
267+
name: "years unit",
268+
input: "1y",
269+
wantDur: 1,
270+
wantPeriod: "y",
271+
},
272+
{
273+
name: "invalid duration",
274+
input: "invalid",
275+
wantErrRegex: regexp.MustCompile(`time: invalid duration "?invalid"?`),
276+
},
277+
{
278+
name: "invalid number",
279+
input: "abc1d",
280+
wantErrRegex: regexp.MustCompile(`time: invalid duration "?abc1d"?`),
281+
},
282+
{
283+
name: "empty string",
284+
input: "",
285+
wantErrRegex: regexp.MustCompile(`empty input`),
286+
},
287+
{
288+
name: "fast path - pure number with date unit",
289+
input: "30d",
290+
wantDur: 30,
291+
wantPeriod: "d",
292+
},
293+
{
294+
name: "fast path - pure number with week unit",
295+
input: "2w",
296+
wantDur: 2,
297+
wantPeriod: "w",
298+
},
299+
{
300+
name: "fast path - pure number with month unit",
301+
input: "6M",
302+
wantDur: 6,
303+
wantPeriod: "M",
304+
},
305+
{
306+
name: "fast path - pure number with year unit",
307+
input: "5y",
308+
wantDur: 5,
309+
wantPeriod: "y",
310+
},
311+
{
312+
name: "non-numeric prefix with date unit",
313+
input: "a5d",
314+
wantErrRegex: regexp.MustCompile(`time: invalid duration "?a5d"?`),
315+
},
316+
{
317+
name: "mixed characters with date unit",
318+
input: "5a3d",
319+
wantErrRegex: regexp.MustCompile(`time: unknown unit "a" in duration "5a3d"`),
320+
},
321+
}
322+
323+
for _, tt := range tests {
324+
t.Run(tt.name, func(t *testing.T) {
325+
gotDur, gotPeriod, err := parse(tt.input)
326+
if tt.wantErrRegex != nil {
327+
require.Error(t, err)
328+
require.Regexp(t, tt.wantErrRegex, err.Error())
329+
return
330+
}
331+
require.NoError(t, err)
332+
require.Equal(t, tt.wantDur, gotDur)
333+
require.Equal(t, tt.wantPeriod, gotPeriod)
334+
})
335+
}
336+
}

0 commit comments

Comments
 (0)