diff --git a/src/cmd/trace/annotations.go b/src/cmd/trace/annotations.go
index 0addc240be..df194a7598 100644
--- a/src/cmd/trace/annotations.go
+++ b/src/cmd/trace/annotations.go
@@ -9,8 +9,8 @@ import (
"fmt"
"html/template"
"internal/trace"
+ "internal/trace/traceviewer"
"log"
- "math"
"net/http"
"net/url"
"reflect"
@@ -808,122 +808,9 @@ func newRegionFilter(r *http.Request) (*regionFilter, error) {
}, nil
}
-type durationHistogram struct {
- Count int
- Buckets []int
- MinBucket, MaxBucket int
-}
-
-// Five buckets for every power of 10.
-var logDiv = math.Log(math.Pow(10, 1.0/5))
-
-func (h *durationHistogram) add(d time.Duration) {
- var bucket int
- if d > 0 {
- bucket = int(math.Log(float64(d)) / logDiv)
- }
- if len(h.Buckets) <= bucket {
- h.Buckets = append(h.Buckets, make([]int, bucket-len(h.Buckets)+1)...)
- h.Buckets = h.Buckets[:cap(h.Buckets)]
- }
- h.Buckets[bucket]++
- if bucket < h.MinBucket || h.MaxBucket == 0 {
- h.MinBucket = bucket
- }
- if bucket > h.MaxBucket {
- h.MaxBucket = bucket
- }
- h.Count++
-}
-
-func (h *durationHistogram) BucketMin(bucket int) time.Duration {
- return time.Duration(math.Exp(float64(bucket) * logDiv))
-}
-
-func niceDuration(d time.Duration) string {
- var rnd time.Duration
- var unit string
- switch {
- case d < 10*time.Microsecond:
- rnd, unit = time.Nanosecond, "ns"
- case d < 10*time.Millisecond:
- rnd, unit = time.Microsecond, "µs"
- case d < 10*time.Second:
- rnd, unit = time.Millisecond, "ms"
- default:
- rnd, unit = time.Second, "s "
- }
- return fmt.Sprintf("%d%s", d/rnd, unit)
-}
-
-func (h *durationHistogram) ToHTML(urlmaker func(min, max time.Duration) string) template.HTML {
- if h == nil || h.Count == 0 {
- return template.HTML("")
- }
-
- const barWidth = 400
-
- maxCount := 0
- for _, count := range h.Buckets {
- if count > maxCount {
- maxCount = count
- }
- }
-
- w := new(strings.Builder)
- fmt.Fprintf(w, `
`)
- for i := h.MinBucket; i <= h.MaxBucket; i++ {
- // Tick label.
- if h.Buckets[i] > 0 {
- fmt.Fprintf(w, `%s | `, urlmaker(h.BucketMin(i), h.BucketMin(i+1)), niceDuration(h.BucketMin(i)))
- } else {
- fmt.Fprintf(w, `
%s | `, niceDuration(h.BucketMin(i)))
- }
- // Bucket bar.
- width := h.Buckets[i] * barWidth / maxCount
- fmt.Fprintf(w, ` | `, width)
- // Bucket count.
- fmt.Fprintf(w, `%d | `, h.Buckets[i])
- fmt.Fprintf(w, "
\n")
-
- }
- // Final tick label.
- fmt.Fprintf(w, `%s |
`, niceDuration(h.BucketMin(h.MaxBucket+1)))
- fmt.Fprintf(w, `
`)
- return template.HTML(w.String())
-}
-
-func (h *durationHistogram) String() string {
- const barWidth = 40
-
- labels := []string{}
- maxLabel := 0
- maxCount := 0
- for i := h.MinBucket; i <= h.MaxBucket; i++ {
- // TODO: This formatting is pretty awful.
- label := fmt.Sprintf("[%-12s%-11s)", h.BucketMin(i).String()+",", h.BucketMin(i+1))
- labels = append(labels, label)
- if len(label) > maxLabel {
- maxLabel = len(label)
- }
- count := h.Buckets[i]
- if count > maxCount {
- maxCount = count
- }
- }
-
- w := new(strings.Builder)
- for i := h.MinBucket; i <= h.MaxBucket; i++ {
- count := h.Buckets[i]
- bar := count * barWidth / maxCount
- fmt.Fprintf(w, "%*s %-*s %d\n", maxLabel, labels[i-h.MinBucket], barWidth, strings.Repeat("█", bar), count)
- }
- return w.String()
-}
-
type regionStats struct {
regionTypeID
- Histogram durationHistogram
+ Histogram traceviewer.TimeHistogram
}
func (s *regionStats) UserRegionURL() func(min, max time.Duration) string {
@@ -933,7 +820,7 @@ func (s *regionStats) UserRegionURL() func(min, max time.Duration) string {
}
func (s *regionStats) add(region regionDesc) {
- s.Histogram.add(region.duration())
+ s.Histogram.Add(region.duration())
}
var templUserRegionTypes = template.Must(template.New("").Parse(`
@@ -966,8 +853,8 @@ var templUserRegionTypes = template.Must(template.New("").Parse(`
type taskStats struct {
Type string
- Count int // Complete + incomplete tasks
- Histogram durationHistogram // Complete tasks only
+ Count int // Complete + incomplete tasks
+ Histogram traceviewer.TimeHistogram // Complete tasks only
}
func (s *taskStats) UserTaskURL(complete bool) func(min, max time.Duration) string {
@@ -979,7 +866,7 @@ func (s *taskStats) UserTaskURL(complete bool) func(min, max time.Duration) stri
func (s *taskStats) add(task *taskDesc) {
s.Count++
if task.complete() {
- s.Histogram.add(task.duration())
+ s.Histogram.Add(task.duration())
}
}
@@ -1169,7 +1056,7 @@ func isUserAnnotationEvent(ev *trace.Event) (taskID uint64, ok bool) {
var templUserRegionType = template.Must(template.New("").Funcs(template.FuncMap{
"prettyDuration": func(nsec int64) template.HTML {
d := time.Duration(nsec) * time.Nanosecond
- return template.HTML(niceDuration(d))
+ return template.HTML(d.String())
},
"percent": func(dividend, divisor int64) template.HTML {
if divisor == 0 {
diff --git a/src/cmd/trace/goroutines.go b/src/cmd/trace/goroutines.go
index 7850fc0ff1..28eace82fa 100644
--- a/src/cmd/trace/goroutines.go
+++ b/src/cmd/trace/goroutines.go
@@ -169,7 +169,7 @@ func httpGoroutine(w http.ResponseWriter, r *http.Request) {
var templGoroutine = template.Must(template.New("").Funcs(template.FuncMap{
"prettyDuration": func(nsec int64) template.HTML {
d := time.Duration(nsec) * time.Nanosecond
- return template.HTML(niceDuration(d))
+ return template.HTML(d.String())
},
"percent": func(dividend, divisor int64) template.HTML {
if divisor == 0 {
diff --git a/src/internal/trace/traceviewer/histogram.go b/src/internal/trace/traceviewer/histogram.go
new file mode 100644
index 0000000000..d4c8749dc9
--- /dev/null
+++ b/src/internal/trace/traceviewer/histogram.go
@@ -0,0 +1,86 @@
+// Copyright 2023 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 traceviewer
+
+import (
+ "fmt"
+ "html/template"
+ "math"
+ "strings"
+ "time"
+)
+
+// TimeHistogram is an high-dynamic-range histogram for durations.
+type TimeHistogram struct {
+ Count int
+ Buckets []int
+ MinBucket, MaxBucket int
+}
+
+// Five buckets for every power of 10.
+var logDiv = math.Log(math.Pow(10, 1.0/5))
+
+// Add adds a single sample to the histogram.
+func (h *TimeHistogram) Add(d time.Duration) {
+ var bucket int
+ if d > 0 {
+ bucket = int(math.Log(float64(d)) / logDiv)
+ }
+ if len(h.Buckets) <= bucket {
+ h.Buckets = append(h.Buckets, make([]int, bucket-len(h.Buckets)+1)...)
+ h.Buckets = h.Buckets[:cap(h.Buckets)]
+ }
+ h.Buckets[bucket]++
+ if bucket < h.MinBucket || h.MaxBucket == 0 {
+ h.MinBucket = bucket
+ }
+ if bucket > h.MaxBucket {
+ h.MaxBucket = bucket
+ }
+ h.Count++
+}
+
+// BucketMin returns the minimum duration value for a provided bucket.
+func (h *TimeHistogram) BucketMin(bucket int) time.Duration {
+ return time.Duration(math.Exp(float64(bucket) * logDiv))
+}
+
+// ToHTML renders the histogram as HTML.
+func (h *TimeHistogram) ToHTML(urlmaker func(min, max time.Duration) string) template.HTML {
+ if h == nil || h.Count == 0 {
+ return template.HTML("")
+ }
+
+ const barWidth = 400
+
+ maxCount := 0
+ for _, count := range h.Buckets {
+ if count > maxCount {
+ maxCount = count
+ }
+ }
+
+ w := new(strings.Builder)
+ fmt.Fprintf(w, ``)
+ for i := h.MinBucket; i <= h.MaxBucket; i++ {
+ // Tick label.
+ if h.Buckets[i] > 0 {
+ fmt.Fprintf(w, `%s | `, urlmaker(h.BucketMin(i), h.BucketMin(i+1)), h.BucketMin(i))
+ } else {
+ fmt.Fprintf(w, `
%s | `, h.BucketMin(i))
+ }
+ // Bucket bar.
+ width := h.Buckets[i] * barWidth / maxCount
+ fmt.Fprintf(w, ` | `, width)
+ // Bucket count.
+ fmt.Fprintf(w, `%d | `, h.Buckets[i])
+ fmt.Fprintf(w, "
\n")
+
+ }
+ // Final tick label.
+ fmt.Fprintf(w, `%s |
`, h.BucketMin(h.MaxBucket+1))
+ fmt.Fprintf(w, `
`)
+ return template.HTML(w.String())
+}