From b005938c00c62465dac06693703ab3f592d15369 Mon Sep 17 00:00:00 2001 From: Ben Burwell Date: Tue, 2 Jun 2020 03:30:49 -0400 Subject: Initial commit --- .gitignore | 1 + Makefile | 13 ++ go.mod | 10 ++ go.sum | 65 +++++++ main.go | 574 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ plan.go | 338 ++++++++++++++++++++++++++++++++++++ 6 files changed, 1001 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 plan.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a00410a --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +pgqt diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e12d79d --- /dev/null +++ b/Makefile @@ -0,0 +1,13 @@ +SOURCES=$(wildcard *.go) $(wildcard */*.go) +BINARY=pgqt + +$(BINARY): $(SOURCES) + go build -o $@ . + +.PHONY: run +run: $(BINARY) + ./$(BINARY) 2>>/tmp/pgqt.log + +.PHONY: clean +clean: + rm -f $(BINARY) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c3a7b22 --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module bnbl.io/pgqt + +go 1.14 + +require ( + github.com/gdamore/tcell v1.3.0 + github.com/jmoiron/sqlx v1.2.0 + github.com/lib/pq v1.6.0 + github.com/rivo/tview v0.0.0-20200528200248-fe953220389f +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2beec68 --- /dev/null +++ b/go.sum @@ -0,0 +1,65 @@ +github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5/go.mod h1:976q2ETgjT2snVCf2ZaBnyBbVoPERGjUz+0sofzEfro= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= +github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= +github.com/gdamore/tcell v1.3.0 h1:r35w0JBADPZCVQijYebl6YMWWtHRqVEGt7kL2eBADRM= +github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.0.0 h1:J7uCkflzTEhUZ64xqKnkDxq3kzc96ajM1Gli5ktUem8= +github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= +github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.2.0 h1:lzPl/30ZLkTveYsYZPKMcgXc8MbnE6RsTd4F9KgiLtk= +github.com/jcmturner/gokrb5/v8 v8.2.0/go.mod h1:T1hnNppQsBtxW0tCHMHTkAt8n/sABdzZgZdoFrZaZNM= +github.com/jcmturner/rpc/v2 v2.0.2 h1:gMB4IwRXYsWw4Bc6o/az2HJgFUA1ffSh90i26ZJ6Xl0= +github.com/jcmturner/rpc/v2 v2.0.2/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA= +github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.6.0 h1:I5DPxhYJChW9KYc66se+oKFFQX6VuQrKiprsX6ivRZc= +github.com/lib/pq v1.6.0/go.mod h1:4vXEAYvW1fRQ2/FhZ78H73A60MHw1geSm145z2mdY1g= +github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s= +github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac= +github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.8 h1:3tS41NlGYSmhhe/8fhGRzc+z3AYCw1Fe1WAyLuujKs0= +github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/tview v0.0.0-20200528200248-fe953220389f h1:tRx/LLIP2PSA7johw9xhf+6NUCLC4BbMhpGdm110MGI= +github.com/rivo/tview v0.0.0-20200528200248-fe953220389f/go.mod h1:6lkG1x+13OShEf0EaOCaTQYyB7d5nSbb181KtjlS+84= +github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200117160349-530e935923ad/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200311171314-f7b00557c8c4 h1:QmwruyY+bKbDDL0BaglrbZABEali68eoMFhTZpCjYVA= +golang.org/x/crypto v0.0.0-20200311171314-f7b00557c8c4/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa h1:F+8P+gmewFQYRk6JoLQLwjBCTu3mcIURZfNkVweuRKA= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/jcmturner/aescts.v1 v1.0.1/go.mod h1:nsR8qBOg+OucoIW+WMhB3GspUQXq9XorLnQb9XtvcOo= +gopkg.in/jcmturner/dnsutils.v1 v1.0.1/go.mod h1:m3v+5svpVOhtFAP/wSz+yzh4Mc0Fg7eRhxkJMWSIz9Q= +gopkg.in/jcmturner/goidentity.v3 v3.0.0/go.mod h1:oG2kH0IvSYNIu80dVAyu/yoefjq1mNfM5bm88whjWx4= +gopkg.in/jcmturner/gokrb5.v7 v7.5.0/go.mod h1:l8VISx+WGYp+Fp7KRbsiUuXTTOnxIc3Tuvyavf11/WM= +gopkg.in/jcmturner/rpc.v1 v1.1.0/go.mod h1:YIdkC4XfD6GXbzje11McwsDuOlZQSb9W4vfLvuNnlv8= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/main.go b/main.go new file mode 100644 index 0000000..3509cd2 --- /dev/null +++ b/main.go @@ -0,0 +1,574 @@ +package main + +import ( + // "database/sql" + "encoding/json" + "fmt" + "log" + "os" + "strconv" + "strings" + "time" + + "github.com/gdamore/tcell" + "github.com/jmoiron/sqlx" + _ "github.com/lib/pq" + "github.com/rivo/tview" +) + +var app = tview.NewApplication() + +func main() { + db, err := sqlx.Open("postgres", "postgres://postgres:password@localhost:5432/postgres?sslmode=disable") + if err != nil { + log.Printf("could not connect to db: %v", err) + os.Exit(1) + } + + schemata := newSchemaSelector(db) + queries := newQuerySelector(db) + pv := newPlanView() + flex := tview.NewFlex(). + AddItem(schemata, 30, 0, true). + AddItem(tview.NewFlex(). + SetDirection(tview.FlexRow). + AddItem(queries, 0, 25, false). + AddItem(pv, 0, 75, false), 0, 100, false) + + schemata.SetSelectedFunc(func(_ int, t, _ string, _ rune) { + go func() { + app.QueueUpdateDraw(func() { + if queries.SelectSchema(t) { + app.SetFocus(queries) + } + }) + }() + }) + + app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + switch event.Key() { + case tcell.KeyF1: + app.SetFocus(schemata) + case tcell.KeyF2: + app.SetFocus(queries) + case tcell.KeyF3: + app.SetFocus(pv) + case tcell.KeyEnter: + if app.GetFocus() == queries { + p, err := queries.Analyze() + if err != nil { + pv.SetError(err) + } else { + pv.SetPlan(p) + app.SetFocus(pv) + } + } + case tcell.KeyTAB: + if app.GetFocus() == pv.tree { + app.SetFocus(pv.detail) + } else if app.GetFocus() == pv.detail { + app.SetFocus(pv.tree) + } + } + return event + }) + + if err := app.SetRoot(flex, true).Run(); err != nil { + log.Printf("could not create application: %v", err) + os.Exit(1) + } +} + +func newSchemaSelector(db *sqlx.DB) *tview.List { + v := tview.NewList() + v.SetTitle("Schemata") + v.SetBorder(true) + v.ShowSecondaryText(false) + v.SetHighlightFullLine(true) + + // v.SetMainTextColor(tcell.ColorDefault) + // v.SetBackgroundColor(tcell.ColorDefault) + // v.SetSelectedTextColor(tcell.ColorRed) + // v.SetSelectedBackgroundColor(tcell.ColorDefault) + + type schema struct { + SchemaName string `db:"schema_name"` + } + + var schemata []*schema + if err := db.Select(&schemata, "SELECT schema_name FROM information_schema.schemata"); err != nil { + v.AddItem(fmt.Sprintf("could not find schemata: %v", err), "", 'a', nil) + return v + } + + for _, s := range schemata { + v.AddItem(s.SchemaName, "", 0, nil) + } + + return v +} + +type pg_stat_statement struct { + UserID int `db:"userid"` + DBID int `db:"dbid"` + QueryID int64 `db:"queryid"` + Query string `db:"query"` + Calls int `db:"calls"` + TotalTime float32 `db:"total_time"` + MinTime float32 `db:"min_time"` + MaxTime float32 `db:"max_time"` + MeanTime float32 `db:"mean_time"` + StddevTime float32 `db:"stddev_time"` + Rows int64 `db:"rows"` + SharedBlksHit int64 `db:"shared_blks_hit"` + SharedBlksRead int64 `db:"shared_blks_read"` + SharedBlksDirtied int64 `db:"shared_blks_dirtied"` + SharedBlksWritten int64 `db:"shared_blks_written"` + LocalBlksHit int64 `db:"local_blks_hit"` + LocalBlksRead int64 `db:"local_blks_read"` + LocalBlksDirtied int64 `db:"local_blks_dirtied"` + LocalBlksWritten int64 `db:"local_blks_written"` + TempBlksRead int64 `db:"temp_blks_read"` + TempBlksWritten int64 `db:"temp_blks_written"` + BlkReadTime float32 `db:"blk_read_time"` + BlkWriteTime float32 `db:"blk_write_time"` +} + +type querySelector struct { + *tview.Table + db *sqlx.DB + stmts []*pg_stat_statement +} + +func newQuerySelector(db *sqlx.DB) *querySelector { + v := tview.NewTable() + v.SetTitle("Queries") + v.SetBorder(true) + v.SetSelectable(true, false) + // v.SetBackgroundColor(tcell.ColorDefault) + // v.SetSelectedStyle(tcell.ColorDefault, tcell.ColorDefault, tcell.AttrReverse) + + // v.SetMainTextColor(tcell.ColorDefault) + // v.SetBackgroundColor(tcell.ColorDefault) + // v.SetSelectedTextColor(tcell.ColorRed) + // v.SetSelectedBackgroundColor(tcell.ColorDefault) + + qs := &querySelector{ + Table: v, + db: db, + } + + return qs +} + +func (q *querySelector) SelectSchema(schema string) bool { + q.Clear() + + q.stmts = make([]*pg_stat_statement, 0) + + if err := q.db.Select(&q.stmts, fmt.Sprintf("SELECT * FROM %s.pg_stat_statements ORDER BY total_time DESC", schema)); err != nil { + log.Printf("could not select schema: %v", err) + cell := tview.NewTableCell(fmt.Sprintf("could not find queries: %v", err)) + // cell.SetTextColor(tcell.ColorYellow) + cell.SetAlign(tview.AlignCenter) + cell.SetSelectable(false) + q.SetCell(0, 0, cell) + return false + } + + q.SetFixed(1, 0) + + newHeaderCell := func(t string) *tview.TableCell { + cell := tview.NewTableCell(t) + cell.SetSelectable(false) + // cell.SetTextColor(tcell.ColorGreen) + return cell + } + + q.SetCell(0, 0, newHeaderCell("User ID")) + q.SetCell(0, 1, newHeaderCell("Query")) + q.SetCell(0, 2, newHeaderCell("Calls")) + q.SetCell(0, 3, newHeaderCell("Total Time")) + + newTableCell := func(t string) *tview.TableCell { + cell := tview.NewTableCell(t) + // cell.SetStyle(tcell.StyleDefault) + return cell + } + + for idx, stmt := range q.stmts { + q.SetCell(idx+1, 0, newTableCell(strconv.Itoa(stmt.UserID))) + q.SetCell(idx+1, 1, newTableCell(stmt.Query)) + q.SetCell(idx+1, 2, newTableCell(strconv.Itoa(stmt.Calls))) + q.SetCell(idx+1, 3, newTableCell(strconv.Itoa(int(stmt.TotalTime)))) + } + + return true +} + +func (q *querySelector) Analyze() ([]*analysis, error) { + row, _ := q.GetSelection() + if len(q.stmts) < row { + return nil, fmt.Errorf("no queries to analyze") + } + stmt := q.stmts[row-1] + tx, err := q.db.Beginx() + if err != nil { + return nil, fmt.Errorf("could not begin transaction: %v", err) + } + defer tx.Rollback() + + type qp struct { + Plan string `db:"QUERY PLAN"` + } + + var plan qp + if err := tx.Get(&plan, fmt.Sprintf("EXPLAIN (ANALYZE, COSTS, BUFFERS, VERBOSE, FORMAT JSON) %s", stmt.Query)); err != nil { + return nil, fmt.Errorf("could not analyze query: %v", err) + } + + if err := tx.Rollback(); err != nil { + return nil, fmt.Errorf("could not roll back transaction: %v", err) + } + + a := make([]*analysis, 0) + // if err := json.Unmarshal([]byte(plan.Plan), &a); err != nil { + if err := json.Unmarshal([]byte(testPlan), &a); err != nil { + return nil, fmt.Errorf("could not unmarshal plan: %v", err) + } + + return a, nil +} + +type analysis struct { + Plan *plan `json:"Plan"` + PlanningTime float32 `json:"Planning Time"` + // Triggers []string `json:"Triggers"` + ExecutionTime float32 `json:"Execution Time"` +} + +type plan struct { + NodeType string `json:"Node Type"` + StartupCost float32 `json:"Startup Cost"` + TotalCost float32 `json:"Total Cost"` + PlanRows int `json:"Plan Rows"` + PlanWidth int `json:"Plan Width"` + ActualStartupTime float32 `json:"Actual Startup Time"` + ActualTotalTime float32 `json:"Actual Total Time"` + ActualRows int `json:"Actual Rows"` + ActualLoops int `json:"Actual Loops"` + Output []string `json:"Output"` + SharedHitBlocks int `json:"Shared Hit Blocks"` + SharedReadBlocks int `json:"Shared Read Blocks"` + SharedDirtiedBlocks int `json:"Shared Dirtied Blocks"` + SharedWrittenBlocks int `json:"Shared Written Blocks"` + LocalHitBlocks int `json:"Local Hit Blocks"` + LocalReadBlocks int `json:"Local Read Blocks"` + LocalDirtiedBlocks int `json:"Local Dirtied Blocks"` + LocalWrittenBlocks int `json:"Local Written Blocks"` + TempReadBlocks int `json:"Temp Read Blocks"` + TempWrittenBlocks int `json:"Temp Written Blocks"` + IOReadTime float32 `json:"I/O Read Time"` + IOWriteTime float32 `json:"I/O Write Time"` + ParentRelationship *string `json:"Parent Relationship"` + SortKey []string `json:"Sort Key"` + SortMethod *string `json:"Sort Method"` + SortSpaceUsed *int `json:"Sort Space Used"` + SortSpaceType *string `json:"Sort Space Type"` + JoinType *string `json:"Join Type"` + Strategy *string `json:"Strategy"` + RelationName *string `json:"Relation Name"` + Schema *string `json:"Schema"` + Alias *string `json:"Alias"` + RecheckCond *string `json:"Recheck Cond"` + RowsRemovedByIndexRecheck *int `json:"Rows Removed by Index Recheck"` + ExactHeapBlocks *int `json:"Exact Heap Blocks"` + LossyHeapBlocks *int `json:"Lossy Heap Blocks"` + IndexName *string `json:"Index Name"` + IndexCond *string `json:"Index Cond"` + ScanDirection *string `json:"Scan Direction"` + ParallelAware *bool `json:"Parallel Aware"` + FunctionName *string `json:"Function Name"` + Plans []*plan `json:"Plans"` +} + +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.(*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 []*analysis) { + 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 *plan) *tview.TreeNode { + dur := f2d(p.ActualTotalTime) + n := tview.NewTreeNode(fmt.Sprintf("%s (%s)", p.NodeType, dur.String())) + n.SetSelectable(true) + n.SetReference(p) + for _, subplan := range p.Plans { + n.AddChild(buildNode(subplan)) + } + return n +} + +func (pv *planView) ShowDetail(p *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 { + return time.Duration(int(f)) * time.Millisecond + // ms := time.Duration(int(f)) * time.Millisecond + // ns := time.Duration(int((f-float32(ms))*1000)) * time.Nanosecond + // return time.Duration(ms + ns) +} diff --git a/plan.go b/plan.go new file mode 100644 index 0000000..89606d6 --- /dev/null +++ b/plan.go @@ -0,0 +1,338 @@ +package main + +const testPlan = ` +[ + { + "Plan": { + "Node Type": "Limit", + "Startup Cost": 60970.08, + "Total Cost": 60970.11, + "Plan Rows": 10, + "Plan Width": 468, + "Actual Startup Time": 1941.751, + "Actual Total Time": 1941.780, + "Actual Rows": 10, + "Actual Loops": 1, + "Output": ["a.id", "(COALESCE(a.display_name, a.alert_name))", "a.external_alert_id", "a.venue_code", "altt.code", "altt.name", "altst.name", "a.stop_type", "a.deploy_mode", "a.estimated_cost", "(sum(s.total_alerts))", "(sum(s.total_followed))", "(sum(s.total_overridden))", "(sum(s.total_ignored))", "(sum(s.total_not_seen))", "(sum(s.total_unknown))", "(percenttotal((sum(s.total_followed)), (sum(s.total_alerts))))", "(percenttotal((sum(s.total_overridden)), (sum(s.total_alerts))))", "(percenttotal((sum(s.total_ignored)), (sum(s.total_alerts))))", "(percenttotal((sum(s.total_not_seen)), (sum(s.total_alerts))))", "(percenttotal((sum(s.total_unknown)), (sum(s.total_alerts))))", "(((sum(s.total_alerts)) * a.estimated_cost))"], + "Shared Hit Blocks": 260587, + "Shared Read Blocks": 0, + "Shared Dirtied Blocks": 0, + "Shared Written Blocks": 0, + "Local Hit Blocks": 0, + "Local Read Blocks": 0, + "Local Dirtied Blocks": 0, + "Local Written Blocks": 0, + "Temp Read Blocks": 0, + "Temp Written Blocks": 0, + "I/O Read Time": 0.000, + "I/O Write Time": 0.000, + "Plans": [ + { + "Node Type": "Sort", + "Parent Relationship": "Outer", + "Startup Cost": 60970.08, + "Total Cost": 60977.40, + "Plan Rows": 2927, + "Plan Width": 468, + "Actual Startup Time": 1941.747, + "Actual Total Time": 1941.757, + "Actual Rows": 10, + "Actual Loops": 1, + "Output": ["a.id", "(COALESCE(a.display_name, a.alert_name))", "a.external_alert_id", "a.venue_code", "altt.code", "altt.name", "altst.name", "a.stop_type", "a.deploy_mode", "a.estimated_cost", "(sum(s.total_alerts))", "(sum(s.total_followed))", "(sum(s.total_overridden))", "(sum(s.total_ignored))", "(sum(s.total_not_seen))", "(sum(s.total_unknown))", "(percenttotal((sum(s.total_followed)), (sum(s.total_alerts))))", "(percenttotal((sum(s.total_overridden)), (sum(s.total_alerts))))", "(percenttotal((sum(s.total_ignored)), (sum(s.total_alerts))))", "(percenttotal((sum(s.total_not_seen)), (sum(s.total_alerts))))", "(percenttotal((sum(s.total_unknown)), (sum(s.total_alerts))))", "(((sum(s.total_alerts)) * a.estimated_cost))"], + "Sort Key": ["(sum(s.total_alerts))"], + "Sort Method": "top-N heapsort", + "Sort Space Used": 27, + "Sort Space Type": "Memory", + "Shared Hit Blocks": 260587, + "Shared Read Blocks": 0, + "Shared Dirtied Blocks": 0, + "Shared Written Blocks": 0, + "Local Hit Blocks": 0, + "Local Read Blocks": 0, + "Local Dirtied Blocks": 0, + "Local Written Blocks": 0, + "Temp Read Blocks": 0, + "Temp Written Blocks": 0, + "I/O Read Time": 0.000, + "I/O Write Time": 0.000, + "Plans": [ + { + "Node Type": "Nested Loop", + "Parent Relationship": "Outer", + "Join Type": "Left", + "Startup Cost": 37552.36, + "Total Cost": 60906.83, + "Plan Rows": 2927, + "Plan Width": 468, + "Actual Startup Time": 522.250, + "Actual Total Time": 1865.979, + "Actual Rows": 32352, + "Actual Loops": 1, + "Output": ["a.id", "COALESCE(a.display_name, a.alert_name)", "a.external_alert_id", "a.venue_code", "altt.code", "altt.name", "altst.name", "a.stop_type", "a.deploy_mode", "a.estimated_cost", "(sum(s.total_alerts))", "(sum(s.total_followed))", "(sum(s.total_overridden))", "(sum(s.total_ignored))", "(sum(s.total_not_seen))", "(sum(s.total_unknown))", "percenttotal((sum(s.total_followed)), (sum(s.total_alerts)))", "percenttotal((sum(s.total_overridden)), (sum(s.total_alerts)))", "percenttotal((sum(s.total_ignored)), (sum(s.total_alerts)))", "percenttotal((sum(s.total_not_seen)), (sum(s.total_alerts)))", "percenttotal((sum(s.total_unknown)), (sum(s.total_alerts)))", "((sum(s.total_alerts)) * a.estimated_cost)"], + "Shared Hit Blocks": 260587, + "Shared Read Blocks": 0, + "Shared Dirtied Blocks": 0, + "Shared Written Blocks": 0, + "Local Hit Blocks": 0, + "Local Read Blocks": 0, + "Local Dirtied Blocks": 0, + "Local Written Blocks": 0, + "Temp Read Blocks": 0, + "Temp Written Blocks": 0, + "I/O Read Time": 0.000, + "I/O Write Time": 0.000, + "Plans": [ + { + "Node Type": "Nested Loop", + "Parent Relationship": "Outer", + "Join Type": "Left", + "Startup Cost": 37552.22, + "Total Cost": 56750.41, + "Plan Rows": 2927, + "Plan Width": 461, + "Actual Startup Time": 522.141, + "Actual Total Time": 1161.187, + "Actual Rows": 32352, + "Actual Loops": 1, + "Output": ["(sum(s.total_alerts))", "(sum(s.total_followed))", "(sum(s.total_overridden))", "(sum(s.total_ignored))", "(sum(s.total_not_seen))", "(sum(s.total_unknown))", "a.id", "a.display_name", "a.alert_name", "a.external_alert_id", "a.venue_code", "a.stop_type", "a.deploy_mode", "a.estimated_cost", "a.subtype_id", "altt.code", "altt.name"], + "Shared Hit Blocks": 197115, + "Shared Read Blocks": 0, + "Shared Dirtied Blocks": 0, + "Shared Written Blocks": 0, + "Local Hit Blocks": 0, + "Local Read Blocks": 0, + "Local Dirtied Blocks": 0, + "Local Written Blocks": 0, + "Temp Read Blocks": 0, + "Temp Written Blocks": 0, + "I/O Read Time": 0.000, + "I/O Write Time": 0.000, + "Plans": [ + { + "Node Type": "Nested Loop", + "Parent Relationship": "Outer", + "Join Type": "Inner", + "Startup Cost": 37552.07, + "Total Cost": 56230.56, + "Plan Rows": 2927, + "Plan Width": 405, + "Actual Startup Time": 522.131, + "Actual Total Time": 918.895, + "Actual Rows": 32352, + "Actual Loops": 1, + "Output": ["(sum(s.total_alerts))", "(sum(s.total_followed))", "(sum(s.total_overridden))", "(sum(s.total_ignored))", "(sum(s.total_not_seen))", "(sum(s.total_unknown))", "a.id", "a.display_name", "a.alert_name", "a.external_alert_id", "a.venue_code", "a.stop_type", "a.deploy_mode", "a.estimated_cost", "a.type_id", "a.subtype_id"], + "Shared Hit Blocks": 132411, + "Shared Read Blocks": 0, + "Shared Dirtied Blocks": 0, + "Shared Written Blocks": 0, + "Local Hit Blocks": 0, + "Local Read Blocks": 0, + "Local Dirtied Blocks": 0, + "Local Written Blocks": 0, + "Temp Read Blocks": 0, + "Temp Written Blocks": 0, + "I/O Read Time": 0.000, + "I/O Write Time": 0.000, + "Plans": [ + { + "Node Type": "Aggregate", + "Strategy": "Hashed", + "Parent Relationship": "Outer", + "Startup Cost": 37551.64, + "Total Cost": 37624.82, + "Plan Rows": 2927, + "Plan Width": 56, + "Actual Startup Time": 522.107, + "Actual Total Time": 587.536, + "Actual Rows": 32352, + "Actual Loops": 1, + "Output": ["s.alert_id", "sum(s.total_alerts)", "sum(s.total_followed)", "sum(s.total_overridden)", "sum(s.total_ignored)", "sum(s.total_not_seen)", "sum(s.total_unknown)"], + "Group Key": ["s.alert_id"], + "Shared Hit Blocks": 2784, + "Shared Read Blocks": 0, + "Shared Dirtied Blocks": 0, + "Shared Written Blocks": 0, + "Local Hit Blocks": 0, + "Local Read Blocks": 0, + "Local Dirtied Blocks": 0, + "Local Written Blocks": 0, + "Temp Read Blocks": 0, + "Temp Written Blocks": 0, + "I/O Read Time": 0.000, + "I/O Write Time": 0.000, + "Plans": [ + { + "Node Type": "Bitmap Heap Scan", + "Parent Relationship": "Outer", + "Relation Name": "alert_daily_summaries", + "Schema": "analytics", + "Alias": "s", + "Startup Cost": 3265.35, + "Total Cost": 35524.50, + "Plan Rows": 115837, + "Plan Width": 56, + "Actual Startup Time": 10.745, + "Actual Total Time": 165.318, + "Actual Rows": 140451, + "Actual Loops": 1, + "Output": ["s.id", "s.org_id", "s.alert_id", "s.alert_day", "s.total_alerts", "s.total_followed", "s.total_overridden", "s.total_ignored", "s.total_not_seen", "s.total_unknown", "s.override_comments"], + "Recheck Cond": "((s.org_id = 2) AND (s.alert_day >= '2015-10-01 00:00:00'::timestamp without time zone) AND (s.alert_day <= '2015-12-01 00:00:00'::timestamp without time zone))", + "Rows Removed by Index Recheck": 0, + "Exact Heap Blocks": 2243, + "Lossy Heap Blocks": 0, + "Shared Hit Blocks": 2784, + "Shared Read Blocks": 0, + "Shared Dirtied Blocks": 0, + "Shared Written Blocks": 0, + "Local Hit Blocks": 0, + "Local Read Blocks": 0, + "Local Dirtied Blocks": 0, + "Local Written Blocks": 0, + "Temp Read Blocks": 0, + "Temp Written Blocks": 0, + "I/O Read Time": 0.000, + "I/O Write Time": 0.000, + "Plans": [ + { + "Node Type": "Bitmap Index Scan", + "Parent Relationship": "Outer", + "Index Name": "alert_daily_summaries_org_id_alert_day_idx", + "Startup Cost": 0.00, + "Total Cost": 3236.39, + "Plan Rows": 115837, + "Plan Width": 0, + "Actual Startup Time": 10.445, + "Actual Total Time": 10.445, + "Actual Rows": 140451, + "Actual Loops": 1, + "Index Cond": "((s.org_id = 2) AND (s.alert_day >= '2015-10-01 00:00:00'::timestamp without time zone) AND (s.alert_day <= '2015-12-01 00:00:00'::timestamp without time zone))", + "Shared Hit Blocks": 541, + "Shared Read Blocks": 0, + "Shared Dirtied Blocks": 0, + "Shared Written Blocks": 0, + "Local Hit Blocks": 0, + "Local Read Blocks": 0, + "Local Dirtied Blocks": 0, + "Local Written Blocks": 0, + "Temp Read Blocks": 0, + "Temp Written Blocks": 0, + "I/O Read Time": 0.000, + "I/O Write Time": 0.000 + } + ] + } + ] + }, + { + "Node Type": "Index Scan", + "Parent Relationship": "Inner", + "Scan Direction": "Forward", + "Index Name": "alerts_pkey1", + "Relation Name": "alerts", + "Schema": "analytics", + "Alias": "a", + "Startup Cost": 0.42, + "Total Cost": 6.34, + "Plan Rows": 1, + "Plan Width": 213, + "Actual Startup Time": 0.004, + "Actual Total Time": 0.005, + "Actual Rows": 1, + "Actual Loops": 32352, + "Output": ["a.id", "a.org_id", "a.external_alert_id", "a.alert_name", "a.display_name", "a.estimated_cost", "a.description", "a.source", "a.venue_code", "a.action_expected", "a.cancel_expected", "a.responsible_provider_type", "a.status", "a.version", "a.created_by", "a.created_date", "a.modified_by", "a.modified_date", "a.tracking_id", "a.record_date", "a.released_date", "a.released", "a.comments", "a.default_lockout_hours", "a.deploy_mode", "a.stop_type", "a.importance_level", "a.type_id", "a.subtype_id"], + "Index Cond": "(a.id = s.alert_id)", + "Rows Removed by Index Recheck": 0, + "Shared Hit Blocks": 129627, + "Shared Read Blocks": 0, + "Shared Dirtied Blocks": 0, + "Shared Written Blocks": 0, + "Local Hit Blocks": 0, + "Local Read Blocks": 0, + "Local Dirtied Blocks": 0, + "Local Written Blocks": 0, + "Temp Read Blocks": 0, + "Temp Written Blocks": 0, + "I/O Read Time": 0.000, + "I/O Write Time": 0.000 + } + ] + }, + { + "Node Type": "Index Scan", + "Parent Relationship": "Inner", + "Scan Direction": "Forward", + "Index Name": "alert_types_id_org_id_key", + "Relation Name": "alert_types", + "Schema": "analytics", + "Alias": "altt", + "Startup Cost": 0.15, + "Total Cost": 0.17, + "Plan Rows": 1, + "Plan Width": 72, + "Actual Startup Time": 0.002, + "Actual Total Time": 0.003, + "Actual Rows": 1, + "Actual Loops": 32352, + "Output": ["altt.id", "altt.version", "altt.code", "altt.name", "altt.org_id"], + "Index Cond": "(a.type_id = altt.id)", + "Rows Removed by Index Recheck": 0, + "Shared Hit Blocks": 64704, + "Shared Read Blocks": 0, + "Shared Dirtied Blocks": 0, + "Shared Written Blocks": 0, + "Local Hit Blocks": 0, + "Local Read Blocks": 0, + "Local Dirtied Blocks": 0, + "Local Written Blocks": 0, + "Temp Read Blocks": 0, + "Temp Written Blocks": 0, + "I/O Read Time": 0.000, + "I/O Write Time": 0.000 + } + ] + }, + { + "Node Type": "Index Scan", + "Parent Relationship": "Inner", + "Scan Direction": "Forward", + "Index Name": "alert_subtypes_id_org_id_key", + "Relation Name": "alert_subtypes", + "Schema": "analytics", + "Alias": "altst", + "Startup Cost": 0.14, + "Total Cost": 0.16, + "Plan Rows": 1, + "Plan Width": 23, + "Actual Startup Time": 0.002, + "Actual Total Time": 0.003, + "Actual Rows": 1, + "Actual Loops": 32352, + "Output": ["altst.id", "altst.version", "altst.code", "altst.name", "altst.org_id"], + "Index Cond": "(a.subtype_id = altst.id)", + "Rows Removed by Index Recheck": 0, + "Shared Hit Blocks": 63472, + "Shared Read Blocks": 0, + "Shared Dirtied Blocks": 0, + "Shared Written Blocks": 0, + "Local Hit Blocks": 0, + "Local Read Blocks": 0, + "Local Dirtied Blocks": 0, + "Local Written Blocks": 0, + "Temp Read Blocks": 0, + "Temp Written Blocks": 0, + "I/O Read Time": 0.000, + "I/O Write Time": 0.000 + } + ] + } + ] + } + ] + }, + "Planning Time": 0.507, + "Triggers": [ + ], + "Execution Time": 1953.684 + } +] +` -- cgit v1.2.3