Skip to content

Commit

Permalink
Improve memory usage for parameter deoptimizations (#4938)
Browse files Browse the repository at this point in the history
* Try to improve memory consumption when deoptimizing call parameters

* Avoid seemingly unnecessary optimization

* Merge thisArgs into args so that args is a unique identifier for interactions

* Add limit for tracked interactions

* Remove caches after clearing them

* Add more safeguards

* Fix access

* 3.20.3-0
  • Loading branch information
lukastaegert committed Apr 16, 2023
1 parent c4cb264 commit bd675b1
Show file tree
Hide file tree
Showing 25 changed files with 169 additions and 154 deletions.
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

0 comments on commit bd675b1

Please sign in to comment.