Repository: gabriel-vasile/mimetype
Pull request #536 (2024-05-26 15:04:21)

tar: remove strconv dependency for tar checksum octal numbers
```
goos: linux
goarch: amd64
pkg: github.com/gabriel-vasile/mimetype
cpu: Intel(R) Core(TM) i7-10510U CPU @ 1.80GHz
               │ common_master │              common_dev              │
               │    sec/op     │    sec/op     vs base                │
Common/.xlsx-8    401.2n ±  3%   312.2n ±  5%  -22.17% (p=0.000 n=10)
Common/.pptx-8    1.708µ ±  4%   1.535µ ±  3%  -10.10% (p=0.000 n=10)
Common/.docx-8    1.260µ ±  7%   1.116µ ±  4%  -11.43% (p=0.000 n=10)
Common/.tar-8    1011.0n ±  7%   785.0n ±  4%  -22.35% (p=0.000 n=10)
Common/.zip-8     742.3n ± 11%   616.2n ± 16%  -16.99% (p=0.003 n=10)
Common/.pdf-8     291.6n ±  4%   191.9n ±  4%  -34.20% (p=0.000 n=10)
Common/.jpg-8     410.7n ±  9%   374.1n ± 17%   -8.90% (p=0.019 n=10)
Common/.png-8     425.1n ±  8%   327.8n ±  8%  -22.89% (p=0.000 n=10)
Common/.gif-8     480.4n ±  4%   396.9n ±  7%  -17.38% (p=0.000 n=10)
Common/.xls-8     427.2n ±  6%   324.7n ±  4%  -24.00% (p=0.000 n=10)
Common/.webm-8   1160.0n ±  5%   724.8n ±  5%  -37.52% (p=0.000 n=10)
Common/.csv-8     6.965µ ±  7%   5.671µ ±  6%  -18.57% (p=0.000 n=10)
geomean           784.9n         620.0n        -21.01%

               │ common_master │              common_dev               │
               │     B/op      │     B/op      vs base                 │
Common/.xlsx-8      312.0 ± 0%     312.0 ± 0%       ~ (p=1.000 n=10) ¹
Common/.pptx-8      592.0 ± 0%     592.0 ± 0%       ~ (p=1.000 n=10) ¹
Common/.docx-8      504.0 ± 0%     504.0 ± 0%       ~ (p=1.000 n=10) ¹
Common/.tar-8       200.0 ± 0%     192.0 ± 0%  -4.00% (p=0.000 n=10)
Common/.zip-8       224.0 ± 0%     224.0 ± 0%       ~ (p=1.000 n=10) ¹
Common/.pdf-8       192.0 ± 0%     192.0 ± 0%       ~ (p=1.000 n=10) ¹
Common/.jpg-8       192.0 ± 0%     192.0 ± 0%       ~ (p=1.000 n=10) ¹
Common/.png-8       192.0 ± 0%     192.0 ± 0%       ~ (p=1.000 n=10) ¹
Common/.gif-8       192.0 ± 0%     192.0 ± 0%       ~ (p=1.000 n=10) ¹
Common/.xls-8       288.0 ± 0%     288.0 ± 0%       ~ (p=1.000 n=10) ¹
Common/.webm-8      192.0 ± 0%     192.0 ± 0%       ~ (p=1.000 n=10) ¹
Common/.csv-8     7.390Ki ± 0%   7.323Ki ± 0%  -0.90% (p=0.000 n=10)
geomean             339.8          338.4       -0.41%
¹ all samples are equal

               │ common_master │              common_dev              │
               │   allocs/op   │ allocs/op   vs base                  │
Common/.xlsx-8      4.000 ± 0%   4.000 ± 0%        ~ (p=1.000 n=10) ¹
Common/.pptx-8      15.00 ± 0%   15.00 ± 0%        ~ (p=1.000 n=10) ¹
Common/.docx-8      13.00 ± 0%   13.00 ± 0%        ~ (p=1.000 n=10) ¹
Common/.tar-8       3.000 ± 0%   2.000 ± 0%  -33.33% (p=0.000 n=10)
Common/.zip-8       6.000 ± 0%   6.000 ± 0%        ~ (p=1.000 n=10) ¹
Common/.pdf-8       2.000 ± 0%   2.000 ± 0%        ~ (p=1.000 n=10) ¹
Common/.jpg-8       2.000 ± 0%   2.000 ± 0%        ~ (p=1.000 n=10) ¹
Common/.png-8       2.000 ± 0%   2.000 ± 0%        ~ (p=1.000 n=10) ¹
Common/.gif-8       2.000 ± 0%   2.000 ± 0%        ~ (p=1.000 n=10) ¹
Common/.xls-8       3.000 ± 0%   3.000 ± 0%        ~ (p=1.000 n=10) ¹
Common/.webm-8      2.000 ± 0%   2.000 ± 0%        ~ (p=1.000 n=10) ¹
Common/.csv-8       34.00 ± 0%   31.00 ± 0%   -8.82% (p=0.000 n=10)
geomean             4.349        4.173        -4.06%
¹ all samples are equal

            │ rand_master │              rand_dev               │
            │   sec/op    │   sec/op     vs base                │
SliceRand-8   457.4n ± 4%   398.1n ± 3%  -12.96% (p=0.000 n=10)

            │ rand_master │              rand_dev              │
            │    B/op     │    B/op     vs base                │
SliceRand-8   160.00 ± 0%   96.00 ± 0%  -40.00% (p=0.000 n=10)

            │ rand_master │              rand_dev              │
            │  allocs/op  │ allocs/op   vs base                │
SliceRand-8    4.000 ± 0%   1.000 ± 0%  -75.00% (p=0.000 n=10)
```


Patch:
diff --git a/internal/json/json.go b/internal/json/json.go
index ee39349..5b2ecee 100644
--- a/internal/json/json.go
+++ b/internal/json/json.go
@@ -34,6 +34,7 @@ package json
 
 import (
 	"fmt"
+	"sync"
 )
 
 type (
@@ -73,10 +74,31 @@ type (
 	}
 )
 
+var scannerPool = sync.Pool{
+	New: func() any {
+		return &scanner{}
+	},
+}
+
+func newScanner() *scanner {
+	s := scannerPool.Get().(*scanner)
+	s.reset()
+	return s
+}
+
+func freeScanner(s *scanner) {
+	// Avoid hanging on to too much memory in extreme cases.
+	if len(s.parseState) > 1024 {
+		s.parseState = nil
+	}
+	scannerPool.Put(s)
+}
+
 // Scan returns the number of bytes scanned and if there was any error
 // in trying to reach the end of data.
 func Scan(data []byte) (int, error) {
-	s := &scanner{}
+	s := newScanner()
+	defer freeScanner(s)
 	_ = checkValid(data, s)
 	return s.index, s.err
 }
@@ -84,7 +106,6 @@ func Scan(data []byte) (int, error) {
 // checkValid verifies that data is valid JSON-encoded data.
 // scan is passed in for use by checkValid to avoid an allocation.
 func checkValid(data []byte, scan *scanner) error {
-	scan.reset()
 	for _, c := range data {
 		scan.index++
 		if scan.step(scan, c) == scanError {
@@ -105,6 +126,8 @@ func (s *scanner) reset() {
 	s.step = stateBeginValue
 	s.parseState = s.parseState[0:0]
 	s.err = nil
+	s.endTop = false
+	s.index = 0
 }
 
 // eof tells the scanner that the end of input has been reached.
diff --git a/internal/magic/archive.go b/internal/magic/archive.go
index 554ac4d..b59042c 100644
--- a/internal/magic/archive.go
+++ b/internal/magic/archive.go
@@ -3,7 +3,6 @@ package magic
 import (
 	"bytes"
 	"encoding/binary"
-	"strconv"
 )
 
 var (
@@ -110,8 +109,8 @@ func Tar(raw []byte, _ uint32) bool {
 	}
 
 	// Get the checksum recorded into the file.
-	recsum, err := tarParseOctal(raw[148:156])
-	if err != nil {
+	recsum := tarParseOctal(raw[148:156])
+	if recsum == -1 {
 		return false
 	}
 	sum1, sum2 := tarChksum(raw)
@@ -119,28 +118,26 @@ func Tar(raw []byte, _ uint32) bool {
 }
 
 // tarParseOctal converts octal string to decimal int.
-func tarParseOctal(b []byte) (int64, error) {
+func tarParseOctal(b []byte) int64 {
 	// Because unused fields are filled with NULs, we need to skip leading NULs.
 	// Fields may also be padded with spaces or NULs.
 	// So we remove leading and trailing NULs and spaces to be sure.
 	b = bytes.Trim(b, " \x00")
 
 	if len(b) == 0 {
-		return 0, nil
+		return -1
 	}
-	x, err := strconv.ParseUint(tarParseString(b), 8, 64)
-	if err != nil {
-		return 0, err
-	}
-	return int64(x), nil
-}
-
-// tarParseString converts a NUL ended bytes slice to a string.
-func tarParseString(b []byte) string {
-	if i := bytes.IndexByte(b, 0); i >= 0 {
-		return string(b[:i])
+	ret := int64(0)
+	for _, b := range b {
+		if b == 0 {
+			break
+		}
+		if !(b >= '0' && b <= '7') {
+			return -1
+		}
+		ret = (ret << 3) | int64(b-'0')
 	}
-	return string(b)
+	return ret
 }
 
 // tarChksum computes the checksum for the header block b.


Test patch:
diff --git a/internal/magic/archive_test.go b/internal/magic/archive_test.go
index 12c9505..2c3d6d6 100644
--- a/internal/magic/archive_test.go
+++ b/internal/magic/archive_test.go
@@ -6,32 +6,23 @@ func TestTarParseOctal(t *testing.T) {
 	tests := []struct {
 		in   string
 		want int64
-		ok   bool
 	}{
-		{"0000000\x00", 0, true},
-		{" \x0000000\x00", 0, true},
-		{" \x0000003\x00", 3, true},
-		{"00000000227\x00", 0227, true},
-		{"032033\x00 ", 032033, true},
-		{"320330\x00 ", 0320330, true},
-		{"0000660\x00 ", 0660, true},
-		{"\x00 0000660\x00 ", 0660, true},
-		{"0123456789abcdef", 0, false},
-		{"0123456789\x00abcdef", 0, false},
-		{"01234567\x0089abcdef", 342391, true},
-		{"0123\x7e\x5f\x264123", 0, false},
+		{"0000000\x00", 0},
+		{" \x0000000\x00", 0},
+		{" \x0000003\x00", 3},
+		{"00000000227\x00", 0227},
+		{"032033\x00 ", 032033},
+		{"320330\x00 ", 0320330},
+		{"0000660\x00 ", 0660},
+		{"\x00 0000660\x00 ", 0660},
+		{"0123456789abcdef", -1},
+		{"0123456789\x00abcdef", -1},
+		{"01234567\x0089abcdef", 01234567},
+		{"0123\x7e\x5f\x264123", -1},
 	}
 
 	for _, tt := range tests {
-		got, err := tarParseOctal([]byte(tt.in))
-		ok := err == nil
-		if ok != tt.ok {
-			if tt.ok {
-				t.Errorf("parseOctal(%q): got parsing failure, want success", tt.in)
-			} else {
-				t.Errorf("parseOctal(%q): got parsing success, want failure", tt.in)
-			}
-		}
+		got := tarParseOctal([]byte(tt.in))
 		if got != tt.want {
 			t.Errorf("parseOctal(%q): got %d, want %d", tt.in, got, tt.want)
 		}
diff --git a/mimetype_test.go b/mimetype_test.go
index b76235b..39d6c43 100644
--- a/mimetype_test.go
+++ b/mimetype_test.go
@@ -488,11 +488,9 @@ func BenchmarkSliceRand(b *testing.B) {
 	b.ResetTimer()
 	b.ReportAllocs()
 
-	b.RunParallel(func(pb *testing.PB) {
-		for pb.Next() {
-			Detect(data)
-		}
-	})
+	for n := 0; n < b.N; n++ {
+		Detect(data)
+	}
 }
 
 func BenchmarkText(b *testing.B) {
@@ -513,6 +511,30 @@ func BenchmarkText(b *testing.B) {
 	}
 }
 
+// BenchmarkFiles benchmarks each detector with his coresponding file.
+func BenchmarkFiles(b *testing.B) {
+	for f, m := range files {
+		data, err := os.ReadFile(filepath.Join(testDataDir, f))
+		if err != nil {
+			b.Fatal(err)
+		}
+		if uint32(len(data)) > defaultLimit {
+			data = data[:defaultLimit]
+		}
+		b.Run(f+"/"+m, func(b *testing.B) {
+			b.ReportAllocs()
+			b.ResetTimer()
+			parsed, _, _ := mime.ParseMediaType(m)
+			mType := Lookup(parsed)
+			for n := 0; n < b.N; n++ {
+				if !mType.detector(data, uint32(len(data))) {
+					b.Fatal("detection should never fail")
+				}
+			}
+		})
+	}
+}
+
 func BenchmarkAll(b *testing.B) {
 	r := rand.New(rand.NewSource(0))
 	data := make([]byte, defaultLimit)