Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: improve tree-shaking by propagate const parameter #5443

Merged
merged 60 commits into from Apr 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
58fba0d
feat: improve tree-shaking by propagate const parameter
liuly0322 Mar 23, 2024
61bbfa0
fix: update old tests (for tree-shaking const param)
liuly0322 Mar 23, 2024
7050e85
test: add test for tree-shaking by propagate const parameter
liuly0322 Mar 23, 2024
fdb84cb
feat&perf: support object param
liuly0322 Mar 23, 2024
7251468
style: update coverage
liuly0322 Mar 24, 2024
7381847
test: update tree-shake-literal-parameter
liuly0322 Mar 24, 2024
87f82de
test: update tree-shake top export
liuly0322 Mar 25, 2024
cdcf53c
refactor: tree-shaking-literal
liuly0322 Mar 25, 2024
1c91f75
fix: test indent
liuly0322 Mar 25, 2024
3c9a810
perf: remove same object SPJ
liuly0322 Mar 25, 2024
6e2e866
refactor: support iife
liuly0322 Mar 25, 2024
8087111
test: tree-shake literal iife
liuly0322 Mar 25, 2024
b64f649
fix: args but not callee should not be optimized
liuly0322 Mar 25, 2024
5339c08
refactor: some logic to function base with comment
liuly0322 Mar 26, 2024
3547388
feat&perf: support implicitly undefined
liuly0322 Mar 26, 2024
1d18ba1
test: tree-shake literal conditional
liuly0322 Mar 26, 2024
ac4794d
feat: integrate with optimizeCache
liuly0322 Mar 26, 2024
9aa6a70
test: fix
liuly0322 Mar 26, 2024
9f536bf
feat: function argument side effect
liuly0322 Mar 26, 2024
f3d8232
Merge branch 'master' into master
liuly0322 Mar 27, 2024
fd90a7b
style: revert export default change since deoptimizePath will detect
liuly0322 Mar 28, 2024
4e3aee0
feat: support foo(bar);foo(bar);
liuly0322 Mar 28, 2024
074e7e7
test: add more side-effect and top-level test
liuly0322 Mar 28, 2024
ce6a8a4
Merge branch 'master' into master
liuly0322 Mar 28, 2024
aabd6f0
4.13.2
lukastaegert Mar 28, 2024
ffe234c
Merge branch 'master' into master
liuly0322 Mar 28, 2024
912bacd
test: add export default test
liuly0322 Mar 28, 2024
e823998
Merge branch 'master' into master
liuly0322 Mar 28, 2024
e1143bc
refactor FunctionParameterState and remove initalization
liuly0322 Mar 29, 2024
495ee87
Merge branch 'master' into master
liuly0322 Mar 29, 2024
bfe6a6c
refactor IIFE
liuly0322 Mar 30, 2024
c63e745
feat: support export default anonymous
liuly0322 Mar 30, 2024
6be48de
fix: nested namespace tracking
liuly0322 Mar 30, 2024
9060ae8
feat: support define then export default
liuly0322 Mar 30, 2024
4e71e66
performance
liuly0322 Mar 30, 2024
35857ff
refactor: UNKNOWN_EXPRESSION
liuly0322 Mar 30, 2024
4d41532
refactor: reduce complexity
liuly0322 Mar 30, 2024
b79c151
Merge branch 'master' into master
liuly0322 Apr 3, 2024
de1a0ab
fix: export default function foo and foo called from same mod
liuly0322 Apr 4, 2024
d54864f
style: NodeType
liuly0322 Apr 4, 2024
a19792b
Merge branch 'master' into master
liuly0322 Apr 4, 2024
2b19f62
style: remove counter
liuly0322 Apr 5, 2024
7d6f5fb
perf: cache onlyfunctioncall result
liuly0322 Apr 5, 2024
bb28846
Merge branch 'master' into master
liuly0322 Apr 5, 2024
095e844
Merge branch 'master' into master
liuly0322 Apr 7, 2024
e13a514
style&perf: remove args slice
liuly0322 Apr 7, 2024
7d54f27
perf: export default variable
liuly0322 Apr 8, 2024
a8d2f86
perf: export default variable
liuly0322 Apr 8, 2024
97fcfad
style: small updates: naming, private...
liuly0322 Apr 18, 2024
dfe4cd3
perf: LogicalExpression deoptimize cache
liuly0322 Apr 18, 2024
047ae3e
Merge branch 'master' into master
liuly0322 Apr 18, 2024
aa8741a
style: remove a condition which is always true
liuly0322 Apr 18, 2024
40f9427
style: add protected
liuly0322 Apr 18, 2024
e02ab0d
style: remove a condition which is always true
liuly0322 Apr 18, 2024
51ffb9b
style: remove a condition
liuly0322 Apr 18, 2024
a559d2d
refactor: lazy bind variable
liuly0322 Apr 19, 2024
f73a365
fix: refresh cache if isReassigned change for ParameterVariable
liuly0322 Apr 19, 2024
ea7ffab
fix: make sure deoptimize give a final state
liuly0322 Apr 19, 2024
0f58a40
style: make coverage more happy
liuly0322 Apr 19, 2024
3f2f45e
Merge branch 'master' into master
lukastaegert Apr 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
15 changes: 8 additions & 7 deletions src/ast/nodes/ConditionalExpression.ts
@@ -1,5 +1,5 @@
import type MagicString from 'magic-string';
import { BLANK, EMPTY_ARRAY } from '../../utils/blank';
import { BLANK } from '../../utils/blank';
import type { NodeRenderOptions, RenderOptions } from '../../utils/renderHelpers';
import {
findFirstOccurrenceOutsideComment,
Expand Down Expand Up @@ -46,15 +46,16 @@ export default class ConditionalExpression extends NodeBase implements Deoptimiz
}

deoptimizeCache(): void {
this.isBranchResolutionAnalysed = false;
const { expressionsToBeDeoptimized } = this;
this.expressionsToBeDeoptimized = [];
for (const expression of expressionsToBeDeoptimized) {
expression.deoptimizeCache();
}
if (this.usedBranch !== null) {
const unusedBranch = this.usedBranch === this.consequent ? this.alternate : this.consequent;
this.usedBranch = null;
unusedBranch.deoptimizePath(UNKNOWN_PATH);
const { expressionsToBeDeoptimized } = this;
this.expressionsToBeDeoptimized = EMPTY_ARRAY as unknown as DeoptimizableEntity[];
for (const expression of expressionsToBeDeoptimized) {
expression.deoptimizeCache();
}
}
}

Expand All @@ -73,9 +74,9 @@ export default class ConditionalExpression extends NodeBase implements Deoptimiz
recursionTracker: PathTracker,
origin: DeoptimizableEntity
): LiteralValueOrUnknown {
this.expressionsToBeDeoptimized.push(origin);
const usedBranch = this.getUsedBranch();
if (!usedBranch) return UnknownValue;
this.expressionsToBeDeoptimized.push(origin);
return usedBranch.getLiteralValueAtPath(path, recursionTracker, origin);
}

Expand Down
5 changes: 5 additions & 0 deletions src/ast/nodes/FunctionDeclaration.ts
Expand Up @@ -14,6 +14,11 @@ export default class FunctionDeclaration extends FunctionNode {
}
}

protected onlyFunctionCallUsed(): boolean {
// call super.onlyFunctionCallUsed for export default anonymous function
return this.id?.variable.getOnlyFunctionCallUsed() ?? super.onlyFunctionCallUsed();
}

parseNode(esTreeNode: GenericEsTreeNode): this {
if (esTreeNode.id !== null) {
this.id = new Identifier(this, this.scope.parent as ChildScope).parseNode(
Expand Down
5 changes: 5 additions & 0 deletions src/ast/nodes/Identifier.ts
Expand Up @@ -60,10 +60,12 @@ export default class Identifier extends NodeBase implements PatternNode {
}
}

private isReferenceVariable = false;
bind(): void {
if (!this.variable && isReference(this, this.parent as NodeWithFieldDefinition)) {
this.variable = this.scope.findVariable(this.name);
this.variable.addReference(this);
this.isReferenceVariable = true;
}
}

Expand Down Expand Up @@ -295,6 +297,9 @@ export default class Identifier extends NodeBase implements PatternNode {
this.variable.consolidateInitializers();
this.scope.context.requestTreeshakingPass();
}
if (this.isReferenceVariable) {
this.variable!.addUsedPlace(this);
}
}

private getVariableRespectingTDZ(): ExpressionEntity | null {
Expand Down
4 changes: 2 additions & 2 deletions src/ast/nodes/IfStatement.ts
Expand Up @@ -7,7 +7,7 @@ import { EMPTY_PATH, SHARED_RECURSION_TRACKER } from '../utils/PathTracker';
import BlockStatement from './BlockStatement';
import type Identifier from './Identifier';
import * as NodeType from './NodeType';
import { type LiteralValueOrUnknown, UnknownValue } from './shared/Expression';
import { type LiteralValueOrUnknown } from './shared/Expression';
import {
type ExpressionNode,
type GenericEsTreeNode,
Expand All @@ -29,7 +29,7 @@ export default class IfStatement extends StatementBase implements DeoptimizableE
private testValue: LiteralValueOrUnknown | typeof unset = unset;

deoptimizeCache(): void {
this.testValue = UnknownValue;
this.testValue = unset;
}

hasEffects(context: HasEffectsContext): boolean {
Expand Down
17 changes: 10 additions & 7 deletions src/ast/nodes/LogicalExpression.ts
@@ -1,5 +1,5 @@
import type MagicString from 'magic-string';
import { BLANK, EMPTY_ARRAY } from '../../utils/blank';
import { BLANK } from '../../utils/blank';
import {
findFirstOccurrenceOutsideComment,
findNonWhiteSpace,
Expand Down Expand Up @@ -57,22 +57,25 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable
}

deoptimizeCache(): void {
if (this.usedBranch) {
const unusedBranch = this.usedBranch === this.left ? this.right : this.left;
this.usedBranch = null;
unusedBranch.deoptimizePath(UNKNOWN_PATH);
this.isBranchResolutionAnalysed = false;
if (this.expressionsToBeDeoptimized.length > 0) {
const {
scope: { context },
expressionsToBeDeoptimized
} = this;
this.expressionsToBeDeoptimized = EMPTY_ARRAY as unknown as DeoptimizableEntity[];
this.expressionsToBeDeoptimized = [];
for (const expression of expressionsToBeDeoptimized) {
expression.deoptimizeCache();
}
// Request another pass because we need to ensure "include" runs again if
// it is rendered
context.requestTreeshakingPass();
}
if (this.usedBranch) {
const unusedBranch = this.usedBranch === this.left ? this.right : this.left;
this.usedBranch = null;
unusedBranch.deoptimizePath(UNKNOWN_PATH);
}
}

deoptimizePath(path: ObjectPath): void {
Expand All @@ -90,9 +93,9 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable
recursionTracker: PathTracker,
origin: DeoptimizableEntity
): LiteralValueOrUnknown {
this.expressionsToBeDeoptimized.push(origin);
const usedBranch = this.getUsedBranch();
if (!usedBranch) return UnknownValue;
this.expressionsToBeDeoptimized.push(origin);
return usedBranch.getLiteralValueAtPath(path, recursionTracker, origin);
}

Expand Down
7 changes: 5 additions & 2 deletions src/ast/nodes/MemberExpression.ts
@@ -1,7 +1,7 @@
import type MagicString from 'magic-string';
import type { AstContext } from '../../Module';
import type { NormalizedTreeshakingOptions } from '../../rollup/types';
import { BLANK, EMPTY_ARRAY } from '../../utils/blank';
import { BLANK } from '../../utils/blank';
import { LOGLEVEL_WARN } from '../../utils/logging';
import { logIllegalImportReassignment, logMissingExport } from '../../utils/logs';
import type { NodeRenderOptions, RenderOptions } from '../../utils/renderHelpers';
Expand Down Expand Up @@ -185,7 +185,7 @@ export default class MemberExpression

deoptimizeCache(): void {
const { expressionsToBeDeoptimized, object } = this;
this.expressionsToBeDeoptimized = EMPTY_ARRAY as unknown as DeoptimizableEntity[];
this.expressionsToBeDeoptimized = [];
this.propertyKey = UnknownKey;
object.deoptimizePath(UNKNOWN_PATH);
for (const expression of expressionsToBeDeoptimized) {
Expand Down Expand Up @@ -396,6 +396,9 @@ export default class MemberExpression
);
this.scope.context.requestTreeshakingPass();
}
if (this.variable) {
this.variable.addUsedPlace(this);
}
}

private applyAssignmentDeoptimization(): void {
Expand Down
2 changes: 1 addition & 1 deletion src/ast/nodes/UpdateExpression.ts
Expand Up @@ -87,7 +87,7 @@ export default class UpdateExpression extends NodeBase {
this.argument.deoptimizePath(EMPTY_PATH);
if (this.argument instanceof Identifier) {
const variable = this.scope.findVariable(this.argument.name);
variable.isReassigned = true;
variable.markReassigned();
}
this.scope.context.requestTreeshakingPass();
}
Expand Down
149 changes: 148 additions & 1 deletion src/ast/nodes/shared/FunctionBase.ts
Expand Up @@ -9,12 +9,23 @@ import {
} from '../../NodeInteractions';
import type ReturnValueScope from '../../scopes/ReturnValueScope';
import type { ObjectPath, PathTracker } from '../../utils/PathTracker';
import { UNKNOWN_PATH, UnknownKey } from '../../utils/PathTracker';
import {
EMPTY_PATH,
SHARED_RECURSION_TRACKER,
UNKNOWN_PATH,
UnknownKey
} from '../../utils/PathTracker';
import { UNDEFINED_EXPRESSION } from '../../values';
import type ParameterVariable from '../../variables/ParameterVariable';
import type Variable from '../../variables/Variable';
import BlockStatement from '../BlockStatement';
import type CallExpression from '../CallExpression';
import type ExportDefaultDeclaration from '../ExportDefaultDeclaration';
import Identifier from '../Identifier';
import * as NodeType from '../NodeType';
import RestElement from '../RestElement';
import type SpreadElement from '../SpreadElement';
import type VariableDeclarator from '../VariableDeclarator';
import { Flag, isFlagSet, setFlag } from './BitFlags';
import type { ExpressionEntity, LiteralValueOrUnknown } from './Expression';
import { UNKNOWN_EXPRESSION, UNKNOWN_RETURN_EXPRESSION } from './Expression';
Expand All @@ -27,6 +38,13 @@ import {
import type { ObjectEntity } from './ObjectEntity';
import type { PatternNode } from './Pattern';

type InteractionCalledArguments = NodeInteractionCalled['args'];

// This handler does nothing.
// Since we always re-evaluate argument values in a new tree-shaking pass,
// we don't need to get notified if it is deoptimized.
const EMPTY_DEOPTIMIZABLE_HANDLER = { deoptimizeCache() {} };

export default abstract class FunctionBase extends NodeBase {
declare body: BlockStatement | ExpressionNode;
declare params: PatternNode[];
Expand Down Expand Up @@ -57,6 +75,107 @@ export default abstract class FunctionBase extends NodeBase {
this.flags = setFlag(this.flags, Flag.generator, value);
}

private knownParameterValues: (ExpressionEntity | undefined)[] = [];
private allArguments: InteractionCalledArguments[] = [];
/**
* update knownParameterValues when a call is made to this function
* @param newArguments arguments of the call
*/
private updateKnownParameterValues(newArguments: InteractionCalledArguments): void {
for (let position = 0; position < this.params.length; position++) {
// only the "this" argument newArguments[0] can be null
// it's possible that some arguments are empty, so the value is undefined
const argument = newArguments[position + 1] ?? UNDEFINED_EXPRESSION;
const parameter = this.params[position];
// RestElement can be, and can only be, the last parameter
if (parameter instanceof RestElement) {
return;
}

const knownParameterValue = this.knownParameterValues[position];
if (knownParameterValue === undefined) {
this.knownParameterValues[position] = argument;
continue;
}
if (
knownParameterValue === UNKNOWN_EXPRESSION ||
knownParameterValue === argument ||
(knownParameterValue instanceof Identifier &&
argument instanceof Identifier &&
knownParameterValue.variable === argument.variable)
) {
continue;
}

const oldValue = knownParameterValue.getLiteralValueAtPath(
EMPTY_PATH,
SHARED_RECURSION_TRACKER,
EMPTY_DEOPTIMIZABLE_HANDLER
);
const newValue = argument.getLiteralValueAtPath(
EMPTY_PATH,
SHARED_RECURSION_TRACKER,
EMPTY_DEOPTIMIZABLE_HANDLER
);
if (oldValue !== newValue || typeof oldValue === 'symbol') {
this.knownParameterValues[position] = UNKNOWN_EXPRESSION;
} // else both are the same literal, no need to update
}
}

private forwardArgumentsForFunctionCalledOnce(newArguments: InteractionCalledArguments): void {
for (let position = 0; position < this.params.length; position++) {
const parameter = this.params[position];
if (parameter instanceof Identifier) {
const ParameterVariable = parameter.variable as ParameterVariable | null;
const argument = newArguments[position + 1] ?? UNDEFINED_EXPRESSION;
ParameterVariable?.setKnownValue(argument);
}
}
}

/**
* each time tree-shake starts, this method should be called to reoptimize the parameters
* a parameter's state will change at most twice:
* `undefined` (no call is made) -> an expression -> `UnknownArgument`
* we are sure it will converge, and can use state from last iteration
*/
private applyFunctionParameterOptimization() {
if (this.allArguments.length === 0) {
return;
}

if (this.allArguments.length === 1) {
// we are sure what knownParameterValues will be, so skip it and do setKnownValue
this.forwardArgumentsForFunctionCalledOnce(this.allArguments[0]);
return;
}

// reoptimize all arguments, that's why we save them
for (const argumentsList of this.allArguments) {
this.updateKnownParameterValues(argumentsList);
}
for (let position = 0; position < this.params.length; position++) {
const parameter = this.params[position];
// Parameters without default values
if (parameter instanceof Identifier) {
const parameterVariable = parameter.variable as ParameterVariable | null;
// Only the RestElement may be undefined
const knownParameterValue = this.knownParameterValues[position]!;
parameterVariable?.setKnownValue(knownParameterValue);
}
}
}

private deoptimizeFunctionParameters() {
for (const parameter of this.params) {
if (parameter instanceof Identifier) {
const parameterVariable = parameter.variable as ParameterVariable | null;
parameterVariable?.markReassigned();
}
}
}

protected objectEntity: ObjectEntity | null = null;

deoptimizeArgumentsOnInteractionAtPath(
Expand Down Expand Up @@ -84,6 +203,7 @@ export default abstract class FunctionBase extends NodeBase {
this.addArgumentToBeDeoptimized(argument);
}
}
this.allArguments.push(args);
} else {
this.getObjectEntity().deoptimizeArgumentsOnInteractionAtPath(
interaction,
Expand All @@ -102,6 +222,7 @@ export default abstract class FunctionBase extends NodeBase {
for (const parameterList of this.scope.parameters) {
for (const parameter of parameterList) {
parameter.deoptimizePath(UNKNOWN_PATH);
parameter.markReassigned();
}
}
}
Expand Down Expand Up @@ -180,7 +301,33 @@ export default abstract class FunctionBase extends NodeBase {
return false;
}

/**
* If the function (expression or declaration) is only used as function calls
*/
protected onlyFunctionCallUsed(): boolean {
let variable: Variable | null = null;
if (this.parent.type === NodeType.VariableDeclarator) {
variable = (this.parent as VariableDeclarator).id.variable ?? null;
}
if (this.parent.type === NodeType.ExportDefaultDeclaration) {
variable = (this.parent as ExportDefaultDeclaration).variable;
}
return variable?.getOnlyFunctionCallUsed() ?? false;
}

private functionParametersOptimized = false;
include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void {
const isIIFE =
this.parent.type === NodeType.CallExpression &&
(this.parent as CallExpression).callee === this;
const shoulOptimizeFunctionParameters = isIIFE || this.onlyFunctionCallUsed();
if (shoulOptimizeFunctionParameters) {
this.applyFunctionParameterOptimization();
} else if (this.functionParametersOptimized) {
this.deoptimizeFunctionParameters();
}
this.functionParametersOptimized = shoulOptimizeFunctionParameters;

if (!this.deoptimized) this.applyDeoptimizations();
this.included = true;
const { brokenFlow } = context;
Expand Down
10 changes: 10 additions & 0 deletions src/ast/variables/ExportDefaultVariable.ts
Expand Up @@ -3,6 +3,7 @@ import ClassDeclaration from '../nodes/ClassDeclaration';
import type ExportDefaultDeclaration from '../nodes/ExportDefaultDeclaration';
import FunctionDeclaration from '../nodes/FunctionDeclaration';
import Identifier, { type IdentifierWithVariable } from '../nodes/Identifier';
import type { NodeBase } from '../nodes/shared/Node';
import LocalVariable from './LocalVariable';
import UndefinedVariable from './UndefinedVariable';
import type Variable from './Variable';
Expand Down Expand Up @@ -37,6 +38,15 @@ export default class ExportDefaultVariable extends LocalVariable {
}
}

addUsedPlace(usedPlace: NodeBase): void {
const original = this.getOriginalVariable();
if (original === this) {
super.addUsedPlace(usedPlace);
} else {
original.addUsedPlace(usedPlace);
}
}

forbidName(name: string) {
const original = this.getOriginalVariable();
if (original === this) {
Expand Down