From 0e4857a11ba57c8c29e27dc063f65da535bd5ff0 Mon Sep 17 00:00:00 2001 From: Rod Vagg Date: Thu, 9 Jan 2025 13:30:18 +1100 Subject: [PATCH] feat(shed): lotus-shed msg --gas-stats (w/ tabular output) (#12817) --- CHANGELOG.md | 1 + cmd/lotus-shed/msg.go | 173 +++++++++++++++++++++++++++++++-- lib/tablewriter/tablewriter.go | 133 ++++++++++++++++++++++++- 3 files changed, 295 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d0a948414d..1d382436648 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ - Lotus now reports the network name as a tag in most metrics. Some untagged metrics will be completed in a follow-up at a later date. ([filecoin-project/lotus#12733](https://github.com/filecoin-project/lotus/pull/12733)) - Refactored Ethereum API implementation into smaller, more manageable modules in a new `github.com/filecoin-project/lotus/node/impl/eth` package. ([filecoin-project/lotus#12796](https://github.com/filecoin-project/lotus/pull/12796)) - Generate the cli docs directly from the code instead compiling and executing binaries' `help` output. ([filecoin-project/lotus#12717](https://github.com/filecoin-project/lotus/pull/12717)) +- Add `lotus-shed msg --gas-stats` to show summarised gas stats for a given message. ([filecoin-project/lotus#12817](https://github.com/filecoin-project/lotus/pull/12817)) # UNRELEASED v.1.32.0 diff --git a/cmd/lotus-shed/msg.go b/cmd/lotus-shed/msg.go index 35f8eed35c1..4de7789a85c 100644 --- a/cmd/lotus-shed/msg.go +++ b/cmd/lotus-shed/msg.go @@ -6,6 +6,8 @@ import ( "encoding/hex" "encoding/json" "fmt" + "io" + "sort" "github.com/fatih/color" "github.com/ipfs/go-cid" @@ -19,6 +21,7 @@ import ( "github.com/filecoin-project/lotus/chain/consensus" "github.com/filecoin-project/lotus/chain/types" lcli "github.com/filecoin-project/lotus/cli" + "github.com/filecoin-project/lotus/lib/tablewriter" ) var msgCmd = &cli.Command{ @@ -27,10 +30,19 @@ var msgCmd = &cli.Command{ Usage: "Translate message between various formats", ArgsUsage: "Message in any form", Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "show-message", + Usage: "Print the message details", + Value: true, + }, &cli.BoolFlag{ Name: "exec-trace", Usage: "Print the execution trace", }, + &cli.BoolFlag{ + Name: "gas-stats", + Usage: "Print a summary of gas charges", + }, }, Action: func(cctx *cli.Context) error { if cctx.NArg() != 1 { @@ -82,16 +94,61 @@ var msgCmd = &cli.Command{ fmt.Printf("Return: %x\n", res.MsgRct.Return) fmt.Printf("Gas Used: %d\n", res.MsgRct.GasUsed) } + + if cctx.Bool("gas-stats") { + var printTrace func(descPfx string, trace types.ExecutionTrace) error + printTrace = func(descPfx string, trace types.ExecutionTrace) error { + typ := "Message" + if descPfx != "" { + typ = "Subcall" + } + _, _ = fmt.Fprintln(cctx.App.Writer, color.New(color.Bold).Sprint(fmt.Sprintf("%s (%s%s) gas charges:", typ, descPfx, trace.Msg.To))) + if err := statsTable(cctx.App.Writer, trace, false); err != nil { + return err + } + for _, subtrace := range trace.Subcalls { + _, _ = fmt.Fprintln(cctx.App.Writer) + if err := printTrace(descPfx+trace.Msg.To.String()+"➜", subtrace); err != nil { + return err + } + } + return nil + } + if err := printTrace("", res.ExecutionTrace); err != nil { + return err + } + if len(res.ExecutionTrace.Subcalls) > 0 { + _, _ = fmt.Fprintln(cctx.App.Writer) + _, _ = fmt.Fprintln(cctx.App.Writer, color.New(color.Bold).Sprint("Total gas charges:")) + if err := statsTable(cctx.App.Writer, res.ExecutionTrace, true); err != nil { + return err + } + perCallTrace := gasTracesPerCall(res.ExecutionTrace) + _, _ = fmt.Fprintln(cctx.App.Writer) + _, _ = fmt.Fprintln(cctx.App.Writer, color.New(color.Bold).Sprint("Gas charges per call:")) + if err := statsTable(cctx.App.Writer, perCallTrace, false); err != nil { + return err + } + } + } } - switch msg := msg.(type) { - case *types.SignedMessage: - return printSignedMessage(cctx, msg) - case *types.Message: - return printMessage(cctx, msg) - default: - return xerrors.Errorf("this error message can't be printed") + if cctx.Bool("show-message") { + switch msg := msg.(type) { + case *types.SignedMessage: + if err := printSignedMessage(cctx, msg); err != nil { + return err + } + case *types.Message: + if err := printMessage(cctx, msg); err != nil { + return err + } + default: + return xerrors.Errorf("this error message can't be printed") + } } + + return nil }, } @@ -335,3 +392,105 @@ func messageFromCID(cctx *cli.Context, c cid.Cid) (types.ChainMsg, error) { return messageFromBytes(cctx, msgb) } + +type gasTally struct { + storageGas int64 + computeGas int64 + count int +} + +func accumGasTallies(charges map[string]*gasTally, totals *gasTally, trace types.ExecutionTrace, recurse bool) { + for _, charge := range trace.GasCharges { + name := charge.Name + if _, ok := charges[name]; !ok { + charges[name] = &gasTally{} + } + charges[name].computeGas += charge.ComputeGas + charges[name].storageGas += charge.StorageGas + charges[name].count++ + totals.computeGas += charge.ComputeGas + totals.storageGas += charge.StorageGas + totals.count++ + } + if recurse { + for _, subtrace := range trace.Subcalls { + accumGasTallies(charges, totals, subtrace, recurse) + } + } +} + +func statsTable(out io.Writer, trace types.ExecutionTrace, recurse bool) error { + tw := tablewriter.New( + tablewriter.Col("Type"), + tablewriter.Col("Count", tablewriter.RightAlign()), + tablewriter.Col("Storage Gas", tablewriter.RightAlign()), + tablewriter.Col("S%", tablewriter.RightAlign()), + tablewriter.Col("Compute Gas", tablewriter.RightAlign()), + tablewriter.Col("C%", tablewriter.RightAlign()), + tablewriter.Col("Total Gas", tablewriter.RightAlign()), + tablewriter.Col("T%", tablewriter.RightAlign()), + ) + + totals := &gasTally{} + charges := make(map[string]*gasTally) + accumGasTallies(charges, totals, trace, recurse) + + // Sort by name + names := make([]string, 0, len(charges)) + for name := range charges { + names = append(names, name) + } + sort.Strings(names) + + for _, name := range names { + charge := charges[name] + tw.Write(map[string]interface{}{ + "Type": name, + "Count": charge.count, + "Storage Gas": charge.storageGas, + "S%": fmt.Sprintf("%.2f", float64(charge.storageGas)/float64(totals.storageGas)*100), + "Compute Gas": charge.computeGas, + "C%": fmt.Sprintf("%.2f", float64(charge.computeGas)/float64(totals.computeGas)*100), + "Total Gas": charge.storageGas + charge.computeGas, + "T%": fmt.Sprintf("%.2f", float64(charge.storageGas+charge.computeGas)/float64(totals.storageGas+totals.computeGas)*100), + }) + } + tw.Write(map[string]interface{}{ + "Type": "Total", + "Count": totals.count, + "Storage Gas": totals.storageGas, + "S%": "100.00", + "Compute Gas": totals.computeGas, + "C%": "100.00", + "Total Gas": totals.storageGas + totals.computeGas, + "T%": "100.00", + }) + return tw.Flush(out, tablewriter.WithBorders()) +} + +// Takes an execution trace and returns a new trace that groups all the gas charges by the message +// they were charged in, with the gas charges named per message; the output is partial and only +// suitable for calling statsTable() with. +func gasTracesPerCall(inTrace types.ExecutionTrace) types.ExecutionTrace { + outTrace := types.ExecutionTrace{ + GasCharges: []*types.GasTrace{}, + } + count := 1 + var accum func(name string, trace types.ExecutionTrace) + accum = func(name string, trace types.ExecutionTrace) { + totals := &gasTally{} + charges := make(map[string]*gasTally) + accumGasTallies(charges, totals, trace, false) + outTrace.GasCharges = append(outTrace.GasCharges, &types.GasTrace{ + Name: fmt.Sprintf("#%d %s", count, name), + ComputeGas: totals.computeGas, + StorageGas: totals.storageGas, + }) + count++ + for _, subtrace := range trace.Subcalls { + accum(name+"➜"+subtrace.Msg.To.String(), subtrace) + } + } + accum(inTrace.Msg.To.String(), inTrace) + return outTrace +} diff --git a/lib/tablewriter/tablewriter.go b/lib/tablewriter/tablewriter.go index 75e089938bd..b826ac5ce5c 100644 --- a/lib/tablewriter/tablewriter.go +++ b/lib/tablewriter/tablewriter.go @@ -13,6 +13,31 @@ type Column struct { Name string SeparateLine bool Lines int + RightAlign bool +} + +type tableCfg struct { + borders bool +} + +type TableOption func(*tableCfg) + +func WithBorders() TableOption { + return func(c *tableCfg) { + c.borders = true + } +} + +type columnCfg struct { + rightAlign bool +} + +type ColumnOption func(*columnCfg) + +func RightAlign() ColumnOption { + return func(c *columnCfg) { + c.rightAlign = true + } } type TableWriter struct { @@ -20,10 +45,15 @@ type TableWriter struct { rows []map[int]string } -func Col(name string) Column { +func Col(name string, opts ...ColumnOption) Column { + cfg := &columnCfg{} + for _, o := range opts { + o(cfg) + } return Column{ Name: name, SeparateLine: false, + RightAlign: cfg.rightAlign, } } @@ -69,7 +99,12 @@ cloop: w.rows = append(w.rows, byColID) } -func (w *TableWriter) Flush(out io.Writer) error { +func (w *TableWriter) Flush(out io.Writer, opts ...TableOption) error { + cfg := &tableCfg{} + for _, o := range opts { + o(cfg) + } + colLengths := make([]int, len(w.cols)) header := map[int]string{} @@ -99,21 +134,62 @@ func (w *TableWriter) Flush(out io.Writer) error { } } - for _, row := range w.rows { + if cfg.borders { + // top line + if _, err := fmt.Fprint(out, "┌"); err != nil { + return err + } + for ci, col := range w.cols { + if col.Lines == 0 { + continue + } + if _, err := fmt.Fprint(out, strings.Repeat("─", colLengths[ci]+2)); err != nil { + return err + } + if ci != len(w.cols)-1 { + if _, err := fmt.Fprint(out, "┬"); err != nil { + return err + } + } + } + if _, err := fmt.Fprintln(out, "┐"); err != nil { + return err + } + } + + for lineNumber, row := range w.rows { cols := make([]string, len(w.cols)) + if cfg.borders { + if _, err := fmt.Fprint(out, "│ "); err != nil { + return err + } + } + for ci, col := range w.cols { if col.Lines == 0 { continue } - e, _ := row[ci] + e := row[ci] pad := colLengths[ci] - cliStringLength(e) + 2 + if cfg.borders { + pad-- + } if !col.SeparateLine && col.Lines > 0 { - e = e + strings.Repeat(" ", pad) + if col.RightAlign { + e = strings.Repeat(" ", pad-1) + e + " " + } else { + e = e + strings.Repeat(" ", pad) + } if _, err := fmt.Fprint(out, e); err != nil { return err } + if cfg.borders { + if _, err := fmt.Fprint(out, "│ "); err != nil { + return err + } + } } cols[ci] = e @@ -132,6 +208,53 @@ func (w *TableWriter) Flush(out io.Writer) error { return err } } + + if lineNumber == 0 && cfg.borders { + // print bottom of header + if _, err := fmt.Fprint(out, "├"); err != nil { + return err + } + for ci, col := range w.cols { + if col.Lines == 0 { + continue + } + + if _, err := fmt.Fprint(out, strings.Repeat("─", colLengths[ci]+2)); err != nil { + return err + } + if ci != len(w.cols)-1 { + if _, err := fmt.Fprint(out, "┼"); err != nil { + return err + } + } + } + if _, err := fmt.Fprintln(out, "┤"); err != nil { + return err + } + } + } + + if cfg.borders { + // bottom line + if _, err := fmt.Fprint(out, "└"); err != nil { + return err + } + for ci, col := range w.cols { + if col.Lines == 0 { + continue + } + if _, err := fmt.Fprint(out, strings.Repeat("─", colLengths[ci]+2)); err != nil { + return err + } + if ci != len(w.cols)-1 { + if _, err := fmt.Fprint(out, "┴"); err != nil { + return err + } + } + } + if _, err := fmt.Fprintln(out, "┘"); err != nil { + return err + } } return nil