From ea7db1c98cc803af358680a7bc41d5d1691da42a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Mon, 5 Oct 2026 14:56:35 -0400 Subject: [PATCH] wip-devtools --- messages_devtools.go | 6 + .../snapshots/darwin/trace_waterfall-dark.png | Bin 0 -> 15132 bytes .../darwin/trace_waterfall-light.png | Bin 0 -> 14635 bytes trace_waterfall.go | 178 ++++++++++++++++++ trace_waterfall_test.go | 78 ++++++++ 5 files changed, 262 insertions(+) create mode 100644 testdata/snapshots/darwin/trace_waterfall-dark.png create mode 100644 testdata/snapshots/darwin/trace_waterfall-light.png create mode 100644 trace_waterfall.go create mode 100644 trace_waterfall_test.go diff --git a/messages_devtools.go b/messages_devtools.go index c5c325c..ebfd5fb 100644 --- a/messages_devtools.go +++ b/messages_devtools.go @@ -72,6 +72,12 @@ func init() { "log_stream.waiting": {"%d new lines", "%d 条新日志"}, "log_stream.empty": {"No log lines", "没有日志"}, + "trace_waterfall.label": {"Trace", "调用链"}, + "trace_waterfall.empty": {"No spans", "没有跨度"}, + "trace_waterfall.summary": {"%d spans · %s", "%d 个跨度 · %s"}, + "trace_waterfall.detail": {"%s · starts at %s · takes %s", "%s · 起于 %s · 耗时 %s"}, + "trace_waterfall.error": {"Error", "错误"}, + "alert_list.empty": {"No alerts", "没有告警"}, }) } diff --git a/testdata/snapshots/darwin/trace_waterfall-dark.png b/testdata/snapshots/darwin/trace_waterfall-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..637f862fc3b86946d646836e2ac6316bf5250cce GIT binary patch literal 15132 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 P3!)&aDpM)-DfoW?(tZK% literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/trace_waterfall-light.png b/testdata/snapshots/darwin/trace_waterfall-light.png new file mode 100644 index 0000000000000000000000000000000000000000..314480ba16e02ab0c1d6f97704580122bd7a98fc GIT binary patch literal 14635 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 literal 0 HcmV?d00001 diff --git a/trace_waterfall.go b/trace_waterfall.go new file mode 100644 index 0000000..9ac6b6b --- /dev/null +++ b/trace_waterfall.go @@ -0,0 +1,178 @@ +package mujica + +import ( + "cmp" + "fmt" + "log/slog" + "slices" + "time" + + "github.com/egoist/mygo/ui" + + "mujicaui/icons" + "mujicaui/internal" + "mujicaui/theme" +) + +// TraceSpan is one timed operation of a request; Parent is "" for the root. +type TraceSpan struct { + ID, Parent string + Name string + Service string + // Start is the offset from the trace start. + Start, Duration time.Duration + Error bool +} + +// TraceWaterfallOptions configure a TraceWaterfall. +type TraceWaterfallOptions struct { + // Height of the span list; zero means 320. + Height float32 +} + +// traceRow is a span in tree order with its depth. +type traceRow struct { + span int + depth int +} + +type traceState struct { + key string + rows []traceRow + total time.Duration + selected string + services map[string]int // palette slot of each service +} + +// TraceWaterfall lays spans out by parent and time; choosing a span shows its detail. +func TraceWaterfall(c *ui.Context, spans []TraceSpan, opts TraceWaterfallOptions) ChoiceResult { + if opts.Height < 0 { + panic("mujica: TraceWaterfall Height is negative") + } + h := opts.Height + if h == 0 { + h = 320 + } + k := Tokens(c) + u := Density(c).Unit() + var r ChoiceResult + r.Element = codePanel(c).Label(msg(c, "trace_waterfall.label")) + st := ui.Local(r.Element, "mujica.trace", func() traceState { return traceState{} }) + traceLayout(st, spans) + r.Element.Children(func() { + if len(st.rows) == 0 { + codeCaption(c, msg(c, "trace_waterfall.empty")).Padding(u * 4) + return + } + codeHeader(c, func() { + codeCaption(c, fmt.Sprintf(msg(c, "trace_waterfall.summary"), len(spans), agentDuration(st.total))).Grow(1) + }) + agentRows(c, len(st.rows), func(i int) { + row := st.rows[i] + s := spans[row.span] + if traceSpanRow(c, s, row.depth, st.total, st.services[s.Service], s.ID == st.selected) { + st.selected, r.changed = s.ID, true + slog.Debug("mujica: trace_waterfall select", "span", s.ID) + } + }).Height(h).FocusGroup(ui.Vertical) + for _, s := range spans { + if s.ID == st.selected { + ui.Column(c).Shrink(0).Gap(u).Padding(u*2, u*3).BorderWidth(theme.BorderWidth, 0, 0, 0).BorderColor(k.Border). + Role(ui.RoleStatus).Children(func() { + ui.Text(c, s.Name).FontWeight(600) + codeCaption(c, fmt.Sprintf(msg(c, "trace_waterfall.detail"), s.Service, agentDuration(s.Start), agentDuration(s.Duration))) + }) + } + } + }) + return r +} + +// traceLayout orders spans depth-first by start, only when the spans change. +func traceLayout(st *traceState, spans []TraceSpan) { + key := fmt.Sprintf("%d", len(spans)) + if len(spans) > 0 { + key += fmt.Sprintf("%p", &spans[0]) + } + if key == st.key { + return + } + st.key, st.rows, st.total, st.services = key, st.rows[:0], 0, map[string]int{} + index := make(map[string]int, len(spans)) + kids := map[string][]int{} + for i, s := range spans { + if s.ID == "" || s.Name == "" || s.Start < 0 || s.Duration < 0 { + panic(fmt.Sprintf("mujica: TraceWaterfall span %d needs an ID, a Name and non-negative times", i)) + } + if _, dup := index[s.ID]; dup { + panic("mujica: TraceWaterfall span IDs must be unique: " + s.ID) + } + index[s.ID] = i + if _, ok := st.services[s.Service]; !ok { + st.services[s.Service] = len(st.services) + } + st.total = max(st.total, s.Start+s.Duration) + } + for i, s := range spans { + if _, ok := index[s.Parent]; s.Parent != "" && !ok { + panic("mujica: TraceWaterfall span " + s.ID + " has an unknown Parent") + } + kids[s.Parent] = append(kids[s.Parent], i) + } + var walk func(parent string, depth int) + walk = func(parent string, depth int) { + list := kids[parent] + slices.SortStableFunc(list, func(a, b int) int { return cmp.Compare(spans[a].Start, spans[b].Start) }) + for _, i := range list { + st.rows = append(st.rows, traceRow{i, depth}) + walk(spans[i].ID, depth+1) + } + } + walk("", 0) + if len(st.rows) != len(spans) { + panic("mujica: TraceWaterfall spans form a cycle") + } +} + +// traceSpanRow shows a span's name indented by depth beside its bar; it reports a choice. +func traceSpanRow(c *ui.Context, s TraceSpan, depth int, total time.Duration, slot int, selected bool) bool { + k := Tokens(c) + d := Density(c) + u := d.Unit() + row := ui.ButtonBase(c).Key(s.ID).Justify(ui.Start).Gap(u*2).MinHeight(d.ControlHeight()-4).Padding(0, u*2). + Label(fmt.Sprintf("%s, %s, %s", s.Name, s.Service, agentDuration(s.Duration))) + internal.Ring(row, k.Focus, theme.ControlRadius) + if selected { + row.Background(k.Selection) + } else if row.Hovered() { + row.Background(k.SurfaceHover) + } + pal := internal.Palette(k) + col := pal[slot%len(pal)] + if s.Error { + col = k.Danger + } + row.Children(func() { + ui.Row(c).Width(220).Shrink(0).Gap(u).Padding(0, 0, 0, float32(depth)*u*4).AlignItems(ui.Center).Children(func() { + if s.Error { + ui.Icon(c, icons.Must("circle-x")).Size(12, 12).TextColor(k.Danger).Label(msg(c, "trace_waterfall.error")) + } + ui.Text(c, s.Name).FontSize(FontSize(c, theme.CaptionSize+1)).SingleLine().Shrink(1) + }) + ui.Box(c).Grow(1).Height(14).Draw(func(p *ui.Painter, r ui.Rect) { + span := max(float64(total), 1) + x0 := r.X + r.W*float32(float64(s.Start)/span) + w := max(r.W*float32(float64(s.Duration)/span), 2) + bar := ui.Rect{X: internal.Snap(p, x0), Y: r.Y, W: w, H: r.H} + if s.Error { // hollow, so errors differ by shape too + p.Fill(bar, col.Alpha(0.25), 2) + p.Stroke(bar, col, 2, 1.5) + return + } + p.Fill(bar, col, 2) + }) + ui.Text(c, agentDuration(s.Duration)).Width(64).TextAlign(ui.End).FontSize(FontSize(c, theme.CaptionSize)). + TextColor(k.TextMuted).FontFeatures("tnum").Shrink(0) + }) + return row.Clicked() +} diff --git a/trace_waterfall_test.go b/trace_waterfall_test.go new file mode 100644 index 0000000..a15ad5a --- /dev/null +++ b/trace_waterfall_test.go @@ -0,0 +1,78 @@ +package mujica + +import ( + "fmt" + "testing" + "time" + + "github.com/egoist/mygo/ui" +) + +var traceSample = []TraceSpan{ + {ID: "db", Parent: "api", Name: "SELECT guests", Service: "postgres", Start: 40 * time.Millisecond, Duration: 90 * time.Millisecond}, + {ID: "root", Name: "GET /ball", Service: "gateway", Duration: 320 * time.Millisecond}, + {ID: "api", Parent: "root", Name: "ListGuests", Service: "api", Start: 20 * time.Millisecond, Duration: 200 * time.Millisecond}, + {ID: "pay", Parent: "root", Name: "Charge", Service: "payments", Start: 230 * time.Millisecond, Duration: 80 * time.Millisecond, Error: true}, +} + +func traceTester(spans []TraceSpan, changes *int) *ui.Tester { + return ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + if TraceWaterfall(c, spans, TraceWaterfallOptions{}).Changed() { + *changes++ + } + }, 700, 460) +} + +func TestTraceWaterfallOrderAndDetail(t *testing.T) { + n := 0 + tt := traceTester(traceSample, &n) + if !tt.HasText("4 spans · 320 ms") { + t.Fatalf("summary: %q", tt.Texts()) + } + root, _ := tt.Find("GET /ball") + api, _ := tt.Find("ListGuests") + db, _ := tt.Find("SELECT guests") + pay, _ := tt.Find("Charge") + if !(root.Y < api.Y && api.Y < db.Y && db.Y < pay.Y && db.X > api.X && api.X > root.X) { + t.Fatalf("tree order: root %v api %v db %v pay %v", root, api, db, pay) + } + _ = tt.Click("Charge") + if n != 1 || !tt.HasText("payments · starts at 230 ms · takes 80 ms") { + t.Fatalf("detail: %q", tt.Texts()) + } + tt.Key(0, ui.KeyUp) + tt.Key(0, ui.KeyEnter) + if !tt.HasText("postgres · starts at 40 ms · takes 90 ms") { + t.Fatalf("keyboard detail: %q", tt.Texts()) + } +} + +func TestTraceWaterfallRulesAndMany(t *testing.T) { + n := 0 + tt := traceTester(nil, &n) + if !tt.HasText("No spans") { + t.Fatal("empty") + } + many := make([]TraceSpan, 100000) + many[0] = TraceSpan{ID: "0", Name: "root", Duration: time.Second} + for i := 1; i < len(many); i++ { + many[i] = TraceSpan{ID: fmt.Sprint(i), Parent: "0", Name: fmt.Sprint("span ", i), Start: time.Duration(i) * time.Microsecond, Duration: time.Microsecond} + } + tt = traceTester(many, &n) + if got := len(tt.Texts()); got > 400 { + t.Fatalf("%d texts for 100000 spans", got) + } + defer func() { + if recover() == nil { + t.Fatal("cycle did not panic") + } + }() + traceTester([]TraceSpan{{ID: "a", Parent: "b", Name: "a"}, {ID: "b", Parent: "a", Name: "b"}}, &n) +} + +func TestTraceWaterfallSnapshot(t *testing.T) { + snapshot(t, "trace_waterfall", 680, 280, func(c *ui.Context) { + ui.Column(c).Padding(16).Children(func() { TraceWaterfall(c, traceSample, TraceWaterfallOptions{Height: 180}) }) + }) +}