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

perf(core): avoid storing LView in __ngContext__ #41908

Closed
wants to merge 1 commit into from
Closed
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
4 changes: 2 additions & 2 deletions goldens/size-tracking/integration-payloads.json
Expand Up @@ -3,7 +3,7 @@
"master": {
"uncompressed": {
"runtime-es2015": 1170,
"main-es2015": 138189,
"main-es2015": 138718,
"polyfills-es2015": 36964
}
}
Expand All @@ -30,7 +30,7 @@
"master": {
"uncompressed": {
"runtime-es2015": 1190,
"main-es2015": 136546,
"main-es2015": 137087,
"polyfills-es2015": 37641
}
}
Expand Down
30 changes: 17 additions & 13 deletions packages/core/src/debug/debug_node.ts
Expand Up @@ -262,9 +262,10 @@ class DebugElement__POST_R3__ extends DebugNode__POST_R3__ implements DebugEleme
}

get name(): string {
const context = getLContext(this.nativeNode);
if (context !== null) {
const lView = context.lView;
const context = getLContext(this.nativeNode)!;
const lView = context ? context.lView : null;

if (lView !== null) {
const tData = lView[TVIEW].data;
const tNode = tData[context.nodeIndex] as TNode;
return tNode.value!;
Expand All @@ -286,12 +287,13 @@ class DebugElement__POST_R3__ extends DebugNode__POST_R3__ implements DebugEleme
* - attribute bindings (e.g. `[attr.role]="menu"`)
*/
get properties(): {[key: string]: any;} {
const context = getLContext(this.nativeNode);
if (context === null) {
const context = getLContext(this.nativeNode)!;
const lView = context ? context.lView : null;

if (lView === null) {
return {};
}

const lView = context.lView;
const tData = lView[TVIEW].data;
const tNode = tData[context.nodeIndex] as TNode;

Expand All @@ -312,12 +314,13 @@ class DebugElement__POST_R3__ extends DebugNode__POST_R3__ implements DebugEleme
return attributes;
}

const context = getLContext(element);
if (context === null) {
const context = getLContext(element)!;
const lView = context ? context.lView : null;

if (lView === null) {
return {};
}

const lView = context.lView;
const tNodeAttrs = (lView[TVIEW].data[context.nodeIndex] as TNode).attrs;
const lowercaseTNodeAttrs: string[] = [];

Expand Down Expand Up @@ -502,11 +505,12 @@ function _queryAllR3(
function _queryAllR3(
parentElement: DebugElement, predicate: Predicate<DebugElement>|Predicate<DebugNode>,
matches: DebugElement[]|DebugNode[], elementsOnly: boolean) {
const context = getLContext(parentElement.nativeNode);
if (context !== null) {
const parentTNode = context.lView[TVIEW].data[context.nodeIndex] as TNode;
const context = getLContext(parentElement.nativeNode)!;
const lView = context ? context.lView : null;
if (lView !== null) {
const parentTNode = lView[TVIEW].data[context.nodeIndex] as TNode;
_queryNodeChildrenR3(
parentTNode, context.lView, predicate, matches, elementsOnly, parentElement.nativeNode);
parentTNode, lView, predicate, matches, elementsOnly, parentElement.nativeNode);
} else {
// If the context is null, then `parentElement` was either created with Renderer2 or native DOM
// APIs.
Expand Down
56 changes: 27 additions & 29 deletions packages/core/src/render3/context_discovery.ts
Expand Up @@ -8,12 +8,15 @@
import '../util/ng_dev_mode';

import {assertDefined, assertDomNode} from '../util/assert';

import {EMPTY_ARRAY} from '../util/empty';

import {assertLView} from './assert';
import {LContext} from './interfaces/context';
import {getLViewById} from './interfaces/lview_tracking';
import {TNode, TNodeFlags} from './interfaces/node';
import {RElement, RNode} from './interfaces/renderer_dom';
import {CONTEXT, HEADER_OFFSET, HOST, LView, TVIEW} from './interfaces/view';
import {isLView} from './interfaces/type_checks';
import {CONTEXT, HEADER_OFFSET, HOST, ID, LView, TVIEW} from './interfaces/view';
import {getComponentLViewByIndex, unwrapRNode} from './util/view_utils';


Expand Down Expand Up @@ -43,7 +46,7 @@ export function getLContext(target: any): LContext|null {
if (mpValue) {
// only when it's an array is it considered an LView instance
// ... otherwise it's an already constructed LContext instance
if (Array.isArray(mpValue)) {
if (isLView(mpValue)) {
const lView: LView = mpValue!;
let nodeIndex: number;
let component: any = undefined;
Expand Down Expand Up @@ -105,12 +108,7 @@ export function getLContext(target: any): LContext|null {
while (parent = parent.parentNode) {
const parentContext = readPatchedData(parent);
if (parentContext) {
let lView: LView|null;
if (Array.isArray(parentContext)) {
lView = parentContext as LView;
} else {
lView = parentContext.lView;
}
const lView = Array.isArray(parentContext) ? parentContext as LView : parentContext.lView;

// the edge of the app was also reached here through another means
// (maybe because the DOM was changed manually).
Expand All @@ -136,14 +134,7 @@ export function getLContext(target: any): LContext|null {
* Creates an empty instance of a `LContext` context
*/
function createLContext(lView: LView, nodeIndex: number, native: RNode): LContext {
return {
lView,
nodeIndex,
native,
component: undefined,
directives: undefined,
localRefs: undefined,
};
return new LContext(lView[ID], nodeIndex, native);
}

/**
Expand All @@ -153,21 +144,24 @@ function createLContext(lView: LView, nodeIndex: number, native: RNode): LContex
* @returns The component's view
*/
export function getComponentViewByInstance(componentInstance: {}): LView {
let lView = readPatchedData(componentInstance);
let view: LView;
let patchedData = readPatchedData(componentInstance);
let lView: LView;

if (Array.isArray(lView)) {
const nodeIndex = findViaComponent(lView, componentInstance);
view = getComponentLViewByIndex(nodeIndex, lView);
const context = createLContext(lView, nodeIndex, view[HOST] as RElement);
if (isLView(patchedData)) {
const contextLView: LView = patchedData;
const nodeIndex = findViaComponent(contextLView, componentInstance);
lView = getComponentLViewByIndex(nodeIndex, contextLView);
const context = createLContext(contextLView, nodeIndex, lView[HOST] as RElement);
context.component = componentInstance;
attachPatchData(componentInstance, context);
attachPatchData(context.native, context);
} else {
const context = lView as any as LContext;
view = getComponentLViewByIndex(context.nodeIndex, context.lView);
const context = patchedData as unknown as LContext;
const contextLView = context.lView!;
ngDevMode && assertLView(contextLView);
lView = getComponentLViewByIndex(context.nodeIndex, contextLView);
}
return view;
return lView;
}

/**
Expand All @@ -181,7 +175,10 @@ const MONKEY_PATCH_KEY_NAME = '__ngContext__';
*/
export function attachPatchData(target: any, data: LView|LContext) {
ngDevMode && assertDefined(target, 'Target expected');
target[MONKEY_PATCH_KEY_NAME] = data;
// Only attach the ID of the view in order to avoid memory leaks (see #41047). We only do this
// for `LView`, because we have control over when an `LView` is created and destroyed, whereas
// we can't know when to remove an `LContext`.
target[MONKEY_PATCH_KEY_NAME] = isLView(data) ? data[ID] : data;
}

/**
Expand All @@ -190,13 +187,14 @@ export function attachPatchData(target: any, data: LView|LContext) {
*/
export function readPatchedData(target: any): LView|LContext|null {
ngDevMode && assertDefined(target, 'Target expected');
return target[MONKEY_PATCH_KEY_NAME] || null;
const data = target[MONKEY_PATCH_KEY_NAME];
return (typeof data === 'number') ? getLViewById(data) : data || null;
}

export function readPatchedLView(target: any): LView|null {
const value = readPatchedData(target);
if (value) {
return Array.isArray(value) ? value : (value as LContext).lView;
return isLView(value) ? value : value.lView;
}
return null;
}
Expand Down
5 changes: 4 additions & 1 deletion packages/core/src/render3/instructions/lview_debug.ts
Expand Up @@ -25,7 +25,7 @@ import {LQueries, TQueries} from '../interfaces/query';
import {Renderer3, RendererFactory3} from '../interfaces/renderer';
import {RComment, RElement, RNode} from '../interfaces/renderer_dom';
import {getTStylingRangeNext, getTStylingRangeNextDuplicate, getTStylingRangePrev, getTStylingRangePrevDuplicate, TStylingKey, TStylingRange} from '../interfaces/styling';
import {CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DebugNode, DECLARATION_VIEW, DestroyHookData, FLAGS, HEADER_OFFSET, HookData, HOST, HostBindingOpCodes, INJECTOR, LContainerDebug as ILContainerDebug, LView, LViewDebug as ILViewDebug, LViewDebugRange, LViewDebugRangeContent, LViewFlags, NEXT, NodeInjectorDebug, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, SANITIZER, T_HOST, TData, TView as ITView, TVIEW, TView, TViewType, TViewTypeAsString} from '../interfaces/view';
import {CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DebugNode, DECLARATION_VIEW, DestroyHookData, FLAGS, HEADER_OFFSET, HookData, HOST, HostBindingOpCodes, ID, INJECTOR, LContainerDebug as ILContainerDebug, LView, LViewDebug as ILViewDebug, LViewDebugRange, LViewDebugRangeContent, LViewFlags, NEXT, NodeInjectorDebug, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, SANITIZER, T_HOST, TData, TView as ITView, TVIEW, TView, TViewType, TViewTypeAsString} from '../interfaces/view';
import {attachDebugObject} from '../util/debug_utils';
import {getParentInjectorIndex, getParentInjectorView} from '../util/injector_utils';
import {unwrapRNode} from '../util/view_utils';
Expand Down Expand Up @@ -513,6 +513,9 @@ export class LViewDebug implements ILViewDebug {
get tHost(): ITNode|null {
return this._raw_lView[T_HOST];
}
get id(): number {
return this._raw_lView[ID];
}

get decls(): LViewDebugRange {
return toLViewRange(this.tView, this._raw_lView, HEADER_OFFSET, this.tView.bindingStartIndex);
Expand Down
13 changes: 9 additions & 4 deletions packages/core/src/render3/instructions/shared.ts
Expand Up @@ -28,12 +28,13 @@ import {executeCheckHooks, executeInitAndCheckHooks, incrementInitPhaseFlags} fr
import {CONTAINER_HEADER_OFFSET, HAS_TRANSPLANTED_VIEWS, LContainer, MOVED_VIEWS} from '../interfaces/container';
import {ComponentDef, ComponentTemplate, DirectiveDef, DirectiveDefListOrFactory, HostBindingsFunction, PipeDefListOrFactory, RenderFlags, ViewQueriesFunction} from '../interfaces/definition';
import {NodeInjectorFactory} from '../interfaces/injector';
import {registerLView} from '../interfaces/lview_tracking';
import {AttributeMarker, InitialInputData, InitialInputs, LocalRefExtractor, PropertyAliases, PropertyAliasValue, TAttributes, TConstantsOrFactory, TContainerNode, TDirectiveHostNode, TElementContainerNode, TElementNode, TIcuContainerNode, TNode, TNodeFlags, TNodeType, TProjectionNode} from '../interfaces/node';
import {isProceduralRenderer, Renderer3, RendererFactory3} from '../interfaces/renderer';
import {RComment, RElement, RNode, RText} from '../interfaces/renderer_dom';
import {SanitizerFn} from '../interfaces/sanitization';
import {isComponentDef, isComponentHost, isContentQueryHost, isRootView} from '../interfaces/type_checks';
import {CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DECLARATION_COMPONENT_VIEW, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, HostBindingOpCodes, InitPhaseState, INJECTOR, LView, LViewFlags, NEXT, PARENT, RENDERER, RENDERER_FACTORY, RootContext, RootContextFlags, SANITIZER, T_HOST, TData, TRANSPLANTED_VIEWS_TO_REFRESH, TVIEW, TView, TViewType} from '../interfaces/view';
import {CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DECLARATION_COMPONENT_VIEW, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, HostBindingOpCodes, ID, InitPhaseState, INJECTOR, LView, LViewFlags, NEXT, PARENT, RENDERER, RENDERER_FACTORY, RootContext, RootContextFlags, SANITIZER, T_HOST, TData, TRANSPLANTED_VIEWS_TO_REFRESH, TVIEW, TView, TViewType} from '../interfaces/view';
import {assertPureTNodeType, assertTNodeType} from '../node_assert';
import {updateTextNode} from '../node_manipulation';
import {isInlineTemplate, isNodeMatchingSelectorList} from '../node_selector_matcher';
Expand Down Expand Up @@ -143,6 +144,7 @@ export function createLView<T>(
lView[SANITIZER] = sanitizer || parentLView && parentLView[SANITIZER] || null!;
lView[INJECTOR as any] = injector || parentLView && parentLView[INJECTOR] || null;
lView[T_HOST] = tHostNode;
lView[ID] = registerLView(lView);
ngDevMode &&
assertEqual(
tView.type == TViewType.Embedded ? parentLView !== null : true, true,
Expand Down Expand Up @@ -1906,9 +1908,12 @@ export function scheduleTick(rootContext: RootContext, flags: RootContextFlags)
export function tickRootContext(rootContext: RootContext) {
for (let i = 0; i < rootContext.components.length; i++) {
const rootComponent = rootContext.components[i];
const lView = readPatchedLView(rootComponent)!;
const tView = lView[TVIEW];
renderComponentOrTemplate(tView, lView, tView.template, rootComponent);
const lView = readPatchedLView(rootComponent);
// We might not have an `LView` if the component was destroyed.
if (lView !== null) {
const tView = lView[TVIEW];
renderComponentOrTemplate(tView, lView, tView.template, rootComponent);
}
}
}

Expand Down
49 changes: 28 additions & 21 deletions packages/core/src/render3/interfaces/context.ts
Expand Up @@ -7,6 +7,7 @@
*/


import {getLViewById} from './lview_tracking';
import {RNode} from './renderer_dom';
import {LView} from './view';

Expand All @@ -21,35 +22,41 @@ import {LView} from './view';
* function. The component, element and each directive instance will share the same instance
* of the context.
*/
export interface LContext {
/**
* The component's parent view data.
*/
lView: LView;

/**
* The index instance of the node.
*/
nodeIndex: number;

/**
* The instance of the DOM node that is attached to the lNode.
*/
native: RNode;

export class LContext {
/**
* The instance of the Component node.
*/
component: {}|null|undefined;
public component: {}|null|undefined;

/**
* The list of active directives that exist on this element.
*/
directives: any[]|null|undefined;
public directives: any[]|null|undefined;

/**
* The map of local references (local reference name => element or directive instance) that exist
* on this element.
* The map of local references (local reference name => element or directive instance) that
* exist on this element.
*/
localRefs: {[key: string]: any}|null|undefined;
public localRefs: {[key: string]: any}|null|undefined;

/** Component's parent view data. */
get lView(): LView|null {
return getLViewById(this.lViewId);
}

constructor(
/**
* ID of the component's parent view data.
*/
private lViewId: number,

/**
* The index instance of the node.
*/
public nodeIndex: number,

/**
* The instance of the DOM node that is attached to the lNode.
*/
public native: RNode) {}
}
35 changes: 35 additions & 0 deletions packages/core/src/render3/interfaces/lview_tracking.ts
@@ -0,0 +1,35 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {assertNumber} from '../../util/assert';
import {ID, LView} from './view';

// Keeps track of the currently-active LViews.
const TRACKED_LVIEWS = new Map<number, LView>();
Copy link
Contributor

@sod sod May 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a test for memory leaks in ssr context? We ran rc.1 which included #41358 and this Map filled up rather quickly with DOM nodes, crashing the server through out of memory in a matter of seconds.

That this is stil a single Map for multiple apps rendering on one global scope makes me rather nervous.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We ended up reverting the original commit due to the memory leak and the fact that fixing the leak revealed some other issues. The reverted code will be in rc.2.

These changes include the original fix plus a fix for the memory leak so that we can debug the issues.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @sod, thanks for testing an RC version and sharing the feedback 👍

As Kristiyan mentioned, the previous version of this change was reverted and this PR has the necessary fixes to make sure the memory leak doesn't happen. If you get a chance, it's be great if you could try to use the following package (based on the changes in this PR) in your package.json file (while keeping other packages at rc.1 version):

...
"@angular/core": "https://978551-24195339-gh.circle-artifacts.com/0/angular/core-pr41908-dc11acb763.tgz",
...

and let us know if the memory leak that you've observed is resolved.

Thank you.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, with https://978551-24195339-gh.circle-artifacts.com/0/angular/core-pr41908-dc11acb763.tgz memory consumption is stable. After 1000 requests the memory footprint is the same. And comparing memory heap dumps, I see no angular or dominos specific retention.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sod thanks for checking! 👍


// Used for generating unique IDs for LViews.
let uniqueIdCounter = 0;

/** Starts tracking an LView and returns a unique ID that can be used for future lookups. */
export function registerLView(lView: LView): number {
const id = uniqueIdCounter++;
TRACKED_LVIEWS.set(id, lView);
return id;
}

/** Gets an LView by its unique ID. */
export function getLViewById(id: number): LView|null {
ngDevMode && assertNumber(id, 'ID used for LView lookup must be a number');
return TRACKED_LVIEWS.get(id) || null;
}

/** Stops tracking an LView. */
export function unregisterLView(lView: LView): void {
ngDevMode && assertNumber(lView[ID], 'Cannot stop tracking an LView that does not have an ID');
TRACKED_LVIEWS.delete(lView[ID]);
}
6 changes: 5 additions & 1 deletion packages/core/src/render3/interfaces/view.ts
Expand Up @@ -47,14 +47,15 @@ export const DECLARATION_COMPONENT_VIEW = 16;
export const DECLARATION_LCONTAINER = 17;
export const PREORDER_HOOK_FLAGS = 18;
export const QUERIES = 19;
export const ID = 20;
/**
* Size of LView's header. Necessary to adjust for it when setting slots.
*
* IMPORTANT: `HEADER_OFFSET` should only be referred to the in the `ɵɵ*` instructions to translate
* instruction index into `LView` index. All other indexes should be in the `LView` index space and
* there should be no need to refer to `HEADER_OFFSET` anywhere else.
*/
export const HEADER_OFFSET = 20;
export const HEADER_OFFSET = 21;


// This interface replaces the real LView interface if it is an arg or a
Expand Down Expand Up @@ -326,6 +327,9 @@ export interface LView extends Array<any> {
* are not `Dirty`/`CheckAlways`.
*/
[TRANSPLANTED_VIEWS_TO_REFRESH]: number;

/** Unique ID of the view. Used for `__ngContext__` lookups in the `LView` registry. */
[ID]: number;
}

/** Flags associated with an LView (saved in LView[FLAGS]) */
Expand Down