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

Improve memory usage for parameter deoptimizations #4938

Merged
merged 8 commits into from Apr 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion browser/package.json
@@ -1,6 +1,6 @@
{
"name": "@rollup/browser",
"version": "3.20.2",
"version": "3.20.3-0",
"description": "Next-generation ES module bundler browser build",
"main": "dist/rollup.browser.js",
"module": "dist/es/rollup.browser.js",
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
@@ -1,6 +1,6 @@
{
"name": "rollup",
"version": "3.20.2",
"version": "3.20.3-0",
"description": "Next-generation ES module bundler",
"main": "dist/rollup.js",
"module": "dist/es/rollup.js",
Expand Down
30 changes: 11 additions & 19 deletions src/ast/NodeInteractions.ts
Expand Up @@ -6,53 +6,45 @@ export const INTERACTION_ACCESSED = 0;
export const INTERACTION_ASSIGNED = 1;
export const INTERACTION_CALLED = 2;

// The first argument is the "this" context
export interface NodeInteractionAccessed {
args: null;
thisArg: ExpressionEntity | null;
args: readonly [ExpressionEntity | null];
type: typeof INTERACTION_ACCESSED;
}

export const NODE_INTERACTION_UNKNOWN_ACCESS: NodeInteractionAccessed = {
args: null,
thisArg: null,
args: [null],
type: INTERACTION_ACCESSED
};

// The first argument is the "this" context, the second argument the assigned expression
export interface NodeInteractionAssigned {
args: readonly [ExpressionEntity];
thisArg: ExpressionEntity | null;
args: readonly [ExpressionEntity | null, ExpressionEntity];
type: typeof INTERACTION_ASSIGNED;
}

export const UNKNOWN_ARG = [UNKNOWN_EXPRESSION] as const;

export const NODE_INTERACTION_UNKNOWN_ASSIGNMENT: NodeInteractionAssigned = {
args: UNKNOWN_ARG,
thisArg: null,
args: [null, UNKNOWN_EXPRESSION],
type: INTERACTION_ASSIGNED
};

// The first argument is the "this" context, the other arguments are the actual arguments
export interface NodeInteractionCalled {
args: readonly (ExpressionEntity | SpreadElement)[];
thisArg: ExpressionEntity | null;
args: readonly [ExpressionEntity | null, ...(ExpressionEntity | SpreadElement)[]];
type: typeof INTERACTION_CALLED;
withNew: boolean;
}

export const NO_ARGS = [];

// While this is technically a call without arguments, we can compare against
// this reference in places where precise values or thisArg would make a
// this reference in places where precise values or this argument would make a
// difference
export const NODE_INTERACTION_UNKNOWN_CALL: NodeInteractionCalled = {
args: NO_ARGS,
thisArg: null,
args: [null],
type: INTERACTION_CALLED,
withNew: false
};

// For tracking, called and assigned are uniquely determined by their .args
// while accessed is determined by .thisArg
// For tracking, interactions are uniquely determined by their .args
export type NodeInteraction =
| NodeInteractionAccessed
| NodeInteractionAssigned
Expand Down
5 changes: 3 additions & 2 deletions src/ast/nodes/CallExpression.ts
Expand Up @@ -41,11 +41,12 @@ export default class CallExpression
}
}
this.interaction = {
args: this.arguments,
thisArg:
args: [
this.callee instanceof MemberExpression && !this.callee.variable
? this.callee.object
: null,
...this.arguments
],
type: INTERACTION_CALLED,
withNew: false
};
Expand Down
6 changes: 4 additions & 2 deletions src/ast/nodes/ConditionalExpression.ts
@@ -1,5 +1,5 @@
import type MagicString from 'magic-string';
import { BLANK } from '../../utils/blank';
import { BLANK, EMPTY_ARRAY } from '../../utils/blank';
import type { NodeRenderOptions, RenderOptions } from '../../utils/renderHelpers';
import {
findFirstOccurrenceOutsideComment,
Expand Down Expand Up @@ -44,7 +44,9 @@ export default class ConditionalExpression extends NodeBase implements Deoptimiz
const unusedBranch = this.usedBranch === this.consequent ? this.alternate : this.consequent;
this.usedBranch = null;
unusedBranch.deoptimizePath(UNKNOWN_PATH);
for (const expression of this.expressionsToBeDeoptimized) {
const { expressionsToBeDeoptimized } = this;
this.expressionsToBeDeoptimized = EMPTY_ARRAY as unknown as DeoptimizableEntity[];
for (const expression of expressionsToBeDeoptimized) {
expression.deoptimizeCache();
}
}
Expand Down
8 changes: 5 additions & 3 deletions src/ast/nodes/LogicalExpression.ts
@@ -1,5 +1,5 @@
import type MagicString from 'magic-string';
import { BLANK } from '../../utils/blank';
import { BLANK, EMPTY_ARRAY } from '../../utils/blank';
import {
findFirstOccurrenceOutsideComment,
findNonWhiteSpace,
Expand Down Expand Up @@ -54,12 +54,14 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable
const unusedBranch = this.usedBranch === this.left ? this.right : this.left;
this.usedBranch = null;
unusedBranch.deoptimizePath(UNKNOWN_PATH);
for (const expression of this.expressionsToBeDeoptimized) {
const { context, expressionsToBeDeoptimized } = this;
this.expressionsToBeDeoptimized = EMPTY_ARRAY as unknown as DeoptimizableEntity[];
for (const expression of expressionsToBeDeoptimized) {
expression.deoptimizeCache();
}
// Request another pass because we need to ensure "include" runs again if
// it is rendered
this.context.requestTreeshakingPass();
context.requestTreeshakingPass();
}
}

Expand Down
25 changes: 12 additions & 13 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 } from '../../utils/blank';
import { BLANK, EMPTY_ARRAY } from '../../utils/blank';
import { errorIllegalImportReassignment, errorMissingExport } from '../../utils/error';
import type { NodeRenderOptions, RenderOptions } from '../../utils/renderHelpers';
import type { DeoptimizableEntity } from '../DeoptimizableEntity';
Expand Down Expand Up @@ -101,8 +101,8 @@ export default class MemberExpression
declare propertyKey: ObjectPathKey | null;
declare type: NodeType.tMemberExpression;
variable: Variable | null = null;
protected declare assignmentInteraction: NodeInteractionAssigned & { thisArg: ExpressionEntity };
private declare accessInteraction: NodeInteractionAccessed & { thisArg: ExpressionEntity };
protected declare assignmentInteraction: NodeInteractionAssigned;
private declare accessInteraction: NodeInteractionAccessed;
private assignmentDeoptimized = false;
private bound = false;
private expressionsToBeDeoptimized: DeoptimizableEntity[] = [];
Expand Down Expand Up @@ -152,10 +152,10 @@ export default class MemberExpression
}

deoptimizeCache(): void {
const expressionsToBeDeoptimized = this.expressionsToBeDeoptimized;
this.expressionsToBeDeoptimized = [];
const { expressionsToBeDeoptimized, object } = this;
this.expressionsToBeDeoptimized = EMPTY_ARRAY as unknown as DeoptimizableEntity[];
this.propertyKey = UnknownKey;
this.object.deoptimizePath(UNKNOWN_PATH);
object.deoptimizePath(UNKNOWN_PATH);
for (const expression of expressionsToBeDeoptimized) {
expression.deoptimizeCache();
}
Expand Down Expand Up @@ -185,8 +185,8 @@ export default class MemberExpression
if (this.isUndefined) {
return undefined;
}
this.expressionsToBeDeoptimized.push(origin);
if (path.length < MAX_PATH_DEPTH) {
if (this.propertyKey !== UnknownKey && path.length < MAX_PATH_DEPTH) {
this.expressionsToBeDeoptimized.push(origin);
return this.object.getLiteralValueAtPath(
[this.getPropertyKey(), ...path],
recursionTracker,
Expand All @@ -213,8 +213,8 @@ export default class MemberExpression
if (this.isUndefined) {
return [UNDEFINED_EXPRESSION, false];
}
this.expressionsToBeDeoptimized.push(origin);
if (path.length < MAX_PATH_DEPTH) {
if (this.propertyKey !== UnknownKey && path.length < MAX_PATH_DEPTH) {
this.expressionsToBeDeoptimized.push(origin);
return this.object.getReturnExpressionWhenCalledAtPath(
[this.getPropertyKey(), ...path],
interaction,
Expand Down Expand Up @@ -297,7 +297,7 @@ export default class MemberExpression

initialise(): void {
this.propertyKey = getResolvablePropertyKey(this);
this.accessInteraction = { args: null, thisArg: this.object, type: INTERACTION_ACCESSED };
this.accessInteraction = { args: [this.object], type: INTERACTION_ACCESSED };
}

isSkippedAsOptional(origin: DeoptimizableEntity): boolean {
Expand Down Expand Up @@ -340,8 +340,7 @@ export default class MemberExpression

setAssignedValue(value: ExpressionEntity) {
this.assignmentInteraction = {
args: [value],
thisArg: this.object,
args: [this.object, value],
type: INTERACTION_ASSIGNED
};
}
Expand Down
3 changes: 1 addition & 2 deletions src/ast/nodes/NewExpression.ts
Expand Up @@ -53,8 +53,7 @@ export default class NewExpression extends NodeBase {

initialise(): void {
this.interaction = {
args: this.arguments,
thisArg: null,
args: [null, ...this.arguments],
type: INTERACTION_CALLED,
withNew: true
};
Expand Down
10 changes: 7 additions & 3 deletions src/ast/nodes/TaggedTemplateExpression.ts
Expand Up @@ -18,6 +18,7 @@ export default class TaggedTemplateExpression extends CallExpressionBase {
declare quasi: TemplateLiteral;
declare tag: ExpressionNode;
declare type: NodeType.tTaggedTemplateExpression;
private declare args: ExpressionEntity[];

bind(): void {
super.bind();
Expand Down Expand Up @@ -54,17 +55,20 @@ export default class TaggedTemplateExpression extends CallExpressionBase {
this.tag.include(context, includeChildrenRecursively);
this.quasi.include(context, includeChildrenRecursively);
}
this.tag.includeCallArguments(context, this.interaction.args);
this.tag.includeCallArguments(context, this.args);
const [returnExpression] = this.getReturnExpression();
if (!returnExpression.included) {
returnExpression.include(context, false);
}
}

initialise(): void {
this.args = [UNKNOWN_EXPRESSION, ...this.quasi.expressions];
this.interaction = {
args: [UNKNOWN_EXPRESSION, ...this.quasi.expressions],
thisArg: this.tag instanceof MemberExpression && !this.tag.variable ? this.tag.object : null,
args: [
this.tag instanceof MemberExpression && !this.tag.variable ? this.tag.object : null,
...this.args
],
type: INTERACTION_CALLED,
withNew: false
};
Expand Down
7 changes: 1 addition & 6 deletions src/ast/nodes/ThisExpression.ts
Expand Up @@ -23,12 +23,7 @@ export default class ThisExpression extends NodeBase {
path: ObjectPath,
recursionTracker: PathTracker
): void {
// We rewrite the parameter so that a ThisVariable can detect self-mutations
this.variable.deoptimizeArgumentsOnInteractionAtPath(
interaction.thisArg === this ? { ...interaction, thisArg: this.variable } : interaction,
path,
recursionTracker
);
this.variable.deoptimizeArgumentsOnInteractionAtPath(interaction, path, recursionTracker);
}

deoptimizePath(path: ObjectPath): void {
Expand Down
34 changes: 17 additions & 17 deletions src/ast/nodes/shared/CallExpressionBase.ts
@@ -1,3 +1,4 @@
import { EMPTY_ARRAY, EMPTY_SET } from '../../../utils/blank';
import type { DeoptimizableEntity } from '../../DeoptimizableEntity';
import type { HasEffectsContext } from '../../ExecutionContext';
import type { NodeInteraction, NodeInteractionCalled } from '../../NodeInteractions';
Expand All @@ -15,36 +16,32 @@ import { NodeBase } from './Node';
export default abstract class CallExpressionBase extends NodeBase implements DeoptimizableEntity {
protected declare interaction: NodeInteractionCalled;
protected returnExpression: [expression: ExpressionEntity, isPure: boolean] | null = null;
private readonly deoptimizableDependentExpressions: DeoptimizableEntity[] = [];
private readonly expressionsToBeDeoptimized = new Set<ExpressionEntity>();
private deoptimizableDependentExpressions: DeoptimizableEntity[] = [];
private expressionsToBeDeoptimized = new Set<ExpressionEntity>();

deoptimizeArgumentsOnInteractionAtPath(
interaction: NodeInteraction,
path: ObjectPath,
recursionTracker: PathTracker
): void {
const { args, thisArg } = interaction;
const { args } = interaction;
const [returnExpression, isPure] = this.getReturnExpression(recursionTracker);
if (isPure) return;
const deoptimizedExpressions = args.filter(
expression => !!expression && expression !== UNKNOWN_EXPRESSION
) as ExpressionEntity[];
if (deoptimizedExpressions.length === 0) return;
if (returnExpression === UNKNOWN_EXPRESSION) {
thisArg?.deoptimizePath(UNKNOWN_PATH);
if (args) {
for (const argument of args) {
argument.deoptimizePath(UNKNOWN_PATH);
}
for (const expression of deoptimizedExpressions) {
expression.deoptimizePath(UNKNOWN_PATH);
}
} else {
recursionTracker.withTrackedEntityAtPath(
path,
returnExpression,
() => {
if (thisArg) {
this.expressionsToBeDeoptimized.add(thisArg);
}
if (args) {
for (const argument of args) {
this.expressionsToBeDeoptimized.add(argument);
}
for (const expression of deoptimizedExpressions) {
this.expressionsToBeDeoptimized.add(expression);
}
returnExpression.deoptimizeArgumentsOnInteractionAtPath(
interaction,
Expand All @@ -60,10 +57,13 @@ export default abstract class CallExpressionBase extends NodeBase implements Deo
deoptimizeCache(): void {
if (this.returnExpression?.[0] !== UNKNOWN_EXPRESSION) {
this.returnExpression = UNKNOWN_RETURN_EXPRESSION;
for (const expression of this.deoptimizableDependentExpressions) {
const { deoptimizableDependentExpressions, expressionsToBeDeoptimized } = this;
this.expressionsToBeDeoptimized = EMPTY_SET;
this.deoptimizableDependentExpressions = EMPTY_ARRAY as unknown as DeoptimizableEntity[];
for (const expression of deoptimizableDependentExpressions) {
expression.deoptimizeCache();
}
for (const expression of this.expressionsToBeDeoptimized) {
for (const expression of expressionsToBeDeoptimized) {
expression.deoptimizePath(UNKNOWN_PATH);
}
}
Expand Down
7 changes: 2 additions & 5 deletions src/ast/nodes/shared/Expression.ts
Expand Up @@ -99,10 +99,7 @@ export const UNKNOWN_RETURN_EXPRESSION: [expression: ExpressionEntity, isPure: b
];

export const deoptimizeInteraction = (interaction: NodeInteraction) => {
interaction.thisArg?.deoptimizePath(UNKNOWN_PATH);
if (interaction.args) {
for (const argument of interaction.args) {
argument.deoptimizePath(UNKNOWN_PATH);
}
for (const argument of interaction.args) {
argument?.deoptimizePath(UNKNOWN_PATH);
}
};
11 changes: 6 additions & 5 deletions src/ast/nodes/shared/FunctionBase.ts
Expand Up @@ -49,16 +49,17 @@ export default abstract class FunctionBase extends NodeBase {
const { parameters } = this.scope;
const { args } = interaction;
let hasRest = false;
for (let position = 0; position < args.length; position++) {
for (let position = 0; position < args.length - 1; position++) {
const parameter = this.params[position];
// Only the "this" argument arg[0] can be null
const argument = args[position + 1]!;
if (hasRest || parameter instanceof RestElement) {
hasRest = true;
args[position].deoptimizePath(UNKNOWN_PATH);
argument.deoptimizePath(UNKNOWN_PATH);
} else if (parameter instanceof Identifier) {
// args[position].deoptimizePath(UNKNOWN_PATH);
parameters[position][0].addEntityToBeDeoptimized(args[position]);
parameters[position][0].addEntityToBeDeoptimized(argument);
} else if (parameter) {
args[position].deoptimizePath(UNKNOWN_PATH);
argument.deoptimizePath(UNKNOWN_PATH);
}
}
} else {
Expand Down