Skip to content

Commit

Permalink
refactor(core): make timer-related @defer logic tree-shakable
Browse files Browse the repository at this point in the history
This commit updates `@defer` logic related to handling `after` and `minimum` parameters tree-shakable.

If `after` or `minimum` was used on a `@loading` or `@placeholder` blocks, compiler generates an extra argument for the `ɵɵdefer` instruction. This extra argument is a reference to a function that brings timer-related code.
  • Loading branch information
AndrewKushnir committed Oct 6, 2023
1 parent 22fa9fe commit 0de6145
Show file tree
Hide file tree
Showing 10 changed files with 151 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@
],
"failureMessage": "Incorrect template"
}
]
],
"skipForTemplatePipeline": true
},
{
"description": "should generate a deferred block with loading block parameters",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ MyApp.ɵcmp = /*@__PURE__*/ $r3$.ɵɵdefineComponent({
template: function MyApp_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵtemplate(0, MyApp_Defer_0_Template, 1, 0)(1, MyApp_DeferLoading_1_Template, 1, 0);
$r3$.ɵɵdefer(2, 0, null, 1, null, null, 0);
$r3$.ɵɵdefer(2, 0, null, 1, null, null, 0, null, $r3$.ɵɵdeferEnableTimerScheduling);
$r3$.ɵɵdeferOnIdle();
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ MyApp.ɵcmp = /*@__PURE__*/ $r3$.ɵɵdefineComponent({
template: function MyApp_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵtemplate(0, MyApp_Defer_0_Template, 1, 0)(1, MyApp_DeferPlaceholder_1_Template, 1, 0);
$r3$.ɵɵdefer(2, 0, null, null, 1, null, null, 0);
$r3$.ɵɵdefer(2, 0, null, null, 1, null, null, 0, $r3$.ɵɵdeferEnableTimerScheduling);
$r3$.ɵɵdeferOnIdle();
}
},
Expand Down
41 changes: 41 additions & 0 deletions packages/compiler-cli/test/ngtsc/ngtsc_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8854,6 +8854,47 @@ function allTests(os: string) {
expect(jsContents).not.toContain('import { CmpA }');
});

it('should include timer scheduler function when ' +
'`after` or `minimum` parameters are used',
() => {
env.write('cmp-a.ts', `
import { Component } from '@angular/core';
@Component({
standalone: true,
selector: 'cmp-a',
template: 'CmpA!'
})
export class CmpA {}
`);

env.write('/test.ts', `
import { Component } from '@angular/core';
import { CmpA } from './cmp-a';
@Component({
selector: 'test-cmp',
standalone: true,
imports: [CmpA],
template: \`
@defer {
<cmp-a />
} @loading (after 500ms; minimum 300ms) {
Loading...
}
\`,
})
export class TestCmp {}
`);

env.driveMain();

const jsContents = env.getContents('test.js');
expect(jsContents)
.toContain(
'ɵɵdefer(2, 0, TestCmp_Defer_2_DepsFn, 1, null, null, 0, null, i0.ɵɵdeferEnableTimerScheduling)');
});

describe('imports', () => {
it('should retain regular imports when symbol is eagerly referenced', () => {
env.write('cmp-a.ts', `
Expand Down
2 changes: 2 additions & 0 deletions packages/compiler/src/render3/r3_identifiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@ export class Identifiers {
o.ExternalReference = {name: 'ɵɵdeferPrefetchOnInteraction', moduleName: CORE};
static deferPrefetchOnViewport:
o.ExternalReference = {name: 'ɵɵdeferPrefetchOnViewport', moduleName: CORE};
static deferEnableTimerScheduling:
o.ExternalReference = {name: 'ɵɵdeferEnableTimerScheduling', moduleName: CORE};

static conditional: o.ExternalReference = {name: 'ɵɵconditional', moduleName: CORE};
static repeater: o.ExternalReference = {name: 'ɵɵrepeater', moduleName: CORE};
Expand Down
3 changes: 3 additions & 0 deletions packages/compiler/src/render3/view/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1324,6 +1324,9 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
o.literal(errorIndex),
loadingConsts?.length ? this.addToConsts(o.literalArr(loadingConsts)) : o.TYPED_NULL_EXPR,
placeholderConsts ? this.addToConsts(placeholderConsts) : o.TYPED_NULL_EXPR,
(loadingConsts?.length || placeholderConsts) ?
o.importExpr(R3.deferEnableTimerScheduling) :
o.TYPED_NULL_EXPR,
]));

this.createDeferTriggerInstructions(deferredIndex, triggers, metadata, false);
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/core_render3_private_export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ export {
ɵɵdeferPrefetchOnHover,
ɵɵdeferPrefetchOnInteraction,
ɵɵdeferPrefetchOnViewport,
ɵɵdeferEnableTimerScheduling,
ɵɵtext,
ɵɵtextInterpolate,
ɵɵtextInterpolate1,
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/render3/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ export {
ɵɵdeferPrefetchOnHover,
ɵɵdeferPrefetchOnInteraction,
ɵɵdeferPrefetchOnViewport,
ɵɵdeferEnableTimerScheduling,

ɵɵtext,
ɵɵtextInterpolate,
Expand Down
143 changes: 98 additions & 45 deletions packages/core/src/render3/instructions/defer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,38 @@ function shouldTriggerDeferBlock(injector: Injector): boolean {
return isPlatformBrowser(injector);
}

/**
* Reference to the timer-based scheduler implementation of defer block state
* rendering method. It's used to make timer-based scheduling tree-shakable.
* If `minimum` or `after` parameters are used, compiler generates an extra
* argument for the `ɵɵdefer` instruction, which references a timer-based
* implementation.
*/
let applyDeferBlockStateWithSchedulingImpl: (typeof applyDeferBlockState)|null = null;

/**
* Enables timer-related scheduling if `after` or `minimum` parameters are setup
* on the `@loading` or `@placeholder` blocks.
*/
export function ɵɵdeferEnableTimerScheduling(
tView: TView, tDetails: TDeferBlockDetails, placeholderConfigIndex?: number|null,
loadingConfigIndex?: number|null) {
const tViewConsts = tView.consts;
if (placeholderConfigIndex != null) {
tDetails.placeholderBlockConfig =
getConstant<DeferredPlaceholderBlockConfig>(tViewConsts, placeholderConfigIndex);
}
if (loadingConfigIndex != null) {
tDetails.loadingBlockConfig =
getConstant<DeferredLoadingBlockConfig>(tViewConsts, loadingConfigIndex);
}

// Enable implementation that supports timer-based scheduling.
if (applyDeferBlockStateWithSchedulingImpl === null) {
applyDeferBlockStateWithSchedulingImpl = applyDeferBlockStateWithScheduling;
}
}

/**
* Creates runtime data structures for defer blocks.
*
Expand All @@ -57,39 +89,37 @@ function shouldTriggerDeferBlock(injector: Injector): boolean {
* block.
* @param placeholderConfigIndex Index in the constants array of the configuration of the
* placeholder block.
* @param enableTimerScheduling Function that enables timer-related scheduling if `after`
* or `minimum` parameters are setup on the `@loading` or `@placeholder` blocks.
*
* @codeGenApi
*/
export function ɵɵdefer(
index: number, primaryTmplIndex: number, dependencyResolverFn?: DependencyResolverFn|null,
loadingTmplIndex?: number|null, placeholderTmplIndex?: number|null,
errorTmplIndex?: number|null, loadingConfigIndex?: number|null,
placeholderConfigIndex?: number|null) {
placeholderConfigIndex?: number|null,
enableTimerScheduling?: typeof ɵɵdeferEnableTimerScheduling) {
const lView = getLView();
const tView = getTView();
const tViewConsts = tView.consts;
const adjustedIndex = index + HEADER_OFFSET;

ɵɵtemplate(index, null, 0, 0);

if (tView.firstCreatePass) {
const deferBlockConfig: TDeferBlockDetails = {
const tDetails: TDeferBlockDetails = {
primaryTmplIndex,
loadingTmplIndex: loadingTmplIndex ?? null,
placeholderTmplIndex: placeholderTmplIndex ?? null,
errorTmplIndex: errorTmplIndex ?? null,
placeholderBlockConfig: placeholderConfigIndex != null ?
getConstant<DeferredPlaceholderBlockConfig>(tViewConsts, placeholderConfigIndex) :
null,
loadingBlockConfig: loadingConfigIndex != null ?
getConstant<DeferredLoadingBlockConfig>(tViewConsts, loadingConfigIndex) :
null,
placeholderBlockConfig: null,
loadingBlockConfig: null,
dependencyResolverFn: dependencyResolverFn ?? null,
loadingState: DeferDependenciesLoadingState.NOT_STARTED,
loadingPromise: null,
};

setTDeferBlockDetails(tView, adjustedIndex, deferBlockConfig);
enableTimerScheduling?.(tView, tDetails, placeholderConfigIndex, loadingConfigIndex);
setTDeferBlockDetails(tView, adjustedIndex, tDetails);
}

const tNode = getCurrentTNode()!;
Expand Down Expand Up @@ -656,16 +686,67 @@ export function renderDeferBlockState(
ngDevMode && assertTNodeForLView(tNode, hostLView);

const lDetails = getLDeferBlockDetails(hostLView, tNode);
const tDetails = getTDeferBlockDetails(hostTView, tNode);

ngDevMode && assertDefined(lDetails, 'Expected a defer block state defined');

const now = Date.now();
const currentState = lDetails[DEFER_BLOCK_STATE];

if (!isValidStateChange(currentState, newState) ||
!isValidStateChange(lDetails[NEXT_DEFER_BLOCK_STATE] ?? -1, newState))
return;
if (isValidStateChange(currentState, newState) &&
isValidStateChange(lDetails[NEXT_DEFER_BLOCK_STATE] ?? -1, newState)) {
const tDetails = getTDeferBlockDetails(hostTView, tNode);
const needsScheduling = getLoadingBlockAfter(tDetails) !== null ||
getMinimumDurationForState(tDetails, DeferBlockState.Loading) !== null ||
getMinimumDurationForState(tDetails, DeferBlockState.Placeholder);

if (ngDevMode && needsScheduling) {
assertDefined(
applyDeferBlockStateWithSchedulingImpl, 'Expected scheduling function to be defined');
}

const applyStateFn =
needsScheduling ? applyDeferBlockStateWithSchedulingImpl! : applyDeferBlockState;
applyStateFn(newState, lDetails, lContainer, tNode, hostLView);
}
}

/**
* Applies changes to the DOM to reflect a given state.
*/
function applyDeferBlockState(
newState: DeferBlockState, lDetails: LDeferBlockDetails, lContainer: LContainer, tNode: TNode,
hostLView: LView<unknown>) {
const stateTmplIndex = getTemplateIndexForState(newState, hostLView, tNode);

if (stateTmplIndex !== null) {
lDetails[DEFER_BLOCK_STATE] = newState;
const hostTView = hostLView[TVIEW];
const adjustedIndex = stateTmplIndex + HEADER_OFFSET;
const tNode = getTNode(hostTView, adjustedIndex) as TContainerNode;

// There is only 1 view that can be present in an LContainer that
// represents a defer block, so always refer to the first one.
const viewIndex = 0;

removeLViewFromLContainer(lContainer, viewIndex);
const dehydratedView = findMatchingDehydratedView(lContainer, tNode.tView!.ssrId);
const embeddedLView = createAndRenderEmbeddedLView(hostLView, tNode, null, {dehydratedView});
addLViewToLContainer(
lContainer, embeddedLView, viewIndex, shouldAddViewToDom(tNode, dehydratedView));
}
}

/**
* Extends the `applyDeferBlockState` with timer-based scheduling.
* This function becomes available on a page if there are defer blocks
* that use `after` or `minimum` parameters in the `@loading` or
* `@placeholder` blocks.
*/
function applyDeferBlockStateWithScheduling(
newState: DeferBlockState, lDetails: LDeferBlockDetails, lContainer: LContainer, tNode: TNode,
hostLView: LView<unknown>) {
const now = Date.now();
const hostTView = hostLView[TVIEW];
const tDetails = getTDeferBlockDetails(hostTView, tNode);

if (lDetails[STATE_IS_FROZEN_UNTIL] === null || lDetails[STATE_IS_FROZEN_UNTIL] <= now) {
lDetails[STATE_IS_FROZEN_UNTIL] = null;
Expand All @@ -689,7 +770,7 @@ export function renderDeferBlockState(
lDetails[NEXT_DEFER_BLOCK_STATE] = null;
}

applyDeferBlockStateToDom(newState, lDetails, lContainer, hostLView, tNode);
applyDeferBlockState(newState, lDetails, lContainer, tNode, hostLView);

const duration = getMinimumDurationForState(tDetails, newState);
if (duration !== null) {
Expand Down Expand Up @@ -719,8 +800,6 @@ function scheduleDeferBlockUpdate(
renderDeferBlockState(nextState, tNode, lContainer);
}
};
// TODO: this needs refactoring to make `TimerScheduler` that is used inside
// of the `scheduleTimerTrigger` function tree-shakable.
return scheduleTimerTrigger(timeout, callback, hostLView, true);
}

Expand All @@ -738,32 +817,6 @@ function isValidStateChange(
return currentState < newState;
}

/**
* Applies changes to the DOM to reflect a given state.
*/
function applyDeferBlockStateToDom(
newState: DeferBlockState, lDetails: LDeferBlockDetails, lContainer: LContainer,
hostLView: LView<unknown>, tNode: TNode) {
const stateTmplIndex = getTemplateIndexForState(newState, hostLView, tNode);

if (stateTmplIndex !== null) {
lDetails[DEFER_BLOCK_STATE] = newState;
const hostTView = hostLView[TVIEW];
const adjustedIndex = stateTmplIndex + HEADER_OFFSET;
const tNode = getTNode(hostTView, adjustedIndex) as TContainerNode;

// There is only 1 view that can be present in an LContainer that
// represents a defer block, so always refer to the first one.
const viewIndex = 0;

removeLViewFromLContainer(lContainer, viewIndex);
const dehydratedView = findMatchingDehydratedView(lContainer, tNode.tView!.ssrId);
const embeddedLView = createAndRenderEmbeddedLView(hostLView, tNode, null, {dehydratedView});
addLViewToLContainer(
lContainer, embeddedLView, viewIndex, shouldAddViewToDom(tNode, dehydratedView));
}
}

/**
* Trigger prefetching of dependencies for a defer block.
*
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/render3/jit/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ export const angularCoreEnv: {[name: string]: Function} =
'ɵɵdeferPrefetchOnHover': r3.ɵɵdeferPrefetchOnHover,
'ɵɵdeferPrefetchOnInteraction': r3.ɵɵdeferPrefetchOnInteraction,
'ɵɵdeferPrefetchOnViewport': r3.ɵɵdeferPrefetchOnViewport,
'ɵɵdeferEnableTimerScheduling': r3.ɵɵdeferEnableTimerScheduling,
'ɵɵrepeater': r3.ɵɵrepeater,
'ɵɵrepeaterCreate': r3.ɵɵrepeaterCreate,
'ɵɵrepeaterTrackByIndex': r3.ɵɵrepeaterTrackByIndex,
Expand Down

0 comments on commit 0de6145

Please sign in to comment.