Skip to content

Commit bee5968

Browse files
committed
slog: Records
Add the Record type, used to communicate between the Loggers and Handlers. We use a combination of a small inline array and a functional list so that copies of a Record do not affect each other's sequence of Attrs. Change-Id: I4f800d6fccf7b4ba9a2d2e4b151f540fc883f335 Reviewed-on: https://go-review.googlesource.com/c/exp/+/426023 Run-TryBot: Jonathan Amsterdam <[email protected]> Reviewed-by: Alan Donovan <[email protected]>
1 parent 145caa8 commit bee5968

File tree

5 files changed

+430
-0
lines changed

5 files changed

+430
-0
lines changed

slog/list.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package slog
2+
3+
// A list[T] is an immutable sequence.
4+
// It supports three operations: append, len and indexing (at).
5+
// The zero value is an empty list.
6+
//
7+
// Repeated calls to append happen in amortized O(1) space and time. (Appending
8+
// an element allocates one node directly, and the normalize operation always
9+
// doubles the front slice, so we can charge two slots to each element.)
10+
//
11+
// The len method takes constant time.
12+
//
13+
// The at method requires a normalized list, and then takes constant time.
14+
//
15+
// It is possible to obtain quadratic behavior by alternating append and at:
16+
// the normalize required by at is called for each appended element, causing
17+
// front to be copied each time.
18+
type list[T any] struct {
19+
front []T
20+
back *node[T] // reversed
21+
lenBack int
22+
}
23+
24+
type node[T any] struct {
25+
el T
26+
next *node[T]
27+
}
28+
29+
// append returns a new list consisting of the receiver with x appended.
30+
func (l list[T]) append(x T) list[T] {
31+
if l.front == nil {
32+
// Empty list; return one with one element.
33+
return list[T]{
34+
front: []T{x},
35+
back: nil,
36+
lenBack: 0,
37+
}
38+
}
39+
if l.lenBack == len(l.front) {
40+
// When there are as many elements in back as in front, grow
41+
// front and move all of back to it.
42+
l = l.normalize()
43+
}
44+
// Push a new node with the element onto back, which is stored in
45+
// reverse order.
46+
return list[T]{
47+
front: l.front,
48+
back: &node[T]{el: x, next: l.back},
49+
lenBack: l.lenBack + 1,
50+
}
51+
}
52+
53+
// len returns the number of elements in the list.
54+
func (l list[T]) len() int {
55+
return len(l.front) + l.lenBack
56+
}
57+
58+
// at returns the ith element of the list.
59+
// The list must be normalized.
60+
func (l list[T]) at(i int) T {
61+
if l.back != nil {
62+
panic("not normalized")
63+
}
64+
return l.front[i]
65+
}
66+
67+
// normalize returns a list whose back is nil and whose front contains all the
68+
// receiver's elements.
69+
func (l list[T]) normalize() list[T] {
70+
if l.back == nil {
71+
return l
72+
}
73+
newFront := make([]T, len(l.front)+l.lenBack)
74+
copy(newFront, l.front)
75+
i := len(newFront) - 1
76+
for b := l.back; b != nil; b = b.next {
77+
newFront[i] = b.el
78+
i--
79+
}
80+
return list[T]{
81+
front: newFront,
82+
back: nil,
83+
lenBack: 0,
84+
}
85+
}

slog/list_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package slog
2+
3+
import (
4+
"testing"
5+
6+
"golang.org/x/exp/slices"
7+
)
8+
9+
func TestList(t *testing.T) {
10+
var l list[int]
11+
for i := 0; i < 10; i++ {
12+
l = l.append(i)
13+
}
14+
l = l.normalize()
15+
var got, want []int
16+
for i := 0; i < l.len(); i++ {
17+
want = append(want, i)
18+
got = append(got, l.at(i))
19+
}
20+
if !slices.Equal(got, want) {
21+
t.Errorf("got %v, want %v", got, want)
22+
}
23+
}
24+
25+
func TestListAlloc(t *testing.T) {
26+
for n := 1; n < 100; n++ {
27+
got := testing.AllocsPerRun(1, func() {
28+
var l list[int]
29+
for i := 0; i < n; i++ {
30+
l = l.append(i)
31+
}
32+
})
33+
want := 1.5 * float64(n)
34+
if got > want {
35+
t.Fatalf("n=%d: got %f allocations, want <= %f",
36+
n, got, want)
37+
}
38+
}
39+
}

slog/record.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// Copyright 2022 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package slog
6+
7+
import (
8+
"runtime"
9+
"time"
10+
)
11+
12+
const nAttrsInline = 5
13+
14+
// A Record holds information about a log event.
15+
type Record struct {
16+
// The time at which the output method (Log, Info, etc.) was called.
17+
time time.Time
18+
19+
// The log message.
20+
message string
21+
22+
// The level of the event.
23+
level Level
24+
25+
// The pc at the time the record was constructed, as determined
26+
// by runtime.Callers using the calldepth argument to NewRecord.
27+
pc uintptr
28+
29+
// Allocation optimization: an inline array sized to hold
30+
// the majority of log calls (based on examination of open-source
31+
// code). The array holds the end of the sequence of Attrs.
32+
tail [nAttrsInline]Attr
33+
34+
// The number of Attrs in tail.
35+
nTail int
36+
37+
// The sequence of Attrs except for the tail, represented as a functional
38+
// list of arrays.
39+
attrs list[[nAttrsInline]Attr]
40+
}
41+
42+
// MakeRecord creates a new Record from the given arguments.
43+
// Use [Record.AddAttr] to add attributes to the Record.
44+
// If calldepth is greater than zero, [Record.SourceLine] will
45+
// return the file and line number at that depth,
46+
// where 1 means the caller of MakeRecord.
47+
//
48+
// MakeRecord is intended for logging APIs that want to support a [Handler] as
49+
// a backend.
50+
func MakeRecord(t time.Time, level Level, msg string, calldepth int) Record {
51+
var p uintptr
52+
if calldepth > 0 {
53+
p = pc(calldepth + 1)
54+
}
55+
return Record{
56+
time: t,
57+
message: msg,
58+
level: level,
59+
pc: p,
60+
}
61+
}
62+
63+
func pc(depth int) uintptr {
64+
var pcs [1]uintptr
65+
runtime.Callers(depth, pcs[:])
66+
return pcs[0]
67+
}
68+
69+
// Time returns the time of the log event.
70+
func (r *Record) Time() time.Time { return r.time }
71+
72+
// Message returns the log message.
73+
func (r *Record) Message() string { return r.message }
74+
75+
// Level returns the level of the log event.
76+
func (r *Record) Level() Level { return r.level }
77+
78+
// SourceLine returns the file and line of the log event.
79+
// If the Record was created without the necessary information,
80+
// or if the location is unavailable, it returns ("", 0).
81+
func (r *Record) SourceLine() (file string, line int) {
82+
fs := runtime.CallersFrames([]uintptr{r.pc})
83+
// TODO: error-checking?
84+
f, _ := fs.Next()
85+
return f.File, f.Line
86+
}
87+
88+
// Attrs returns a copy of the sequence of Attrs in r.
89+
func (r *Record) Attrs() []Attr {
90+
res := make([]Attr, 0, r.attrs.len()*nAttrsInline+r.nTail)
91+
r.attrs = r.attrs.normalize()
92+
for _, f := range r.attrs.front {
93+
res = append(res, f[:]...)
94+
}
95+
for _, a := range r.tail[:r.nTail] {
96+
res = append(res, a)
97+
}
98+
return res
99+
}
100+
101+
// NumAttrs returns the number of Attrs in r.
102+
func (r *Record) NumAttrs() int {
103+
return r.attrs.len()*nAttrsInline + r.nTail
104+
}
105+
106+
// Attr returns the i'th Attr in r.
107+
func (r *Record) Attr(i int) Attr {
108+
if r.attrs.back != nil {
109+
r.attrs = r.attrs.normalize()
110+
}
111+
alen := r.attrs.len() * nAttrsInline
112+
if i < alen {
113+
return r.attrs.at(i / nAttrsInline)[i%nAttrsInline]
114+
}
115+
return r.tail[i-alen]
116+
}
117+
118+
// AddAttr appends an attributes to the record's list of attributes.
119+
// It does not check for duplicate keys.
120+
func (r *Record) AddAttr(a Attr) {
121+
if r.nTail == len(r.tail) {
122+
r.attrs = r.attrs.append(r.tail)
123+
r.nTail = 0
124+
}
125+
r.tail[r.nTail] = a
126+
r.nTail++
127+
}

slog/record_test.go

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
// Copyright 2022 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package slog
6+
7+
import (
8+
"strings"
9+
"testing"
10+
"time"
11+
12+
"golang.org/x/exp/slices"
13+
"golang.org/x/exp/slog/internal/buffer"
14+
)
15+
16+
func TestRecordAttrs(t *testing.T) {
17+
as := []Attr{Int("k1", 1), String("k2", "foo"), Int("k3", 3),
18+
Int64("k4", -1), Float64("f", 3.1), Uint64("u", 999)}
19+
r := newRecordWithAttrs(as)
20+
if g, w := r.NumAttrs(), len(as); g != w {
21+
t.Errorf("NumAttrs: got %d, want %d", g, w)
22+
}
23+
if got := r.Attrs(); !attrsEqual(got, as) {
24+
t.Errorf("got %v, want %v", got, as)
25+
}
26+
}
27+
28+
func TestRecordSourceLine(t *testing.T) {
29+
// Zero call depth => empty file/line
30+
for _, test := range []struct {
31+
depth int
32+
wantFile string
33+
wantLinePositive bool
34+
}{
35+
{0, "", false},
36+
{-16, "", false},
37+
{1, "record.go", true},
38+
} {
39+
r := MakeRecord(time.Time{}, 0, "", test.depth)
40+
gotFile, gotLine := r.SourceLine()
41+
if i := strings.LastIndexByte(gotFile, '/'); i >= 0 {
42+
gotFile = gotFile[i+1:]
43+
}
44+
if gotFile != test.wantFile || (gotLine > 0) != test.wantLinePositive {
45+
t.Errorf("depth %d: got (%q, %d), want (%q, %t)",
46+
test.depth, gotFile, gotLine, test.wantFile, test.wantLinePositive)
47+
}
48+
}
49+
}
50+
51+
func TestAliasing(t *testing.T) {
52+
intAttrs := func(from, to int) []Attr {
53+
var as []Attr
54+
for i := from; i < to; i++ {
55+
as = append(as, Int("k", i))
56+
}
57+
return as
58+
}
59+
60+
check := func(r *Record, want []Attr) {
61+
t.Helper()
62+
got := r.Attrs()
63+
if !attrsEqual(got, want) {
64+
t.Errorf("got %v, want %v", got, want)
65+
}
66+
}
67+
68+
r1 := MakeRecord(time.Time{}, 0, "", 0)
69+
for i := 0; i < nAttrsInline+3; i++ {
70+
r1.AddAttr(Int("k", i))
71+
}
72+
check(&r1, intAttrs(0, nAttrsInline+3))
73+
r2 := r1
74+
check(&r2, intAttrs(0, nAttrsInline+3))
75+
// if cap(r1.attrs2) <= len(r1.attrs2) {
76+
// t.Fatal("cap not greater than len")
77+
// }
78+
r1.AddAttr(Int("k", nAttrsInline+3))
79+
r2.AddAttr(Int("k", -1))
80+
check(&r1, intAttrs(0, nAttrsInline+4))
81+
check(&r2, append(intAttrs(0, nAttrsInline+3), Int("k", -1)))
82+
}
83+
84+
func newRecordWithAttrs(as []Attr) Record {
85+
r := MakeRecord(time.Now(), InfoLevel, "", 0)
86+
for _, a := range as {
87+
r.AddAttr(a)
88+
}
89+
return r
90+
}
91+
92+
func attrsEqual(as1, as2 []Attr) bool {
93+
return slices.EqualFunc(as1, as2, Attr.Equal)
94+
}
95+
96+
// Currently, pc(2) takes over 400ns, which is too expensive
97+
// to call it for every log message.
98+
func BenchmarkPC(b *testing.B) {
99+
b.ReportAllocs()
100+
var x uintptr
101+
for i := 0; i < b.N; i++ {
102+
x = pc(3)
103+
}
104+
_ = x
105+
}
106+
107+
func BenchmarkSourceLine(b *testing.B) {
108+
r := MakeRecord(time.Now(), InfoLevel, "", 5)
109+
b.Run("alone", func(b *testing.B) {
110+
for i := 0; i < b.N; i++ {
111+
file, line := r.SourceLine()
112+
_ = file
113+
_ = line
114+
}
115+
})
116+
b.Run("stringifying", func(b *testing.B) {
117+
for i := 0; i < b.N; i++ {
118+
file, line := r.SourceLine()
119+
buf := buffer.New()
120+
buf.WriteString(file)
121+
buf.WriteByte(':')
122+
itoa((*[]byte)(buf), line, -1)
123+
s := string(*buf)
124+
buf.Free()
125+
_ = s
126+
}
127+
})
128+
}
129+
130+
func BenchmarkRecord(b *testing.B) {
131+
const nAttrs = nAttrsInline * 10
132+
var a Attr
133+
134+
for i := 0; i < b.N; i++ {
135+
r := MakeRecord(time.Time{}, InfoLevel, "", 0)
136+
for j := 0; j < nAttrs; j++ {
137+
r.AddAttr(Int("k", j))
138+
}
139+
for j := 0; j < nAttrs; j++ {
140+
a = r.Attr(j)
141+
}
142+
}
143+
_ = a
144+
}

0 commit comments

Comments
 (0)