Skip to content

Commit

Permalink
perf(codegen): optimize line / column calculation during codegen
Browse files Browse the repository at this point in the history
Previously, many CodegenContext.push() calls were unnecessarily
iterating through the entire pushed string to find newlines, when we
already know the newline positions for most of calls. Providing fast
paths for these calls significantly improves codegen performance when
source map is needed.

In benchmarks, this PR improves full SFC compilation performance by ~6%.
  • Loading branch information
yyx990803 committed Nov 25, 2023
1 parent e8e3ec6 commit 3be53d9
Showing 1 changed file with 87 additions and 28 deletions.
115 changes: 87 additions & 28 deletions packages/compiler-core/src/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,13 @@ export interface CodegenResult {
map?: RawSourceMap
}

const enum NewlineType {
Start = 0,
End = -1,
None = -2,
Unknown = -3
}

export interface CodegenContext
extends Omit<Required<CodegenOptions>, 'bindingMetadata' | 'inline'> {
source: string
Expand All @@ -80,7 +87,7 @@ export interface CodegenContext
pure: boolean
map?: SourceMapGenerator
helper(key: symbol): string
push(code: string, node?: CodegenNode): void
push(code: string, newlineIndex?: number, node?: CodegenNode): void
indent(): void
deindent(withoutNewLine?: boolean): void
newline(): void
Expand Down Expand Up @@ -127,7 +134,7 @@ function createCodegenContext(
helper(key) {
return `_${helperNameMap[key]}`
},
push(code, node) {
push(code, newlineIndex = NewlineType.None, node) {
context.code += code
if (!__BROWSER__ && context.map) {
if (node) {
Expand All @@ -140,7 +147,41 @@ function createCodegenContext(
}
addMapping(node.loc.start, name)
}
advancePositionWithMutation(context, code)
if (newlineIndex === NewlineType.Unknown) {
// multiple newlines, full iteration
advancePositionWithMutation(context, code)
} else {
// fast paths
context.offset += code.length
if (newlineIndex === NewlineType.None) {
// no newlines; fast path to avoid newline detection
if (__TEST__ && code.includes('\n')) {
throw new Error(
`CodegenContext.push() called newlineIndex: none, but contains` +
`newlines: ${code.replace(/\n/g, '\\n')}`
)
}
context.column += code.length
} else {
// single newline at known index
if (newlineIndex === NewlineType.End) {
newlineIndex = code.length - 1
}
if (
__TEST__ &&
(code.charAt(newlineIndex) !== '\n' ||
code.slice(0, newlineIndex).includes('\n') ||
code.slice(newlineIndex + 1).includes('\n'))
) {
throw new Error(
`CodegenContext.push() called with newlineIndex: ${newlineIndex} ` +
`but does not conform: ${code.replace(/\n/g, '\\n')}`
)
}
context.line++
context.column = code.length - newlineIndex
}
}
if (node && node.loc !== locStub) {
addMapping(node.loc.end)
}
Expand All @@ -162,7 +203,7 @@ function createCodegenContext(
}

function newline(n: number) {
context.push('\n' + ` `.repeat(n))
context.push('\n' + ` `.repeat(n), NewlineType.Start)
}

function addMapping(loc: Position, name?: string) {
Expand Down Expand Up @@ -250,8 +291,10 @@ export function generate(
// function mode const declarations should be inside with block
// also they should be renamed to avoid collision with user properties
if (hasHelpers) {
push(`const { ${helpers.map(aliasHelper).join(', ')} } = _Vue`)
push(`\n`)
push(
`const { ${helpers.map(aliasHelper).join(', ')} } = _Vue\n`,
NewlineType.End
)
newline()
}
}
Expand Down Expand Up @@ -282,7 +325,7 @@ export function generate(
}
}
if (ast.components.length || ast.directives.length || ast.temps) {
push(`\n`)
push(`\n`, NewlineType.Start)
newline()
}

Expand Down Expand Up @@ -334,11 +377,14 @@ function genFunctionPreamble(ast: RootNode, context: CodegenContext) {
const helpers = Array.from(ast.helpers)
if (helpers.length > 0) {
if (!__BROWSER__ && prefixIdentifiers) {
push(`const { ${helpers.map(aliasHelper).join(', ')} } = ${VueBinding}\n`)
push(
`const { ${helpers.map(aliasHelper).join(', ')} } = ${VueBinding}\n`,
NewlineType.End
)
} else {
// "with" mode.
// save Vue in a separate variable to avoid collision
push(`const _Vue = ${VueBinding}\n`)
push(`const _Vue = ${VueBinding}\n`, NewlineType.End)
// in "with" mode, helpers are declared inside the with block to avoid
// has check cost, but hoists are lifted out of the function - we need
// to provide the helper here.
Expand All @@ -353,7 +399,7 @@ function genFunctionPreamble(ast: RootNode, context: CodegenContext) {
.filter(helper => helpers.includes(helper))
.map(aliasHelper)
.join(', ')
push(`const { ${staticHelpers} } = _Vue\n`)
push(`const { ${staticHelpers} } = _Vue\n`, NewlineType.End)
}
}
}
Expand All @@ -363,7 +409,8 @@ function genFunctionPreamble(ast: RootNode, context: CodegenContext) {
push(
`const { ${ast.ssrHelpers
.map(aliasHelper)
.join(', ')} } = require("${ssrRuntimeModuleName}")\n`
.join(', ')} } = require("${ssrRuntimeModuleName}")\n`,
NewlineType.End
)
}
genHoists(ast.hoists, context)
Expand Down Expand Up @@ -402,18 +449,21 @@ function genModulePreamble(
push(
`import { ${helpers
.map(s => helperNameMap[s])
.join(', ')} } from ${JSON.stringify(runtimeModuleName)}\n`
.join(', ')} } from ${JSON.stringify(runtimeModuleName)}\n`,
NewlineType.End
)
push(
`\n// Binding optimization for webpack code-split\nconst ${helpers
.map(s => `_${helperNameMap[s]} = ${helperNameMap[s]}`)
.join(', ')}\n`
.join(', ')}\n`,
NewlineType.End
)
} else {
push(
`import { ${helpers
.map(s => `${helperNameMap[s]} as _${helperNameMap[s]}`)
.join(', ')} } from ${JSON.stringify(runtimeModuleName)}\n`
.join(', ')} } from ${JSON.stringify(runtimeModuleName)}\n`,
NewlineType.End
)
}
}
Expand All @@ -422,7 +472,8 @@ function genModulePreamble(
push(
`import { ${ast.ssrHelpers
.map(s => `${helperNameMap[s]} as _${helperNameMap[s]}`)
.join(', ')} } from "${ssrRuntimeModuleName}"\n`
.join(', ')} } from "${ssrRuntimeModuleName}"\n`,
NewlineType.End
)
}

Expand Down Expand Up @@ -554,7 +605,7 @@ function genNodeList(
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]
if (isString(node)) {
push(node)
push(node, NewlineType.Unknown)
} else if (isArray(node)) {
genNodeListAsArray(node, context)
} else {
Expand All @@ -573,7 +624,7 @@ function genNodeList(

function genNode(node: CodegenNode | symbol | string, context: CodegenContext) {
if (isString(node)) {
context.push(node)
context.push(node, NewlineType.Unknown)
return
}
if (isSymbol(node)) {
Expand Down Expand Up @@ -671,12 +722,16 @@ function genText(
node: TextNode | SimpleExpressionNode,
context: CodegenContext
) {
context.push(JSON.stringify(node.content), node)
context.push(JSON.stringify(node.content), NewlineType.Unknown, node)
}

function genExpression(node: SimpleExpressionNode, context: CodegenContext) {
const { content, isStatic } = node
context.push(isStatic ? JSON.stringify(content) : content, node)
context.push(
isStatic ? JSON.stringify(content) : content,
NewlineType.Unknown,
node
)
}

function genInterpolation(node: InterpolationNode, context: CodegenContext) {
Expand All @@ -694,7 +749,7 @@ function genCompoundExpression(
for (let i = 0; i < node.children!.length; i++) {
const child = node.children![i]
if (isString(child)) {
context.push(child)
context.push(child, NewlineType.Unknown)
} else {
genNode(child, context)
}
Expand All @@ -715,9 +770,9 @@ function genExpressionAsPropertyKey(
const text = isSimpleIdentifier(node.content)
? node.content
: JSON.stringify(node.content)
push(text, node)
push(text, NewlineType.None, node)
} else {
push(`[${node.content}]`, node)
push(`[${node.content}]`, NewlineType.Unknown, node)
}
}

Expand All @@ -726,7 +781,11 @@ function genComment(node: CommentNode, context: CodegenContext) {
if (pure) {
push(PURE_ANNOTATION)
}
push(`${helper(CREATE_COMMENT)}(${JSON.stringify(node.content)})`, node)
push(
`${helper(CREATE_COMMENT)}(${JSON.stringify(node.content)})`,
NewlineType.Unknown,
node
)
}

function genVNodeCall(node: VNodeCall, context: CodegenContext) {
Expand Down Expand Up @@ -754,7 +813,7 @@ function genVNodeCall(node: VNodeCall, context: CodegenContext) {
const callHelper: symbol = isBlock
? getVNodeBlockHelper(context.inSSR, isComponent)
: getVNodeHelper(context.inSSR, isComponent)
push(helper(callHelper) + `(`, node)
push(helper(callHelper) + `(`, NewlineType.None, node)
genNodeList(
genNullableArgs([tag, props, children, patchFlag, dynamicProps]),
context
Expand Down Expand Up @@ -785,7 +844,7 @@ function genCallExpression(node: CallExpression, context: CodegenContext) {
if (pure) {
push(PURE_ANNOTATION)
}
push(callee + `(`, node)
push(callee + `(`, NewlineType.None, node)
genNodeList(node.arguments, context)
push(`)`)
}
Expand All @@ -794,7 +853,7 @@ function genObjectExpression(node: ObjectExpression, context: CodegenContext) {
const { push, indent, deindent, newline } = context
const { properties } = node
if (!properties.length) {
push(`{}`, node)
push(`{}`, NewlineType.None, node)
return
}
const multilines =
Expand Down Expand Up @@ -834,7 +893,7 @@ function genFunctionExpression(
// wrap slot functions with owner context
push(`_${helperNameMap[WITH_CTX]}(`)
}
push(`(`, node)
push(`(`, NewlineType.None, node)
if (isArray(params)) {
genNodeList(params, context)
} else if (params) {
Expand Down Expand Up @@ -934,7 +993,7 @@ function genTemplateLiteral(node: TemplateLiteral, context: CodegenContext) {
for (let i = 0; i < l; i++) {
const e = node.elements[i]
if (isString(e)) {
push(e.replace(/(`|\$|\\)/g, '\\$1'))
push(e.replace(/(`|\$|\\)/g, '\\$1'), NewlineType.Unknown)
} else {
push('${')
if (multilines) indent()
Expand Down

0 comments on commit 3be53d9

Please sign in to comment.