Skip to content

Commit 4399757

Browse files
authored
perf(cdk/testing): reduce change detections when running parallel actions (#21071)
Switches all of our `testing` code to use `parallel` instead of `Promise.all` which reduces the number of change detections that we'll trigger during tests. Also adds more type overloads to `parallel`, because the previous types didn't allow the `values` function to return mixed value arrays which we had in ~15 instances. I've tried to reduce the amount of code by only implementing up to 5 overloads, but we may want to expand it to 9 like `Promise.all`.
1 parent 4874320 commit 4399757

File tree

56 files changed

+397
-284
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+397
-284
lines changed

src/cdk/testing/change-detection.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,13 +110,63 @@ export async function manualChangeDetection<T>(fn: () => Promise<T>) {
110110
return batchChangeDetection(fn, false);
111111
}
112112

113+
114+
115+
/**
116+
* Resolves the given list of async values in parallel (i.e. via Promise.all) while batching change
117+
* detection over the entire operation such that change detection occurs exactly once before
118+
* resolving the values and once after.
119+
* @param values A getter for the async values to resolve in parallel with batched change detection.
120+
* @return The resolved values.
121+
*/
122+
export function parallel<T1, T2, T3, T4, T5>(
123+
values: () =>
124+
[T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>, T4 | PromiseLike<T4>,
125+
T5 | PromiseLike<T5>
126+
]): Promise<[T1, T2, T3, T4, T5]>;
127+
128+
/**
129+
* Resolves the given list of async values in parallel (i.e. via Promise.all) while batching change
130+
* detection over the entire operation such that change detection occurs exactly once before
131+
* resolving the values and once after.
132+
* @param values A getter for the async values to resolve in parallel with batched change detection.
133+
* @return The resolved values.
134+
*/
135+
export function parallel<T1, T2, T3, T4>(
136+
values: () =>
137+
[T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>, T4 | PromiseLike<T4>]):
138+
Promise<[T1, T2, T3, T4]>;
139+
113140
/**
114141
* Resolves the given list of async values in parallel (i.e. via Promise.all) while batching change
115142
* detection over the entire operation such that change detection occurs exactly once before
116143
* resolving the values and once after.
117144
* @param values A getter for the async values to resolve in parallel with batched change detection.
118145
* @return The resolved values.
119146
*/
120-
export async function parallel<T>(values: () => Iterable<T | PromiseLike<T>>) {
147+
export function parallel<T1, T2, T3>(
148+
values: () => [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>]):
149+
Promise<[T1, T2, T3]>;
150+
151+
/**
152+
* Resolves the given list of async values in parallel (i.e. via Promise.all) while batching change
153+
* detection over the entire operation such that change detection occurs exactly once before
154+
* resolving the values and once after.
155+
* @param values A getter for the async values to resolve in parallel with batched change detection.
156+
* @return The resolved values.
157+
*/
158+
export function parallel<T1, T2>(values: () => [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>]):
159+
Promise<[T1, T2]>;
160+
161+
/**
162+
* Resolves the given list of async values in parallel (i.e. via Promise.all) while batching change
163+
* detection over the entire operation such that change detection occurs exactly once before
164+
* resolving the values and once after.
165+
* @param values A getter for the async values to resolve in parallel with batched change detection.
166+
* @return The resolved values.
167+
*/
168+
export function parallel<T>(values: () => (T | PromiseLike<T>)[]): Promise<T[]>;
169+
170+
export async function parallel<T>(values: () => Iterable<T | PromiseLike<T>>): Promise<T[]> {
121171
return batchChangeDetection(() => Promise.all(values()), true);
122172
}

src/cdk/testing/harness-environment.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,12 +168,12 @@ export abstract class HarnessEnvironment<E> implements HarnessLoader, LocatorFac
168168

169169
const perElementMatches = await parallel(() => rawElements.map(async rawElement => {
170170
const testElement = this.createTestElement(rawElement);
171-
const allResultsForElement = await Promise.all(
171+
const allResultsForElement = await parallel(
172172
// For each query, get `null` if it doesn't match, or a `TestElement` or
173173
// `ComponentHarness` as appropriate if it does match. This gives us everything that
174174
// matches the current raw element, but it may contain duplicate entries (e.g.
175175
// multiple `TestElement` or multiple `ComponentHarness` of the same type).
176-
allQueries.map(query => this._getQueryResultForElement(
176+
() => allQueries.map(query => this._getQueryResultForElement(
177177
query, rawElement, testElement, skipSelectorCheck)));
178178
return _removeDuplicateQueryResults(allResultsForElement);
179179
}));

src/cdk/testing/tests/cross-environment.spec.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
ComponentHarness,
1111
ComponentHarnessConstructor,
1212
HarnessLoader,
13+
parallel,
1314
TestElement,
1415
} from '@angular/cdk/testing';
1516
import {MainComponentHarness} from './harnesses/main-component-harness';
@@ -214,7 +215,7 @@ export function crossEnvironmentSpecs(
214215
});
215216

216217
it('should load optional harness with ancestor selector restriction', async () => {
217-
const [subcomp1, subcomp2] = await Promise.all([
218+
const [subcomp1, subcomp2] = await parallel(() => [
218219
harness.optionalAncestorRestrictedSubcomponent(),
219220
harness.optionalAncestorRestrictedMissingSubcomponent()
220221
]);
@@ -224,14 +225,14 @@ export function crossEnvironmentSpecs(
224225
});
225226

226227
it('should load all harnesses with ancestor selector restriction', async () => {
227-
const [subcomps1, subcomps2] = await Promise.all([
228+
const [subcomps1, subcomps2] = await parallel(() => [
228229
harness.allAncestorRestrictedSubcomponent(),
229230
harness.allAncestorRestrictedMissingSubcomponent()
230231
]);
231232
expect(subcomps1.length).toBe(2);
232233
expect(subcomps2.length).toBe(0);
233234
const [title1, title2] =
234-
await Promise.all(subcomps1.map(async comp => (await comp.title()).text()));
235+
await parallel(() => subcomps1.map(async comp => (await comp.title()).text()));
235236
expect(title1).toBe('List of other 1');
236237
expect(title2).toBe('List of other 2');
237238
});
@@ -421,7 +422,7 @@ export function crossEnvironmentSpecs(
421422
});
422423

423424
it('should be able to set the value of a select in single selection mode', async () => {
424-
const [select, value, changeEventCounter] = await Promise.all([
425+
const [select, value, changeEventCounter] = await parallel(() => [
425426
harness.singleSelect(),
426427
harness.singleSelectValue(),
427428
harness.singleSelectChangeEventCounter()
@@ -434,7 +435,7 @@ export function crossEnvironmentSpecs(
434435
});
435436

436437
it('should be able to set the value of a select in multi-selection mode', async () => {
437-
const [select, value, changeEventCounter] = await Promise.all([
438+
const [select, value, changeEventCounter] = await parallel(() => [
438439
harness.multiSelect(),
439440
harness.multiSelectValue(),
440441
harness.multiSelectChangeEventCounter()

src/cdk/testing/tests/protractor.e2e.spec.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {HarnessLoader} from '@angular/cdk/testing';
22
import {ProtractorHarnessEnvironment} from '@angular/cdk/testing/protractor';
33
import {browser, by, element as protractorElement, ElementFinder} from 'protractor';
4+
import {parallel} from '../change-detection';
45
import {crossEnvironmentSpecs} from './cross-environment.spec';
56
import {MainComponentHarness} from './harnesses/main-component-harness';
67

@@ -70,7 +71,9 @@ describe('ProtractorHarnessEnvironment', () => {
7071
const harness = await ProtractorHarnessEnvironment.loader({queryFn: piercingQueryFn})
7172
.getHarness(MainComponentHarness);
7273
const shadows = await harness.shadows();
73-
expect(await Promise.all(shadows.map(el => el.text()))).toEqual(['Shadow 1', 'Shadow 2']);
74+
expect(await parallel(() => {
75+
return shadows.map(el => el.text());
76+
})).toEqual(['Shadow 1', 'Shadow 2']);
7477
});
7578

7679
it('should allow querying across shadow boundary', async () => {

src/cdk/testing/tests/testbed.spec.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ describe('TestbedHarnessEnvironment', () => {
115115
// promises.
116116
detectChangesSpy.calls.reset();
117117
expect(detectChangesSpy).toHaveBeenCalledTimes(0);
118-
await parallel<unknown>(() => {
118+
await parallel(() => {
119119
// Chain together our promises to ensure the before clause runs first and the after clause
120120
// runs last.
121121
const before =
@@ -153,7 +153,9 @@ describe('TestbedHarnessEnvironment', () => {
153153
fixture, MainComponentHarness, {queryFn: piercingQuerySelectorAll},
154154
);
155155
const shadows = await harness.shadows();
156-
expect(await Promise.all(shadows.map(el => el.text()))).toEqual(['Shadow 1', 'Shadow 2']);
156+
expect(await parallel(() => {
157+
return shadows.map(el => el.text());
158+
})).toEqual(['Shadow 1', 'Shadow 2']);
157159
});
158160

159161
it('should allow querying across shadow boundary', async () => {

src/material-experimental/mdc-form-field/testing/form-field-harness.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
ComponentHarnessConstructor,
1212
HarnessPredicate,
1313
HarnessQuery,
14+
parallel,
1415
TestElement
1516
} from '@angular/cdk/testing';
1617
import {FormFieldHarnessFilters} from '@angular/material/form-field/testing';
@@ -90,7 +91,7 @@ export class MatFormFieldHarness extends ComponentHarness {
9091
return this.locatorForOptional(type)();
9192
}
9293
const hostEl = await this.host();
93-
const [isInput, isSelect] = await Promise.all([
94+
const [isInput, isSelect] = await parallel(() => [
9495
hostEl.hasClass('mat-mdc-form-field-type-mat-input'),
9596
hostEl.hasClass('mat-mdc-form-field-type-mat-select'),
9697
]);
@@ -138,7 +139,7 @@ export class MatFormFieldHarness extends ComponentHarness {
138139
async getThemeColor(): Promise<'primary'|'accent'|'warn'> {
139140
const hostEl = await this.host();
140141
const [isAccent, isWarn] =
141-
await Promise.all([hostEl.hasClass('mat-accent'), hostEl.hasClass('mat-warn')]);
142+
await parallel(() => [hostEl.hasClass('mat-accent'), hostEl.hasClass('mat-warn')]);
142143
if (isAccent) {
143144
return 'accent';
144145
} else if (isWarn) {
@@ -149,12 +150,14 @@ export class MatFormFieldHarness extends ComponentHarness {
149150

150151
/** Gets error messages which are currently displayed in the form-field. */
151152
async getTextErrors(): Promise<string[]> {
152-
return Promise.all((await this._errors()).map(e => e.text()));
153+
const errors = await this._errors();
154+
return parallel(() => errors.map(e => e.text()));
153155
}
154156

155157
/** Gets hint messages which are currently displayed in the form-field. */
156158
async getTextHints(): Promise<string[]> {
157-
return Promise.all((await this._hints()).map(e => e.text()));
159+
const hints = await this._hints();
160+
return parallel(() => hints.map(e => e.text()));
158161
}
159162

160163
/**
@@ -240,7 +243,7 @@ export class MatFormFieldHarness extends ComponentHarness {
240243
// is not able to forward any control status classes. Therefore if either the
241244
// "ng-touched" or "ng-untouched" class is set, we know that it has a form control
242245
const [isTouched, isUntouched] =
243-
await Promise.all([hostEl.hasClass('ng-touched'), hostEl.hasClass('ng-untouched')]);
246+
await parallel(() => [hostEl.hasClass('ng-touched'), hostEl.hasClass('ng-untouched')]);
244247
return isTouched || isUntouched;
245248
}
246249
}

src/material-experimental/mdc-list/testing/list-harness-base.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
import {
1010
ComponentHarness,
1111
ComponentHarnessConstructor,
12-
HarnessPredicate
12+
HarnessPredicate,
13+
parallel
1314
} from '@angular/cdk/testing';
1415
import {DividerHarnessFilters, MatDividerHarness} from '@angular/material/divider/testing';
1516
import {BaseListItemHarnessFilters, SubheaderHarnessFilters} from './list-harness-filters';
@@ -53,8 +54,9 @@ export abstract class MatListHarnessBase<
5354
* @return The list of items matching the given filters, grouped into sections by subheader.
5455
*/
5556
async getItemsGroupedBySubheader(filters?: F): Promise<ListSection<C>[]> {
56-
const listSections = [];
57-
let currentSection: {items: C[], heading?: Promise<string>} = {items: []};
57+
type Section = {items: C[], heading?: Promise<string>};
58+
const listSections: Section[] = [];
59+
let currentSection: Section = {items: []};
5860
const itemsAndSubheaders =
5961
await this.getItemsWithSubheadersAndDividers({item: filters, divider: false});
6062

@@ -74,7 +76,7 @@ export abstract class MatListHarnessBase<
7476
}
7577

7678
// Concurrently wait for all sections to resolve their heading if present.
77-
return Promise.all(listSections.map(async (s) =>
79+
return parallel(() => listSections.map(async (s) =>
7880
({items: s.items, heading: await s.heading})));
7981
}
8082

src/material-experimental/mdc-list/testing/list-item-harness-base.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import {
1010
ComponentHarness,
1111
ComponentHarnessConstructor,
1212
ContentContainerComponentHarness,
13-
HarnessPredicate
13+
HarnessPredicate,
14+
parallel
1415
} from '@angular/cdk/testing';
1516
import {BaseListItemHarnessFilters, SubheaderHarnessFilters} from './list-harness-filters';
1617

@@ -74,7 +75,8 @@ export abstract class MatListItemHarnessBase
7475

7576
/** Gets the lines of text (`mat-line` elements) in this nav list item. */
7677
async getLinesText(): Promise<string[]> {
77-
return Promise.all((await this._lines()).map(l => l.text()));
78+
const lines = await this._lines();
79+
return parallel(() => lines.map(l => l.text()));
7880
}
7981

8082
/** Whether this list item has an avatar. */

src/material-experimental/mdc-list/testing/selection-list-harness.ts

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

9-
import {HarnessPredicate} from '@angular/cdk/testing';
9+
import {HarnessPredicate, parallel} from '@angular/cdk/testing';
1010
import {MatListOptionCheckboxPosition} from '@angular/material-experimental/mdc-list';
1111
import {MatListHarnessBase} from './list-harness-base';
1212
import {
@@ -46,7 +46,7 @@ export class MatSelectionListHarness extends MatListHarnessBase<
4646
*/
4747
async selectItems(...filters: ListOptionHarnessFilters[]): Promise<void> {
4848
const items = await this._getItems(filters);
49-
await Promise.all(items.map(item => item.select()));
49+
await parallel(() => items.map(item => item.select()));
5050
}
5151

5252
/**
@@ -55,15 +55,15 @@ export class MatSelectionListHarness extends MatListHarnessBase<
5555
*/
5656
async deselectItems(...filters: ListItemHarnessFilters[]): Promise<void> {
5757
const items = await this._getItems(filters);
58-
await Promise.all(items.map(item => item.deselect()));
58+
await parallel(() => items.map(item => item.deselect()));
5959
}
6060

6161
/** Gets all items matching the given list of filters. */
6262
private async _getItems(filters: ListOptionHarnessFilters[]): Promise<MatListOptionHarness[]> {
6363
if (!filters.length) {
6464
return this.getItems();
6565
}
66-
const matches = await Promise.all(
66+
const matches = await parallel(() =>
6767
filters.map(filter => this.locatorForAll(MatListOptionHarness.with(filter))()));
6868
return matches.reduce((result, current) => [...result, ...current], []);
6969
}

src/material-experimental/mdc-select/testing/select-harness.ts

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

9-
import {HarnessPredicate} from '@angular/cdk/testing';
9+
import {HarnessPredicate, parallel} from '@angular/cdk/testing';
1010
import {MatFormFieldControlHarness} from '@angular/material/form-field/testing/control';
1111
import {
1212
MatOptionHarness,
@@ -118,14 +118,15 @@ export class MatSelectHarness extends MatFormFieldControlHarness {
118118
async clickOptions(filter: OptionHarnessFilters = {}): Promise<void> {
119119
await this.open();
120120

121-
const [isMultiple, options] = await Promise.all([this.isMultiple(), this.getOptions(filter)]);
121+
const [isMultiple, options] =
122+
await parallel(() => [this.isMultiple(), this.getOptions(filter)]);
122123

123124
if (options.length === 0) {
124125
throw Error('Select does not have options matching the specified filter');
125126
}
126127

127128
if (isMultiple) {
128-
await Promise.all(options.map(option => option.click()));
129+
await parallel(() => options.map(option => option.click()));
129130
} else {
130131
await options[0].click();
131132
}

src/material-experimental/mdc-slider/testing/slider-harness.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import {coerceBooleanProperty, coerceNumberProperty} from '@angular/cdk/coercion';
10-
import {ComponentHarness, HarnessPredicate} from '@angular/cdk/testing';
10+
import {ComponentHarness, HarnessPredicate, parallel} from '@angular/cdk/testing';
1111
import {SliderHarnessFilters} from '@angular/material/slider/testing';
1212

1313
/** Harness for interacting with a MDC mat-slider in tests. */
@@ -95,7 +95,7 @@ export class MatSliderHarness extends ComponentHarness {
9595
await this.waitForTasksOutsideAngular();
9696

9797
const [sliderEl, trackContainer] =
98-
await Promise.all([this.host(), this._trackContainer()]);
98+
await parallel(() => [this.host(), this._trackContainer()]);
9999
let percentage = await this._calculatePercentage(value);
100100
const {width} = await trackContainer.getDimensions();
101101

@@ -133,7 +133,7 @@ export class MatSliderHarness extends ComponentHarness {
133133

134134
/** Calculates the percentage of the given value. */
135135
private async _calculatePercentage(value: number) {
136-
const [min, max] = await Promise.all([this.getMinValue(), this.getMaxValue()]);
136+
const [min, max] = await parallel(() => [this.getMinValue(), this.getMaxValue()]);
137137
return (value - min) / (max - min);
138138
}
139139
}

src/material-experimental/mdc-snack-bar/testing/snack-bar-harness.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import {AriaLivePoliteness} from '@angular/cdk/a11y';
10-
import {ComponentHarness, HarnessPredicate} from '@angular/cdk/testing';
10+
import {ComponentHarness, HarnessPredicate, parallel} from '@angular/cdk/testing';
1111
import {SnackBarHarnessFilters} from './snack-bar-harness-filters';
1212

1313
/** Harness for interacting with an MDC-based mat-snack-bar in tests. */
@@ -97,7 +97,7 @@ export class MatSnackBarHarness extends ComponentHarness {
9797
// element isn't in the DOM by seeing that its width and height are zero.
9898

9999
const host = await this.host();
100-
const [exit, dimensions] = await Promise.all([
100+
const [exit, dimensions] = await parallel(() => [
101101
// The snackbar container is marked with the "exit" attribute after it has been dismissed
102102
// but before the animation has finished (after which it's removed from the DOM).
103103
host.getAttribute('mat-exit'),

0 commit comments

Comments
 (0)