Skip to content

Commit 1062960

Browse files
perf(ivy): split hooks processing into init and check phases (#32131)
Angular hooks come after 2 flavours: - init hooks (OnInit, AfterContentInit, AfterViewInit); - check hooks (OnChanges, DoChanges, AfterContentChecked, AfterViewChecked). We need to do more processing for init hooks to ensure that those hooks are run once and only once for a given directive (even in case of errors). As soon as all init hooks execute to completion we are only left with the checks to execute. It turns out that keeping track of the remaining init hooks to execute is rather expensive (multiple LView flags reads, writes and checks). But we can observe that non of this tracking is needed as soon as all init hooks are completed. This PR takes advantage of the above observations and splits hooks processing functions into: - init-specific (slower but less common); - check-specific (faster and more common). NOTE: there is code duplication in this PR and it is left like this intentinally: hand-inlining this perf-critical code makes the view refresh process substentially faster. PR Close #32131
1 parent 4d549f6 commit 1062960

File tree

8 files changed

+151
-77
lines changed

8 files changed

+151
-77
lines changed

integration/_payload-limits.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"master": {
1313
"uncompressed": {
1414
"runtime": 1440,
15-
"main": 13164,
15+
"main": 13411,
1616
"polyfills": 45340
1717
}
1818
}

packages/core/src/render3/hooks.ts

Lines changed: 40 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {assertEqual} from '../util/assert';
9+
import {assertEqual, assertNotEqual} from '../util/assert';
1010

1111
import {DirectiveDef} from './interfaces/definition';
1212
import {TNode} from './interfaces/node';
1313
import {FLAGS, HookData, InitPhaseState, LView, LViewFlags, PREORDER_HOOK_FLAGS, PreOrderHookFlags, TView} from './interfaces/view';
14+
import {getCheckNoChangesMode} from './state';
1415

1516

1617

@@ -138,70 +139,57 @@ export function registerPostOrderHooks(tView: TView, tNode: TNode): void {
138139
* They are are stored as flags in LView[PREORDER_HOOK_FLAGS].
139140
*/
140141

142+
141143
/**
142-
* Executes necessary hooks at the start of executing a template.
143-
*
144-
* Executes hooks that are to be run during the initialization of a directive such
145-
* as `onChanges`, `onInit`, and `doCheck`.
146-
*
147-
* @param lView The current view
148-
* @param tView Static data for the view containing the hooks to be executed
149-
* @param checkNoChangesMode Whether or not we're in checkNoChanges mode.
150-
* @param @param currentNodeIndex 2 cases depending the the value:
151-
* - undefined: execute hooks only from the saved index until the end of the array (pre-order case,
152-
* when flushing the remaining hooks)
144+
* Executes pre-order check hooks ( OnChanges, DoChanges) given a view where all the init hooks were
145+
* executed once. This is a light version of executeInitAndCheckPreOrderHooks where we can skip read
146+
* / write of the init-hooks related flags.
147+
* @param lView The LView where hooks are defined
148+
* @param hooks Hooks to be run
149+
* @param nodeIndex 3 cases depending on the value:
150+
* - undefined: all hooks from the array should be executed (post-order case)
151+
* - null: execute hooks only from the saved index until the end of the array (pre-order case, when
152+
* flushing the remaining hooks)
153153
* - number: execute hooks only from the saved index until that node index exclusive (pre-order
154154
* case, when executing select(number))
155155
*/
156-
export function executePreOrderHooks(
157-
currentView: LView, tView: TView, checkNoChangesMode: boolean,
158-
currentNodeIndex: number | undefined): void {
159-
if (!checkNoChangesMode) {
160-
executeHooks(
161-
currentView, tView.preOrderHooks, tView.preOrderCheckHooks, checkNoChangesMode,
162-
InitPhaseState.OnInitHooksToBeRun,
163-
currentNodeIndex !== undefined ? currentNodeIndex : null);
164-
}
156+
export function executeCheckHooks(lView: LView, hooks: HookData, nodeIndex?: number | null) {
157+
callHooks(lView, hooks, InitPhaseState.InitPhaseCompleted, nodeIndex);
165158
}
166159

167160
/**
168-
* Executes hooks against the given `LView` based off of whether or not
169-
* This is the first pass.
170-
*
171-
* @param currentView The view instance data to run the hooks against
172-
* @param firstPassHooks An array of hooks to run if we're in the first view pass
173-
* @param checkHooks An Array of hooks to run if we're not in the first view pass.
174-
* @param checkNoChangesMode Whether or not we're in no changes mode.
175-
* @param initPhaseState the current state of the init phase
176-
* @param currentNodeIndex 3 cases depending the the value:
161+
* Executes post-order init and check hooks (one of AfterContentInit, AfterContentChecked,
162+
* AfterViewInit, AfterViewChecked) given a view where there are pending init hooks to be executed.
163+
* @param lView The LView where hooks are defined
164+
* @param hooks Hooks to be run
165+
* @param initPhase A phase for which hooks should be run
166+
* @param nodeIndex 3 cases depending on the value:
177167
* - undefined: all hooks from the array should be executed (post-order case)
178168
* - null: execute hooks only from the saved index until the end of the array (pre-order case, when
179169
* flushing the remaining hooks)
180170
* - number: execute hooks only from the saved index until that node index exclusive (pre-order
181171
* case, when executing select(number))
182172
*/
183-
export function executeHooks(
184-
currentView: LView, firstPassHooks: HookData | null, checkHooks: HookData | null,
185-
checkNoChangesMode: boolean, initPhaseState: InitPhaseState,
186-
currentNodeIndex: number | null | undefined): void {
187-
if (checkNoChangesMode) return;
188-
189-
if (checkHooks !== null || firstPassHooks !== null) {
190-
const hooksToCall = (currentView[FLAGS] & LViewFlags.InitPhaseStateMask) === initPhaseState ?
191-
firstPassHooks :
192-
checkHooks;
193-
if (hooksToCall !== null) {
194-
callHooks(currentView, hooksToCall, initPhaseState, currentNodeIndex);
195-
}
173+
export function executeInitAndCheckHooks(
174+
lView: LView, hooks: HookData, initPhase: InitPhaseState, nodeIndex?: number | null) {
175+
ngDevMode && assertNotEqual(
176+
initPhase, InitPhaseState.InitPhaseCompleted,
177+
'Init pre-order hooks should not be called more than once');
178+
if ((lView[FLAGS] & LViewFlags.InitPhaseStateMask) === initPhase) {
179+
callHooks(lView, hooks, initPhase, nodeIndex);
196180
}
181+
}
197182

198-
// The init phase state must be always checked here as it may have been recursively updated
199-
let flags = currentView[FLAGS];
200-
if (currentNodeIndex == null && (flags & LViewFlags.InitPhaseStateMask) === initPhaseState &&
201-
initPhaseState !== InitPhaseState.InitPhaseCompleted) {
183+
export function incrementInitPhaseFlags(lView: LView, initPhase: InitPhaseState): void {
184+
ngDevMode &&
185+
assertNotEqual(
186+
initPhase, InitPhaseState.InitPhaseCompleted,
187+
'Init hooks phase should not be incremented after all init hooks have been run.');
188+
let flags = lView[FLAGS];
189+
if ((flags & LViewFlags.InitPhaseStateMask) === initPhase) {
202190
flags &= LViewFlags.IndexWithinInitPhaseReset;
203191
flags += LViewFlags.InitPhaseStateIncrementer;
204-
currentView[FLAGS] = flags;
192+
lView[FLAGS] = flags;
205193
}
206194
}
207195

@@ -212,7 +200,7 @@ export function executeHooks(
212200
* @param currentView The current view
213201
* @param arr The array in which the hooks are found
214202
* @param initPhaseState the current state of the init phase
215-
* @param currentNodeIndex 3 cases depending the the value:
203+
* @param currentNodeIndex 3 cases depending on the value:
216204
* - undefined: all hooks from the array should be executed (post-order case)
217205
* - null: execute hooks only from the saved index until the end of the array (pre-order case, when
218206
* flushing the remaining hooks)
@@ -222,6 +210,9 @@ export function executeHooks(
222210
function callHooks(
223211
currentView: LView, arr: HookData, initPhase: InitPhaseState,
224212
currentNodeIndex: number | null | undefined): void {
213+
ngDevMode && assertEqual(
214+
getCheckNoChangesMode(), false,
215+
'Hooks should never be run in the check no changes mode.');
225216
const startIndex = currentNodeIndex !== undefined ?
226217
(currentView[PREORDER_HOOK_FLAGS] & PreOrderHookFlags.IndexOfTheNextPreOrderHookMaskMask) :
227218
0;

packages/core/src/render3/instructions/container.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88
import {assertDataInRange, assertEqual} from '../../util/assert';
99
import {assertHasParent} from '../assert';
1010
import {attachPatchData} from '../context_discovery';
11-
import {executePreOrderHooks, registerPostOrderHooks} from '../hooks';
11+
import {executeCheckHooks, executeInitAndCheckHooks, incrementInitPhaseFlags, registerPostOrderHooks} from '../hooks';
1212
import {ACTIVE_INDEX, CONTAINER_HEADER_OFFSET, LContainer} from '../interfaces/container';
1313
import {ComponentTemplate} from '../interfaces/definition';
1414
import {LocalRefExtractor, TAttributes, TContainerNode, TNode, TNodeType, TViewNode} from '../interfaces/node';
15-
import {BINDING_INDEX, HEADER_OFFSET, LView, RENDERER, TVIEW, T_HOST} from '../interfaces/view';
15+
import {BINDING_INDEX, FLAGS, HEADER_OFFSET, InitPhaseState, LView, LViewFlags, RENDERER, TVIEW, T_HOST} from '../interfaces/view';
1616
import {assertNodeType} from '../node_assert';
1717
import {appendChild, removeView} from '../node_manipulation';
1818
import {getCheckNoChangesMode, getIsParent, getLView, getPreviousOrParentTNode, setIsNotParent, setPreviousOrParentTNode} from '../state';
@@ -112,7 +112,22 @@ export function ɵɵcontainerRefreshStart(index: number): void {
112112

113113
// We need to execute init hooks here so ngOnInit hooks are called in top level views
114114
// before they are called in embedded views (for backwards compatibility).
115-
executePreOrderHooks(lView, tView, getCheckNoChangesMode(), undefined);
115+
if (!getCheckNoChangesMode()) {
116+
const hooksInitPhaseCompleted =
117+
(lView[FLAGS] & LViewFlags.InitPhaseStateMask) === InitPhaseState.InitPhaseCompleted;
118+
if (hooksInitPhaseCompleted) {
119+
const preOrderCheckHooks = tView.preOrderCheckHooks;
120+
if (preOrderCheckHooks !== null) {
121+
executeCheckHooks(lView, preOrderCheckHooks, null);
122+
}
123+
} else {
124+
const preOrderHooks = tView.preOrderHooks;
125+
if (preOrderHooks !== null) {
126+
executeInitAndCheckHooks(lView, preOrderHooks, InitPhaseState.OnInitHooksToBeRun, null);
127+
}
128+
incrementInitPhaseFlags(lView, InitPhaseState.OnInitHooksToBeRun);
129+
}
130+
}
116131
}
117132

118133
/**

packages/core/src/render3/instructions/select.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88
import {assertDataInRange, assertGreaterThan} from '../../util/assert';
9-
import {executePreOrderHooks} from '../hooks';
10-
import {HEADER_OFFSET, LView, TVIEW} from '../interfaces/view';
9+
import {executeCheckHooks, executeInitAndCheckHooks} from '../hooks';
10+
import {FLAGS, HEADER_OFFSET, InitPhaseState, LView, LViewFlags, TVIEW} from '../interfaces/view';
1111
import {getCheckNoChangesMode, getLView, setSelectedIndex} from '../state';
1212

1313

14+
1415
/**
1516
* Selects an element for later binding instructions.
1617
*
@@ -41,7 +42,22 @@ export function selectInternal(lView: LView, index: number, checkNoChangesMode:
4142
ngDevMode && assertDataInRange(lView, index + HEADER_OFFSET);
4243

4344
// Flush the initial hooks for elements in the view that have been added up to this point.
44-
executePreOrderHooks(lView, lView[TVIEW], checkNoChangesMode, index);
45+
// PERF WARNING: do NOT extract this to a separate function without running benchmarks
46+
if (!checkNoChangesMode) {
47+
const hooksInitPhaseCompleted =
48+
(lView[FLAGS] & LViewFlags.InitPhaseStateMask) === InitPhaseState.InitPhaseCompleted;
49+
if (hooksInitPhaseCompleted) {
50+
const preOrderCheckHooks = lView[TVIEW].preOrderCheckHooks;
51+
if (preOrderCheckHooks !== null) {
52+
executeCheckHooks(lView, preOrderCheckHooks, index);
53+
}
54+
} else {
55+
const preOrderHooks = lView[TVIEW].preOrderHooks;
56+
if (preOrderHooks !== null) {
57+
executeInitAndCheckHooks(lView, preOrderHooks, InitPhaseState.OnInitHooksToBeRun, index);
58+
}
59+
}
60+
}
4561

4662
// We must set the selected index *after* running the hooks, because hooks may have side-effects
4763
// that cause other template functions to run, thus updating the selected index, which is global

packages/core/src/render3/instructions/shared.ts

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {assertFirstTemplatePass, assertLView} from '../assert';
1717
import {attachPatchData, getComponentViewByInstance} from '../context_discovery';
1818
import {diPublicInInjector, getNodeInjectable, getOrCreateNodeInjectorForNode} from '../di';
1919
import {throwMultipleComponentError} from '../errors';
20-
import {executeHooks, executePreOrderHooks, registerPreOrderHooks} from '../hooks';
20+
import {executeCheckHooks, executeInitAndCheckHooks, incrementInitPhaseFlags, registerPreOrderHooks} from '../hooks';
2121
import {ACTIVE_INDEX, CONTAINER_HEADER_OFFSET, LContainer} from '../interfaces/container';
2222
import {ComponentDef, ComponentTemplate, DirectiveDef, DirectiveDefListOrFactory, FactoryFn, PipeDefListOrFactory, RenderFlags, ViewQueriesFunction} from '../interfaces/definition';
2323
import {INJECTOR_BLOOM_PARENT_SIZE, NodeInjectorFactory} from '../interfaces/injector';
@@ -372,7 +372,8 @@ export function renderView<T>(lView: LView, tView: TView, context: T): void {
372372
export function refreshView<T>(
373373
lView: LView, tView: TView, templateFn: ComponentTemplate<{}>| null, context: T) {
374374
ngDevMode && assertEqual(isCreationMode(lView), false, 'Should be run in update mode');
375-
let oldView = enterView(lView, lView[T_HOST]);
375+
const oldView = enterView(lView, lView[T_HOST]);
376+
const flags = lView[FLAGS];
376377
try {
377378
resetPreOrderHookFlags(lView);
378379

@@ -385,8 +386,25 @@ export function refreshView<T>(
385386
lView[BINDING_INDEX] = tView.bindingStartIndex;
386387

387388
const checkNoChangesMode = getCheckNoChangesMode();
388-
389-
executePreOrderHooks(lView, tView, checkNoChangesMode, undefined);
389+
const hooksInitPhaseCompleted =
390+
(flags & LViewFlags.InitPhaseStateMask) === InitPhaseState.InitPhaseCompleted;
391+
392+
// execute pre-order hooks (OnInit, OnChanges, DoChanges)
393+
// PERF WARNING: do NOT extract this to a separate function without running benchmarks
394+
if (!checkNoChangesMode) {
395+
if (hooksInitPhaseCompleted) {
396+
const preOrderCheckHooks = tView.preOrderCheckHooks;
397+
if (preOrderCheckHooks !== null) {
398+
executeCheckHooks(lView, preOrderCheckHooks, null);
399+
}
400+
} else {
401+
const preOrderHooks = tView.preOrderHooks;
402+
if (preOrderHooks !== null) {
403+
executeInitAndCheckHooks(lView, preOrderHooks, InitPhaseState.OnInitHooksToBeRun, null);
404+
}
405+
incrementInitPhaseFlags(lView, InitPhaseState.OnInitHooksToBeRun);
406+
}
407+
}
390408

391409
refreshDynamicEmbeddedViews(lView);
392410

@@ -395,10 +413,23 @@ export function refreshView<T>(
395413
refreshContentQueries(tView, lView);
396414
}
397415

398-
resetPreOrderHookFlags(lView);
399-
executeHooks(
400-
lView, tView.contentHooks, tView.contentCheckHooks, checkNoChangesMode,
401-
InitPhaseState.AfterContentInitHooksToBeRun, undefined);
416+
// execute content hooks (AfterContentInit, AfterContentChecked)
417+
// PERF WARNING: do NOT extract this to a separate function without running benchmarks
418+
if (!checkNoChangesMode) {
419+
if (hooksInitPhaseCompleted) {
420+
const contentCheckHooks = tView.contentCheckHooks;
421+
if (contentCheckHooks !== null) {
422+
executeCheckHooks(lView, contentCheckHooks);
423+
}
424+
} else {
425+
const contentHooks = tView.contentHooks;
426+
if (contentHooks !== null) {
427+
executeInitAndCheckHooks(
428+
lView, contentHooks, InitPhaseState.AfterContentInitHooksToBeRun);
429+
}
430+
incrementInitPhaseFlags(lView, InitPhaseState.AfterContentInitHooksToBeRun);
431+
}
432+
}
402433

403434
setHostBindings(tView, lView);
404435

@@ -413,10 +444,22 @@ export function refreshView<T>(
413444
refreshChildComponents(lView, components);
414445
}
415446

416-
resetPreOrderHookFlags(lView);
417-
executeHooks(
418-
lView, tView.viewHooks, tView.viewCheckHooks, checkNoChangesMode,
419-
InitPhaseState.AfterViewInitHooksToBeRun, undefined);
447+
// execute view hooks (AfterViewInit, AfterViewChecked)
448+
// PERF WARNING: do NOT extract this to a separate function without running benchmarks
449+
if (!checkNoChangesMode) {
450+
if (hooksInitPhaseCompleted) {
451+
const viewCheckHooks = tView.viewCheckHooks;
452+
if (viewCheckHooks !== null) {
453+
executeCheckHooks(lView, viewCheckHooks);
454+
}
455+
} else {
456+
const viewHooks = tView.viewHooks;
457+
if (viewHooks !== null) {
458+
executeInitAndCheckHooks(lView, viewHooks, InitPhaseState.AfterViewInitHooksToBeRun);
459+
}
460+
incrementInitPhaseFlags(lView, InitPhaseState.AfterViewInitHooksToBeRun);
461+
}
462+
}
420463

421464
} finally {
422465
lView[FLAGS] &= ~(LViewFlags.Dirty | LViewFlags.FirstLViewPass);

packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -252,13 +252,13 @@
252252
"name": "enterView"
253253
},
254254
{
255-
"name": "executeContentQueries"
255+
"name": "executeCheckHooks"
256256
},
257257
{
258-
"name": "executeHooks"
258+
"name": "executeContentQueries"
259259
},
260260
{
261-
"name": "executePreOrderHooks"
261+
"name": "executeInitAndCheckHooks"
262262
},
263263
{
264264
"name": "executeTemplate"
@@ -416,6 +416,9 @@
416416
{
417417
"name": "incrementActiveDirectiveId"
418418
},
419+
{
420+
"name": "incrementInitPhaseFlags"
421+
},
419422
{
420423
"name": "initNodeFlags"
421424
},

packages/core/test/bundling/hello_world/bundle.golden_symbols.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,10 +210,10 @@
210210
"name": "enterView"
211211
},
212212
{
213-
"name": "executeHooks"
213+
"name": "executeCheckHooks"
214214
},
215215
{
216-
"name": "executePreOrderHooks"
216+
"name": "executeInitAndCheckHooks"
217217
},
218218
{
219219
"name": "executeTemplate"
@@ -323,6 +323,9 @@
323323
{
324324
"name": "incrementActiveDirectiveId"
325325
},
326+
{
327+
"name": "incrementInitPhaseFlags"
328+
},
326329
{
327330
"name": "initNodeFlags"
328331
},

0 commit comments

Comments
 (0)