Skip to content

Commit dd5eaef

Browse files
authored
Optimize loadTreeForQuery by filtering eagerly in ParamsBuilder (#2890)
* nit: simplify constructions of RunCaseSpecific * Optimize loadTreeForQuery by filtering eagerly in ParamsBuilder Previously, if you load a query like `webgpu:api,validation,encoding,cmds,render,draw:vertex_buffer_OOB:type="draw";VBSize="exile";IBSize="exile";VStride0=false;IStride0=true;AStride="zero";offset=1` or `webgpu:api,validation,encoding,cmds,render,draw:vertex_buffer_OOB:type="draw";VBSize="exile";IBSize="exile";*` loadTreeForQuery would iterate *all* of the cases for that test `webgpu:api,validation,encoding,cmds,render,draw:*` and filter out the wrong ones at the very end. This changes the ParamsBuilder iteration to be filter-aware, so it doesn't even start generating any of the subspaces of the combinatorial space that it can already see have a mismatch. For the case above, this improves the runtime of loadTreeForQuery from 205ms to ~2ms (100x faster!). This saves tons of time if tests are run in a way where loadTreeForQuery is called for many sub-test or single-case queries (as is done in Chromium). Bug: https://crbug.com/1470849
1 parent 548d2c1 commit dd5eaef

File tree

7 files changed

+233
-151
lines changed

7 files changed

+233
-151
lines changed

src/common/framework/params_builder.ts

Lines changed: 111 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
import { Merged, assertMergedWithoutOverlap, mergeParams } from '../internal/params_utils.js';
1+
import { Merged, mergeParams, mergeParamsChecked } from '../internal/params_utils.js';
2+
import { comparePublicParamsPaths, Ordering } from '../internal/query/compare.js';
23
import { stringifyPublicParams } from '../internal/query/stringify_params.js';
34
import { assert, mapLazy } from '../util/util.js';
45

6+
import { TestParams } from './fixture.js';
7+
58
// ================================================================
69
// "Public" ParamsBuilder API / Documentation
710
// ================================================================
@@ -102,27 +105,32 @@ export type CaseSubcaseIterable<CaseP, SubcaseP> = Iterable<
102105
* Base class for `CaseParamsBuilder` and `SubcaseParamsBuilder`.
103106
*/
104107
export abstract class ParamsBuilderBase<CaseP extends {}, SubcaseP extends {}> {
105-
protected readonly cases: () => Generator<CaseP>;
108+
protected readonly cases: (caseFilter: TestParams | null) => Generator<CaseP>;
106109

107-
constructor(cases: () => Generator<CaseP>) {
110+
constructor(cases: (caseFilter: TestParams | null) => Generator<CaseP>) {
108111
this.cases = cases;
109112
}
110113

111114
/**
112115
* Hidden from test files. Use `builderIterateCasesWithSubcases` to access this.
113116
*/
114-
protected abstract iterateCasesWithSubcases(): CaseSubcaseIterable<CaseP, SubcaseP>;
117+
protected abstract iterateCasesWithSubcases(
118+
caseFilter: TestParams | null
119+
): CaseSubcaseIterable<CaseP, SubcaseP>;
115120
}
116121

117122
/**
118123
* Calls the (normally hidden) `iterateCasesWithSubcases()` method.
119124
*/
120-
export function builderIterateCasesWithSubcases(builder: ParamsBuilderBase<{}, {}>) {
125+
export function builderIterateCasesWithSubcases(
126+
builder: ParamsBuilderBase<{}, {}>,
127+
caseFilter: TestParams | null
128+
) {
121129
interface IterableParamsBuilder {
122-
iterateCasesWithSubcases(): CaseSubcaseIterable<{}, {}>;
130+
iterateCasesWithSubcases(caseFilter: TestParams | null): CaseSubcaseIterable<{}, {}>;
123131
}
124132

125-
return ((builder as unknown) as IterableParamsBuilder).iterateCasesWithSubcases();
133+
return ((builder as unknown) as IterableParamsBuilder).iterateCasesWithSubcases(caseFilter);
126134
}
127135

128136
/**
@@ -136,31 +144,66 @@ export function builderIterateCasesWithSubcases(builder: ParamsBuilderBase<{}, {
136144
export class CaseParamsBuilder<CaseP extends {}>
137145
extends ParamsBuilderBase<CaseP, {}>
138146
implements Iterable<CaseP>, ParamsBuilder {
139-
*iterateCasesWithSubcases(): CaseSubcaseIterable<CaseP, {}> {
140-
for (const a of this.cases()) {
141-
yield [a, undefined];
147+
*iterateCasesWithSubcases(caseFilter: TestParams | null): CaseSubcaseIterable<CaseP, {}> {
148+
for (const caseP of this.cases(caseFilter)) {
149+
if (caseFilter) {
150+
// this.cases() only filters out cases which conflict with caseFilter. Now that we have
151+
// the final caseP, filter out cases which are missing keys that caseFilter requires.
152+
const ordering = comparePublicParamsPaths(caseP, caseFilter);
153+
if (ordering === Ordering.StrictSuperset || ordering === Ordering.Unordered) {
154+
continue;
155+
}
156+
}
157+
158+
yield [caseP, undefined];
142159
}
143160
}
144161

145162
[Symbol.iterator](): Iterator<CaseP> {
146-
return this.cases();
163+
return this.cases(null);
147164
}
148165

149166
/** @inheritDoc */
150167
expandWithParams<NewP extends {}>(
151-
expander: (_: Merged<{}, CaseP>) => Iterable<NewP>
168+
expander: (_: CaseP) => Iterable<NewP>
152169
): CaseParamsBuilder<Merged<CaseP, NewP>> {
153-
const newGenerator = genExpandWithParams(this.cases, expander);
154-
return new CaseParamsBuilder(() => newGenerator({}));
170+
const baseGenerator = this.cases;
171+
return new CaseParamsBuilder(function* (caseFilter) {
172+
for (const a of baseGenerator(caseFilter)) {
173+
for (const b of expander(a)) {
174+
if (caseFilter) {
175+
// If the expander generated any key-value pair that conflicts with caseFilter, skip.
176+
if (Object.entries(b).some(([k, v]) => k in caseFilter && caseFilter[k] !== v)) {
177+
continue;
178+
}
179+
}
180+
181+
yield mergeParamsChecked(a, b);
182+
}
183+
}
184+
});
155185
}
156186

157187
/** @inheritDoc */
158188
expand<NewPKey extends string, NewPValue>(
159189
key: NewPKey,
160-
expander: (_: Merged<{}, CaseP>) => Iterable<NewPValue>
190+
expander: (_: CaseP) => Iterable<NewPValue>
161191
): CaseParamsBuilder<Merged<CaseP, { [name in NewPKey]: NewPValue }>> {
162-
const newGenerator = genExpand(this.cases, key, expander);
163-
return new CaseParamsBuilder(() => newGenerator({}));
192+
const baseGenerator = this.cases;
193+
return new CaseParamsBuilder(function* (caseFilter) {
194+
for (const a of baseGenerator(caseFilter)) {
195+
assert(!(key in a), `New key '${key}' already exists in ${JSON.stringify(a)}`);
196+
197+
const caseFilterV = caseFilter?.[key];
198+
for (const v of expander(a)) {
199+
// If the expander generated a value for this key that conflicts with caseFilter, skip.
200+
if (caseFilter && (caseFilterV as {}) !== v) {
201+
continue;
202+
}
203+
yield { ...a, [key]: v } as Merged<CaseP, { [name in NewPKey]: NewPValue }>;
204+
}
205+
}
206+
});
164207
}
165208

166209
/** @inheritDoc */
@@ -189,13 +232,17 @@ export class CaseParamsBuilder<CaseP extends {}>
189232
}
190233

191234
/** @inheritDoc */
192-
filter(pred: (_: Merged<{}, CaseP>) => boolean): CaseParamsBuilder<CaseP> {
193-
const newGenerator = filterGenerator(this.cases, pred);
194-
return new CaseParamsBuilder(() => newGenerator({}));
235+
filter(pred: (_: CaseP) => boolean): CaseParamsBuilder<CaseP> {
236+
const baseGenerator = this.cases;
237+
return new CaseParamsBuilder(function* (caseFilter) {
238+
for (const a of baseGenerator(caseFilter)) {
239+
if (pred(a)) yield a;
240+
}
241+
});
195242
}
196243

197244
/** @inheritDoc */
198-
unless(pred: (_: Merged<{}, CaseP>) => boolean): CaseParamsBuilder<CaseP> {
245+
unless(pred: (_: CaseP) => boolean): CaseParamsBuilder<CaseP> {
199246
return this.filter(x => !pred(x));
200247
}
201248

@@ -205,12 +252,9 @@ export class CaseParamsBuilder<CaseP extends {}>
205252
* generate new subcases instead of new cases.
206253
*/
207254
beginSubcases(): SubcaseParamsBuilder<CaseP, {}> {
208-
return new SubcaseParamsBuilder(
209-
() => this.cases(),
210-
function* () {
211-
yield {};
212-
}
213-
);
255+
return new SubcaseParamsBuilder(this.cases, function* () {
256+
yield {};
257+
});
214258
}
215259
}
216260

@@ -235,13 +279,25 @@ export class SubcaseParamsBuilder<CaseP extends {}, SubcaseP extends {}>
235279
implements ParamsBuilder {
236280
protected readonly subcases: (_: CaseP) => Generator<SubcaseP>;
237281

238-
constructor(cases: () => Generator<CaseP>, generator: (_: CaseP) => Generator<SubcaseP>) {
282+
constructor(
283+
cases: (caseFilter: TestParams | null) => Generator<CaseP>,
284+
generator: (_: CaseP) => Generator<SubcaseP>
285+
) {
239286
super(cases);
240287
this.subcases = generator;
241288
}
242289

243-
*iterateCasesWithSubcases(): CaseSubcaseIterable<CaseP, SubcaseP> {
244-
for (const caseP of this.cases()) {
290+
*iterateCasesWithSubcases(caseFilter: TestParams | null): CaseSubcaseIterable<CaseP, SubcaseP> {
291+
for (const caseP of this.cases(caseFilter)) {
292+
if (caseFilter) {
293+
// this.cases() only filters out cases which conflict with caseFilter. Now that we have
294+
// the final caseP, filter out cases which are missing keys that caseFilter requires.
295+
const ordering = comparePublicParamsPaths(caseP, caseFilter);
296+
if (ordering === Ordering.StrictSuperset || ordering === Ordering.Unordered) {
297+
continue;
298+
}
299+
}
300+
245301
const subcases = Array.from(this.subcases(caseP));
246302
if (subcases.length) {
247303
yield [caseP, subcases];
@@ -253,15 +309,32 @@ export class SubcaseParamsBuilder<CaseP extends {}, SubcaseP extends {}>
253309
expandWithParams<NewP extends {}>(
254310
expander: (_: Merged<CaseP, SubcaseP>) => Iterable<NewP>
255311
): SubcaseParamsBuilder<CaseP, Merged<SubcaseP, NewP>> {
256-
return new SubcaseParamsBuilder(this.cases, genExpandWithParams(this.subcases, expander));
312+
const baseGenerator = this.subcases;
313+
return new SubcaseParamsBuilder(this.cases, function* (base) {
314+
for (const a of baseGenerator(base)) {
315+
for (const b of expander(mergeParams(base, a))) {
316+
yield mergeParamsChecked(a, b);
317+
}
318+
}
319+
});
257320
}
258321

259322
/** @inheritDoc */
260323
expand<NewPKey extends string, NewPValue>(
261324
key: NewPKey,
262325
expander: (_: Merged<CaseP, SubcaseP>) => Iterable<NewPValue>
263326
): SubcaseParamsBuilder<CaseP, Merged<SubcaseP, { [name in NewPKey]: NewPValue }>> {
264-
return new SubcaseParamsBuilder(this.cases, genExpand(this.subcases, key, expander));
327+
const baseGenerator = this.subcases;
328+
return new SubcaseParamsBuilder(this.cases, function* (base) {
329+
for (const a of baseGenerator(base)) {
330+
const before = mergeParams(base, a);
331+
assert(!(key in before), () => `Key '${key}' already exists in ${JSON.stringify(before)}`);
332+
333+
for (const v of expander(before)) {
334+
yield { ...a, [key]: v } as Merged<SubcaseP, { [k in NewPKey]: NewPValue }>;
335+
}
336+
}
337+
});
265338
}
266339

267340
/** @inheritDoc */
@@ -283,7 +356,12 @@ export class SubcaseParamsBuilder<CaseP extends {}, SubcaseP extends {}>
283356

284357
/** @inheritDoc */
285358
filter(pred: (_: Merged<CaseP, SubcaseP>) => boolean): SubcaseParamsBuilder<CaseP, SubcaseP> {
286-
return new SubcaseParamsBuilder(this.cases, filterGenerator(this.subcases, pred));
359+
const baseGenerator = this.subcases;
360+
return new SubcaseParamsBuilder(this.cases, function* (base) {
361+
for (const a of baseGenerator(base)) {
362+
if (pred(mergeParams(base, a))) yield a;
363+
}
364+
});
287365
}
288366

289367
/** @inheritDoc */
@@ -292,54 +370,6 @@ export class SubcaseParamsBuilder<CaseP extends {}, SubcaseP extends {}>
292370
}
293371
}
294372

295-
/** Creates a generator function for expandWithParams() methods above. */
296-
function genExpandWithParams<Base, A, B>(
297-
baseGenerator: (_: Base) => Generator<A>,
298-
expander: (_: Merged<Base, A>) => Iterable<B>
299-
): (_: Base) => Generator<Merged<A, B>> {
300-
return function* (base: Base) {
301-
for (const a of baseGenerator(base)) {
302-
for (const b of expander(mergeParams(base, a))) {
303-
const merged = mergeParams(a, b);
304-
assertMergedWithoutOverlap([a, b], merged);
305-
306-
yield merged;
307-
}
308-
}
309-
};
310-
}
311-
312-
/** Creates a generator function for expand() methods above. */
313-
function genExpand<Base, A, NewPKey extends string, NewPValue>(
314-
baseGenerator: (_: Base) => Generator<A>,
315-
key: NewPKey,
316-
expander: (_: Merged<Base, A>) => Iterable<NewPValue>
317-
): (_: Base) => Generator<Merged<A, { [k in NewPKey]: NewPValue }>> {
318-
return function* (base: Base) {
319-
for (const a of baseGenerator(base)) {
320-
const before = mergeParams(base, a);
321-
assert(!(key in before), () => `Key '${key}' already exists in ${JSON.stringify(before)}`);
322-
323-
for (const v of expander(before)) {
324-
yield { ...a, [key]: v } as Merged<A, { [k in NewPKey]: NewPValue }>;
325-
}
326-
}
327-
};
328-
}
329-
330-
function filterGenerator<Base, A>(
331-
baseGenerator: (_: Base) => Generator<A>,
332-
pred: (_: Merged<Base, A>) => boolean
333-
): (_: Base) => Generator<A> {
334-
return function* (base: Base) {
335-
for (const a of baseGenerator(base)) {
336-
if (pred(mergeParams(base, a))) {
337-
yield a;
338-
}
339-
}
340-
};
341-
}
342-
343373
/** Assert an object is not a Generator (a thing returned from a generator function). */
344374
function assertNotGenerator(x: object) {
345375
if ('constructor' in x) {

src/common/internal/params_utils.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,10 +124,15 @@ export function mergeParams<A extends {}, B extends {}>(a: A, b: B): Merged<A, B
124124
return { ...a, ...b } as Merged<A, B>;
125125
}
126126

127-
/** Asserts that the result of a mergeParams didn't have overlap. This is not extremely fast. */
128-
export function assertMergedWithoutOverlap([a, b]: [{}, {}], merged: {}): void {
127+
/**
128+
* Merges two objects into one `{ ...a, ...b }` and asserts they had no overlapping keys.
129+
* This is slower than {@link mergeParams}.
130+
*/
131+
export function mergeParamsChecked<A extends {}, B extends {}>(a: A, b: B): Merged<A, B> {
132+
const merged = mergeParams(a, b);
129133
assert(
130134
Object.keys(merged).length === Object.keys(a).length + Object.keys(b).length,
131135
() => `Duplicate key between ${JSON.stringify(a)} and ${JSON.stringify(b)}`
132136
);
137+
return merged;
133138
}

0 commit comments

Comments
 (0)