package main import ( "fmt" "log" // "strconv" "strings" "time" "github.com/gdamore/tcell" "github.com/rivo/tview" "bnbl.io/pgqt/postgres" ) type planView struct { *tview.Flex tree *tview.TreeView detail *tview.Flex } func newPlanView() *planView { f := tview.NewFlex() f.SetDirection(tview.FlexRow) t := tview.NewTreeView() d := tview.NewFlex() d.SetBackgroundColor(tcell.ColorBlack) t.SetBorder(true).SetTitle("Plan") d.SetBorder(true).SetTitle("Node Detail") 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 planHeader(p *postgres.Plan) tview.Primitive { f := tview.NewFlex() f.SetDirection(tview.FlexColumn) f.AddItem(planHeaderRowStats(p), 0, 50, false) f.AddItem(planHeaderBlocksTable(p), 0, 50, false) return f } func planHeaderRowStats(p *postgres.Plan) tview.Primitive { f := tview.NewFlex() f.SetDirection(tview.FlexRow) f.SetBackgroundColor(tcell.ColorBlack) f.AddItem(tview.NewFlex(). SetDirection(tview.FlexColumn). AddItem(tview.NewTextView().SetTextAlign(tview.AlignCenter).SetText(fmt.Sprintf("%d", p.PlanRows)), 0, 50, false). AddItem(tview.NewTextView().SetTextAlign(tview.AlignCenter).SetText(fmt.Sprintf("%d", p.ActualRows)), 0, 50, false), 1, 0, false) f.AddItem(tview.NewFlex(). SetDirection(tview.FlexColumn). AddItem(tview.NewTextView().SetTextAlign(tview.AlignCenter).SetText("planned rows"), 0, 50, false). AddItem(tview.NewTextView().SetTextAlign(tview.AlignCenter).SetText("actual rows"), 0, 50, false), 1, 0, false) overUnder, wrongness := p.EstimateWrongness() if wrongness > 1 { f.AddItem(tview.NewTextView().SetTextAlign(tview.AlignCenter).SetText(fmt.Sprintf("%s estimate by %dx", overUnder, wrongness)), 1, 0, false) } return f } func planHeaderBlocksTable(p *postgres.Plan) tview.Primitive { t := tview.NewTable() const ( rowShared = 1 rowLocal = 2 rowTemp = 3 colHit = 1 colDirtied = 2 colRead = 3 colWritten = 4 ) t.SetCellSimple(0, colHit, "Hit") t.SetCellSimple(0, colDirtied, "Dirtied") t.SetCellSimple(0, colRead, "Read") t.SetCellSimple(0, colWritten, "Written") t.SetCellSimple(rowShared, 0, "Shared Blocks") t.SetCellSimple(rowLocal, 0, "Local Blocks") t.SetCellSimple(rowTemp, 0, "Temp Blocks") t.SetCellSimple(rowShared, colHit, fmt.Sprintf("%d", p.SharedHitBlocks)) t.SetCellSimple(rowShared, colDirtied, fmt.Sprintf("%d", p.SharedDirtiedBlocks)) t.SetCellSimple(rowShared, colRead, fmt.Sprintf("%d", p.SharedReadBlocks)) t.SetCellSimple(rowShared, colWritten, fmt.Sprintf("%d", p.SharedWrittenBlocks)) t.SetCellSimple(rowLocal, colHit, fmt.Sprintf("%d", p.LocalHitBlocks)) t.SetCellSimple(rowLocal, colDirtied, fmt.Sprintf("%d", p.LocalDirtiedBlocks)) t.SetCellSimple(rowLocal, colRead, fmt.Sprintf("%d", p.LocalReadBlocks)) t.SetCellSimple(rowLocal, colWritten, fmt.Sprintf("%d", p.LocalWrittenBlocks)) t.SetCellSimple(rowTemp, colHit, "-") t.SetCellSimple(rowTemp, colDirtied, "-") t.SetCellSimple(rowTemp, colRead, fmt.Sprintf("%d", p.TempReadBlocks)) t.SetCellSimple(rowTemp, colWritten, fmt.Sprintf("%d", p.TempWrittenBlocks)) return t } func (pv *planView) ShowDetail(p *postgres.Plan) { pv.detail.Clear() if p == nil { return } pv.detail.SetTitle(fmt.Sprintf("Node Detail: %s", p.NodeType)) pv.detail.SetDirection(tview.FlexRow) pv.detail.AddItem(planHeader(p), 4, 0, false) pv.detail.AddItem(tview.NewTextView().SetText("lorem ipsum blah blah"), 0, 100, false) // 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(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(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 }