Skip to content

Commit 4c8587f

Browse files
committed
Peak memory use optimizations.
Reduce peak memory usage of swift-format by discarding collections once they are no longer needed. - Remove `Token` arrays from `beforeMap` and `afterMap` while processing the associated token. - Empty `preVisitedExprs` after visitation finishes for the root expression of the tree. By the end of a file, these maps can accumulate a meaningful number of tokens such that they significantly impact the peak memory usage. This admittedly replaces a O(1) lookup operation with a O(N) lookup-and-remove operation, by replacing `subscript` with `removeValue(forKey:)`. In my testing, I found that this had no impact on the runtime. My hypothesis is that removing from these dictionaries keeps them small enough that O(N) runtime isn't a problem. This set previously contained `ExprSyntax` objects and was maintained for the entire file. Once the visitor has finished with all children of the node that initiated inserting the contextual breaks, it will never visit any of the children again. This set can be emptied at that point to keep it from growing until the visitor finishes with the whole file. The runtime is unchanged, but this reduces peak memory usage especially for large files. - Large file (30k LOC): 108.3 MB peak to 72.1 MB peak - Medium file (10k LOC) : 35.9 MB peak to 24.4 MB peak - swift-protobuf library (many small files): 77.6 MB peak to 75.6 MB peak
1 parent 34ccba1 commit 4c8587f

File tree

1 file changed

+40
-16
lines changed

1 file changed

+40
-16
lines changed

Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,11 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
4343

4444
/// Lists the expressions that have been visited, from the outermost expression, where contextual
4545
/// breaks and start/end contextual breaking tokens have been inserted.
46-
private var preVisitedExprs = Set<ExprSyntax>()
46+
private var preVisitedExprs = Set<SyntaxIdentifier>()
47+
48+
/// Tracks the "root" exprs where previsiting for contextual breaks started so that
49+
/// `preVisitedExprs` can be emptied after exiting an expr tree.
50+
private var rootExprs = Set<SyntaxIdentifier>()
4751

4852
/// Lists the tokens that are the closing or final delimiter of a node that shouldn't be split
4953
/// from the preceding token. When breaks are inserted around compound expressions, the breaks are
@@ -850,6 +854,10 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
850854
return .visitChildren
851855
}
852856

857+
override func visitPost(_ node: MemberAccessExprSyntax) {
858+
clearContextualBreakState(node)
859+
}
860+
853861
override func visit(_ node: FunctionCallExprSyntax) -> SyntaxVisitorContinueKind {
854862
preVisitInsertingContextualBreaks(node)
855863

@@ -881,6 +889,10 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
881889
return .visitChildren
882890
}
883891

892+
override func visitPost(_ node: FunctionCallExprSyntax) {
893+
clearContextualBreakState(node)
894+
}
895+
884896
/// Arrange the given argument list (or equivalently, tuple expression list) as a list of function
885897
/// arguments.
886898
///
@@ -1076,6 +1088,10 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
10761088
return .visitChildren
10771089
}
10781090

1091+
override func visitPost(_ node: SubscriptExprSyntax) {
1092+
clearContextualBreakState(node)
1093+
}
1094+
10791095
override func visit(_ node: ExpressionSegmentSyntax) -> SyntaxVisitorContinueKind {
10801096
// TODO: For now, just use the raw text of the node and don't try to format it deeper. In the
10811097
// future, we should find a way to format the expression but without wrapping so that at least
@@ -2271,7 +2287,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
22712287

22722288
/// Appends the before-tokens of the given syntax token to the token stream.
22732289
private func appendBeforeTokens(_ token: TokenSyntax) {
2274-
if let before = beforeMap[token] {
2290+
if let before = beforeMap.removeValue(forKey: token) {
22752291
before.forEach(appendToken)
22762292
}
22772293
}
@@ -2331,7 +2347,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
23312347
/// not incorrectly inserted into the token stream *after* a break or newline.
23322348
private func appendAfterTokensAndTrailingComments(_ token: TokenSyntax) {
23332349
let (wasLineComment, trailingCommentTokens) = afterTokensForTrailingComment(token)
2334-
let afterGroups = afterMap[token] ?? []
2350+
let afterGroups = afterMap.removeValue(forKey: token) ?? []
23352351
var hasAppendedTrailingComment = false
23362352

23372353
if !wasLineComment {
@@ -3177,23 +3193,34 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
31773193
appendToken(.break(.same, size: 0))
31783194
}
31793195

3196+
/// Cleans up state related to inserting contextual breaks throughout expressions during
3197+
/// `visitPost` for an expression that is the root of an expression tree.
3198+
private func clearContextualBreakState<T: ExprSyntaxProtocol>(_ expr: T) {
3199+
let exprID = expr.id
3200+
if rootExprs.remove(exprID) != nil {
3201+
preVisitedExprs.removeAll()
3202+
}
3203+
}
3204+
31803205
/// Visits the given expression node and all of the nested expression nodes, inserting tokens
31813206
/// necessary for contextual breaking throughout the expression. Records the nodes that were
31823207
/// visited so that they can be skipped later.
31833208
private func preVisitInsertingContextualBreaks<T: ExprSyntaxProtocol & Equatable>(_ expr: T) {
3184-
let exprSyntax = ExprSyntax(expr)
3185-
if !preVisitedExprs.contains(exprSyntax) {
3186-
let (visited, _, _) = insertContextualBreaks(exprSyntax, isTopLevel: true)
3187-
preVisitedExprs.formUnion(visited)
3209+
let exprID = expr.id
3210+
if !preVisitedExprs.contains(exprID) {
3211+
rootExprs.insert(exprID)
3212+
insertContextualBreaks(ExprSyntax(expr), isTopLevel: true)
31883213
}
31893214
}
31903215

31913216
/// Recursively visits nested expressions from the given expression inserting contextual breaking
31923217
/// tokens. When visiting an expression node, `preVisitInsertingContextualBreaks(_:)` should be
31933218
/// called instead of this helper.
3219+
@discardableResult
31943220
private func insertContextualBreaks(_ expr: ExprSyntax, isTopLevel: Bool) -> (
3195-
[ExprSyntax], hasCompoundExpression: Bool, hasMemberAccess: Bool
3221+
hasCompoundExpression: Bool, hasMemberAccess: Bool
31963222
) {
3223+
preVisitedExprs.insert(expr.id)
31973224
if let memberAccessExpr = expr.as(MemberAccessExprSyntax.self) {
31983225
// When the member access is part of a calling expression, the break before the dot is
31993226
// inserted when visiting the parent node instead so that the break is inserted before any
@@ -3202,21 +3229,18 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
32023229
expr.parent?.isProtocol(CallingExprSyntaxProtocol.self) != true {
32033230
before(memberAccessExpr.dot, tokens: .break(.contextual, size: 0))
32043231
}
3205-
let children: [ExprSyntax]
32063232
var hasCompoundExpression = false
32073233
if let base = memberAccessExpr.base {
3208-
(children, hasCompoundExpression, _) = insertContextualBreaks(base, isTopLevel: false)
3209-
} else {
3210-
children = []
3234+
(hasCompoundExpression, _) = insertContextualBreaks(base, isTopLevel: false)
32113235
}
32123236
if isTopLevel {
32133237
before(expr.firstToken, tokens: .contextualBreakingStart)
32143238
after(expr.lastToken, tokens: .contextualBreakingEnd)
32153239
}
3216-
return ([expr] + children, hasCompoundExpression, true)
3240+
return (hasCompoundExpression, true)
32173241
} else if let callingExpr = expr.asProtocol(CallingExprSyntaxProtocol.self) {
32183242
let calledExpression = callingExpr.calledExpression
3219-
let (children, hasCompoundExpression, hasMemberAccess) =
3243+
let (hasCompoundExpression, hasMemberAccess) =
32203244
insertContextualBreaks(calledExpression, isTopLevel: false)
32213245

32223246
let shouldGroup =
@@ -3241,7 +3265,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
32413265
before(expr.firstToken, tokens: beforeTokens)
32423266
after(expr.lastToken, tokens: afterTokens)
32433267
}
3244-
return ([expr] + children, true, hasMemberAccess)
3268+
return (true, hasMemberAccess)
32453269
}
32463270

32473271
// Otherwise, it's an expression that isn't calling another expression (e.g. array or
@@ -3250,7 +3274,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
32503274
before(expr.firstToken, tokens: .contextualBreakingStart)
32513275
after(expr.lastToken, tokens: .contextualBreakingEnd)
32523276
let hasCompoundExpression = !expr.is(IdentifierExprSyntax.self)
3253-
return ([expr], hasCompoundExpression, false)
3277+
return (hasCompoundExpression, false)
32543278
}
32553279
}
32563280

0 commit comments

Comments
 (0)