Skip to content

Commit

Permalink
⚡️ perf(ctx.Range): reduce allocations (#2705)
Browse files Browse the repository at this point in the history
* perf(ctx.Range): reduce allocations

strings.Split was causing extra allocations where using
strings.IndexByte can suffice. ALso switch from strconv.Atoi because it
causes an allocation when parsing a non-integer, which is common for
Ranges.

* chore: fix lint
  • Loading branch information
nickajacks1 authored Nov 10, 2023
1 parent b99712f commit 5d888ce
Show file tree
Hide file tree
Showing 2 changed files with 97 additions and 47 deletions.
58 changes: 40 additions & 18 deletions ctx.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,10 +115,13 @@ func (t *TLSHandler) GetClientInfo(info *tls.ClientHelloInfo) (*tls.Certificate,
// Range data for c.Range
type Range struct {
Type string
Ranges []struct {
Start int
End int
}
Ranges []RangeSet
}

// RangeSet represents a single content range from a request.
type RangeSet struct {
Start int
End int
}

// Cookie data for c.Cookie
Expand Down Expand Up @@ -1392,25 +1395,44 @@ var (

// Range returns a struct containing the type and a slice of ranges.
func (c *Ctx) Range(size int) (Range, error) {
var rangeData Range
var (
rangeData Range
ranges string
)
rangeStr := c.Get(HeaderRange)
if rangeStr == "" || !strings.Contains(rangeStr, "=") {
return rangeData, ErrRangeMalformed
}
data := strings.Split(rangeStr, "=")
const expectedDataParts = 2
if len(data) != expectedDataParts {

i := strings.IndexByte(rangeStr, '=')
if i == -1 || strings.Contains(rangeStr[i+1:], "=") {
return rangeData, ErrRangeMalformed
}
rangeData.Type = data[0]
arr := strings.Split(data[1], ",")
for i := 0; i < len(arr); i++ {
item := strings.Split(arr[i], "-")
if len(item) == 1 {
rangeData.Type = rangeStr[:i]
ranges = rangeStr[i+1:]

var (
singleRange string
moreRanges = ranges
)
for moreRanges != "" {
singleRange = moreRanges
if i := strings.IndexByte(moreRanges, ','); i >= 0 {
singleRange = moreRanges[:i]
moreRanges = moreRanges[i+1:]
} else {
moreRanges = ""
}

var (
startStr, endStr string
i int
)
if i = strings.IndexByte(singleRange, '-'); i == -1 {
return rangeData, ErrRangeMalformed
}
start, startErr := strconv.Atoi(item[0])
end, endErr := strconv.Atoi(item[1])
startStr = singleRange[:i]
endStr = singleRange[i+1:]

start, startErr := fasthttp.ParseUint(utils.UnsafeBytes(startStr))
end, endErr := fasthttp.ParseUint(utils.UnsafeBytes(endStr))
if startErr != nil { // -nnn
start = size - end
end = size - 1
Expand Down
86 changes: 57 additions & 29 deletions ctx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2521,39 +2521,67 @@ func Test_Ctx_Range(t *testing.T) {
c := app.AcquireCtx(&fasthttp.RequestCtx{})
defer app.ReleaseCtx(c)

var (
result Range
err error
)

_, err = c.Range(1000)
utils.AssertEqual(t, true, err != nil)

c.Request().Header.Set(HeaderRange, "bytes=500")
_, err = c.Range(1000)
utils.AssertEqual(t, true, err != nil)
testRange := func(header string, ranges ...RangeSet) {
c.Request().Header.Set(HeaderRange, header)
result, err := c.Range(1000)
if len(ranges) == 0 {
utils.AssertEqual(t, true, err != nil)
} else {
utils.AssertEqual(t, "bytes", result.Type)
utils.AssertEqual(t, true, err == nil)
}
utils.AssertEqual(t, len(ranges), len(result.Ranges))
for i := range ranges {
utils.AssertEqual(t, ranges[i], result.Ranges[i])
}
}

c.Request().Header.Set(HeaderRange, "bytes=500=")
_, err = c.Range(1000)
utils.AssertEqual(t, true, err != nil)
testRange("bytes=500")
testRange("bytes=")
testRange("bytes=500=")
testRange("bytes=500-300")
testRange("bytes=a-700", RangeSet{300, 999})
testRange("bytes=500-b", RangeSet{500, 999})
testRange("bytes=500-1000", RangeSet{500, 999})
testRange("bytes=500-700", RangeSet{500, 700})
testRange("bytes=0-0,2-1000", RangeSet{0, 0}, RangeSet{2, 999})
testRange("bytes=0-99,450-549,-100", RangeSet{0, 99}, RangeSet{450, 549}, RangeSet{900, 999})
testRange("bytes=500-700,601-999", RangeSet{500, 700}, RangeSet{601, 999})
}

c.Request().Header.Set(HeaderRange, "bytes=500-300")
_, err = c.Range(1000)
utils.AssertEqual(t, true, err != nil)
// go test -v -run=^$ -bench=Benchmark_Ctx_Range -benchmem -count=4
func Benchmark_Ctx_Range(b *testing.B) {
app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{})
defer app.ReleaseCtx(c)

testRange := func(header string, start, end int) {
c.Request().Header.Set(HeaderRange, header)
result, err = c.Range(1000)
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, "bytes", result.Type)
utils.AssertEqual(t, start, result.Ranges[0].Start)
utils.AssertEqual(t, end, result.Ranges[0].End)
testCases := []struct {
str string
start int
end int
}{
{"bytes=-700", 300, 999},
{"bytes=500-", 500, 999},
{"bytes=500-1000", 500, 999},
{"bytes=0-700,800-1000", 0, 700},
}

for _, tc := range testCases {
b.Run(tc.str, func(b *testing.B) {
c.Request().Header.Set(HeaderRange, tc.str)
var (
result Range
err error
)
for n := 0; n < b.N; n++ {
result, err = c.Range(1000)
}
utils.AssertEqual(b, nil, err)
utils.AssertEqual(b, "bytes", result.Type)
utils.AssertEqual(b, tc.start, result.Ranges[0].Start)
utils.AssertEqual(b, tc.end, result.Ranges[0].End)
})
}

testRange("bytes=a-700", 300, 999)
testRange("bytes=500-b", 500, 999)
testRange("bytes=500-1000", 500, 999)
testRange("bytes=500-700", 500, 700)
}

// go test -run Test_Ctx_Route
Expand Down

0 comments on commit 5d888ce

Please sign in to comment.