Skip to content

Commit

Permalink
Merge pull request #71 from planetscale/tx-summarize
Browse files Browse the repository at this point in the history
Add transaction information to summary
  • Loading branch information
systay authored Nov 27, 2024
2 parents 24afb2b + f5a1cef commit 367ffb5
Show file tree
Hide file tree
Showing 32 changed files with 3,158 additions and 2,874 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require (

require (
github.com/alecthomas/chroma v0.10.0
github.com/dustin/go-humanize v1.0.1
github.com/fatih/color v1.18.0
github.com/jstemmer/go-junit-report/v2 v2.1.0
github.com/olekukonko/tablewriter v0.0.5
Expand Down Expand Up @@ -37,7 +38,6 @@ require (
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dlclark/regexp2 v1.11.4 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/eapache/queue/v2 v2.0.0-20230407133247-75960ed334e4 // indirect
github.com/ebitengine/purego v0.8.1 // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect
Expand Down
10 changes: 6 additions & 4 deletions go/keys/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@ type (
}
// Output represents the output generated by 'vt keys'
Output struct {
Queries []QueryAnalysisResult `json:"queries"`
Failed []QueryFailedResult `json:"failed,omitempty"`
FileType string `json:"fileType"`
Queries []QueryAnalysisResult `json:"queries"`
Failed []QueryFailedResult `json:"failed,omitempty"`
}
queryList struct {
queries map[string]*QueryAnalysisResult
Expand Down Expand Up @@ -214,8 +215,9 @@ func (ql *queryList) writeJSONTo(w io.Writer) error {
})

res := Output{
Queries: values,
Failed: failedQueries,
FileType: "keys",
Queries: values,
Failed: failedQueries,
}

jsonData, err := json.MarshalIndent(res, " ", " ")
Expand Down
12 changes: 6 additions & 6 deletions go/keys/keys_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,28 +37,28 @@ func TestKeys(t *testing.T) {
FileName: "../../t/tpch_failing_queries.test",
Loader: data.SlowQueryLogLoader{},
},
expectedFile: "../testdata/keys-log.json",
expectedFile: "keys-log.json",
},
{
cfg: Config{
FileName: "../testdata/vtgate.query.log",
Loader: data.VtGateLogLoader{NeedsBindVars: false},
},
expectedFile: "../testdata/keys-log-vtgate.json",
expectedFile: "keys-log-vtgate.json",
},
{
cfg: Config{
FileName: "../testdata/slow_query_log",
Loader: data.SlowQueryLogLoader{},
},
expectedFile: "../testdata/slow-query-log.json",
expectedFile: "slow-query-log.json",
},
{
cfg: Config{
FileName: "../testdata/bigger_slow_query_log.log",
Loader: data.SlowQueryLogLoader{},
},
expectedFile: "../testdata/bigger_slow_query_log.json",
expectedFile: "bigger_slow_query_log.json",
},
}

Expand All @@ -68,12 +68,12 @@ func TestKeys(t *testing.T) {
err := run(sb, tcase.cfg)
require.NoError(t, err)

out, err := os.ReadFile(tcase.expectedFile)
out, err := os.ReadFile("../testdata/" + tcase.expectedFile)
require.NoError(t, err)

assert.Equal(t, string(out), sb.String())
if t.Failed() {
_ = os.WriteFile(tcase.expectedFile+".correct", []byte(sb.String()), 0o644)
_ = os.WriteFile("../testdata/expected/"+tcase.expectedFile, []byte(sb.String()), 0o644)
}
})
}
Expand Down
262 changes: 262 additions & 0 deletions go/summarize/markdown.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
/*
Copyright 2024 The Vitess Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package summarize

import (
"fmt"
"maps"
"slices"
"sort"
"strconv"
"strings"

humanize "github.com/dustin/go-humanize"
"vitess.io/vitess/go/vt/vtgate/planbuilder/operators"

"github.com/vitessio/vt/go/keys"
"github.com/vitessio/vt/go/markdown"
)

func renderHotQueries(md *markdown.MarkDown, queries []keys.QueryAnalysisResult, metricReader getMetric) {
if len(queries) == 0 {
return
}

hasTime := false
// Sort the queries in descending order of hotness
sort.Slice(queries, func(i, j int) bool {
if queries[i].QueryTime != 0 {
hasTime = true
}
return metricReader(queries[i]) > metricReader(queries[j])
})

if !hasTime {
return
}

md.PrintHeader("Top Queries", 2)

// Prepare table headers and rows
headers := []string{"Query ID", "Usage Count", "Total Query Time (ms)", "Avg Query Time (ms)", "Total Rows Examined"}
var rows [][]string

for i, query := range queries {
queryID := fmt.Sprintf("Q%d", i+1)
avgQueryTime := query.QueryTime / float64(query.UsageCount)
rows = append(rows, []string{
queryID,
humanize.Comma(int64(query.UsageCount)),
fmt.Sprintf("%.2f", query.QueryTime),
fmt.Sprintf("%.2f", avgQueryTime),
humanize.Comma(int64(query.RowsExamined)),
})
}

// Print the table
md.PrintTable(headers, rows)

// After the table, list the full queries with their IDs
md.PrintHeader("Query Details", 3)
for i, query := range queries {
queryID := fmt.Sprintf("Q%d", i+1)
md.PrintHeader(queryID, 4)
md.Println("```sql")
md.Println(query.QueryStructure)
md.Println("```")
md.NewLine()
}
}

func renderTableUsage(md *markdown.MarkDown, tableSummaries []*TableSummary, includeRowCount bool) {
if len(tableSummaries) == 0 {
return
}

sort.Slice(tableSummaries, func(i, j int) bool {
if tableSummaries[i].UseCount() == tableSummaries[j].UseCount() {
return tableSummaries[i].Table < tableSummaries[j].Table
}
return tableSummaries[i].UseCount() > tableSummaries[j].UseCount()
})

md.PrintHeader("Tables", 2)
renderTableOverview(md, tableSummaries, includeRowCount)

md.PrintHeader("Column Usage", 3)
for _, summary := range tableSummaries {
renderColumnUsageTable(md, summary)
}
}

func renderTableOverview(md *markdown.MarkDown, tableSummaries []*TableSummary, includeRowCount bool) {
headers := []string{"Table Name", "Reads", "Writes"}
if includeRowCount {
headers = append(headers, "Number of Rows")
}
var rows [][]string
for _, summary := range tableSummaries {
thisRow := []string{
summary.Table,
humanize.Comma(int64(summary.ReadQueryCount)),
humanize.Comma(int64(summary.WriteQueryCount)),
}
if includeRowCount {
thisRow = append(thisRow, humanize.Comma(int64(summary.RowCount)))
}

rows = append(rows, thisRow)
}
md.PrintTable(headers, rows)
}

func renderColumnUsageTable(md *markdown.MarkDown, summary *TableSummary) {
md.PrintHeader(fmt.Sprintf("Table: `%s` (%d reads and %d writes)", summary.Table, summary.ReadQueryCount, summary.WriteQueryCount), 4)

headers := []string{"Column", "Position", "Used %"}
var rows [][]string
var lastName string
for colInfo, usage := range summary.GetColumns() {
name := colInfo.Name
if lastName == name {
name = ""
} else {
lastName = name
}
rows = append(rows, []string{
name,
colInfo.Pos.String(),
fmt.Sprintf("%.0f%%", usage.Percentage),
})
}

md.PrintTable(headers, rows)
}

func renderTablesJoined(md *markdown.MarkDown, summary *Summary) {
type joinDetails struct {
Tbl1, Tbl2 string
Occurrences int
predicates []operators.JoinPredicate
}

var joins []joinDetails
for tables, predicates := range summary.queryGraph {
occurrences := 0
for _, count := range predicates {
occurrences += count
}
joinPredicates := slices.Collect(maps.Keys(predicates))
sort.Slice(joinPredicates, func(i, j int) bool {
return joinPredicates[i].String() < joinPredicates[j].String()
})
joins = append(joins, joinDetails{
Tbl1: tables.Tbl1,
Tbl2: tables.Tbl2,
Occurrences: occurrences,
predicates: joinPredicates,
})
}

if len(joins) == 0 {
return
}

if len(summary.queryGraph) > 0 {
md.PrintHeader("Tables Joined", 2)
}

sort.Slice(joins, func(i, j int) bool {
if joins[i].Occurrences != joins[j].Occurrences {
return joins[i].Occurrences > joins[j].Occurrences
}
if joins[i].Tbl1 != joins[j].Tbl1 {
return joins[i].Tbl1 < joins[j].Tbl1
}
return joins[i].Tbl2 < joins[j].Tbl2
})

md.Println("```")
for _, join := range joins {
md.Printf("%s ↔ %s (Occurrences: %d)\n", join.Tbl1, join.Tbl2, join.Occurrences)
for i, pred := range join.predicates {
var s string
if i == len(join.predicates)-1 {
s = "└─"
} else {
s = "├─"
}
md.Printf("%s %s\n", s, pred.String())
}
md.NewLine()
}
md.Println("```")
}

func renderFailures(md *markdown.MarkDown, failures []FailuresSummary) {
if len(failures) == 0 {
return
}
md.PrintHeader("Failures", 2)

headers := []string{"Error", "Count"}
var rows [][]string
for _, failure := range failures {
rows = append(rows, []string{failure.Error, strconv.Itoa(failure.Count)})
}
md.PrintTable(headers, rows)
}

func renderTransactions(md *markdown.MarkDown, transactions []TransactionSummary) {
if len(transactions) == 0 {
return
}

md.PrintHeader("Transaction Patterns", 2)

for i, tx := range transactions {
var tables []string
for _, query := range tx.Queries {
tables = append(tables, query.Table)
}
tables = uniquefy(tables)
md.NewLine()
md.PrintHeader(fmt.Sprintf("Pattern %d (Observed %d times)\n\n", i+1, tx.Count), 3)
md.Printf("Tables Involved: %s\n", strings.Join(tables, ", "))
md.PrintHeader("Query Patterns", 3)
for i, query := range tx.Queries {
md.Printf("%d. **%s** on `%s` \n", i+1, strings.ToTitle(query.Type), query.Table)
md.Printf(" Predicates: %s\n\n", strings.Join(query.Predicates, " AND "))
}

md.PrintHeader("Shared Predicate Values", 3)
for idx, join := range tx.Joins {
md.Printf("* Value %d applied to:\n", idx)
for _, s := range join {
md.Printf(" - %s\n", s)
}
}
if i != len(transactions)-1 {
md.Printf("---\n")
}
}
}

func uniquefy(s []string) []string {
sort.Strings(s)
return slices.Compact(s)
}
Loading

0 comments on commit 367ffb5

Please sign in to comment.