slog: add methods to Buffer

Add the methods Reset, WriteRune, WritePosInt and WritePosIntWidth to
buffer.Buffer and rewrite clients to use them.

This removes some ugliness.

Remove itoa and put concat in the only file it is used.

Change-Id: I3321f925e514606c97480454a3efbb511416fa2b
Reviewed-on: https://go-review.googlesource.com/c/exp/+/443160
Run-TryBot: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
diff --git a/slog/handler.go b/slog/handler.go
index 0a1afff..3200160 100644
--- a/slog/handler.go
+++ b/slog/handler.go
@@ -244,7 +244,7 @@
 				buf := buffer.New()
 				buf.WriteString(file) // TODO: escape?
 				buf.WriteByte(':')
-				itoa((*[]byte)(buf), line, -1)
+				buf.WritePosInt(line)
 				s := buf.String()
 				buf.Free()
 				state.appendAttr(String(key, s))
@@ -321,7 +321,7 @@
 // Separator for group names and keys.
 // We use the uncommon character middle-dot rather than an ordinary dot
 // to reduce the likelihood of ambiguous group structure.
-const keyComponentSep = "·" // Unicode middle dot
+const keyComponentSep = "·"
 
 // openGroup starts a new group of attributes
 // with the given name.
@@ -396,7 +396,7 @@
 		s.buf.WriteByte('"')
 		*s.buf = appendEscapedJSONString(*s.buf, file)
 		s.buf.WriteByte(':')
-		itoa((*[]byte)(s.buf), line, -1)
+		s.buf.WritePosInt(line)
 		s.buf.WriteByte('"')
 	} else {
 		// text
@@ -406,7 +406,7 @@
 			// common case: no quoting needed.
 			s.appendString(file)
 			s.buf.WriteByte(':')
-			itoa((*[]byte)(s.buf), line, -1)
+			s.buf.WritePosInt(line)
 		}
 	}
 }
@@ -442,47 +442,41 @@
 	if s.h.json {
 		appendJSONTime(s, t)
 	} else {
-		*s.buf = appendTimeRFC3339Millis(*s.buf, t)
+		writeTimeRFC3339Millis(s.buf, t)
 	}
 }
 
 // This takes half the time of Time.AppendFormat.
-func appendTimeRFC3339Millis(buf []byte, t time.Time) []byte {
-	// TODO: try to speed up by indexing the buffer.
-	char := func(b byte) {
-		buf = append(buf, b)
-	}
-
+func writeTimeRFC3339Millis(buf *buffer.Buffer, t time.Time) {
 	year, month, day := t.Date()
-	itoa(&buf, year, 4)
-	char('-')
-	itoa(&buf, int(month), 2)
-	char('-')
-	itoa(&buf, day, 2)
-	char('T')
+	buf.WritePosIntWidth(year, 4)
+	buf.WriteByte('-')
+	buf.WritePosIntWidth(int(month), 2)
+	buf.WriteByte('-')
+	buf.WritePosIntWidth(day, 2)
+	buf.WriteByte('T')
 	hour, min, sec := t.Clock()
-	itoa(&buf, hour, 2)
-	char(':')
-	itoa(&buf, min, 2)
-	char(':')
-	itoa(&buf, sec, 2)
+	buf.WritePosIntWidth(hour, 2)
+	buf.WriteByte(':')
+	buf.WritePosIntWidth(min, 2)
+	buf.WriteByte(':')
+	buf.WritePosIntWidth(sec, 2)
 	ns := t.Nanosecond()
-	char('.')
-	itoa(&buf, ns/1e6, 3)
+	buf.WriteByte('.')
+	buf.WritePosIntWidth(ns/1e6, 3)
 	_, offsetSeconds := t.Zone()
 	if offsetSeconds == 0 {
-		char('Z')
+		buf.WriteByte('Z')
 	} else {
 		offsetMinutes := offsetSeconds / 60
 		if offsetMinutes < 0 {
-			char('-')
+			buf.WriteByte('-')
 			offsetMinutes = -offsetMinutes
 		} else {
-			char('+')
+			buf.WriteByte('+')
 		}
-		itoa(&buf, offsetMinutes/60, 2)
-		char(':')
-		itoa(&buf, offsetMinutes%60, 2)
+		buf.WritePosIntWidth(offsetMinutes/60, 2)
+		buf.WriteByte(':')
+		buf.WritePosIntWidth(offsetMinutes%60, 2)
 	}
-	return buf
 }
diff --git a/slog/handler_test.go b/slog/handler_test.go
index fb5080d..db48930 100644
--- a/slog/handler_test.go
+++ b/slog/handler_test.go
@@ -373,28 +373,30 @@
 
 const rfc3339Millis = "2006-01-02T15:04:05.000Z07:00"
 
-func TestAppendTimeRFC3339(t *testing.T) {
+func TestWriteTimeRFC3339(t *testing.T) {
 	for _, tm := range []time.Time{
 		time.Date(2000, 1, 2, 3, 4, 5, 0, time.UTC),
 		time.Date(2000, 1, 2, 3, 4, 5, 400, time.Local),
 		time.Date(2000, 11, 12, 3, 4, 500, 5e7, time.UTC),
 	} {
 		want := tm.Format(rfc3339Millis)
-		var buf []byte
-		buf = appendTimeRFC3339Millis(buf, tm)
-		got := string(buf)
+		buf := buffer.New()
+		defer buf.Free()
+		writeTimeRFC3339Millis(buf, tm)
+		got := buf.String()
 		if got != want {
 			t.Errorf("got %s, want %s", got, want)
 		}
 	}
 }
 
-func BenchmarkAppendTime(b *testing.B) {
-	buf := make([]byte, 0, 100)
+func BenchmarkWriteTime(b *testing.B) {
+	buf := buffer.New()
+	defer buf.Free()
 	tm := time.Date(2022, 3, 4, 5, 6, 7, 823456789, time.Local)
 	b.ResetTimer()
 	for i := 0; i < b.N; i++ {
-		buf = appendTimeRFC3339Millis(buf, tm)
-		buf = buf[:0]
+		writeTimeRFC3339Millis(buf, tm)
+		buf.Reset()
 	}
 }
diff --git a/slog/internal/buffer/buffer.go b/slog/internal/buffer/buffer.go
index 60e6646..02cd08e 100644
--- a/slog/internal/buffer/buffer.go
+++ b/slog/internal/buffer/buffer.go
@@ -32,6 +32,11 @@
 		bufPool.Put(b)
 	}
 }
+
+func (b *Buffer) Reset() {
+	*b = (*b)[:0]
+}
+
 func (b *Buffer) Write(p []byte) (int, error) {
 	*b = append(*b, p...)
 	return len(p), nil
@@ -45,6 +50,35 @@
 	*b = append(*b, c)
 }
 
+func (b *Buffer) WritePosInt(i int) {
+	b.WritePosIntWidth(i, 0)
+}
+
+// WritePosIntWidth writes non-negative integer i to the buffer, padded on the left
+// by zeroes to the given width. Use a width of 0 to omit padding.
+func (b *Buffer) WritePosIntWidth(i, width int) {
+	// Cheap integer to fixed-width decimal ASCII.
+	// Copied from log/log.go.
+
+	if i < 0 {
+		panic("negative int")
+	}
+
+	// Assemble decimal in reverse order.
+	var bb [20]byte
+	bp := len(bb) - 1
+	for i >= 10 || width > 1 {
+		width--
+		q := i / 10
+		bb[bp] = byte('0' + i - q*10)
+		bp--
+		i = q
+	}
+	// i < 10
+	bb[bp] = byte('0' + i)
+	b.Write(bb[bp:])
+}
+
 func (b *Buffer) String() string {
 	return string(*b)
 }
diff --git a/slog/internal/buffer/buffer_test.go b/slog/internal/buffer/buffer_test.go
index dc3966d..323d411 100644
--- a/slog/internal/buffer/buffer_test.go
+++ b/slog/internal/buffer/buffer_test.go
@@ -12,8 +12,10 @@
 	b.WriteString("hello")
 	b.WriteByte(',')
 	b.Write([]byte(" world"))
+	b.WritePosIntWidth(17, 4)
+
 	got := b.String()
-	want := "hello, world"
+	want := "hello, world0017"
 	if got != want {
 		t.Errorf("got %q, want %q", got, want)
 	}
diff --git a/slog/logger_test.go b/slog/logger_test.go
index 3f3faab..4d171b4 100644
--- a/slog/logger_test.go
+++ b/slog/logger_test.go
@@ -372,3 +372,12 @@
 		t.Errorf("wanted canceled, got %v", err)
 	}
 }
+
+// concat returns a new slice with the elements of s1 followed
+// by those of s2. The slice has no additional capacity.
+func concat[T any](s1, s2 []T) []T {
+	s := make([]T, len(s1)+len(s2))
+	copy(s, s1)
+	copy(s[len(s1):], s2)
+	return s
+}
diff --git a/slog/record_test.go b/slog/record_test.go
index 08e3a09..78ebf46 100644
--- a/slog/record_test.go
+++ b/slog/record_test.go
@@ -131,7 +131,7 @@
 			buf := buffer.New()
 			buf.WriteString(file)
 			buf.WriteByte(':')
-			itoa((*[]byte)(buf), line, -1)
+			buf.WritePosInt(line)
 			s := buf.String()
 			buf.Free()
 			_ = s
diff --git a/slog/util.go b/slog/util.go
deleted file mode 100644
index f811ccd..0000000
--- a/slog/util.go
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright 2022 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 slog
-
-// concat returns a new slice with the elements of s1 followed
-// by those of s2. The slice has no additional capacity.
-func concat[T any](s1, s2 []T) []T {
-	s := make([]T, len(s1)+len(s2))
-	copy(s, s1)
-	copy(s[len(s1):], s2)
-	return s
-}
-
-// Cheap integer to fixed-width decimal ASCII. Give a negative width to avoid zero-padding.
-// Copied from log/log.go.
-func itoa(buf *[]byte, i int, wid int) {
-	// Assemble decimal in reverse order.
-	var b [20]byte
-	bp := len(b) - 1
-	for i >= 10 || wid > 1 {
-		wid--
-		q := i / 10
-		b[bp] = byte('0' + i - q*10)
-		bp--
-		i = q
-	}
-	// i < 10
-	b[bp] = byte('0' + i)
-	*buf = append(*buf, b[bp:]...)
-}