Skip to content

Commit 06450a8

Browse files
committed
internal/runtime/cgroup: add line-by-line reader using a single scratch buffer
Change-Id: I6a6a636ca21edcc6f16705fbb72a5241d4f7f22d Reviewed-on: https://go-review.googlesource.com/c/go/+/668637 Reviewed-by: Michael Knyszek <mknyszek@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
1 parent e59e128 commit 06450a8

File tree

5 files changed

+375
-0
lines changed

5 files changed

+375
-0
lines changed

src/cmd/internal/objabi/pkgspecial.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ var runtimePkgs = []string{
4949
"runtime",
5050

5151
"internal/runtime/atomic",
52+
"internal/runtime/cgroup",
5253
"internal/runtime/exithook",
5354
"internal/runtime/gc",
5455
"internal/runtime/maps",

src/go/build/deps_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ var depsRules = `
9898
< internal/runtime/math
9999
< internal/runtime/maps
100100
< internal/runtime/strconv
101+
< internal/runtime/cgroup
101102
< runtime
102103
< sync/atomic
103104
< internal/sync
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright 2025 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 cgroup
6+
7+
type LineReader = lineReader
8+
9+
func (l *LineReader) Next() error {
10+
return l.next()
11+
}
12+
13+
func (l *LineReader) Line() []byte {
14+
return l.line()
15+
}
16+
17+
func NewLineReader(fd int, scratch []byte, read func(fd int, b []byte) (int, uintptr)) *LineReader {
18+
return newLineReader(fd, scratch, read)
19+
}
20+
21+
var (
22+
ErrEOF = errEOF
23+
ErrIncompleteLine = errIncompleteLine
24+
)
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
// Copyright 2025 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 cgroup
6+
7+
import (
8+
"internal/bytealg"
9+
)
10+
11+
// stringError is a trival implementation of error, equivalent to errors.New,
12+
// which cannot be imported from a runtime package.
13+
type stringError string
14+
15+
func (e stringError) Error() string {
16+
return string(e)
17+
}
18+
19+
// All errors are explicit converted to type error in global initialization to
20+
// ensure that the linker allocates a static interface value. This is necessary
21+
// because these errors may be used before the allocator is available.
22+
23+
var (
24+
// The entire line did not fit into the scratch buffer.
25+
errIncompleteLine error = stringError("incomplete line")
26+
27+
// A system call failed.
28+
errSyscallFailed error = stringError("syscall failed")
29+
30+
// Reached EOF.
31+
errEOF error = stringError("end of file")
32+
)
33+
34+
// lineReader reads line-by-line using only a single fixed scratch buffer.
35+
//
36+
// When a single line is too long for the scratch buffer, the remainder of the
37+
// line will be skipped.
38+
type lineReader struct {
39+
read func(fd int, b []byte) (int, uintptr)
40+
fd int
41+
scratch []byte
42+
43+
n int // bytes of scratch in use.
44+
newline int // index of the first newline in scratch.
45+
46+
eof bool // read reached EOF.
47+
}
48+
49+
// newLineReader returns a lineReader which reads lines from fd.
50+
//
51+
// fd is the file descriptor to read from.
52+
//
53+
// scratch is the scratch buffer to read into. Note that len(scratch) is the
54+
// longest line that can be read. Lines longer than len(scratch) will have the
55+
// remainder of the line skipped. See next for more details.
56+
//
57+
// read is the function used to read more bytes from fd. This is usually
58+
// internal/runtime/syscall.Read. Note that this follows syscall semantics (not
59+
// io.Reader), so EOF is indicated with n=0, errno=0.
60+
func newLineReader(fd int, scratch []byte, read func(fd int, b []byte) (n int, errno uintptr)) *lineReader {
61+
return &lineReader{
62+
read: read,
63+
fd: fd,
64+
scratch: scratch,
65+
n: 0,
66+
newline: -1,
67+
}
68+
}
69+
70+
// next advances to the next line.
71+
//
72+
// May return errIncompleteLine if the scratch buffer is too small to hold the
73+
// entire line, in which case [r.line] will return the beginning of the line. A
74+
// subsequent call to next will skip the remainder of the incomplete line.
75+
//
76+
// N.B. this behavior is important for /proc/self/mountinfo. Some lines
77+
// (mounts), such as overlayfs, may be extremely long due to long super-block
78+
// options, but we don't care about those. The mount type will appear early in
79+
// the line.
80+
//
81+
// Returns errEOF when there are no more lines.
82+
func (r *lineReader) next() error {
83+
// Three cases:
84+
//
85+
// 1. First call, no data read.
86+
// 2. Previous call had a complete line. Drop it and look for the end
87+
// of the next line.
88+
// 3. Previous call had an incomplete line. Find the end of that line
89+
// (start of the next line), and the end of the next line.
90+
91+
prevComplete := r.newline >= 0
92+
firstCall := r.n == 0
93+
94+
for {
95+
if prevComplete {
96+
// Drop the previous line.
97+
copy(r.scratch, r.scratch[r.newline+1:r.n])
98+
r.n -= r.newline + 1
99+
100+
r.newline = bytealg.IndexByte(r.scratch[:r.n], '\n')
101+
if r.newline >= 0 {
102+
// We have another line already in scratch. Done.
103+
return nil
104+
}
105+
}
106+
107+
// No newline available.
108+
109+
if !prevComplete {
110+
// If the previous line was incomplete, we are
111+
// searching for the end of that line and have no need
112+
// for any buffered data.
113+
r.n = 0
114+
}
115+
116+
n, errno := r.read(r.fd, r.scratch[r.n:len(r.scratch)])
117+
if errno != 0 {
118+
return errSyscallFailed
119+
}
120+
r.n += n
121+
122+
if r.n == 0 {
123+
// Nothing left.
124+
//
125+
// N.B. we can't immediately return EOF when read
126+
// returns 0 as we may still need to return an
127+
// incomplete line.
128+
return errEOF
129+
}
130+
131+
r.newline = bytealg.IndexByte(r.scratch[:r.n], '\n')
132+
if prevComplete || firstCall {
133+
// Already have the start of the line, just need to find the end.
134+
135+
if r.newline < 0 {
136+
// We filled the entire buffer or hit EOF, but
137+
// still no newline.
138+
return errIncompleteLine
139+
}
140+
141+
// Found the end of the line. Done.
142+
return nil
143+
} else {
144+
// Don't have the start of the line. We are currently
145+
// looking for the end of the previous line.
146+
147+
if r.newline < 0 {
148+
// Not there yet.
149+
if n == 0 {
150+
// No more to read.
151+
return errEOF
152+
}
153+
continue
154+
}
155+
156+
// Found the end of the previous line. The next
157+
// iteration will drop the remainder of the previous
158+
// line and look for the next line.
159+
prevComplete = true
160+
}
161+
}
162+
}
163+
164+
// line returns a view of the current line, excluding the trailing newline.
165+
//
166+
// If [r.next] returned errIncompleteLine, then this returns only the beginning
167+
// of the line.
168+
//
169+
// Preconditions: [r.next] is called prior to the first call to line.
170+
//
171+
// Postconditions: The caller must not keep a reference to the returned slice.
172+
func (r *lineReader) line() []byte {
173+
if r.newline < 0 {
174+
// Incomplete line
175+
return r.scratch[:r.n]
176+
}
177+
// Complete line.
178+
return r.scratch[:r.newline]
179+
}
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
// Copyright 2025 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 cgroup_test
6+
7+
import (
8+
"internal/runtime/cgroup"
9+
"io"
10+
"strings"
11+
"testing"
12+
)
13+
14+
func TestLineReader(t *testing.T) {
15+
type nextLine struct {
16+
line string
17+
incomplete bool // next call before this line should return incomplete
18+
}
19+
complete := func(s string) nextLine {
20+
return nextLine{line: s}
21+
}
22+
incomplete := func(s string) nextLine {
23+
return nextLine{line: s, incomplete: true}
24+
}
25+
26+
const scratchSize = 8
27+
28+
tests := []struct {
29+
name string
30+
contents string
31+
want []nextLine
32+
}{
33+
{
34+
name: "empty",
35+
contents: "",
36+
},
37+
{
38+
name: "single",
39+
contents: "1234\n",
40+
want: []nextLine{
41+
complete("1234"),
42+
},
43+
},
44+
{
45+
name: "single-incomplete",
46+
contents: "1234",
47+
want: []nextLine{
48+
incomplete("1234"),
49+
},
50+
},
51+
{
52+
name: "single-exact",
53+
contents: "1234567\n",
54+
want: []nextLine{
55+
complete("1234567"),
56+
},
57+
},
58+
{
59+
name: "single-exact-incomplete",
60+
contents: "12345678",
61+
want: []nextLine{
62+
incomplete("12345678"),
63+
},
64+
},
65+
{
66+
name: "multi",
67+
contents: `1234
68+
5678
69+
`,
70+
want: []nextLine{
71+
complete("1234"),
72+
complete("5678"),
73+
},
74+
},
75+
{
76+
name: "multi-short",
77+
contents: `12
78+
34
79+
56
80+
78
81+
`,
82+
want: []nextLine{
83+
complete("12"),
84+
complete("34"),
85+
complete("56"),
86+
complete("78"),
87+
},
88+
},
89+
{
90+
name: "multi-notrailingnewline",
91+
contents: `1234
92+
5678`,
93+
want: []nextLine{
94+
complete("1234"),
95+
incomplete("5678"),
96+
},
97+
},
98+
{
99+
name: "middle-too-long",
100+
contents: `1234
101+
1234567890
102+
5678
103+
`,
104+
want: []nextLine{
105+
complete("1234"),
106+
incomplete("12345678"),
107+
complete("5678"),
108+
},
109+
},
110+
{
111+
// Multiple reads required to find newline.
112+
name: "middle-way-too-long",
113+
contents: `1234
114+
12345678900000000000000000000000000000000000000000000000000
115+
5678
116+
`,
117+
want: []nextLine{
118+
complete("1234"),
119+
incomplete("12345678"),
120+
complete("5678"),
121+
},
122+
},
123+
}
124+
125+
for _, tc := range tests {
126+
t.Run(tc.name, func(t *testing.T) {
127+
r := strings.NewReader(tc.contents)
128+
read := func(fd int, b []byte) (int, uintptr) {
129+
n, err := r.Read(b)
130+
if err != nil && err != io.EOF {
131+
const dummyErrno = 42
132+
return n, dummyErrno
133+
}
134+
return n, 0
135+
}
136+
137+
var scratch [scratchSize]byte
138+
l := cgroup.NewLineReader(0, scratch[:], read)
139+
140+
var got []nextLine
141+
for {
142+
err := l.Next()
143+
if err == cgroup.ErrEOF {
144+
break
145+
} else if err == cgroup.ErrIncompleteLine {
146+
got = append(got, incomplete(string(l.Line())))
147+
} else if err != nil {
148+
t.Fatalf("next got err %v", err)
149+
} else {
150+
got = append(got, complete(string(l.Line())))
151+
}
152+
}
153+
154+
if len(got) != len(tc.want) {
155+
t.Logf("got lines %+v", got)
156+
t.Logf("want lines %+v", tc.want)
157+
t.Fatalf("lineReader got %d lines, want %d", len(got), len(tc.want))
158+
}
159+
160+
for i := range got {
161+
if got[i].line != tc.want[i].line {
162+
t.Errorf("line %d got %q want %q", i, got[i].line, tc.want[i].line)
163+
}
164+
if got[i].incomplete != tc.want[i].incomplete {
165+
t.Errorf("line %d got incomplete %v want %v", i, got[i].incomplete, tc.want[i].incomplete)
166+
}
167+
}
168+
})
169+
}
170+
}

0 commit comments

Comments
 (0)