Skip to content

Commit ecc18e0

Browse files
committed
table: automatically widen columns if needed
The table code currently defines a fixed column width and effectively truncates any value that is longer (except in the last column). It would be unfortunate to miss some useful part of a metric because of this. This change makes the table code automatically widen the columns if necessary. We also automatically add a vertical space between columns that don't have a divider.
1 parent f6951af commit ecc18e0

File tree

6 files changed

+110
-94
lines changed

6 files changed

+110
-94
lines changed

internal/ascii/table/table.go

Lines changed: 55 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"fmt"
99
"iter"
1010
"math"
11+
"slices"
1112
"strconv"
1213
"strings"
1314

@@ -47,58 +48,32 @@ import (
4748
// Mai 2 10
4849
// Yuumi 5 10
4950
func Define[T any](fields ...Element) Layout[T] {
50-
var verticalHeader strings.Builder
51-
var verticalHeaderSep strings.Builder
52-
defFields := make([]definitionField, len(fields))
5351
maxFieldWidth := 0
5452
for i := range len(fields) {
5553
maxFieldWidth = max(maxFieldWidth, fields[i].width())
5654
}
5755

56+
cumulativeFieldWidth := 0
5857
for i := range len(fields) {
5958
w := fields[i].width()
6059
h := fields[i].header(Vertically, maxFieldWidth)
6160
if len(h) > w {
6261
panic(fmt.Sprintf("header %q is too long for column %d", h, i))
6362
}
64-
65-
defFields[i] = definitionField{
66-
f: fields[i],
67-
off: verticalHeaderSep.Len(),
68-
}
69-
70-
// Create the vertical header strings.
71-
if _, ok := fields[i].(divider); ok {
72-
verticalHeaderSep.WriteString("-+-")
73-
} else {
74-
verticalHeaderSep.WriteString(strings.Repeat("-", w))
75-
}
76-
padding := w - len(h)
77-
verticalHeader.WriteString(fields[i].align().maybePadding(AlignRight, padding))
78-
verticalHeader.WriteString(h)
79-
verticalHeader.WriteString(fields[i].align().maybePadding(AlignLeft, padding))
63+
cumulativeFieldWidth += w
8064
}
8165
return Layout[T]{
82-
CumulativeFieldWidth: verticalHeaderSep.Len(),
66+
CumulativeFieldWidth: cumulativeFieldWidth,
8367
MaxFieldWidth: maxFieldWidth,
84-
fields: defFields,
85-
verticalHeaderLine: verticalHeader.String(),
86-
verticalHeaderSep: verticalHeaderSep.String(),
68+
fields: fields,
8769
}
8870
}
8971

9072
// A Layout defines the layout of a table.
9173
type Layout[T any] struct {
9274
CumulativeFieldWidth int
9375
MaxFieldWidth int
94-
fields []definitionField
95-
verticalHeaderLine string
96-
verticalHeaderSep string
97-
}
98-
99-
type definitionField struct {
100-
f Element
101-
off int
76+
fields []Element
10277
}
10378

10479
// RenderOptions specifies the options for rendering a table.
@@ -110,34 +85,61 @@ type RenderOptions struct {
11085
// returning the modified cursor.
11186
func (d *Layout[T]) Render(start ascii.Cursor, opts RenderOptions, rows iter.Seq[T]) ascii.Cursor {
11287
cur := start
88+
tuples := slices.Collect(rows)
11389

11490
if opts.Orientation == Vertically {
115-
cur.Offset(0, 0).WriteString(d.verticalHeaderLine)
116-
cur.Offset(1, 0).WriteString(d.verticalHeaderSep)
117-
tupleIndex := 0
118-
for t := range rows {
119-
for _, c := range d.fields {
120-
if div, ok := c.f.(divider); ok {
121-
div.renderStatic(Vertically, d.MaxFieldWidth, cur.Offset(2+tupleIndex, c.off))
122-
} else {
123-
ctx := RenderContext[T]{
124-
Orientation: Vertically,
125-
Pos: cur.Offset(2+tupleIndex, c.off),
126-
MaxFieldWidth: d.MaxFieldWidth,
127-
}
128-
width := c.f.width()
129-
spec := widthStr(width, c.f.align()) + "s"
130-
ctx.PaddedPos(width).Printf(spec, c.f.(Field[T]).renderValue(tupleIndex, t))
91+
vals := make([]string, len(tuples))
92+
for fieldIdx, c := range d.fields {
93+
if fieldIdx > 0 {
94+
cur.Offset(1, 0).WriteString("-")
95+
// Each column is separated by a space from the previous column or
96+
// separator.
97+
cur = cur.Offset(0, 1)
98+
}
99+
if _, ok := c.(divider); ok {
100+
cur.Offset(0, 0).WriteString("|")
101+
cur.Offset(1, 0).WriteString("+")
102+
for i := range tuples {
103+
cur.Offset(2+i, 0).WriteString("|")
104+
}
105+
cur = cur.Offset(0, 1)
106+
continue
107+
}
108+
for i, t := range tuples {
109+
vals[i] = c.(Field[T]).renderValue(i, t)
110+
}
111+
112+
width := c.width()
113+
// If one of the values exceeds the column width, widen the column as
114+
// necessary.
115+
for i := range vals {
116+
width = max(width, len(vals[i]))
117+
}
118+
header := c.header(Vertically, width)
119+
align := c.align()
120+
padding := width - len(header)
121+
cur.Offset(0, 0).WriteString(
122+
align.maybePadding(AlignRight, padding) + header + align.maybePadding(AlignLeft, padding),
123+
)
124+
cur.Offset(1, 0).WriteString(strings.Repeat("-", width))
125+
126+
for i := range vals {
127+
ctx := RenderContext[T]{
128+
Orientation: Vertically,
129+
Pos: cur.Offset(2+i, 0),
130+
MaxFieldWidth: d.MaxFieldWidth,
131131
}
132+
spec := widthStr(width, c.align()) + "s"
133+
ctx.PaddedPos(width).Printf(spec, vals[i])
132134
}
133-
tupleIndex++
135+
cur = cur.Offset(0, width)
134136
}
135-
return cur.Offset(2+tupleIndex, 0)
137+
return start.Offset(2+len(tuples), 0)
136138
}
137139

138140
for i := range d.fields {
139-
cur.Offset(i, 0).WriteString(d.fields[i].f.header(Horizontally, d.MaxFieldWidth))
140-
if _, ok := d.fields[i].f.(divider); ok {
141+
cur.Offset(i, 0).WriteString(d.fields[i].header(Horizontally, d.MaxFieldWidth))
142+
if _, ok := d.fields[i].(divider); ok {
141143
cur.Offset(i, d.MaxFieldWidth).WriteString("-+-")
142144
} else {
143145
cur.Offset(i, d.MaxFieldWidth).WriteString(" | ")
@@ -147,15 +149,15 @@ func (d *Layout[T]) Render(start ascii.Cursor, opts RenderOptions, rows iter.Seq
147149
c := d.MaxFieldWidth + 3
148150
for t := range rows {
149151
for i := range d.fields {
150-
if div, ok := d.fields[i].f.(divider); ok {
152+
if div, ok := d.fields[i].(divider); ok {
151153
div.renderStatic(Horizontally, d.MaxFieldWidth, cur.Offset(i, c))
152154
} else {
153155
ctx := RenderContext[T]{
154156
Orientation: Horizontally,
155157
Pos: cur.Offset(i, c),
156158
MaxFieldWidth: d.MaxFieldWidth,
157159
}
158-
f := d.fields[i].f.(Field[T])
160+
f := d.fields[i].(Field[T])
159161
width := f.width()
160162
spec := widthStr(width, f.align()) + "s"
161163
ctx.PaddedPos(width).Printf(spec, f.renderValue(tupleIndex, t))

internal/ascii/table/table_test.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,27 @@ func TestTable(t *testing.T) {
4444
return wb.String()
4545
case "cats-nodiv":
4646
def := Define[Cat](
47-
String("name", 7, AlignLeft, func(c Cat) string { return c.Name }),
48-
Int("age", 4, AlignRight, func(c Cat) int { return c.Age }),
47+
String("name", 6, AlignLeft, func(c Cat) string { return c.Name }),
48+
Int("age", 3, AlignRight, func(c Cat) int { return c.Age }),
4949
Int("cuteness", 8, AlignRight, func(c Cat) int { return c.Cuteness }),
5050
)
5151
wb.Reset(def.CumulativeFieldWidth)
5252
def.Render(wb.At(0, 0), RenderOptions{}, slices.Values(cats))
5353
return wb.String()
54+
case "cats-column-too-wide":
55+
c := slices.Clone(cats)
56+
for i := range c {
57+
c[i].Age *= 1_000_000
58+
}
59+
def := Define[Cat](
60+
String("name", 6, AlignLeft, func(c Cat) string { return c.Name }),
61+
Div(),
62+
Int("age", 3, AlignRight, func(c Cat) int { return c.Age }),
63+
Int("c", 1, AlignRight, func(c Cat) int { return c.Cuteness }),
64+
)
65+
wb.Reset(def.CumulativeFieldWidth)
66+
def.Render(wb.At(0, 0), RenderOptions{}, slices.Values(c))
67+
return wb.String()
5468
default:
5569
return fmt.Sprintf("unknown command: %s", td.Cmd)
5670
}
Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
cats-nodiv
22
----
3-
name agecuteness
4-
-------------------
5-
Chicken 5 10
6-
Heart 4 10
7-
Mai 2 10
8-
Poi 15 10
9-
Pigeon 2 10
10-
Sugar 8 10
11-
Yaya 5 10
12-
Yuumi 5 10
3+
name age cuteness
4+
--------------------
5+
Chicken 5 10
6+
Heart 4 10
7+
Mai 2 10
8+
Poi 15 10
9+
Pigeon 2 10
10+
Sugar 8 10
11+
Yaya 5 10
12+
Yuumi 5 10
1313

1414
cats-autoincrement
1515
----
@@ -23,3 +23,16 @@ idx | name
2323
5 | Sugar
2424
6 | Yaya
2525
7 | Yuumi
26+
27+
cats-column-too-wide
28+
----
29+
name | age c
30+
--------+------------
31+
Chicken | 5000000 10
32+
Heart | 4000000 10
33+
Mai | 2000000 10
34+
Poi | 15000000 10
35+
Pigeon | 2000000 10
36+
Sugar | 8000000 10
37+
Yaya | 5000000 10
38+
Yuumi | 5000000 10

metrics.go

Lines changed: 10 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -614,73 +614,60 @@ var (
614614
levelMetricsTableTopHeader = `LSM | vtables | value sep | | ingested | amp`
615615
levelMetricsTable = table.Define[*LevelMetrics](
616616
table.AutoIncrement[*LevelMetrics]("level", 5, table.AlignRight),
617-
table.Bytes("size", 11, table.AlignRight, func(m *LevelMetrics) uint64 { return uint64(m.TablesSize) + m.EstimatedReferencesSize }),
617+
table.Bytes("size", 10, table.AlignRight, func(m *LevelMetrics) uint64 { return uint64(m.TablesSize) + m.EstimatedReferencesSize }),
618618
table.Div(),
619619
table.Count("tables", 6, table.AlignRight, func(m *LevelMetrics) int64 { return m.TablesCount }),
620-
table.Literal[*LevelMetrics](" "),
621620
table.Bytes("size", 5, table.AlignRight, func(m *LevelMetrics) int64 { return m.TablesSize }),
622621
table.Div(),
623622
table.Count("count", 6, table.AlignRight, func(m *LevelMetrics) uint64 { return m.VirtualTablesCount }),
624-
table.Literal[*LevelMetrics](" "),
625623
table.Count("size", 5, table.AlignRight, func(m *LevelMetrics) uint64 { return m.VirtualTablesSize }),
626624
table.Div(),
627625
table.Bytes("refsz", 6, table.AlignRight, func(m *LevelMetrics) uint64 { return m.EstimatedReferencesSize }),
628-
table.Literal[*LevelMetrics](" "),
629626
table.Bytes("valblk", 6, table.AlignRight, func(m *LevelMetrics) uint64 { return m.Additional.ValueBlocksSize }),
630627
table.Div(),
631628
table.Bytes("in", 6, table.AlignRight, func(m *LevelMetrics) uint64 { return m.TableBytesIn }),
632629
table.Div(),
633630
table.Count("tables", 6, table.AlignRight, func(m *LevelMetrics) uint64 { return m.TablesIngested }),
634-
table.Literal[*LevelMetrics](" "),
635631
table.Bytes("size", 5, table.AlignRight, func(m *LevelMetrics) uint64 { return m.TableBytesIngested }),
636632
table.Div(),
637633
table.Int("r", 3, table.AlignRight, func(m *LevelMetrics) int { return int(m.Sublevels) }),
638-
table.Literal[*LevelMetrics](" "),
639634
table.Float("w", 5, table.AlignRight, func(m *LevelMetrics) float64 { return m.WriteAmp() }),
640635
)
641636
levelCompactionMetricsTableTopHeader = `COMPACTIONS | moved | multilevel | read | written`
642637
compactionLevelMetricsTable = table.Define[*LevelMetrics](
643638
table.AutoIncrement[*LevelMetrics]("level", 5, table.AlignRight),
644639
table.Div(),
645640
table.Float("score", 5, table.AlignRight, func(m *LevelMetrics) float64 { return m.Score }),
646-
table.Literal[*LevelMetrics](" "),
647641
table.Float("ff", 5, table.AlignRight, func(m *LevelMetrics) float64 { return m.FillFactor }),
648-
table.Literal[*LevelMetrics](" "),
649642
table.Float("cff", 5, table.AlignRight, func(m *LevelMetrics) float64 { return m.CompensatedFillFactor }),
650643
table.Div(),
651644
table.Count("tables", 6, table.AlignRight, func(m *LevelMetrics) uint64 { return m.TablesMoved }),
652-
table.Literal[*LevelMetrics](" "),
653645
table.Bytes("size", 5, table.AlignRight, func(m *LevelMetrics) uint64 { return m.TableBytesMoved }),
654646
table.Div(),
655647
table.Bytes("top", 5, table.AlignRight, func(m *LevelMetrics) uint64 { return m.MultiLevel.TableBytesInTop }),
656-
table.Literal[*LevelMetrics](" "),
657648
table.Bytes("in", 5, table.AlignRight, func(m *LevelMetrics) uint64 { return m.MultiLevel.TableBytesIn }),
658-
table.Literal[*LevelMetrics](" "),
659649
table.Bytes("read", 5, table.AlignRight, func(m *LevelMetrics) uint64 { return m.MultiLevel.TableBytesRead }),
660650
table.Div(),
661651
table.Bytes("tables", 6, table.AlignRight, func(m *LevelMetrics) uint64 { return m.TableBytesRead }),
662-
table.Literal[*LevelMetrics](" "),
663652
table.Bytes("blob", 5, table.AlignRight, func(m *LevelMetrics) uint64 { return m.BlobBytesRead }),
664653
table.Div(),
665654
table.Count("tables", 6, table.AlignRight, func(m *LevelMetrics) uint64 { return m.TablesFlushed + m.TablesCompacted }),
666-
table.Literal[*LevelMetrics](" "),
667655
table.Bytes("sstsz", 6, table.AlignRight, func(m *LevelMetrics) uint64 { return m.TableBytesFlushed + m.TableBytesCompacted }),
668-
table.Literal[*LevelMetrics](" "),
669656
table.Bytes("blobsz", 6, table.AlignRight, func(m *LevelMetrics) uint64 { return m.BlobBytesFlushed + m.BlobBytesCompacted }),
670657
)
671658
compactionKindTable = table.Define[compactionKindsInfo](
672659
table.String("kind", 6, table.AlignRight, func(i compactionKindsInfo) string { return "count" }),
673660
table.Div(),
674661
table.String("default", 8, table.AlignRight, func(i compactionKindsInfo) string { return i.def }),
675-
table.String("delete", 9, table.AlignRight, func(i compactionKindsInfo) string { return i.delete }),
676-
table.String("elision", 9, table.AlignRight, func(i compactionKindsInfo) string { return i.elision }),
677-
table.String("copy", 9, table.AlignRight, func(i compactionKindsInfo) string { return i.copy }),
678-
table.String("move", 9, table.AlignRight, func(i compactionKindsInfo) string { return i.move }),
679-
table.String("read", 9, table.AlignRight, func(i compactionKindsInfo) string { return i.read }),
680-
table.String("tomb", 9, table.AlignRight, func(i compactionKindsInfo) string { return i.tombstone }),
681-
table.String("rewrite", 9, table.AlignRight, func(i compactionKindsInfo) string { return i.rewrite }),
682-
table.String("multi", 9, table.AlignRight, func(i compactionKindsInfo) string { return i.multilevel }),
683-
table.String("blob", 9, table.AlignRight, func(i compactionKindsInfo) string { return i.blob }),
662+
table.String("delete", 8, table.AlignRight, func(i compactionKindsInfo) string { return i.delete }),
663+
table.String("elision", 8, table.AlignRight, func(i compactionKindsInfo) string { return i.elision }),
664+
table.String("copy", 8, table.AlignRight, func(i compactionKindsInfo) string { return i.copy }),
665+
table.String("move", 8, table.AlignRight, func(i compactionKindsInfo) string { return i.move }),
666+
table.String("read", 8, table.AlignRight, func(i compactionKindsInfo) string { return i.read }),
667+
table.String("tomb", 8, table.AlignRight, func(i compactionKindsInfo) string { return i.tombstone }),
668+
table.String("rewrite", 8, table.AlignRight, func(i compactionKindsInfo) string { return i.rewrite }),
669+
table.String("multi", 8, table.AlignRight, func(i compactionKindsInfo) string { return i.multilevel }),
670+
table.String("blob", 8, table.AlignRight, func(i compactionKindsInfo) string { return i.blob }),
684671
)
685672
commitPipelineInfoTableTopHeader = `COMMIT PIPELINE`
686673
commitPipelineInfoTableSubHeader = ` wals | memtables | ingestions`

testdata/compaction/l0_to_lbase_compaction

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,9 @@ total | 0 0.09 0.09 | 3 6MB | 0B 0B 0B | 0B 0B |
6161

6262
COMMIT PIPELINE
6363
wals | memtables | ingestions
64-
files | written | overhead | flushes | live | zombie | total | flushable
65-
----------+------------+-----------+-----------+------------+------------+-----------+------------
66-
1 (0B) | 4.5MB: 4.5 | 0.0% | 2 | 1 (512KB) | 1 (512KB) | 9.5 M | 0 (0B)
64+
files | written | overhead | flushes | live | zombie | total | flushable
65+
----------+--------------+-----------+-----------+------------+------------+-----------+------------
66+
1 (0B) | 4.5MB: 4.5MB | 0.0% | 2 | 1 (512KB) | 1 (512KB) | 9.5 M | 0 (0B)
6767

6868
ITERATORS
6969
block cache | file cache | filter | sst iters | snapshots

testdata/metrics

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,9 @@ ITERATORS
4040
100 (1GB) | 66.7% | 180 (1MB) | 48.7% | 47.4% | 21 | 4
4141

4242
FILES tables | blob files | blob values
43-
stats prog | backing | zombie | live | zombie | total | refed
44-
--------------+------------+-----------------------+------------+------------+--------+-----------
45-
31 pending | 1 (2MB) | 18 (17MB local:30MB) | 1.2K (15GB | 14 (30MB) | 14GB | 79% (11GB)
43+
stats prog | backing | zombie | live | zombie | total | refed
44+
--------------+------------+-----------------------+-------------+------------+--------+-----------
45+
31 pending | 1 (2MB) | 18 (17MB local:30MB) | 1.2K (15GB) | 14 (30MB) | 14GB | 79% (11GB)
4646

4747
CGO MEMORY | block cache | memtables
4848
tot | tot | data | maps | ents | tot

0 commit comments

Comments
 (0)