livelog: annotate log truncation

Build log truncation is currently silent and can easily go unnoticed
unless you notice missing expected output (such as a filename line
following a function name in a stack trace).

Adjust livelog to explicitly annotate truncation at the end of a log so
that it is very clear to readers.

Fixes golang/go#45972

Change-Id: I5ad555232ba670bfb37d2eb3d2260307468c28a9
Reviewed-on: https://go-review.googlesource.com/c/build/+/317209
Trust: Michael Pratt <mpratt@google.com>
Run-TryBot: Michael Pratt <mpratt@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
diff --git a/livelog/livelog.go b/livelog/livelog.go
index c5b0c77..0f954af 100644
--- a/livelog/livelog.go
+++ b/livelog/livelog.go
@@ -11,15 +11,32 @@
 	"sync"
 )
 
-const MaxBufferSize = 2 << 20 // 2MB of output is way more than we expect.
+const (
+	// MaxBufferSize is the maximum buffer size, as it is more output than
+	// we expect from reasonable tests.
+	MaxBufferSize = 2 << 20 // 2 MB
 
-// Buffer is a WriteCloser that provides multiple Readers that each yield the same data.
-// It is safe to Write to a Buffer while Readers consume that data.
-// Its zero value is a ready-to-use buffer.
+	// truncationMessage is added to the end of the log when it reaches the
+	// maximum size.
+	truncationMessage = "\n\n... log truncated ..."
+
+	// maxUserSize is the total user output we can place in the buffer
+	// while still leaving room for the truncation message.
+	maxUserSize = MaxBufferSize - len(truncationMessage)
+)
+
+// Buffer is an io.WriteCloser that provides multiple Readers that each yield
+// the same data.
+//
+// It is safe to Write to a Buffer while Readers consume data. A Buffer has a
+// maximum size of MaxBufferSize, after which Write will silently drop
+// additional data and the buffer will contain a truncation note at the end.
+//
+// The zero value is a ready-to-use buffer.
 type Buffer struct {
 	mu     sync.Mutex // Guards the fields below.
 	wake   *sync.Cond // Created on demand by reader.
-	buf    []byte
+	buf    []byte // Length is in the range [0, MaxBufferSize].
 	eof    bool
 	lastID int
 }
@@ -30,11 +47,20 @@
 	b.mu.Lock()
 	defer b.mu.Unlock()
 
+	needTrunc := false
 	b2len := len(b2)
-	if len(b.buf)+b2len > MaxBufferSize {
-		b2 = b2[:MaxBufferSize-len(b.buf)]
+	if len(b.buf) == MaxBufferSize {
+		// b.buf is full and truncationMessage was written.
+		b2 = nil
+	} else if len(b.buf)+b2len > maxUserSize {
+		b2 = b2[:maxUserSize-len(b.buf)]
+		needTrunc = true
+		// After this write, b.buf will reach MaxBufferSize length.
 	}
 	b.buf = append(b.buf, b2...)
+	if needTrunc {
+		b.buf = append(b.buf, []byte(truncationMessage)...)
+	}
 	b.wakeReaders()
 	return b2len, nil
 }