From a4634d7c538929ce64df57d5192823faf94ba17a Mon Sep 17 00:00:00 2001 From: karitham Date: Fri, 7 Aug 2026 22:48:47 +0200 Subject: [PATCH] formatter: cut doc IR allocation by another 8x on the hot path Continuing from the pooled arena: the remaining format-side allocations were variadic slice arguments to f.Concat (one heap slice per call), undersized parts regions growing on append, per-field pad strings, and the printer pool being dropped by the GC between calls. - annotationsDoc builds its group from Parts regions instead of three variadic f.Concat calls; emitWithAnnotations and emitTokens request regions sized to their real append counts. - the printer moved into the Arena: its scratch survives the GC as long as the arena does, where the sync.Pool was cleared every cycle. - padRight slices a shared spaces constant for empty bases; fieldPads pre-sizes its pad list. BenchmarkParseFormat: 73.4k -> 8.3k allocs/op, 4.77MB -> 1.16MB, 4.9ms -> ~2.6ms. The formatter now allocates about as much as the parser, whose AST nodes are kept and cannot be pooled. --- doc/doc.go | 6 ++-- doc/print.go | 69 ++++++++++++++++++++++++++++++++++++++++----- formatter/body.go | 30 ++++++++++++++++---- formatter/field.go | 66 ++++++++++++++++++++++++++++++------------- formatter/format.go | 60 ++++++++++++++++++++++++--------------- formatter/value.go | 30 ++++++++++++++------ 6 files changed, 195 insertions(+), 66 deletions(-) diff --git a/doc/doc.go b/doc/doc.go index 15af542..eedf492 100644 --- a/doc/doc.go +++ b/doc/doc.go @@ -71,8 +71,9 @@ func Join(sep Doc, parts []Doc) Doc { // arena allocates its nodes from a few growing regions instead of one // heap allocation per node, at the cost of the arena retaining the // regions until it is discarded. The arena must outlive the printing of -// the documents it built, and must not be used by concurrent builders. -// The zero value is ready to use. +// the documents it built, must not be used by concurrent builders, and +// must not be copied after first use (it holds a printer with scratch +// buffers). The zero value is ready to use. type Arena struct { texts []textNode concats []concatNode @@ -83,6 +84,7 @@ type Arena struct { suffixes []lineSuffix parts []Doc partsLen int + printer printer // reused by Print; not safe to copy once used } // Reset reuses the arena's regions for a fresh document: the previous diff --git a/doc/print.go b/doc/print.go index e3f4094..572f15b 100644 --- a/doc/print.go +++ b/doc/print.go @@ -5,6 +5,7 @@ import ( "math" "slices" "strings" + "sync" ) // Options control how a document is printed. @@ -68,18 +69,63 @@ func (i indentation) align(n int, o Options) indentation { return i } +// printerPool reuses printer scratch (the output buffer, the fits +// lookahead builder and command stack) across prints; a printer is +// single-threaded and must not be used concurrently. The pool's contents +// are dropped by the GC, so hot paths prefer an Arena's own printer, +// which lives as long as the arena. +var printerPool = sync.Pool{New: func() any { return &printer{} }} + // Print renders doc to a string. Options are validated; a document of an // unknown shape returns an error (unreachable with the sealed Doc // interface). Print mutates doc: break propagation sets group break flags. func Print(d Doc, o Options) (string, error) { + if err := validateOptions(o); err != nil { + return "", err + } + + propagateBreaks(d) + + p := printerPool.Get().(*printer) + p.reset(o) + + res, err := p.run(d) + + printerPool.Put(p) + + return res, err +} + +// Print renders doc with the arena's own printer, whose scratch survives +// the GC as long as the arena does. Like Print, it mutates doc, and the +// arena must not be used concurrently. +func (a *Arena) Print(d Doc, o Options) (string, error) { + if err := validateOptions(o); err != nil { + return "", err + } + + propagateBreaks(d) + + p := &a.printer + p.reset(o) + + return p.run(d) +} + +func validateOptions(o Options) error { if o.PrintWidth <= 0 { - return "", fmt.Errorf("doc: PrintWidth must be positive, got %d", o.PrintWidth) + return fmt.Errorf("doc: PrintWidth must be positive, got %d", o.PrintWidth) } if o.TabWidth <= 0 { - return "", fmt.Errorf("doc: TabWidth must be positive, got %d", o.TabWidth) + return fmt.Errorf("doc: TabWidth must be positive, got %d", o.TabWidth) } + return nil +} + +// reset prepares a printer for a fresh document, retaining its scratch. +func (p *printer) reset(o Options) { if o.NewLine == "" { o.NewLine = "\n" } @@ -88,11 +134,20 @@ func Print(d Doc, o Options) (string, error) { o.Indent = strings.Repeat(" ", o.TabWidth) } - propagateBreaks(d) - - p := &printer{o: o, groupMode: map[int]mode{}} - - return p.run(d) + p.o = o + p.position = 0 + p.out = p.out[:0] + if p.groupMode == nil { + p.groupMode = map[int]mode{} + } else { + clear(p.groupMode) + } + p.lineSuffix = p.lineSuffix[:0] + p.shouldRemeasure = false + p.lastLineComment = false + p.fitOutput.Reset() + p.fitCommands = p.fitCommands[:0] + p.prefixes = p.prefixes[:0] } type printer struct { diff --git a/formatter/body.go b/formatter/body.go index cfdec5c..9dca428 100644 --- a/formatter/body.go +++ b/formatter/body.go @@ -92,7 +92,10 @@ func (f *formatter) bracedBody(fields []*syntax.Field, open, close int, closeTra ) if len(fields) > 0 && (f.opts.Break.Get(c) || sepForcesBreak(sepsOfFields(fields), sepMode)) { // BreakParent inside the group forces it to the broken layout. - content = f.Concat(doc.BreakParent, content) + p := f.Parts(2) + p = append(p, doc.BreakParent) + p = append(p, content) + content = f.Concat(p...) } return f.GroupID(bodyID, content) @@ -122,7 +125,10 @@ func (f *formatter) bracedEnumBody(values []*syntax.EnumValue, open, close int, f.emitTokens(close, close, emitOpts{trailing: closeTrailing}), ) if len(values) > 0 && (f.opts.Break.Get(ConstructEnum) || sepForcesBreak(sepsOfValues(values), f.opts.Separator.Get(ConstructEnum))) { - content = f.Concat(doc.BreakParent, content) + p := f.Parts(2) + p = append(p, doc.BreakParent) + p = append(p, content) + content = f.Concat(p...) } return f.GroupID(bodyID, content) @@ -149,7 +155,10 @@ func (f *formatter) service(v *syntax.Service) doc.Doc { parts = append(parts, f.function(fn)) } - inner := f.Concat(f.Concat(parts...), f.Concat(f.ownLineComments(close)...)) + p := f.Parts(2) + p = append(p, f.Concat(parts...)) + p = append(p, f.Concat(f.ownLineComments(close)...)) + inner := f.Concat(p...) if len(v.Functions) > 0 { // The first function starts its own line; the closing trivia // provides the break before it for empty bodies, so the blank @@ -158,7 +167,10 @@ func (f *formatter) service(v *syntax.Service) doc.Doc { } else if !f.hasOwnLineComments(close) && f.token(close).BlankLinesBefore > 0 { // Empty body with blank lines before the close and no comments: // the blanks round-trip through the close's own line. - inner = f.Concat(f.Concat(f.blankLineDocs(f.token(close).BlankLinesBefore, doc.HardLine)...), inner) + p := f.Parts(2) + p = append(p, f.Concat(f.blankLineDocs(f.token(close).BlankLinesBefore, doc.HardLine)...)) + p = append(p, inner) + inner = f.Concat(p...) } openComments := f.sameLineComments(open) @@ -241,7 +253,10 @@ func (f *formatter) functionBody(v *syntax.Function) doc.Doc { func (f *formatter) parenGroup(fields []*syntax.Field, open, close int, forced bool, sepMode SeparatorMode) doc.Doc { broken := f.brokenParens(fields, open, close, sepMode) if forced { - broken = f.Concat(broken, doc.BreakParent) + p := f.Parts(2) + p = append(p, broken) + p = append(p, doc.BreakParent) + broken = f.Concat(p...) } flat := append([]doc.Doc{f.Text("(")}, f.sameLineComments(open)...) @@ -258,7 +273,10 @@ func (f *formatter) parenGroup(fields []*syntax.Field, open, close int, forced b func (f *formatter) throwsGroup(v *syntax.Function) doc.Doc { forced := f.fieldsForcedBroken(v.Throws.Fields) || sepForcesBreak(sepsOfFields(v.Throws.Fields), f.opts.Separator.Get(ConstructThrows)) || f.opts.Break.Get(ConstructThrows) - return f.Concat(f.Text(" throws "), f.parenGroup(v.Throws.Fields, v.Throws.TokStart(), v.Throws.TokEnd(), forced, f.opts.Separator.Get(ConstructThrows))) + p := f.Parts(2) + p = append(p, f.Text(" throws ")) + p = append(p, f.parenGroup(v.Throws.Fields, v.Throws.TokStart(), v.Throws.TokEnd(), forced, f.opts.Separator.Get(ConstructThrows))) + return f.Concat(p...) } // parenClose returns the close paren index matching the open paren at diff --git a/formatter/field.go b/formatter/field.go index 595e243..1dea6fa 100644 --- a/formatter/field.go +++ b/formatter/field.go @@ -27,7 +27,7 @@ func (f *formatter) fieldList(fields []*syntax.Field, bodyID int, sepMode Separa parts = append(parts, f.field(field, f.alignmentFor(fields, i, sepMode), bodyID, sepMode)) } - return doc.Concat(parts) + return f.Concat(parts...) } // fieldSep is the separator between two list items: a newline when the @@ -35,7 +35,11 @@ func (f *formatter) fieldList(fields []*syntax.Field, bodyID int, sepMode Separa // preserving (each item keeps its own trailing separator), or a single // forced separator per the comma mode. func (f *formatter) fieldSep(prevSep syntax.TokenKind, mode SeparatorMode) doc.Doc { - return f.IfBreak(doc.Line, f.Concat(f.Text(sepText(prevSep, mode)), doc.Line)) + p := f.Parts(2) + p = append(p, f.Text(sepText(prevSep, mode))) + p = append(p, doc.Line) + + return f.IfBreak(doc.Line, f.Concat(p...)) } // sepText is the separator text between two flat list items: each item's @@ -76,7 +80,7 @@ func (f *formatter) enumValueList(values []*syntax.EnumValue, bodyID int) doc.Do parts = append(parts, f.enumValue(value, f.alignmentForEnum(values, i, f.opts.Separator.Get(ConstructEnum)), bodyID)) } - return doc.Concat(parts) + return f.Concat(parts...) } // groupedWith reports whether the node joins the alignment group of the @@ -320,13 +324,17 @@ func (f *formatter) nodeTrailingInline(end int, sep syntax.TokenKind, sepMode Se func (f *formatter) fieldDoc(v *syntax.Field, align *columnAlign, bodyID int, sepMode SeparatorMode) doc.Doc { content := f.fieldContent(v, align, false, sepMode) if bodyID != 0 { - content = f.IfBreakFor( - f.Concat(f.fieldContent(v, align, true, sepMode), f.trailingSep(v.Sep, sepMode)), - content, - bodyID, - ) + broken := f.Parts(2) + broken = append(broken, f.fieldContent(v, align, true, sepMode)) + broken = append(broken, f.trailingSep(v.Sep, sepMode)) + + content = f.IfBreakFor(f.Concat(broken...), content, bodyID) } else { - content = f.Concat(f.fieldContent(v, align, true, sepMode), f.trailingSep(v.Sep, sepMode)) + broken := f.Parts(2) + broken = append(broken, f.fieldContent(v, align, true, sepMode)) + broken = append(broken, f.trailingSep(v.Sep, sepMode)) + + content = f.Concat(broken...) } parts := append(f.ownLineComments(v.TokStart()), content) @@ -336,7 +344,7 @@ func (f *formatter) fieldDoc(v *syntax.Field, align *columnAlign, bodyID int, se parts = append(parts, f.suppressedSepComments(v.TokEnd())...) } - return doc.Concat(parts) + return f.Concat(parts...) } // field assembles a struct-like body field, switching on the body group's @@ -356,13 +364,14 @@ func (f *formatter) emitWithAnnotations(start, end int, ann *syntax.Annotations, first := o first.trailing = true - parts := []doc.Doc{f.emitTokens(start, ann.TokStart()-1, first)} + parts := f.Parts(3) + parts = append(parts, f.emitTokens(start, ann.TokStart()-1, first)) parts = append(parts, f.annotationsDoc(ann, ann.TokEnd() == end)) if ann.TokEnd() < end { parts = append(parts, f.emitTokens(f.nextReal(ann.TokEnd()+1), end, emitOpts{leading: true, skipText: o.skipText})) } - return doc.Concat(parts) + return f.Concat(parts...) } // fieldContent renders the field as a token run. padded selects the @@ -405,7 +414,7 @@ func (f *formatter) fieldPads(v *syntax.Field, a *columnAlign) ([]padEntry, stri } } - var pads []padEntry + pads := make([]padEntry, 0, 3) if v.FieldID != nil { pads = append(pads, padEntry{v.TokStart() + 1, padRight("", a.idWidth-len(v.FieldID.Text)-1)}) } @@ -444,11 +453,11 @@ func (f *formatter) fieldPads(v *syntax.Field, a *columnAlign) ([]padEntry, stri func (f *formatter) enumValue(v *syntax.EnumValue, align *columnAlign, bodyID int) doc.Doc { content := f.enumValueContent(v, align, false, f.opts.Separator.Get(ConstructEnum)) if bodyID != 0 { - content = f.IfBreakFor( - f.Concat(f.enumValueContent(v, align, true, f.opts.Separator.Get(ConstructEnum)), f.trailingSep(v.Sep, f.opts.Separator.Get(ConstructEnum))), - content, - bodyID, - ) + broken := f.Parts(2) + broken = append(broken, f.enumValueContent(v, align, true, f.opts.Separator.Get(ConstructEnum))) + broken = append(broken, f.trailingSep(v.Sep, f.opts.Separator.Get(ConstructEnum))) + + content = f.IfBreakFor(f.Concat(broken...), content, bodyID) } parts := append(f.ownLineComments(v.TokStart()), content) @@ -458,7 +467,7 @@ func (f *formatter) enumValue(v *syntax.EnumValue, align *columnAlign, bodyID in parts = append(parts, f.suppressedSepComments(v.TokEnd())...) } - return doc.Concat(parts) + return f.Concat(parts...) } func (f *formatter) enumValueContent(v *syntax.EnumValue, align *columnAlign, padded bool, sepMode SeparatorMode) doc.Doc { @@ -539,9 +548,26 @@ func (f *formatter) trailingSep(sep syntax.TokenKind, mode SeparatorMode) doc.Do } } -// padRight pads s with trailing spaces to width w. +// spaces is a shared run of spaces for column padding; padRight slices +// it for widths within it instead of allocating. +const spaces = " " // 64 + +// padRight pads s with trailing spaces to width w. With an empty base +// the pad is a slice of the shared spaces constant, no allocation. func padRight(s string, w int) string { if n := w - len(s); n > 0 { + if n <= len(spaces) { + if s == "" { + return spaces[:n] + } + + return s + spaces[:n] + } + + if s == "" { + return strings.Repeat(" ", n) + } + return s + strings.Repeat(" ", n) } diff --git a/formatter/format.go b/formatter/format.go index 7ec1ade..877bef8 100644 --- a/formatter/format.go +++ b/formatter/format.go @@ -239,7 +239,7 @@ func Format(d *syntax.Document, o Options) (string, error) { opts: o, } - return doc.Print(f.document(), printOptions(o)) + return formatArena.Print(f.document(), printOptions(o)) } // printOptions maps formatter options to printer options. @@ -293,7 +293,7 @@ func FormatNode(d *syntax.Document, n syntax.Node, o Options) (string, error) { opts: o, } - return doc.Print(f.node(n), printOptions(o)) + return formatArena.Print(f.node(n), printOptions(o)) } type formatter struct { @@ -389,7 +389,7 @@ func (f *formatter) nextReal(idx int) int { // structural layout emits itself (text replaces the suppressed text when // set); pads widen alignment columns. func (f *formatter) emitTokens(start, end int, o emitOpts) doc.Doc { - parts := f.Parts(8) + parts := f.Parts(12) if o.prefix != "" { parts = append(parts, f.Text(o.prefix)) } @@ -484,10 +484,11 @@ func (f *formatter) foldBreak(i int, flat string) doc.Doc { // commaSep renders a separating comma with its trivia, then the foldable // gap after it. func (f *formatter) commaSep(comma int) []doc.Doc { - return []doc.Doc{ - f.emitTokens(comma, comma, emitOpts{leading: true, trailing: true}), - f.foldBreak(comma, " "), - } + p := f.Parts(2) + p = append(p, f.emitTokens(comma, comma, emitOpts{leading: true, trailing: true})) + p = append(p, f.foldBreak(comma, " ")) + + return p } // rawTokenGap returns the canonical text between two adjacent tokens: @@ -560,7 +561,7 @@ func (f *formatter) nodeBody(n syntax.Node) doc.Doc { // document assembles the whole file: top-level nodes separated by // collapsible lines, blank lines preserved, and trailing comments. func (f *formatter) document() doc.Doc { - parts := f.Parts(8) + parts := f.Parts(12) for i, n := range f.doc.Nodes { if i > 0 { @@ -606,7 +607,8 @@ func (f *formatter) namespace(v *syntax.Namespace) doc.Doc { o.trailing = true } - parts := []doc.Doc{f.emitTokens(v.TokStart(), end, o)} + parts := f.Parts(3) + parts = append(parts, f.emitTokens(v.TokStart(), end, o)) parts = append(parts, f.annotationsDoc(v.Annotations, v.Annotations != nil && v.Annotations.TokEnd() == v.TokEnd())) parts = append(parts, f.afterAnnotations(v.Annotations, v.TokEnd())) @@ -624,7 +626,8 @@ func (f *formatter) typedef(v *syntax.Typedef) doc.Doc { o.trailing = true } - parts := []doc.Doc{f.emitTokens(v.TokStart(), end, o)} + parts := f.Parts(3) + parts = append(parts, f.emitTokens(v.TokStart(), end, o)) parts = append(parts, f.annotationsDoc(v.Annotations, v.Annotations != nil && v.Annotations.TokEnd() == v.TokEnd())) parts = append(parts, f.afterAnnotations(v.Annotations, v.TokEnd())) @@ -639,10 +642,9 @@ func (f *formatter) constant(v *syntax.Const) doc.Doc { eq := f.prevReal(value.TokStart() - 1) - parts := []doc.Doc{ - f.emitTokens(v.TokStart(), eq, emitOpts{trailing: true}), - f.tokenGap(eq, value.TokStart()), - } + parts := f.Parts(2) + parts = append(parts, f.emitTokens(v.TokStart(), eq, emitOpts{trailing: true})) + parts = append(parts, f.tokenGap(eq, value.TokStart())) // Own-line comments before the value render at the value boundary, // outside the value's own group. parts = append(parts, f.ownLineComments(value.TokStart())...) @@ -726,21 +728,33 @@ func (f *formatter) annotationsDoc(a *syntax.Annotations, isLast bool) doc.Doc { last = lastItem.TokEnd() } - group := f.Group(f.Concat( - f.emitTokens(open, open, all), - f.Indent(f.Concat(f.foldBreak(open, ""), f.Concat(middle...))), - f.foldBreak(last, ""), - f.emitTokens(close, close, emitOpts{leading: true}), - )) + openBreak := f.Parts(2) + openBreak = append(openBreak, f.foldBreak(open, "")) + openBreak = append(openBreak, f.Concat(middle...)) + + parts := f.Parts(4) + parts = append(parts, f.emitTokens(open, open, all)) + parts = append(parts, f.Indent(f.Concat(openBreak...))) + parts = append(parts, f.foldBreak(last, "")) + parts = append(parts, f.emitTokens(close, close, emitOpts{leading: true})) + + group := f.Group(f.Concat(parts...)) + + out := f.Parts(2) + out = append(out, f.Text(" ")) + out = append(out, group) - out := f.Concat(f.Text(" "), group) if !isLast { // Same-line comments after the close render at the group boundary, // outside the group, so the group folds independently. - out = f.Concat(out, f.Concat(f.sameLineComments(close)...)) + withSuffix := f.Parts(2) + withSuffix = append(withSuffix, f.Concat(out...)) + withSuffix = append(withSuffix, f.Concat(f.sameLineComments(close)...)) + + return f.Concat(withSuffix...) } - return out + return f.Concat(out...) } // trimComment returns the comment text without trailing whitespace, which diff --git a/formatter/value.go b/formatter/value.go index 68237ff..90dcdba 100644 --- a/formatter/value.go +++ b/formatter/value.go @@ -127,7 +127,10 @@ func (f *formatter) constItems(items []constItem, open, close int, c Construct, ) if f.opts.Break.Get(c) || sepForcesBreakList(f.sepsOf(items), sepMode) { // BreakParent inside the group forces it to the broken layout. - inner = f.Concat(doc.BreakParent, inner) + p := f.Parts(2) + p = append(p, doc.BreakParent) + p = append(p, inner) + inner = f.Concat(p...) } return f.Group(inner) @@ -169,17 +172,22 @@ func (f *formatter) itemSep(sep int, mode SeparatorMode) []doc.Doc { } if text == f.token(sep).Text { - return []doc.Doc{f.emitTokens(sep, sep, emitOpts{leading: true, trailing: true}), f.foldBreak(sep, " ")} + p := f.Parts(2) + p = append(p, f.emitTokens(sep, sep, emitOpts{leading: true, trailing: true})) + p = append(p, f.foldBreak(sep, " ")) + + return p } // Forced separator differing from the source: the forced text replaces // the suppressed text inside the run, so the source token's comments // stay ordered around it — own-line comments before it, same-line // comments after. - return []doc.Doc{ - f.emitTokens(sep, sep, emitOpts{leading: true, trailing: true, skipText: []int{sep}, text: text}), - f.foldBreak(sep, " "), - } + p := f.Parts(2) + p = append(p, f.emitTokens(sep, sep, emitOpts{leading: true, trailing: true, skipText: []int{sep}, text: text})) + p = append(p, f.foldBreak(sep, " ")) + + return p } // trailingItemSep is the trailing separator of a list/map: the source @@ -215,7 +223,10 @@ func (f *formatter) trailingItemSep(last int, mode SeparatorMode) doc.Doc { // line end instead. sepDoc := f.emitTokens(sep, sep, emitOpts{leading: true, trailing: true}) if !f.sameLineEndsLine(sep) { - sepDoc = f.Concat(sepDoc, f.Text(" ")) + p := f.Parts(2) + p = append(p, sepDoc) + p = append(p, f.Text(" ")) + sepDoc = f.Concat(p...) } return sepDoc @@ -234,7 +245,10 @@ func (f *formatter) trailingItemSep(last int, mode SeparatorMode) doc.Doc { // is stable across a reparse. sepDoc := f.emitTokens(sep, sep, emitOpts{leading: true, trailing: true, skipText: []int{sep}, text: text}) if text != "" && !f.sameLineEndsLine(sep) { - sepDoc = f.Concat(sepDoc, f.Text(" ")) + p := f.Parts(2) + p = append(p, sepDoc) + p = append(p, f.Text(" ")) + sepDoc = f.Concat(p...) } return sepDoc -- 2.51.2