maintner: replace author/committer regexp with manual code

regexp was 25% of the CPU. Now it's lost in the long tail.

Change-Id: Ifc522d0690dff2e2524ec12354d3790946e5a54a
Reviewed-on: https://go-review.googlesource.com/38725
Reviewed-by: Kevin Burke <kev@inburke.com>
diff --git a/maintner/git.go b/maintner/git.go
index b37c929..8063af4 100644
--- a/maintner/git.go
+++ b/maintner/git.go
@@ -13,7 +13,6 @@
 	"fmt"
 	"log"
 	"os/exec"
-	"regexp"
 	"sort"
 	"strconv"
 	"strings"
@@ -368,28 +367,36 @@
 	return nil
 }
 
-var personRx = regexp.MustCompile(`^(.+) (\d+) ([\+\-]\d\d\d\d)\s*$`)
-
-//
 // parsePerson parses an "author" or "committer" value from "git cat-file -p COMMIT"
 // The values are like:
 //    Foo Bar <foobar@gmail.com> 1488624439 +0900
 // c.mu must be held for writing.
 func (c *Corpus) parsePerson(v []byte) (*gitPerson, time.Time, error) {
-	m := personRx.FindSubmatch(v) // TODO(bradfitz): for speed, don't use regexp :(
-	if m == nil {
+	v = bytes.TrimSpace(v)
+
+	lastSpace := bytes.LastIndexByte(v, ' ')
+	if lastSpace < 0 {
 		return nil, time.Time{}, errors.New("failed to match person")
 	}
+	tz := v[lastSpace+1:] // "+0800"
+	v = v[:lastSpace]     // now v is "Foo Bar <foobar@gmail.com> 1488624439"
 
-	ut, err := strconv.ParseInt(string(m[2]), 10, 64)
+	lastSpace = bytes.LastIndexByte(v, ' ')
+	if lastSpace < 0 {
+		return nil, time.Time{}, errors.New("failed to match person")
+	}
+	unixTime := v[lastSpace+1:]
+	nameEmail := v[:lastSpace] // now v is "Foo Bar <foobar@gmail.com>"
+
+	ut, err := strconv.ParseInt(string(unixTime), 10, 64)
 	if err != nil {
 		return nil, time.Time{}, err
 	}
-	t := time.Unix(ut, 0).In(c.gitLocation(string(m[3])))
+	t := time.Unix(ut, 0).In(c.gitLocation(tz))
 
-	p, ok := c.gitPeople[string(m[1])]
+	p, ok := c.gitPeople[string(nameEmail)]
 	if !ok {
-		p = &gitPerson{str: string(m[1])}
+		p = &gitPerson{str: string(nameEmail)}
 		if c.gitPeople == nil {
 			c.gitPeople = map[string]*gitPerson{}
 		}
@@ -401,21 +408,22 @@
 
 // v is like '[+-]hhmm'
 // c.mu must be held for writing.
-func (c *Corpus) gitLocation(v string) *time.Location {
-	if loc, ok := c.zoneCache[v]; ok {
+func (c *Corpus) gitLocation(v []byte) *time.Location {
+	if loc, ok := c.zoneCache[string(v)]; ok {
 		return loc
 	}
-	h, _ := strconv.Atoi(v[1:3])
-	m, _ := strconv.Atoi(v[3:5])
+	s := string(v)
+	h, _ := strconv.Atoi(s[1:3])
+	m, _ := strconv.Atoi(s[3:5])
 	east := 1
 	if v[0] == '-' {
 		east = -1
 	}
-	loc := time.FixedZone(v, east*(h*3600+m*60))
+	loc := time.FixedZone(s, east*(h*3600+m*60))
 	if c.zoneCache == nil {
 		c.zoneCache = map[string]*time.Location{}
 	}
-	c.zoneCache[v] = loc
+	c.zoneCache[s] = loc
 	return loc
 }
 
diff --git a/maintner/git_test.go b/maintner/git_test.go
new file mode 100644
index 0000000..4a40af9
--- /dev/null
+++ b/maintner/git_test.go
@@ -0,0 +1,58 @@
+// Copyright 2017 The Go 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 maintner
+
+import (
+	"reflect"
+	"testing"
+	"time"
+)
+
+func TestParsePerson(t *testing.T) {
+	var c Corpus
+
+	p, ct, err := c.parsePerson([]byte(" Foo Bar <foo@bar.com> 1257894000 -0800"))
+	if err != nil {
+		t.Fatal(err)
+	}
+	wantp := &gitPerson{str: "Foo Bar <foo@bar.com>"}
+	if !reflect.DeepEqual(p, wantp) {
+		t.Errorf("person = %+v; want %+v", p, wantp)
+	}
+	wantct := time.Unix(1257894000, 0)
+	if !ct.Equal(wantct) {
+		t.Errorf("commit time = %v; want %v", ct, wantct)
+	}
+	zoneName, off := ct.Zone()
+	if want := "-0800"; zoneName != want {
+		t.Errorf("zone name = %q; want %q", zoneName, want)
+	}
+	if want := -28800; off != want {
+		t.Errorf("offset = %v; want %v", off, want)
+	}
+
+	p2, ct2, err := c.parsePerson([]byte("Foo Bar <foo@bar.com> 1257894001 -0800"))
+	if err != nil {
+		t.Fatal(err)
+	}
+	if p != p2 {
+		t.Errorf("gitPerson pointer values differ; not sharing memory")
+	}
+	if !ct2.Equal(ct.Add(time.Second)) {
+		t.Errorf("wrong time")
+	}
+}
+
+func BenchmarkParsePerson(b *testing.B) {
+	b.ReportAllocs()
+	in := []byte(" Foo Bar <foo@bar.com> 1257894000 -0800")
+	var c Corpus
+	for i := 0; i < b.N; i++ {
+		_, _, err := c.parsePerson(in)
+		if err != nil {
+			b.Fatal(err)
+		}
+	}
+}