snapshot: sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1

Содержимое пина, зафиксированного в go.mod sing-box-lx, одним коммитом
без истории. Полная история SagerNet/gvisor — 1.45 ГБ и клонируется в
каждой CI-джобе; наша дельта — одна вставка в одну функцию, история для
неё не нужна.

Module path github.com/sagernet/gvisor сохранён намеренно: на него
опирается replace-директива суперпроекта.

Патч поверх — отдельным коммитом, чтобы дельта читалась одним git show
и переносилась на новый пин копированием.

SPECS/TASKS/048-GVISOR_HANDSHAKE_NIL_CRASH
This commit is contained in:
Leadaxe 2026-08-04 15:50:08 +03:00
commit 2c4ae3b0a4
712 changed files with 185689 additions and 0 deletions

901
pkg/timing/timing.go Normal file
View file

@ -0,0 +1,901 @@
// Copyright 2025 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package timing provides a way to record the timing of a series of
// operations across one or more goroutines.
package timing
import (
"fmt"
"sort"
"strings"
"time"
"unicode/utf8"
"github.com/sagernet/gvisor/pkg/atomicbitops"
"github.com/sagernet/gvisor/pkg/log"
)
const (
// DEBUG TIP: Set this to true if you find yourself debugging the case where
// you have stranded timers
// (Log messages like "Still waiting for XYZ child timelines to finish".)
// This will enable **thread-unsafe** code that logs which timers are still
// running.
debugStrandedTimers = false
// fullTimestampFormat is the format string for a timestamp with nanosecond
// precision but no date component.
fullTimestampFormat = "15:04:05.000000000"
// microsTimestampFormat is the format string for a timestamp with
// microsecond precision but no date component.
microsTimestampFormat = "15:04:05.000000"
)
// Timeline is a series of points in time.
//
// A Timeline always has a defined start time, and will eventually have an end
// time. For this reason, `End` must always be called on a Timeline.
//
// A Timeline may have zero or more mid-points contained between the start and
// end times.
//
// A Timeline may fork to represent other timelines running concurrently. Such
// children Timelines may or may not end later than the parent does.
//
// A single Timeline struct should be owned by a single goroutine at a given
// time until it ends (i.e. its endpoint becomes defined), at which point
// ownership transfers to the goroutine that owns the Timer that created it.
//
// A Timeline may be nil, in which case all methods are no-ops. This means all
// code that takes in a Timeline parameter does not need to check for nilness.
type Timeline struct {
// name is the name of the Timeline.
name string
// fullName is the fully-qualified name of this Timeline, including the
// names of its ancestors.
fullName string
// timer is the Timer that owns this Timeline.
// This is nil on orphaned Timelines.
timer *Timer
// start is when the Timeline started.
start time.Time
// midpoints is a list of MidPoints that have been reached on this Timeline.
midpoints []MidPoint
// end is when the Timeline ended.
// The zero value means the Timeline has not yet ended.
end time.Time
// children is a list of forked timelines that are children of this one.
// Note that children do not necessarily need to end before the parent does.
children []*Timeline
// invalidReason, if non-empty, is the reason why this Timeline is invalid.
// A Timeline is also invalid if any of its children are invalid.
invalidReason string
}
// MidPoint is a named point in time on a Timeline.
// The starting and ending points of a Timeline are not MidPoints.
type MidPoint struct {
// when is when the midpoint was reached.
when time.Time
// name is the name of the midpoint.
name string
}
// Reached records a new midpoint on the Timeline.
func (s *Timeline) Reached(name string) {
if s == nil {
return
}
s.ReachedAt(name, time.Now())
}
// ReachedAt records a new midpoint on the root Timeline of the Timer with
// the given timestamp.
func (s *Timeline) ReachedAt(name string, when time.Time) {
if s == nil {
return
}
s.midpoints = append(s.midpoints, MidPoint{
when: when,
name: name,
})
if log.IsLogging(log.Debug) {
if s.timer != nil {
log.Debugf("Timer for %s: Timeline %s reached midpoint %s at %s (unix nanos: %d)", s.timer.root.name, s.fullName, name, when.Format(fullTimestampFormat), when.UnixNano())
} else {
log.Debugf("Orphaned timeline %s reached midpoint %s at %s (unix nanos: %d)", s.name, name, when.Format(fullTimestampFormat), when.UnixNano())
}
}
}
// Fork creates a new Timeline that is a child of this one.
// A midpoint is implicitly added to the current Timeline.
//
// The returned Timeline is initially owned by the caller, but may be passed
// to another goroutine if desired.
//
// A child timeline may but does not need to end before the parent does.
//
// Forked timelines are useful to represent parallel operations like separate
// goroutines, and are actually required in such cases so that the goroutine
// can own its own Timeline, but non-concurrent code may also use Fork to
// represent its own linear operations as a tree if it so desires.
func (s *Timeline) Fork(name string) *Timeline {
if s == nil {
return nil
}
now := time.Now()
if s.timer == nil {
panic("timing.Timeline.Fork called on Timeline that has no parent; must call Timer.Adopt first")
}
s.timer.runningTimelines.Add(1)
sub := &Timeline{
name: name,
fullName: fmt.Sprintf("%s/%s", s.fullName, name),
timer: s.timer,
start: now,
}
s.children = append(s.children, sub)
s.midpoints = append(s.midpoints, MidPoint{
when: now,
name: "forked",
})
// Check for log level here to avoid allocating a string to format the
// timestamp if it is not going to be logged.
if log.IsLogging(log.Debug) {
log.Debugf("Timer for %s: Timeline %s forked into child timeline %s at %s (unix nanos: %d)", s.timer.root.name, s.fullName, sub.fullName, now.Format(fullTimestampFormat), now.UnixNano())
}
return sub
}
// MultiFork creates new Timelines that are children of this one.
// It returns as many Timelines as there are names in `names`.
// All of them share the same start time.
// A midpoint is implicitly added to the current Timeline.
// The same semantics as `Timeline.Fork` apply.
func (s *Timeline) MultiFork(names []string) []*Timeline {
if len(names) == 0 {
return nil
}
if s == nil {
return make([]*Timeline, len(names))
}
now := time.Now()
if s.timer == nil {
panic("timing.Timeline.MultiFork called on Timeline that has no parent; must call Timer.Adopt first")
}
s.timer.runningTimelines.Add(int64(len(names)))
children := make([]*Timeline, len(names))
for i, name := range names {
children[i] = &Timeline{
name: name,
fullName: fmt.Sprintf("%s/%s", s.fullName, name),
timer: s.timer,
start: now,
}
}
s.children = append(s.children, children...)
s.midpoints = append(s.midpoints, MidPoint{
when: now,
name: "forked",
})
// Check for log level here to avoid allocating a string to format the
// timestamp if it is not going to be logged.
if log.IsLogging(log.Debug) {
log.Debugf("Timer for %s: Timeline %s forked %d-way into child timelines %v at %s (unix nanos: %d)", s.timer.root.name, s.fullName, len(names), names, now.Format(fullTimestampFormat), now.UnixNano())
}
return children
}
// traverse visits all Timelines in the tree rooted at s.
// fn is called exactly once per Timeline as the `child` argument.
// The root Timeline has a `nil` parent.
func (s *Timeline) traverse(parent *Timeline, fn func(parent, child *Timeline)) {
if s == nil {
return
}
fn(parent, s)
for _, child := range s.children {
child.traverse(s, fn)
}
}
// End marks the Timeline as having ended. It must be eventually called on all
// Timelines.
// After End is called, the ownership of the Timeline struct moves to the
// goroutine that owns the Timer that created it.
func (s *Timeline) End() {
if s == nil {
return
}
end := time.Now()
if s.timer == nil {
log.Debugf("Orphaned timeline %s ended without having been adopted. This is possibly unintended.", s.name)
return
}
if !s.end.IsZero() {
log.Debugf("Timer for %s: Timeline %s ended twice. This is possibly unintended.", s.timer.root.name, s.fullName)
return
}
s.end = end
s.timer.runningTimelines.Add(-1)
// Check for log level here to avoid allocating a string to format the
// timestamp if it is not going to be logged.
if log.IsLogging(log.Debug) {
log.Debugf("Timer for %s: Timeline %s ended at %s (unix nanos: %d)", s.timer.root.name, s.fullName, s.end.Format(fullTimestampFormat), s.end.UnixNano())
}
}
// Invalidate marks the Timeline as invalid.
func (s *Timeline) Invalidate(reason string) {
if s == nil {
return
}
if s.invalidReason != "" {
log.Warningf("Timer for %s: Timeline %s was already invalid (%v), but tried to invalidate again (%v)", s.timer.root.name, s.fullName, s.invalidReason, reason)
return
}
s.invalidReason = reason
}
// A Lease is a reference to a Timeline that is valid until the Lease is
// canceled. After calling Lease on a Timeline, the caller should no longer
// use the Timeline directly, and should instead use the Lease exclusively.
//
// Leases should typically not cross function boundaries.
//
// Leases are useful in complex functions where ownership of a Timeline
// needs to be *conditionally transferred* to a different goroutine at some
// late point in the function. Consider this example:
//
// ```
//
// func SomeLongFunction(timeline *timing.Timeline) {
// defer timeline.End() // Convenient to defer `End` to hit all the `return` branches.
// timeline.Reached("some_point")
//
// if err := something(timeline); err != nil {
// return
// }
// timeline.Reached("some_other_point")
// if err := somethingElse(); err != nil {
// timeline.Reached("some_error")
// return
// }
// timeline.Reached("another_point")
// go doSomethingElse(timeline)
//
// // Don't want to call `End` anymore here!
// }
//
// ```
//
// With a Lease:
//
// ```
//
// func SomeLongFunction(timeline *timing.Timeline) {
// lease := timeline.Lease()
// defer lease.End()
// lease.Reached("some_point")
//
// if err := something(); err != nil {
// lease.Reached("some_error")
// return
// }
// if err := somethingElse(); err != nil {
// lease.Reached("some_other_error")
// return
// }
// lease.Reached("another_point")
// go doSomethingElse(lease.Transfer())
//
// // `End` is not called anymore here.
// }
//
// ```
type Lease struct {
timeline *Timeline
valid bool
}
// Reached records a new midpoint on the Timeline if the Lease is valid.
// See `Timeline.Reached` for more details.
func (l *Lease) Reached(name string) {
if l == nil || !l.valid {
return
}
l.timeline.Reached(name)
}
// Fork forks the Timeline if the Lease is valid.
// See `Timeline.Fork` for more details.
func (l *Lease) Fork(name string) *Timeline {
if l == nil || !l.valid {
return nil
}
return l.timeline.Fork(name)
}
// MultiFork forks the Timeline if the Lease is valid.
// See `Timeline.MultiFork` for more details.
func (l *Lease) MultiFork(names []string) []*Timeline {
if l == nil || !l.valid {
return nil
}
return l.timeline.MultiFork(names)
}
// End ends the Timeline if the Lease is valid.
// The lease is invalidated after this call.
// See `Timeline.End` for more details.
func (l *Lease) End() {
if l == nil || !l.valid {
return
}
l.valid = false
l.timeline.End()
}
// Invalidate invalidates the Timeline if the Lease is valid.
// See `Timeline.Invalidate` for more details.
func (l *Lease) Invalidate(reason string) {
if l == nil || !l.valid {
return
}
l.timeline.Invalidate(reason)
}
// Transfer invalidates the current Lease and returns the underlying Timeline.
// Typically useful when transferring ownership of a Timeline to a different
// goroutine while giving up ownership in the current one.
// See `Lease` documentation for example usage.
func (l *Lease) Transfer() *Timeline {
if l == nil {
return nil
}
if !l.valid {
panic("timing.Lease.Transfer called on invalid Lease")
}
l.valid = false
return l.timeline
}
// Lease returns a Lease for the Timeline.
// The Lease is valid until it is canceled by calling `End` or `Transfer`.
// See `Lease` for example usage.
func (s *Timeline) Lease() *Lease {
if s == nil {
return nil
}
return &Lease{
timeline: s,
valid: true,
}
}
// OrphanTimeline creates a new Timeline that is not owned by any Timer.
// This is useful for operations that are part of a broader sequence of
// operations represented by a Timer, but where timing measurements are
// desired before this broader parent Timer is known.
// The returned Timeline must be parented with `timer.Adopt` in order to
// be useful.
func OrphanTimeline(name string, startTime time.Time) *Timeline {
// Check for log level here to avoid allocating a string to format the
// timestamp if it is not going to be logged.
if log.IsLogging(log.Debug) {
log.Debugf("Orphaned timeline %s started at %s (unix nanos: %d)", name, startTime.Format(fullTimestampFormat), startTime.UnixNano())
}
return &Timeline{
name: name,
fullName: "_ORPHANED_",
start: startTime,
}
}
// Timer is a root Timeline. It keeps track of one or more running Timelines,
// and can be pretty-printed to show timing information once all Timelines have
// ended.
//
// A Timer struct may move between goroutines, but only one goroutine may own
// it at a time.
//
// A Timer struct may be nil, in which case all methods are no-ops. This means
// all code that takes in a Timer parameter does not need to check for nilness.
type Timer struct {
// root is the root Timeline of the Timer.
root *Timeline
// runningTimelines is the number of Timelines that have not yet ended.
// When dumping timing data, this is used to wait for all Timelines to end.
runningTimelines atomicbitops.Int64
}
// New creates a new Timer.
// The given name is used to identify the Timer in pretty-printed output.
// The given startTime is used as the start time of the Timer's root Timeline.
func New(name string, startTime time.Time) *Timer {
log.Infof("Timer for %s: Starting.", name)
timer := &Timer{}
timer.runningTimelines.Store(1)
root := &Timeline{
name: name,
fullName: name,
timer: timer,
start: startTime,
}
timer.root = root
return timer
}
// StartTime returns the start time of the Timer.
func (t *Timer) StartTime() time.Time {
if t == nil {
return time.Time{}
}
return t.root.start
}
// Reached records a new midpoint on the root Timeline of the Timer.
func (t *Timer) Reached(name string) {
if t == nil {
return
}
t.root.Reached(name)
}
// ReachedAt records a new midpoint on the root Timeline of the Timer with
// the given timestamp.
func (t *Timer) ReachedAt(name string, when time.Time) {
if t == nil {
return
}
t.root.ReachedAt(name, when)
}
// Fork creates a new Timeline that is a child of the root Timeline of this
// Timer.
// The returned Timeline is initially owned by the caller, but may be passed
// to another goroutine if desired.
// This child Timeline may but does not need to end before the root timeline
// does.
// Forked timelines are useful to represent parallel operations like separate
// goroutines, and are actually required in such cases so that the goroutine
// can own its own Timeline, but non-concurrent code may also use Fork to
// represent its own linear operations as a tree if it so desires.
func (t *Timer) Fork(name string) *Timeline {
if t == nil {
return nil
}
return t.root.Fork(name)
}
// MultiFork creates new Timelines that are children of the root Timeline of
// this Timer.
// See Timeline.MultiFork for more details.
func (t *Timer) MultiFork(names []string) []*Timeline {
if len(names) == 0 {
return nil
}
if t == nil {
return make([]*Timeline, len(names))
}
return t.root.MultiFork(names)
}
// Adopt adopts a Timeline into this Timer.
// May only be called with Timelines created by `OrphanTimeline`,
// and may only be called once per such Timeline.
// The calling goroutine must own both the Timeline and the Timer.
func (t *Timer) Adopt(child *Timeline) {
if t == nil || child == nil {
return
}
if child.timer != nil {
panic("timing.Timeline.Adopt called on Timeline that already has a parent")
}
if child.end.IsZero() {
t.runningTimelines.Add(1)
}
t.root.children = append(t.root.children, child)
t.root.midpoints = append(t.root.midpoints, MidPoint{
name: "new orphan",
when: child.start,
})
child.timer = t
child.fullName = fmt.Sprintf("%s/%s", t.root.fullName, child.name)
log.Debugf("Timer for %s: Timeline %s adopted.", t.root.name, child.fullName)
}
// End waits for all Timelines owned by this Timer to end, then pretty-prints
// timing information.
// If not all Timelines have ended by the time End is called, End will spin in
// place until they do, and eventually print a warning log if it spins for too
// long (but will not give up).
// End is called implicitly by Log, so it is not necessary to call End
// explicitly unless there is a need to end the root timeline at a different
// time than when logging its data is desired.
func (t *Timer) End() {
if t == nil {
return
}
var peakRunningChildren int64
if t.root.end.IsZero() {
t.root.End()
if peakRunningChildren = t.runningTimelines.Load(); peakRunningChildren > 0 {
log.Infof("Timer for %s: Root timeline ended, but %d child timelines are still running...", t.root.name, peakRunningChildren)
} else {
log.Infof("Timer for %s: Ended.", t.root.name)
}
}
const (
stillWaitingLogThreshold = 10 * time.Second
stillWaitingLogInterval = 1 * time.Second
)
startedWaiting := time.Now()
var rlLogger log.Logger
for runningTimelines := t.runningTimelines.Load(); runningTimelines != 0; runningTimelines = t.runningTimelines.Load() {
if runningTimelines < 0 {
panic("timing.Timeline.End called too many times in aggregate")
}
peakRunningChildren = max(peakRunningChildren, runningTimelines)
time.Sleep(1 * time.Millisecond)
if rlLogger == nil && time.Since(startedWaiting) > stillWaitingLogThreshold {
rlLogger = log.BasicRateLimitedLogger(stillWaitingLogInterval)
}
if rlLogger != nil {
if debugStrandedTimers {
// **Thread-unsafe** code to traverse the tree and print out all
// timelines that are still running. Only executed when
// the `debugStrandedTimers` const is true.
var timelineNames []string
t.root.traverse(nil, func(_, child *Timeline) {
if child.end.IsZero() {
timelineNames = append(timelineNames, child.fullName)
}
})
rlLogger.Debugf("Timer for %s: Still waiting for %d child timelines to finish: %v", t.root.name, runningTimelines, timelineNames)
// Sleep longer to make sure we don't do the above traversal every single millisecond.
time.Sleep(100 * time.Millisecond)
} else {
rlLogger.Infof("Timer for %s: Still waiting for %d child timelines to finish... (If you are not expecting this, flip `debugStrandedTimers` to debug.)", t.root.name, runningTimelines)
}
}
}
if peakRunningChildren > 0 {
log.Infof("Timer for %s: All child timelines have ended.", t.root.name)
}
}
// Log pretty-prints timing information for the root Timeline of the Timer.
// If `t.End` has not yet been called, it will be called implicitly.
// This also means that this function will wait for all child Timelines to end
// before pretty-printing, and will spin in place until this is the case.
// If debug logging is enabled, this function will also log a flat list of
// events that can be easily machine-parsed to the debug log.
func (t *Timer) Log() {
if t == nil {
return
}
t.End()
type pointType int
const (
pointTypeStart pointType = iota
pointTypeMid
pointTypeEnd
)
type point struct {
timeline *Timeline
pointType pointType
midpointName string
}
type event struct {
when time.Time
point point
}
totalDuration := t.root.end.Sub(t.root.start)
formatDuration := func(d time.Duration) string {
switch {
case totalDuration < time.Second:
us := d.Microseconds()
if us >= 1000 {
return fmt.Sprintf("%d %03dµs", us/1000, us%1000)
}
return fmt.Sprintf("%dµs", us)
case totalDuration < 3*time.Minute:
return fmt.Sprintf("%.3fs", float64(d.Milliseconds())/1000)
default:
return d.Truncate(time.Second).String()
}
}
var flatTimelines []*Timeline
t.root.traverse(nil, func(_, child *Timeline) {
flatTimelines = append(flatTimelines, child)
})
var invalidReasons []string
for _, timeline := range flatTimelines {
if timeline.invalidReason != "" {
invalidReasons = append(invalidReasons, timeline.invalidReason)
}
}
if len(invalidReasons) > 0 {
log.Warningf("Timer for %s: Timeline was invalidated, so not displaying timing data: %v", t.root.name, invalidReasons)
return
}
var events []event
for _, timeline := range flatTimelines {
events = append(events, event{when: timeline.start, point: point{timeline: timeline, pointType: pointTypeStart}})
for _, mid := range timeline.midpoints {
events = append(events, event{when: mid.when, point: point{timeline: timeline, pointType: pointTypeMid, midpointName: mid.name}})
}
events = append(events, event{when: timeline.end, point: point{timeline: timeline, pointType: pointTypeEnd}})
}
sort.Slice(events, func(i, j int) bool {
return events[i].when.Before(events[j].when)
})
type dedupEvent struct {
when time.Time
points []point
}
var dedupEvents []dedupEvent
for _, e := range events {
if len(dedupEvents) == 0 || !dedupEvents[len(dedupEvents)-1].when.Equal(e.when) {
dedupEvents = append(dedupEvents, dedupEvent{when: e.when, points: []point{e.point}})
} else {
dedupEvents[len(dedupEvents)-1].points = append(dedupEvents[len(dedupEvents)-1].points, e.point)
}
}
if len(dedupEvents) == 0 {
return
}
largestInterval := time.Duration(0)
for i := 1; i < len(dedupEvents); i++ {
if interval := dedupEvents[i].when.Sub(dedupEvents[i-1].when); interval > largestInterval {
largestInterval = interval
}
}
rows := make([][]string, len(dedupEvents))
colWidths := make([]int, len(flatTimelines)+3)
lastTimestampPerTimeline := make(map[*Timeline]time.Time)
for i, e := range dedupEvents {
colData := make([]string, 0, len(flatTimelines)+3)
colData = append(colData, e.when.Format(microsTimestampFormat))
if i == 0 {
colData = append(colData, "")
colData = append(colData, "")
} else {
sincePrevious := e.when.Sub(dedupEvents[i-1].when)
colData = append(colData, fmt.Sprintf("+%s", formatDuration(sincePrevious)))
colData = append(colData, barChart(float64(sincePrevious)/float64(largestInterval), 12))
}
for _, timeline := range flatTimelines {
lastTimestamp, ok := lastTimestampPerTimeline[timeline]
if !ok {
lastTimestamp = timeline.start
}
timelineChanged := false
for _, p := range e.points {
if p.timeline == timeline {
switch p.pointType {
case pointTypeStart:
colData = append(colData, fmt.Sprintf("╭─ %s", timeline.name))
case pointTypeMid:
colData = append(colData, fmt.Sprintf("├─ %s: %s", p.midpointName, formatDuration(e.when.Sub(lastTimestamp))))
case pointTypeEnd:
colData = append(colData, fmt.Sprintf("╰─ END %s, total %s", formatDuration(timeline.end.Sub(lastTimestamp)), formatDuration(timeline.end.Sub(timeline.start))))
}
timelineChanged = true
break
}
}
if timelineChanged {
lastTimestampPerTimeline[timeline] = e.when
continue
}
if e.when.Before(timeline.start) || e.when.After(timeline.end) {
colData = append(colData, "")
} else {
colData = append(colData, fmt.Sprintf("│ ... %s ...", formatDuration(e.when.Sub(lastTimestamp))))
}
}
rows[i] = colData
}
// Best-effort minimization of the number of columns.
for collapsed := true; collapsed; {
collapsed = false
for col := 4; col < len(rows[0]); col++ {
// Find continuous ranges of non-blank cells.
type colRange struct{ start, end int }
var ranges []colRange
rangeStart := -1
for i := 0; i < len(rows); i++ {
if isBlank := rows[i][col] == ""; rangeStart == -1 && !isBlank {
rangeStart = i
} else if rangeStart != -1 && isBlank {
ranges = append(ranges, colRange{start: rangeStart, end: i})
rangeStart = -1
}
}
if rangeStart != -1 {
ranges = append(ranges, colRange{start: rangeStart, end: len(rows) - 1})
}
// For each range, check if it can be collapsed.
rangIndex := -1
for k, rang := range ranges {
allClear := true
for i := rang.start; i <= rang.end; i++ {
if rows[i][col-1] != "" {
allClear = false
break
}
}
if allClear {
rangIndex = k
break
}
}
if rangIndex != -1 {
for i := ranges[rangIndex].start; i <= ranges[rangIndex].end; i++ {
rows[i][col-1], rows[i][col] = rows[i][col], rows[i][col-1]
}
collapsed = true
}
}
}
// Trim empty columns from the right.
largestNonEmptyCol := len(rows[0]) - 1
for col := len(rows[0]) - 1; col >= 4; col-- {
allEmpty := true
for i := range rows {
if rows[i][col] != "" {
allEmpty = false
break
}
}
if !allEmpty {
largestNonEmptyCol = col
break
}
}
for i := range rows {
rows[i] = rows[i][:largestNonEmptyCol+1]
}
// Measure column widths.
for _, row := range rows {
for i, cell := range row {
if i < len(row)-1 {
restEmpty := true
if i < len(row)-1 {
for j := i + 1; j < len(row); j++ {
if row[j] != "" {
restEmpty = false
break
}
}
}
// Only count cells where the rest of the row is not empty.
if !restEmpty {
colWidths[i] = max(colWidths[i], utf8.RuneCountInString(cell))
}
}
}
}
// Build table.
var sb strings.Builder
sb.WriteString("---- ")
sb.WriteString(t.root.name)
sb.WriteString(" timing information ----\n")
for i, row := range rows {
for j, cell := range row {
// Check if the rest of the row is empty, and if so, break.
emptyRest := true
for k := j; k < len(row); k++ {
if row[k] != "" {
emptyRest = false
break
}
}
if emptyRest {
break
}
// Process cell.
switch j {
case 0: // Timestamp column.
sb.WriteRune('[')
for s := utf8.RuneCountInString(cell); s < colWidths[j]; s++ {
sb.WriteRune(' ')
}
sb.WriteString(cell)
sb.WriteRune(']')
case 1: // Delta column.
sb.WriteRune('\t')
for s := utf8.RuneCountInString(cell); s < colWidths[j]; s++ {
sb.WriteRune(' ')
}
sb.WriteString(cell)
case 2: // Delta bar chart column.
sb.WriteRune(' ') // Only one space of width from first column since it is reflecting the same quantity.
sb.WriteString(cell)
for s := utf8.RuneCountInString(cell); s < colWidths[j]; s++ {
sb.WriteRune(' ')
}
default:
// Other columns.
sb.WriteRune('\t')
sb.WriteString(cell)
if j < len(rows[i])-1 {
for s := utf8.RuneCountInString(cell); s < colWidths[j]; s++ {
sb.WriteRune(' ')
}
}
}
}
sb.WriteRune('\n')
}
sb.WriteString("---- End of ")
sb.WriteString(t.root.name)
sb.WriteString(" timing information ----\n")
// Log it.
log.Infof("%s", sb.String())
// In debug mode, also log a flat list of events that can be easily machine-parsed.
if log.IsLogging(log.Debug) {
for _, e := range events {
switch e.point.pointType {
case pointTypeStart:
log.Debugf("Timer for %s: time %d %s start %s", t.root.name, e.when.UnixNano(), e.when.Format(fullTimestampFormat), e.point.timeline.fullName)
case pointTypeMid:
log.Debugf("Timer for %s: time %d %s mid %s = %s", t.root.name, e.when.UnixNano(), e.when.Format(fullTimestampFormat), e.point.timeline.fullName, e.point.midpointName)
case pointTypeEnd:
log.Debugf("Timer for %s: time %d %s end %s", t.root.name, e.when.UnixNano(), e.when.Format(fullTimestampFormat), e.point.timeline.fullName)
}
}
}
}
// barChart returns a string of width characters that represents the given
// fraction of the given width.
func barChart(fraction float64, width int) string {
const chars = " ▏▎▍▌▋▊█▉"
numChars := utf8.RuneCountInString(chars)
pivotIndex := int(fraction * float64(width))
pivotRuneIndex := max(0, min(numChars-1, int((fraction-(float64(pivotIndex)/float64(width)))*float64(width*numChars))))
runes := make([]rune, width)
for i := 0; i < width; i++ {
if i < pivotIndex {
runes[i] = []rune(chars)[numChars-1]
} else if i == pivotIndex {
runes[i] = []rune(chars)[pivotRuneIndex]
} else {
runes[i] = []rune(chars)[0]
}
}
return string(runes)
}