test(core): Add benchmark for transplanted views when insertion is dirty (#36722)

The current benchmark for transplanted views only exercises the path
when the declaration location is dirty and the insertion is not. This
test adds a benchmark for when both insertion and declaration are dirty.

PR Close #36722
This commit is contained in:
Andrew Scott 2020-04-20 10:39:09 -07:00 committed by Alex Rickabaugh
parent e30e1325f3
commit 35d61c11fd
5 changed files with 50 additions and 23 deletions

View File

@ -23,8 +23,14 @@ describe('change detection benchmark', () => {
expect(await $('#root').getText()).toEqual(''); expect(await $('#root').getText()).toEqual('');
await $('#createDom').click(); await $('#createDom').click();
expect($('#root').getText()).toContain('1'); expect($('#root').getText()).toContain('1');
await $('#markInsertionComponentForCheck').click();
await $('#detectChanges').click(); await $('#detectChanges').click();
expect($('#root').getText()).toContain('2'); // Ivy currently refreshes at *both* declaration and insertion while VE only refreshes at
// insertion. Simply assert that the view was updated at least once since the first update.
expect(Number(await $('#root').getText())).toBeGreaterThan(1);
// The button click causes change detection to trigger at the root
await $('#destroyDom').click(); await $('#destroyDom').click();
expect(await $('#root').getText()).toEqual(''); expect(await $('#root').getText()).toEqual('');
}); });

View File

@ -15,8 +15,9 @@ interface Worker {
work(): void; work(): void;
} }
const UpdateWorker: Worker = { // Used to benchmark performance when insertion tree is not dirty.
id: 'createOnly', const InsertionNotDirtyWorker: Worker = {
id: 'insertionNotDirty',
prepare: () => { prepare: () => {
$('#destroyDom').click(); $('#destroyDom').click();
$('#createDom').click(); $('#createDom').click();
@ -24,6 +25,17 @@ const UpdateWorker: Worker = {
work: () => $('#detectChanges').click() work: () => $('#detectChanges').click()
}; };
// Used to benchmark performance when both declaration and insertion trees are dirty.
const AllComponentsDirtyWorker: Worker = {
id: 'allComponentsDirty',
prepare: () => {
$('#destroyDom').click();
$('#createDom').click();
$('#markInsertionComponentForCheck').click();
},
work: () => $('#detectChanges').click()
};
// In order to make sure that we don't change the ids of the benchmarks, we need to // In order to make sure that we don't change the ids of the benchmarks, we need to
// determine the current test package name from the Bazel target. This is necessary // determine the current test package name from the Bazel target. This is necessary
@ -36,7 +48,7 @@ const testPackageName = process.env['BAZEL_TARGET']!.split(':')[0].split('/').po
describe('change detection benchmark perf', () => { describe('change detection benchmark perf', () => {
afterEach(verifyNoBrowserErrors); afterEach(verifyNoBrowserErrors);
[UpdateWorker].forEach((worker) => { [InsertionNotDirtyWorker, AllComponentsDirtyWorker].forEach((worker) => {
describe(worker.id, () => { describe(worker.id, () => {
it(`should run benchmark for ${testPackageName}`, async () => { it(`should run benchmark for ${testPackageName}`, async () => {
await runChangeDetectionBenchmark({ await runChangeDetectionBenchmark({

View File

@ -18,6 +18,7 @@
<p> <p>
<button id="destroyDom">destroyDom</button> <button id="destroyDom">destroyDom</button>
<button id="createDom">createDom</button> <button id="createDom">createDom</button>
<button id="markInsertionComponentForCheck">markInsertionComponentForCheck</button>
<button id="detectChanges">detectChanges</button> <button id="detectChanges">detectChanges</button>
<button id="detectChangesProfile">profile detectChanges</button> <button id="detectChangesProfile">profile detectChanges</button>
</p> </p>

View File

@ -19,6 +19,7 @@ export function init(moduleRef: NgModuleRef<TransplantedViewsModule>) {
bindAction('#destroyDom', destroyDom); bindAction('#destroyDom', destroyDom);
bindAction('#createDom', createDom); bindAction('#createDom', createDom);
bindAction('#markInsertionComponentForCheck', markInsertionComponentForCheck);
bindAction('#detectChanges', detectChanges); bindAction('#detectChanges', detectChanges);
bindAction('#detectChangesProfile', profile(detectChanges, noop, 'detectChanges')); bindAction('#detectChangesProfile', profile(detectChanges, noop, 'detectChanges'));
@ -35,6 +36,10 @@ export function init(moduleRef: NgModuleRef<TransplantedViewsModule>) {
appRef.tick(); appRef.tick();
} }
function markInsertionComponentForCheck() {
declaration.insertionComponent.changeDetector.markForCheck();
}
function detectChanges() { function detectChanges() {
appRef.tick(); appRef.tick();
} }

View File

@ -6,29 +6,11 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {ChangeDetectionStrategy, Component, Input, NgModule, TemplateRef} from '@angular/core'; import {ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, NgModule, TemplateRef, ViewChild} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser'; import {BrowserModule} from '@angular/platform-browser';
import {newArray} from '../util'; import {newArray} from '../util';
@Component({
selector: 'declaration-component',
template: `
<ng-template #template>{{trackTemplateRefresh()}}</ng-template>
<insertion-component [template]="template" [viewCount]="viewCount"></insertion-component>
`,
})
export class DeclarationComponent {
@Input() viewCount = 1;
// Tracks number of times the template was executed to ensure it was updated during CD.
templateRefreshCount = 0;
trackTemplateRefresh() {
this.templateRefreshCount++;
return this.templateRefreshCount;
}
}
@Component({ @Component({
selector: 'insertion-component', selector: 'insertion-component',
template: ` template: `
@ -44,12 +26,33 @@ export class InsertionComponent {
this.views = n > 0 ? newArray<any>(n) : []; this.views = n > 0 ? newArray<any>(n) : [];
} }
constructor(readonly changeDetector: ChangeDetectorRef) {}
// use trackBy to ensure profile isn't affected by the cost to refresh ngFor. // use trackBy to ensure profile isn't affected by the cost to refresh ngFor.
trackByIndex(index: number, item: any) { trackByIndex(index: number, item: any) {
return index; return index;
} }
} }
@Component({
selector: 'declaration-component',
template: `
<ng-template #template>{{trackTemplateRefresh()}}</ng-template>
<insertion-component [template]="template" [viewCount]="viewCount"></insertion-component>
`,
})
export class DeclarationComponent {
@Input() viewCount = 1;
@ViewChild(InsertionComponent) insertionComponent!: InsertionComponent;
// Tracks number of times the template was executed to ensure it was updated during CD.
templateRefreshCount = 0;
trackTemplateRefresh() {
this.templateRefreshCount++;
return this.templateRefreshCount;
}
}
@NgModule({ @NgModule({
declarations: [DeclarationComponent, InsertionComponent], declarations: [DeclarationComponent, InsertionComponent],
bootstrap: [DeclarationComponent], bootstrap: [DeclarationComponent],