package main import ( "fmt" "log" "strconv" "strings" "time" "github.com/rivo/tview" "bnbl.io/pgqt/postgres" ) type planView struct { *tview.Flex tree *tview.TreeView detail *tview.Table } func newPlanView() *planView { f := tview.NewFlex() f.SetBorder(true).SetTitle("Plan") f.SetDirection(tview.FlexRow) t := tview.NewTreeView() d := tview.NewTable() f.AddItem(t, 0, 50, true) f.AddItem(d, 0, 50, false) pv := &planView{ Flex: f, tree: t, detail: d, } t.SetSelectedFunc(func(node *tview.TreeNode) { go func() { app.QueueUpdateDraw(func() { ref := node.GetReference() if ref == nil { pv.ShowDetail(nil) return } p, ok := ref.(*postgres.Plan) if !ok { pv.ShowDetail(nil) return } log.Printf("selected node named %s", p.NodeType) pv.ShowDetail(p) }) }() }) return pv } func (p *planView) SetError(err error) { p.tree.SetRoot(tview.NewTreeNode(err.Error())) } func (p *planView) SetPlan(as []*postgres.Explain) { if len(as) == 0 { p.tree.SetRoot(tview.NewTreeNode("No plan results to display")) return } if len(as) > 1 { log.Printf("skipping display of more than one analysis result") } a := as[0] root := tview.NewTreeNode(fmt.Sprintf("Query Plan (planning=%s execution=%s)", f2d(a.PlanningTime), f2d(a.ExecutionTime))) root.AddChild(buildNode(a.Plan)) p.tree.SetRoot(root) p.tree.SetCurrentNode(root) } func buildNode(p *postgres.Plan) *tview.TreeNode { dur := p.EffectiveTotalTime() var ( badEstimate string sortBy string scanOn string scanIndex string ) if p.IsBadEstimate() { badEstimate = " [red](bad estimate)[white]" } if len(p.SortKey) > 0 { sortBy = " by " + strings.Join(p.SortKey, ", ") } if p.RelationName != nil { scanOn = " on " + *p.RelationName } if p.IndexName != nil { scanIndex = " using " + *p.IndexName } label := fmt.Sprintf("%s (%s)%s%s%s%s", p.NodeType, dur.String(), badEstimate, sortBy, scanOn, scanIndex) n := tview.NewTreeNode(label) n.SetSelectable(true) n.SetReference(p) for _, subplan := range p.Plans { n.AddChild(buildNode(subplan)) } return n } func (pv *planView) ShowDetail(p *postgres.Plan) { pv.detail.Clear() if p == nil { return } pv.detail.SetCellSimple(0, 0, "Node Type") pv.detail.SetCellSimple(0, 1, p.NodeType) pv.detail.SetCellSimple(1, 0, "Startup Cost") pv.detail.SetCellSimple(1, 1, f2a(p.StartupCost)) pv.detail.SetCellSimple(2, 0, "Total Cost") pv.detail.SetCellSimple(2, 1, f2a(p.TotalCost)) pv.detail.SetCellSimple(3, 0, "Plan Rows") pv.detail.SetCellSimple(3, 1, strconv.Itoa(p.PlanRows)) pv.detail.SetCellSimple(4, 0, "Actual Rows") pv.detail.SetCellSimple(4, 1, strconv.Itoa(p.ActualRows)) pv.detail.SetCellSimple(5, 0, "Plan Width") pv.detail.SetCellSimple(5, 1, strconv.Itoa(p.PlanWidth)) pv.detail.SetCellSimple(6, 0, "Actual Startup Time") pv.detail.SetCellSimple(6, 1, f2d(p.ActualStartupTime).String()) pv.detail.SetCellSimple(7, 0, "Actual Total Time") pv.detail.SetCellSimple(7, 1, f2d(p.ActualTotalTime).String()) pv.detail.SetCellSimple(8, 0, "Actual Loops") pv.detail.SetCellSimple(8, 1, strconv.Itoa(p.ActualLoops)) pv.detail.SetCellSimple(9, 0, "Shared Hit Blocks") pv.detail.SetCellSimple(9, 1, strconv.Itoa(p.SharedHitBlocks)) pv.detail.SetCellSimple(10, 0, "Shared Read Blocks") pv.detail.SetCellSimple(10, 1, strconv.Itoa(p.SharedReadBlocks)) pv.detail.SetCellSimple(11, 0, "Shared Dirtied Blocks") pv.detail.SetCellSimple(11, 1, strconv.Itoa(p.SharedDirtiedBlocks)) pv.detail.SetCellSimple(12, 0, "Shared Written Blocks") pv.detail.SetCellSimple(12, 1, strconv.Itoa(p.SharedWrittenBlocks)) pv.detail.SetCellSimple(13, 0, "Local Hit Blocks") pv.detail.SetCellSimple(13, 1, strconv.Itoa(p.LocalHitBlocks)) pv.detail.SetCellSimple(14, 0, "Local Read Blocks") pv.detail.SetCellSimple(14, 1, strconv.Itoa(p.LocalReadBlocks)) pv.detail.SetCellSimple(15, 0, "Local Dirtied Blocks") pv.detail.SetCellSimple(15, 1, strconv.Itoa(p.LocalDirtiedBlocks)) pv.detail.SetCellSimple(16, 0, "Local Written Blocks") pv.detail.SetCellSimple(16, 1, strconv.Itoa(p.LocalWrittenBlocks)) pv.detail.SetCellSimple(17, 0, "Temp Read Blocks") pv.detail.SetCellSimple(17, 1, strconv.Itoa(p.TempReadBlocks)) pv.detail.SetCellSimple(18, 0, "Temp Written Blocks") pv.detail.SetCellSimple(18, 1, strconv.Itoa(p.TempWrittenBlocks)) pv.detail.SetCellSimple(19, 0, "I/O Read Time") pv.detail.SetCellSimple(19, 1, f2d(p.IOReadTime).String()) pv.detail.SetCellSimple(20, 0, "I/O Write Time") pv.detail.SetCellSimple(20, 1, f2d(p.IOWriteTime).String()) idx := 21 if p.ParentRelationship != nil { pv.detail.SetCellSimple(idx, 0, "Parent Relationship") pv.detail.SetCellSimple(idx, 1, *p.ParentRelationship) idx++ } if len(p.Output) > 0 { pv.detail.SetCellSimple(idx, 0, "Output") pv.detail.SetCellSimple(idx, 1, strings.Join(p.Output, ", ")) idx++ } if len(p.SortKey) > 0 { pv.detail.SetCellSimple(idx, 0, "Sort Key") pv.detail.SetCellSimple(idx, 1, strings.Join(p.SortKey, ", ")) idx++ } if p.SortMethod != nil { pv.detail.SetCellSimple(idx, 0, "Sort Method") pv.detail.SetCellSimple(idx, 1, *p.SortMethod) idx++ } if p.SortSpaceUsed != nil { pv.detail.SetCellSimple(idx, 0, "Sort Space Used") pv.detail.SetCellSimple(idx, 1, strconv.Itoa(*p.SortSpaceUsed)) idx++ } if p.SortSpaceType != nil { pv.detail.SetCellSimple(idx, 0, "Sort Space Type") pv.detail.SetCellSimple(idx, 1, *p.SortSpaceType) idx++ } if p.JoinType != nil { pv.detail.SetCellSimple(idx, 0, "Join Type") pv.detail.SetCellSimple(idx, 1, *p.JoinType) idx++ } if p.Strategy != nil { pv.detail.SetCellSimple(idx, 0, "Strategy") pv.detail.SetCellSimple(idx, 1, *p.Strategy) idx++ } if p.RelationName != nil { pv.detail.SetCellSimple(idx, 0, "Relation Name") pv.detail.SetCellSimple(idx, 1, *p.RelationName) idx++ } if p.Schema != nil { pv.detail.SetCellSimple(idx, 0, "Schema") pv.detail.SetCellSimple(idx, 1, *p.Schema) idx++ } if p.Alias != nil { pv.detail.SetCellSimple(idx, 0, "Alias") pv.detail.SetCellSimple(idx, 1, *p.Alias) idx++ } if p.RecheckCond != nil { pv.detail.SetCellSimple(idx, 0, "Recheck Cond") pv.detail.SetCellSimple(idx, 1, *p.RecheckCond) idx++ } if p.RowsRemovedByIndexRecheck != nil { pv.detail.SetCellSimple(idx, 0, "Rows Removed by Index Recheck") pv.detail.SetCellSimple(idx, 1, strconv.Itoa(*p.RowsRemovedByIndexRecheck)) idx++ } if p.ExactHeapBlocks != nil { pv.detail.SetCellSimple(idx, 0, "Exact Heap Blocks") pv.detail.SetCellSimple(idx, 1, strconv.Itoa(*p.ExactHeapBlocks)) idx++ } if p.LossyHeapBlocks != nil { pv.detail.SetCellSimple(idx, 0, "Lossy Heap Blocks") pv.detail.SetCellSimple(idx, 1, strconv.Itoa(*p.LossyHeapBlocks)) idx++ } if p.IndexName != nil { pv.detail.SetCellSimple(idx, 0, "Index Name") pv.detail.SetCellSimple(idx, 1, *p.IndexName) idx++ } if p.IndexCond != nil { pv.detail.SetCellSimple(idx, 0, "Index Cond") pv.detail.SetCellSimple(idx, 1, *p.IndexCond) idx++ } if p.ScanDirection != nil { pv.detail.SetCellSimple(idx, 0, "Scan Direction") pv.detail.SetCellSimple(idx, 1, *p.ScanDirection) idx++ } if p.ParallelAware != nil { pv.detail.SetCellSimple(idx, 0, "Parallel Aware") pv.detail.SetCellSimple(idx, 1, fmt.Sprintf("%t", *p.ParallelAware)) idx++ } if p.FunctionName != nil { pv.detail.SetCellSimple(idx, 0, "Function Name") pv.detail.SetCellSimple(idx, 1, *p.FunctionName) idx++ } } func f2a(f float32) string { return fmt.Sprintf("%f", f) } func f2d(f float32) time.Duration { // f is in milliseconds, multiply by 1k to get microseconds // (thousandths of milliseconds) return time.Duration(int(f)*1000) * time.Microsecond }