@pixel-point/toolcraft 0.0.7 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/templates/runtime/contracts/component-contracts.test.ts +84 -12
- package/templates/runtime/contracts/component-contracts.ts +40 -17
- package/templates/runtime/contracts/decision-contracts.ts +1 -1
- package/templates/runtime/export/export.test.ts +31 -0
- package/templates/runtime/export/export.ts +41 -0
- package/templates/runtime/react/canvas-shell.test.tsx +77 -1
- package/templates/runtime/react/canvas-shell.tsx +178 -23
- package/templates/runtime/react/control-conditions.ts +166 -0
- package/templates/runtime/react/controls-panel-filedrop-reorder.test.tsx +176 -0
- package/templates/runtime/react/controls-panel.test.tsx +404 -8
- package/templates/runtime/react/controls-panel.tsx +90 -4
- package/templates/runtime/react/media-file.ts +19 -0
- package/templates/runtime/schema/define-toolcraft.test.ts +1 -0
- package/templates/runtime/schema/define-toolcraft.ts +4 -2
- package/templates/runtime/schema/types.ts +4 -0
- package/templates/runtime/state/reducer.test.ts +304 -0
- package/templates/runtime/state/reducer.ts +148 -9
- package/templates/runtime/state/types.ts +10 -2
- package/templates/runtime/testing/performance.test.ts +1282 -48
- package/templates/runtime/testing/performance.ts +676 -39
- package/templates/starter/AGENTS.md +6 -5
- package/templates/starter/docs/toolcraft/acceptance-testing.md +11 -5
- package/templates/starter/docs/toolcraft/assembly-workflow.md +4 -2
- package/templates/starter/docs/toolcraft/component-rules.md +16 -7
- package/templates/starter/docs/toolcraft/custom-controls.md +8 -4
- package/templates/starter/docs/toolcraft/performance.md +47 -5
- package/templates/starter/docs/toolcraft/renderer-technique.md +4 -0
- package/templates/starter/docs/toolcraft/schema-reference.md +6 -6
- package/templates/starter/docs/toolcraft/workflow.md +2 -2
- package/templates/starter/e2e/app-performance.spec.ts +136 -3
- package/templates/starter/e2e/performance-helpers.ts +197 -0
- package/templates/starter/package.json +3 -0
- package/templates/starter/src/app/starter-acceptance.test.ts +529 -19
- package/templates/starter/src/app/starter-acceptance.ts +269 -12
- package/templates/starter/src/app/starter-performance.test.ts +66 -5
- package/templates/ui/components/controls/actions/actions-control.tsx +3 -5
- package/templates/ui/components/controls/file-drop/file-drop-control.tsx +340 -44
- package/templates/ui/components/controls/file-drop/index.ts +1 -1
- package/templates/ui/components/controls/index.ts +1 -0
- package/templates/ui/components/panel/panel-section.tsx +53 -1
|
@@ -9,8 +9,11 @@ export type ToolcraftPerformanceInteraction =
|
|
|
9
9
|
| "control-change"
|
|
10
10
|
| "control-drag"
|
|
11
11
|
| "export-copy"
|
|
12
|
+
| "mask-drag"
|
|
12
13
|
| "media-import"
|
|
13
14
|
| "preview-render"
|
|
15
|
+
| "timeline-playback"
|
|
16
|
+
| "timeline-scrub"
|
|
14
17
|
| "viewport-zoom-stress"
|
|
15
18
|
| "viewport-stability";
|
|
16
19
|
|
|
@@ -36,7 +39,7 @@ export type ToolcraftPerformanceValueSet = {
|
|
|
36
39
|
min?: unknown;
|
|
37
40
|
};
|
|
38
41
|
|
|
39
|
-
export type
|
|
42
|
+
export type ToolcraftPerformanceFixtureKind =
|
|
40
43
|
| "custom"
|
|
41
44
|
| "high-density"
|
|
42
45
|
| "large-canvas"
|
|
@@ -45,8 +48,8 @@ export type ToolcraftPerformanceStressFixtureKind =
|
|
|
45
48
|
| "max-value"
|
|
46
49
|
| "media";
|
|
47
50
|
|
|
48
|
-
export type
|
|
49
|
-
kind:
|
|
51
|
+
export type ToolcraftPerformanceFixture = {
|
|
52
|
+
kind: ToolcraftPerformanceFixtureKind;
|
|
50
53
|
minChars?: number;
|
|
51
54
|
minCount?: number;
|
|
52
55
|
minLines?: number;
|
|
@@ -54,6 +57,10 @@ export type ToolcraftPerformanceStressFixture = {
|
|
|
54
57
|
value?: unknown;
|
|
55
58
|
};
|
|
56
59
|
|
|
60
|
+
export type ToolcraftPerformanceStressFixtureKind = ToolcraftPerformanceFixtureKind;
|
|
61
|
+
export type ToolcraftPerformanceStressFixture = ToolcraftPerformanceFixture;
|
|
62
|
+
export type ToolcraftPerformanceWorkloadFixture = ToolcraftPerformanceFixture;
|
|
63
|
+
|
|
57
64
|
export type ToolcraftPerformanceScenario = ToolcraftPerformanceCoverage & {
|
|
58
65
|
budget: ToolcraftPerformanceBudget;
|
|
59
66
|
controlLabel?: string;
|
|
@@ -64,6 +71,7 @@ export type ToolcraftPerformanceScenario = ToolcraftPerformanceCoverage & {
|
|
|
64
71
|
target?: string;
|
|
65
72
|
stress?: boolean;
|
|
66
73
|
stressFixture?: ToolcraftPerformanceStressFixture;
|
|
74
|
+
workloadFixture?: ToolcraftPerformanceWorkloadFixture;
|
|
67
75
|
uiSelector?: string;
|
|
68
76
|
values?: ToolcraftPerformanceValueSet;
|
|
69
77
|
workload: boolean;
|
|
@@ -170,7 +178,70 @@ export type ToolcraftRendererTechnique = {
|
|
|
170
178
|
whyNotAlternativeStrategies: readonly string[];
|
|
171
179
|
};
|
|
172
180
|
|
|
181
|
+
export type ToolcraftRenderPassKind =
|
|
182
|
+
| "decode"
|
|
183
|
+
| "preprocess"
|
|
184
|
+
| "pixel-transform"
|
|
185
|
+
| "vector-build"
|
|
186
|
+
| "text-layout"
|
|
187
|
+
| "rasterize"
|
|
188
|
+
| "composite"
|
|
189
|
+
| "handles"
|
|
190
|
+
| "export";
|
|
191
|
+
|
|
192
|
+
export type ToolcraftRenderPassRunLocation =
|
|
193
|
+
| "main"
|
|
194
|
+
| "worker"
|
|
195
|
+
| "gpu"
|
|
196
|
+
| "worker-or-gpu"
|
|
197
|
+
| "export-only";
|
|
198
|
+
|
|
199
|
+
export type ToolcraftRenderPassOutput =
|
|
200
|
+
| "source"
|
|
201
|
+
| "intermediate"
|
|
202
|
+
| "preview"
|
|
203
|
+
| "overlay"
|
|
204
|
+
| "export";
|
|
205
|
+
|
|
206
|
+
export type ToolcraftRenderPassQuality = "preview" | "full" | "retina" | "export";
|
|
207
|
+
|
|
208
|
+
export type ToolcraftRenderPass = {
|
|
209
|
+
cacheKey?: readonly string[];
|
|
210
|
+
id: string;
|
|
211
|
+
inputs: readonly string[];
|
|
212
|
+
invalidatedBy: readonly string[];
|
|
213
|
+
kind: ToolcraftRenderPassKind;
|
|
214
|
+
output: ToolcraftRenderPassOutput;
|
|
215
|
+
quality: ToolcraftRenderPassQuality;
|
|
216
|
+
runsOn: ToolcraftRenderPassRunLocation;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
export type ToolcraftPipelineInteraction =
|
|
220
|
+
| "animation-frame"
|
|
221
|
+
| "control-change"
|
|
222
|
+
| "control-drag"
|
|
223
|
+
| "media-import"
|
|
224
|
+
| "mask-drag"
|
|
225
|
+
| "viewport-drag"
|
|
226
|
+
| "viewport-zoom"
|
|
227
|
+
| "timeline-playback"
|
|
228
|
+
| "timeline-scrub"
|
|
229
|
+
| "export";
|
|
230
|
+
|
|
231
|
+
export type ToolcraftInteractionInvalidation = {
|
|
232
|
+
interaction: ToolcraftPipelineInteraction;
|
|
233
|
+
invalidates: readonly string[];
|
|
234
|
+
mustNotInvalidate?: readonly string[];
|
|
235
|
+
targets: readonly string[];
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
export type ToolcraftRendererPipeline = {
|
|
239
|
+
interactionInvalidation: readonly ToolcraftInteractionInvalidation[];
|
|
240
|
+
passes: readonly ToolcraftRenderPass[];
|
|
241
|
+
};
|
|
242
|
+
|
|
173
243
|
export type ToolcraftPerformanceConfig = {
|
|
244
|
+
rendererPipeline?: ToolcraftRendererPipeline;
|
|
174
245
|
rendererStrategy: ToolcraftRendererStrategy;
|
|
175
246
|
rendererTechnique?: ToolcraftRendererTechnique;
|
|
176
247
|
rendererWorkload: ToolcraftRendererWorkload;
|
|
@@ -213,8 +284,10 @@ const heavyTextInputPattern = /code|css|instruction|json|prompt|script|shader|te
|
|
|
213
284
|
|
|
214
285
|
const largeTextStressMinChars = 50_000;
|
|
215
286
|
const largeTextStressMinLines = 1_000;
|
|
287
|
+
const mediaStressMinArea = 1920 * 1080;
|
|
288
|
+
const mediaStressMinLongEdge = 1920;
|
|
216
289
|
|
|
217
|
-
const
|
|
290
|
+
const performanceFixtureKinds = new Set<ToolcraftPerformanceFixtureKind>([
|
|
218
291
|
"custom",
|
|
219
292
|
"high-density",
|
|
220
293
|
"large-canvas",
|
|
@@ -224,6 +297,79 @@ const stressFixtureKinds = new Set<ToolcraftPerformanceStressFixtureKind>([
|
|
|
224
297
|
"media",
|
|
225
298
|
]);
|
|
226
299
|
|
|
300
|
+
const renderPassKinds = new Set<ToolcraftRenderPassKind>([
|
|
301
|
+
"decode",
|
|
302
|
+
"preprocess",
|
|
303
|
+
"pixel-transform",
|
|
304
|
+
"vector-build",
|
|
305
|
+
"text-layout",
|
|
306
|
+
"rasterize",
|
|
307
|
+
"composite",
|
|
308
|
+
"handles",
|
|
309
|
+
"export",
|
|
310
|
+
]);
|
|
311
|
+
|
|
312
|
+
const renderPassRunLocations = new Set<ToolcraftRenderPassRunLocation>([
|
|
313
|
+
"main",
|
|
314
|
+
"worker",
|
|
315
|
+
"gpu",
|
|
316
|
+
"worker-or-gpu",
|
|
317
|
+
"export-only",
|
|
318
|
+
]);
|
|
319
|
+
|
|
320
|
+
const renderPassOutputs = new Set<ToolcraftRenderPassOutput>([
|
|
321
|
+
"source",
|
|
322
|
+
"intermediate",
|
|
323
|
+
"preview",
|
|
324
|
+
"overlay",
|
|
325
|
+
"export",
|
|
326
|
+
]);
|
|
327
|
+
|
|
328
|
+
const renderPassQualities = new Set<ToolcraftRenderPassQuality>([
|
|
329
|
+
"preview",
|
|
330
|
+
"full",
|
|
331
|
+
"retina",
|
|
332
|
+
"export",
|
|
333
|
+
]);
|
|
334
|
+
|
|
335
|
+
const pipelineInteractions = new Set<ToolcraftPipelineInteraction>([
|
|
336
|
+
"animation-frame",
|
|
337
|
+
"control-change",
|
|
338
|
+
"control-drag",
|
|
339
|
+
"media-import",
|
|
340
|
+
"mask-drag",
|
|
341
|
+
"viewport-drag",
|
|
342
|
+
"viewport-zoom",
|
|
343
|
+
"timeline-playback",
|
|
344
|
+
"timeline-scrub",
|
|
345
|
+
"export",
|
|
346
|
+
]);
|
|
347
|
+
|
|
348
|
+
const expensiveRenderPassKinds = new Set<ToolcraftRenderPassKind>([
|
|
349
|
+
"decode",
|
|
350
|
+
"preprocess",
|
|
351
|
+
"pixel-transform",
|
|
352
|
+
"text-layout",
|
|
353
|
+
"rasterize",
|
|
354
|
+
]);
|
|
355
|
+
|
|
356
|
+
const cacheRequiredRenderPassKinds = new Set<ToolcraftRenderPassKind>([
|
|
357
|
+
...expensiveRenderPassKinds,
|
|
358
|
+
"composite",
|
|
359
|
+
]);
|
|
360
|
+
|
|
361
|
+
const highFrequencyViewportInteractions = new Set<ToolcraftPipelineInteraction>([
|
|
362
|
+
"animation-frame",
|
|
363
|
+
"mask-drag",
|
|
364
|
+
"timeline-playback",
|
|
365
|
+
"timeline-scrub",
|
|
366
|
+
"viewport-drag",
|
|
367
|
+
"viewport-zoom",
|
|
368
|
+
]);
|
|
369
|
+
|
|
370
|
+
const vaguePipelineReferencePattern =
|
|
371
|
+
/^(?:all|all values|everything|props|runtime|settings|state|values)$/i;
|
|
372
|
+
|
|
227
373
|
function getControlSemanticText(control: ToolcraftControlSchema): string {
|
|
228
374
|
return [
|
|
229
375
|
control.target,
|
|
@@ -309,12 +455,33 @@ function hasMinDefaultMax(values: ToolcraftPerformanceScenario["values"]): boole
|
|
|
309
455
|
return values !== undefined && "default" in values && "min" in values && "max" in values;
|
|
310
456
|
}
|
|
311
457
|
|
|
312
|
-
function
|
|
313
|
-
fixture:
|
|
314
|
-
): fixture is
|
|
458
|
+
function hasPerformanceFixtureValue(
|
|
459
|
+
fixture: ToolcraftPerformanceFixture,
|
|
460
|
+
): fixture is ToolcraftPerformanceFixture & { value: unknown } {
|
|
315
461
|
return Object.prototype.hasOwnProperty.call(fixture, "value");
|
|
316
462
|
}
|
|
317
463
|
|
|
464
|
+
function isPerformanceFixtureObjectValue(value: unknown): value is Record<string, unknown> {
|
|
465
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function getMediaStressFixtureDimensions(
|
|
469
|
+
value: unknown,
|
|
470
|
+
): { height: number; width: number } | null {
|
|
471
|
+
if (!isPerformanceFixtureObjectValue(value)) {
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const width = Number(value.width);
|
|
476
|
+
const height = Number(value.height);
|
|
477
|
+
|
|
478
|
+
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return { height, width };
|
|
483
|
+
}
|
|
484
|
+
|
|
318
485
|
function getTextLineCount(value: string): number {
|
|
319
486
|
if (value.length === 0) {
|
|
320
487
|
return 0;
|
|
@@ -340,50 +507,79 @@ function isLargeTextWorkloadControl(
|
|
|
340
507
|
);
|
|
341
508
|
}
|
|
342
509
|
|
|
343
|
-
function
|
|
344
|
-
|
|
345
|
-
|
|
510
|
+
function getPerformanceFixtureShapeErrors(
|
|
511
|
+
scenarioId: string,
|
|
512
|
+
fieldName: "stressFixture" | "workloadFixture",
|
|
513
|
+
fixture: ToolcraftPerformanceFixture,
|
|
514
|
+
options: {
|
|
515
|
+
requireLargeText?: boolean;
|
|
516
|
+
requireMedia?: boolean;
|
|
517
|
+
} = {},
|
|
346
518
|
): string[] {
|
|
347
|
-
if (!scenario.workload) {
|
|
348
|
-
return [];
|
|
349
|
-
}
|
|
350
|
-
|
|
351
519
|
const errors: string[] = [];
|
|
352
|
-
const fixture = scenario.stressFixture;
|
|
353
520
|
|
|
354
|
-
if (!fixture) {
|
|
355
|
-
|
|
356
|
-
`${scenario.id} workload scenario must declare stressFixture with the real heaviest value used by browser performance tests.`,
|
|
357
|
-
];
|
|
521
|
+
if (!performanceFixtureKinds.has(fixture.kind)) {
|
|
522
|
+
errors.push(`${scenarioId} ${fieldName}.kind "${fixture.kind}" is not supported.`);
|
|
358
523
|
}
|
|
359
524
|
|
|
360
|
-
if (
|
|
361
|
-
|
|
525
|
+
if (options.requireMedia) {
|
|
526
|
+
if (fixture.kind !== "media") {
|
|
527
|
+
errors.push(
|
|
528
|
+
`${scenarioId} media-import scenario must use ${fieldName}.kind "media" with a realistic uploaded source size.`,
|
|
529
|
+
);
|
|
530
|
+
}
|
|
362
531
|
}
|
|
363
532
|
|
|
364
533
|
if (!fixture.reason.trim()) {
|
|
365
534
|
errors.push(
|
|
366
|
-
`${
|
|
535
|
+
`${scenarioId} ${fieldName} must explain why this is the heaviest useful fixture.`,
|
|
367
536
|
);
|
|
368
537
|
}
|
|
369
538
|
|
|
370
|
-
if (
|
|
371
|
-
errors.push(
|
|
539
|
+
if (!hasPerformanceFixtureValue(fixture)) {
|
|
540
|
+
errors.push(
|
|
541
|
+
`${scenarioId} ${fieldName} must include value so browser tests can apply the exact heavy state.`,
|
|
542
|
+
);
|
|
543
|
+
} else if (fixture.kind === "custom") {
|
|
544
|
+
if (!isPerformanceFixtureObjectValue(fixture.value)) {
|
|
545
|
+
errors.push(
|
|
546
|
+
`${scenarioId} custom ${fieldName}.value must be an object with one key per heavy state part so browser tests can apply every key.`,
|
|
547
|
+
);
|
|
548
|
+
} else if (Object.keys(fixture.value).length === 0) {
|
|
549
|
+
errors.push(
|
|
550
|
+
`${scenarioId} custom ${fieldName}.value must include at least one heavy state key.`,
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
} else if (fixture.kind === "media") {
|
|
554
|
+
const dimensions = getMediaStressFixtureDimensions(fixture.value);
|
|
555
|
+
|
|
556
|
+
if (!dimensions) {
|
|
557
|
+
errors.push(
|
|
558
|
+
`${scenarioId} media ${fieldName}.value must be an object with numeric width and height so browser tests can generate or load a realistic source image.`,
|
|
559
|
+
);
|
|
560
|
+
} else if (
|
|
561
|
+
Math.max(dimensions.width, dimensions.height) < mediaStressMinLongEdge ||
|
|
562
|
+
dimensions.width * dimensions.height < mediaStressMinArea
|
|
563
|
+
) {
|
|
564
|
+
errors.push(
|
|
565
|
+
`${scenarioId} media ${fieldName}.value must be at least 1920x1080-equivalent; received ${dimensions.width}x${dimensions.height}.`,
|
|
566
|
+
);
|
|
567
|
+
}
|
|
372
568
|
}
|
|
373
569
|
|
|
374
|
-
if (!
|
|
570
|
+
if (!options.requireLargeText && fixture.kind !== "large-text") {
|
|
375
571
|
return errors;
|
|
376
572
|
}
|
|
377
573
|
|
|
378
574
|
if (fixture.kind !== "large-text") {
|
|
379
575
|
errors.push(
|
|
380
|
-
`${
|
|
576
|
+
`${scenarioId} text workload scenario must use ${fieldName}.kind "large-text" with a real long text value.`,
|
|
381
577
|
);
|
|
382
578
|
return errors;
|
|
383
579
|
}
|
|
384
580
|
|
|
385
581
|
if (typeof fixture.value !== "string") {
|
|
386
|
-
errors.push(`${
|
|
582
|
+
errors.push(`${scenarioId} large-text ${fieldName}.value must be a string.`);
|
|
387
583
|
return errors;
|
|
388
584
|
}
|
|
389
585
|
|
|
@@ -392,31 +588,148 @@ function getStressFixtureErrors(
|
|
|
392
588
|
|
|
393
589
|
if (minChars < largeTextStressMinChars) {
|
|
394
590
|
errors.push(
|
|
395
|
-
`${
|
|
591
|
+
`${scenarioId} large-text ${fieldName}.minChars must be >= ${largeTextStressMinChars}.`,
|
|
396
592
|
);
|
|
397
593
|
}
|
|
398
594
|
|
|
399
595
|
if (minLines < largeTextStressMinLines) {
|
|
400
596
|
errors.push(
|
|
401
|
-
`${
|
|
597
|
+
`${scenarioId} large-text ${fieldName}.minLines must be >= ${largeTextStressMinLines}.`,
|
|
402
598
|
);
|
|
403
599
|
}
|
|
404
600
|
|
|
405
601
|
if (fixture.value.length < minChars) {
|
|
406
602
|
errors.push(
|
|
407
|
-
`${
|
|
603
|
+
`${scenarioId} large-text ${fieldName}.value must contain at least ${minChars} characters.`,
|
|
408
604
|
);
|
|
409
605
|
}
|
|
410
606
|
|
|
411
607
|
if (getTextLineCount(fixture.value) < minLines) {
|
|
412
608
|
errors.push(
|
|
413
|
-
`${
|
|
609
|
+
`${scenarioId} large-text ${fieldName}.value must contain at least ${minLines} lines.`,
|
|
414
610
|
);
|
|
415
611
|
}
|
|
416
612
|
|
|
417
613
|
return errors;
|
|
418
614
|
}
|
|
419
615
|
|
|
616
|
+
function getStressFixtureErrors(
|
|
617
|
+
scenario: ToolcraftPerformanceScenario,
|
|
618
|
+
control: ToolcraftControlSchema | undefined,
|
|
619
|
+
): string[] {
|
|
620
|
+
const isMediaImport = scenario.interaction === "media-import";
|
|
621
|
+
|
|
622
|
+
if (!scenario.workload && scenario.stress !== true && !scenario.stressFixture && !isMediaImport) {
|
|
623
|
+
return [];
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const fixture = scenario.stressFixture;
|
|
627
|
+
|
|
628
|
+
if (!fixture) {
|
|
629
|
+
if (isMediaImport) {
|
|
630
|
+
return [
|
|
631
|
+
`${scenario.id} media-import scenario must declare stressFixture.kind "media" with a realistic uploaded source size.`,
|
|
632
|
+
];
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
return scenario.workload
|
|
636
|
+
? [
|
|
637
|
+
`${scenario.id} workload scenario must declare stressFixture with the real heaviest value used by browser performance tests.`,
|
|
638
|
+
]
|
|
639
|
+
: [
|
|
640
|
+
`${scenario.id} stress scenario must declare stressFixture with the real heaviest state used by browser performance tests.`,
|
|
641
|
+
];
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const errors: string[] = [];
|
|
645
|
+
|
|
646
|
+
if (isMediaImport && !scenario.workload) {
|
|
647
|
+
errors.push(
|
|
648
|
+
`${scenario.id} media-import scenario must set workload true because decoded source size changes renderer workload.`,
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
errors.push(
|
|
653
|
+
...getPerformanceFixtureShapeErrors(scenario.id, "stressFixture", fixture, {
|
|
654
|
+
requireLargeText: isLargeTextWorkloadControl(control),
|
|
655
|
+
requireMedia: isMediaImport,
|
|
656
|
+
}),
|
|
657
|
+
);
|
|
658
|
+
|
|
659
|
+
return errors;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function stressFixtureAlreadyDefinesIndependentWorkload(
|
|
663
|
+
scenario: ToolcraftPerformanceScenario,
|
|
664
|
+
control: ToolcraftControlSchema | undefined,
|
|
665
|
+
): boolean {
|
|
666
|
+
return (
|
|
667
|
+
scenario.stressFixture?.kind === "large-text" &&
|
|
668
|
+
isLargeTextWorkloadControl(control)
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function isWorkloadBaselineSensitiveScenario(
|
|
673
|
+
config: ToolcraftPerformanceConfig,
|
|
674
|
+
scenario: ToolcraftPerformanceScenario,
|
|
675
|
+
control: ToolcraftControlSchema | undefined,
|
|
676
|
+
): boolean {
|
|
677
|
+
if (
|
|
678
|
+
!scenario.workload ||
|
|
679
|
+
(scenario.interaction !== "control-change" && scenario.interaction !== "control-drag")
|
|
680
|
+
) {
|
|
681
|
+
return false;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
if (!config.usesCustomRenderer) {
|
|
685
|
+
return false;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
if (config.rendererWorkload === "none" || config.rendererWorkload === "simple-composition") {
|
|
689
|
+
return false;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
return !stressFixtureAlreadyDefinesIndependentWorkload(scenario, control);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function getWorkloadFixtureErrors(
|
|
696
|
+
config: ToolcraftPerformanceConfig,
|
|
697
|
+
scenario: ToolcraftPerformanceScenario,
|
|
698
|
+
control: ToolcraftControlSchema | undefined,
|
|
699
|
+
): string[] {
|
|
700
|
+
const requiresBaseline = isWorkloadBaselineSensitiveScenario(config, scenario, control);
|
|
701
|
+
const errors: string[] = [];
|
|
702
|
+
|
|
703
|
+
if (scenario.workloadFixture && !scenario.stressFixture) {
|
|
704
|
+
errors.push(
|
|
705
|
+
`${scenario.id} workloadFixture must be paired with stressFixture so tests apply a heavy baseline and then the measured scenario value.`,
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
if (!requiresBaseline && !scenario.workloadFixture) {
|
|
710
|
+
return errors;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const fixture = scenario.workloadFixture;
|
|
714
|
+
|
|
715
|
+
if (!fixture) {
|
|
716
|
+
errors.push(
|
|
717
|
+
`${scenario.id} workload control scenario must declare workloadFixture for the app's heavy baseline state; stressFixture covers the control value only.`,
|
|
718
|
+
);
|
|
719
|
+
return errors;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
errors.push(
|
|
723
|
+
...getPerformanceFixtureShapeErrors(
|
|
724
|
+
scenario.id,
|
|
725
|
+
"workloadFixture",
|
|
726
|
+
fixture,
|
|
727
|
+
),
|
|
728
|
+
);
|
|
729
|
+
|
|
730
|
+
return errors;
|
|
731
|
+
}
|
|
732
|
+
|
|
420
733
|
function hasPositiveBudgetField(
|
|
421
734
|
budget: ToolcraftPerformanceBudget,
|
|
422
735
|
field: keyof ToolcraftPerformanceBudget,
|
|
@@ -438,6 +751,7 @@ function getMissingInteractionBudgetFields(
|
|
|
438
751
|
? ["maxFrameGapMs"]
|
|
439
752
|
: ["maxFrameGapMs", "maxLongTaskMs"];
|
|
440
753
|
case "animation-viewport-drag":
|
|
754
|
+
case "mask-drag":
|
|
441
755
|
case "viewport-zoom-stress":
|
|
442
756
|
return (["maxInteractionMs", "maxFrameGapMs", "maxLongTaskMs"] as const).filter(
|
|
443
757
|
(field) => !hasPositiveBudgetField(scenario.budget, field),
|
|
@@ -459,6 +773,14 @@ function getMissingInteractionBudgetFields(
|
|
|
459
773
|
hasPositiveBudgetField(scenario.budget, "maxRenderMs")
|
|
460
774
|
? []
|
|
461
775
|
: ["maxPreviewMs or maxRenderMs"];
|
|
776
|
+
case "timeline-playback":
|
|
777
|
+
return (["maxFrameGapMs", "maxLongTaskMs"] as const).filter(
|
|
778
|
+
(field) => !hasPositiveBudgetField(scenario.budget, field),
|
|
779
|
+
);
|
|
780
|
+
case "timeline-scrub":
|
|
781
|
+
return (["maxInteractionMs", "maxFrameGapMs", "maxLongTaskMs"] as const).filter(
|
|
782
|
+
(field) => !hasPositiveBudgetField(scenario.budget, field),
|
|
783
|
+
);
|
|
462
784
|
}
|
|
463
785
|
}
|
|
464
786
|
|
|
@@ -476,7 +798,13 @@ function getBudgetCapErrors(scenario: ToolcraftPerformanceScenario): string[] {
|
|
|
476
798
|
}
|
|
477
799
|
|
|
478
800
|
function requiresConcreteUiTarget(interaction: ToolcraftPerformanceInteraction): boolean {
|
|
479
|
-
return
|
|
801
|
+
return (
|
|
802
|
+
interaction === "control-change" ||
|
|
803
|
+
interaction === "control-drag" ||
|
|
804
|
+
interaction === "mask-drag" ||
|
|
805
|
+
interaction === "timeline-playback" ||
|
|
806
|
+
interaction === "timeline-scrub"
|
|
807
|
+
);
|
|
480
808
|
}
|
|
481
809
|
|
|
482
810
|
function getAllSchemaControls(
|
|
@@ -588,6 +916,32 @@ function hasHighCountCanvas2DRendererLayer(
|
|
|
588
916
|
);
|
|
589
917
|
}
|
|
590
918
|
|
|
919
|
+
function hasDetailHeavyCanvas2DRendererLayer(
|
|
920
|
+
technique: ToolcraftRendererTechnique | undefined,
|
|
921
|
+
): boolean {
|
|
922
|
+
return (technique?.layers ?? []).some(
|
|
923
|
+
(layer) =>
|
|
924
|
+
layer.renderer === "canvas-2d" &&
|
|
925
|
+
layer.content.some((content) => detailHeavyRendererContent.has(content)),
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function hasMeasuredGpuAlternativeEvidence(
|
|
930
|
+
technique: ToolcraftRendererTechnique,
|
|
931
|
+
): boolean {
|
|
932
|
+
const evidenceText = [
|
|
933
|
+
...technique.whyNotAlternativeStrategies,
|
|
934
|
+
...technique.performanceRisks,
|
|
935
|
+
].join(" ");
|
|
936
|
+
|
|
937
|
+
return (
|
|
938
|
+
/\b(?:webgl|webgpu|gpu)\b/i.test(evidenceText) &&
|
|
939
|
+
/\b(?:budget|evidence|frame|long task|measure|measured|perf|performance|stress)\b/i.test(
|
|
940
|
+
evidenceText,
|
|
941
|
+
)
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
|
|
591
945
|
function hasStressPreviewOrAnimationScenario(config: ToolcraftPerformanceConfig): boolean {
|
|
592
946
|
return config.scenarios.some(
|
|
593
947
|
(scenario) =>
|
|
@@ -767,6 +1121,267 @@ function getRendererTechniqueErrors(config: ToolcraftPerformanceConfig): string[
|
|
|
767
1121
|
|
|
768
1122
|
errors.push(...getRendererLayerErrors(technique));
|
|
769
1123
|
|
|
1124
|
+
if (
|
|
1125
|
+
config.usesCustomRenderer &&
|
|
1126
|
+
technique.rendererStrategy === "canvas-2d" &&
|
|
1127
|
+
hasDetailHeavyCanvas2DRendererLayer(technique) &&
|
|
1128
|
+
!hasMeasuredGpuAlternativeEvidence(technique)
|
|
1129
|
+
) {
|
|
1130
|
+
errors.push(
|
|
1131
|
+
"Detail-heavy Canvas 2D renderers must evaluate WebGL/WebGPU in rendererTechnique.whyNotAlternativeStrategies or performanceRisks with measured stress evidence before keeping the pixel work on CPU.",
|
|
1132
|
+
);
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
return errors;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
function getPassById(
|
|
1139
|
+
pipeline: ToolcraftRendererPipeline,
|
|
1140
|
+
): Map<string, ToolcraftRenderPass> {
|
|
1141
|
+
return new Map(pipeline.passes.map((pass) => [pass.id, pass]));
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
function hasPipelineReference(value: string): boolean {
|
|
1145
|
+
return value.trim().length > 0 && !vaguePipelineReferencePattern.test(value.trim());
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
function getPipelineReferenceErrors(
|
|
1149
|
+
passId: string,
|
|
1150
|
+
field: string,
|
|
1151
|
+
references: readonly string[] | undefined,
|
|
1152
|
+
): string[] {
|
|
1153
|
+
if (!references || references.length === 0) {
|
|
1154
|
+
return [`rendererPipeline pass "${passId}" must list ${field}.`];
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
return references.flatMap((reference) =>
|
|
1158
|
+
hasPipelineReference(reference)
|
|
1159
|
+
? []
|
|
1160
|
+
: [
|
|
1161
|
+
`rendererPipeline pass "${passId}" ${field} entry "${reference}" is too vague. Name the concrete runtime target, source key, resource key, or cache key part.`,
|
|
1162
|
+
],
|
|
1163
|
+
);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
function getPipelineInteractionForScenario(
|
|
1167
|
+
interaction: ToolcraftPerformanceInteraction,
|
|
1168
|
+
): ToolcraftPipelineInteraction | null {
|
|
1169
|
+
switch (interaction) {
|
|
1170
|
+
case "animation-frame":
|
|
1171
|
+
return "animation-frame";
|
|
1172
|
+
case "animation-viewport-drag":
|
|
1173
|
+
return "viewport-drag";
|
|
1174
|
+
case "control-change":
|
|
1175
|
+
return "control-change";
|
|
1176
|
+
case "control-drag":
|
|
1177
|
+
return "control-drag";
|
|
1178
|
+
case "export-copy":
|
|
1179
|
+
return "export";
|
|
1180
|
+
case "mask-drag":
|
|
1181
|
+
return "mask-drag";
|
|
1182
|
+
case "media-import":
|
|
1183
|
+
return "media-import";
|
|
1184
|
+
case "preview-render":
|
|
1185
|
+
return null;
|
|
1186
|
+
case "timeline-playback":
|
|
1187
|
+
return "timeline-playback";
|
|
1188
|
+
case "timeline-scrub":
|
|
1189
|
+
return "timeline-scrub";
|
|
1190
|
+
case "viewport-zoom-stress":
|
|
1191
|
+
return "viewport-zoom";
|
|
1192
|
+
case "viewport-stability":
|
|
1193
|
+
return null;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
function getRendererPipelineErrors(
|
|
1198
|
+
_schema: ResolvedToolcraftAppSchema,
|
|
1199
|
+
config: ToolcraftPerformanceConfig,
|
|
1200
|
+
): string[] {
|
|
1201
|
+
const errors: string[] = [];
|
|
1202
|
+
const pipeline = config.rendererPipeline;
|
|
1203
|
+
|
|
1204
|
+
if (config.usesCustomRenderer && !pipeline) {
|
|
1205
|
+
return [
|
|
1206
|
+
"Custom renderers must declare rendererPipeline so render passes, cache keys, and invalidation are machine-checkable.",
|
|
1207
|
+
];
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
if (!config.usesCustomRenderer && pipeline) {
|
|
1211
|
+
return ["Non-custom renderer configs must omit rendererPipeline."];
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
if (!pipeline) {
|
|
1215
|
+
return errors;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
if (pipeline.passes.length === 0) {
|
|
1219
|
+
errors.push("rendererPipeline must declare at least one render pass.");
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
if (pipeline.interactionInvalidation.length === 0) {
|
|
1223
|
+
errors.push(
|
|
1224
|
+
"rendererPipeline must declare interactionInvalidation so high-frequency UI work cannot accidentally invalidate expensive passes.",
|
|
1225
|
+
);
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
const passIds = new Set<string>();
|
|
1229
|
+
|
|
1230
|
+
for (const pass of pipeline.passes) {
|
|
1231
|
+
const passId = pass.id.trim();
|
|
1232
|
+
|
|
1233
|
+
if (!passId) {
|
|
1234
|
+
errors.push("rendererPipeline passes must have non-empty ids.");
|
|
1235
|
+
} else if (passIds.has(passId)) {
|
|
1236
|
+
errors.push(`rendererPipeline pass id "${passId}" must be unique.`);
|
|
1237
|
+
} else {
|
|
1238
|
+
passIds.add(passId);
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
if (!renderPassKinds.has(pass.kind)) {
|
|
1242
|
+
errors.push(`rendererPipeline pass "${pass.id}" kind "${pass.kind}" is not supported.`);
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
if (!renderPassRunLocations.has(pass.runsOn)) {
|
|
1246
|
+
errors.push(
|
|
1247
|
+
`rendererPipeline pass "${pass.id}" runsOn "${pass.runsOn}" is not supported.`,
|
|
1248
|
+
);
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
if (!renderPassOutputs.has(pass.output)) {
|
|
1252
|
+
errors.push(
|
|
1253
|
+
`rendererPipeline pass "${pass.id}" output "${pass.output}" is not supported.`,
|
|
1254
|
+
);
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
if (!renderPassQualities.has(pass.quality)) {
|
|
1258
|
+
errors.push(
|
|
1259
|
+
`rendererPipeline pass "${pass.id}" quality "${pass.quality}" is not supported.`,
|
|
1260
|
+
);
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
errors.push(...getPipelineReferenceErrors(pass.id, "inputs", pass.inputs));
|
|
1264
|
+
errors.push(...getPipelineReferenceErrors(pass.id, "invalidatedBy", pass.invalidatedBy));
|
|
1265
|
+
|
|
1266
|
+
if (pass.cacheKey) {
|
|
1267
|
+
errors.push(...getPipelineReferenceErrors(pass.id, "cacheKey", pass.cacheKey));
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
if (
|
|
1271
|
+
cacheRequiredRenderPassKinds.has(pass.kind) &&
|
|
1272
|
+
(!pass.cacheKey || pass.cacheKey.length === 0)
|
|
1273
|
+
) {
|
|
1274
|
+
errors.push(
|
|
1275
|
+
`rendererPipeline pass "${pass.id}" is a cache-sensitive ${pass.kind} pass and must declare cacheKey so tests can reject full recomputation on every control change.`,
|
|
1276
|
+
);
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
if (pass.kind === "decode") {
|
|
1280
|
+
const hasMediaImportScenario = config.scenarios.some(
|
|
1281
|
+
(scenario) => scenario.interaction === "media-import",
|
|
1282
|
+
);
|
|
1283
|
+
|
|
1284
|
+
if (!hasMediaImportScenario) {
|
|
1285
|
+
errors.push(
|
|
1286
|
+
`rendererPipeline pass "${pass.id}" decodes media, so performance scenarios must include media-import coverage.`,
|
|
1287
|
+
);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
const passesById = getPassById(pipeline);
|
|
1293
|
+
const invalidationTargets = new Set<string>();
|
|
1294
|
+
const pipelineInteractionSet = new Set(
|
|
1295
|
+
pipeline.interactionInvalidation.map((invalidation) => invalidation.interaction),
|
|
1296
|
+
);
|
|
1297
|
+
|
|
1298
|
+
for (const invalidation of pipeline.interactionInvalidation) {
|
|
1299
|
+
if (!pipelineInteractions.has(invalidation.interaction)) {
|
|
1300
|
+
errors.push(
|
|
1301
|
+
`rendererPipeline interaction "${invalidation.interaction}" is not supported.`,
|
|
1302
|
+
);
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
errors.push(
|
|
1306
|
+
...getPipelineReferenceErrors(
|
|
1307
|
+
invalidation.interaction,
|
|
1308
|
+
"targets",
|
|
1309
|
+
invalidation.targets,
|
|
1310
|
+
),
|
|
1311
|
+
);
|
|
1312
|
+
|
|
1313
|
+
for (const target of invalidation.targets) {
|
|
1314
|
+
if (hasPipelineReference(target)) {
|
|
1315
|
+
invalidationTargets.add(target);
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
const mustNotInvalidate = new Set(invalidation.mustNotInvalidate ?? []);
|
|
1320
|
+
|
|
1321
|
+
for (const passId of invalidation.invalidates) {
|
|
1322
|
+
if (!passId.trim()) {
|
|
1323
|
+
errors.push(
|
|
1324
|
+
`rendererPipeline ${invalidation.interaction} invalidates contains an empty pass id.`,
|
|
1325
|
+
);
|
|
1326
|
+
continue;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
const pass = passesById.get(passId);
|
|
1330
|
+
|
|
1331
|
+
if (!pass) {
|
|
1332
|
+
errors.push(
|
|
1333
|
+
`rendererPipeline ${invalidation.interaction} invalidates unknown pass "${passId}".`,
|
|
1334
|
+
);
|
|
1335
|
+
continue;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
if (mustNotInvalidate.has(passId)) {
|
|
1339
|
+
errors.push(
|
|
1340
|
+
`rendererPipeline ${invalidation.interaction} cannot both invalidate and mustNotInvalidate pass "${passId}".`,
|
|
1341
|
+
);
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
if (
|
|
1345
|
+
highFrequencyViewportInteractions.has(invalidation.interaction) &&
|
|
1346
|
+
expensiveRenderPassKinds.has(pass.kind)
|
|
1347
|
+
) {
|
|
1348
|
+
errors.push(
|
|
1349
|
+
`rendererPipeline ${invalidation.interaction} must not invalidate expensive pass "${passId}" (${pass.kind}). Move viewport work to transforms/uniforms or explain it through a cheaper pass.`,
|
|
1350
|
+
);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
for (const passId of invalidation.mustNotInvalidate ?? []) {
|
|
1355
|
+
if (!passId.trim()) {
|
|
1356
|
+
errors.push(
|
|
1357
|
+
`rendererPipeline ${invalidation.interaction} mustNotInvalidate contains an empty pass id.`,
|
|
1358
|
+
);
|
|
1359
|
+
} else if (!passesById.has(passId)) {
|
|
1360
|
+
errors.push(
|
|
1361
|
+
`rendererPipeline ${invalidation.interaction} mustNotInvalidate unknown pass "${passId}".`,
|
|
1362
|
+
);
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
for (const scenario of config.scenarios) {
|
|
1368
|
+
const pipelineInteraction = getPipelineInteractionForScenario(scenario.interaction);
|
|
1369
|
+
|
|
1370
|
+
if (pipelineInteraction && !pipelineInteractionSet.has(pipelineInteraction)) {
|
|
1371
|
+
errors.push(
|
|
1372
|
+
`Performance scenario ${scenario.id} exercises ${pipelineInteraction}, so rendererPipeline.interactionInvalidation must declare that interaction.`,
|
|
1373
|
+
);
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
for (const target of config.workloadTargets) {
|
|
1378
|
+
if (!invalidationTargets.has(target)) {
|
|
1379
|
+
errors.push(
|
|
1380
|
+
`Performance workload target ${target} must appear in rendererPipeline interactionInvalidation targets.`,
|
|
1381
|
+
);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
|
|
770
1385
|
return errors;
|
|
771
1386
|
}
|
|
772
1387
|
|
|
@@ -790,6 +1405,7 @@ export function validateToolcraftPerformanceCoverage(
|
|
|
790
1405
|
);
|
|
791
1406
|
|
|
792
1407
|
errors.push(...getRendererTechniqueErrors(config));
|
|
1408
|
+
errors.push(...getRendererPipelineErrors(schema, config));
|
|
793
1409
|
|
|
794
1410
|
if (config.usesCustomRenderer && !customRendererStrategies.has(config.rendererStrategy)) {
|
|
795
1411
|
errors.push(
|
|
@@ -817,10 +1433,11 @@ export function validateToolcraftPerformanceCoverage(
|
|
|
817
1433
|
|
|
818
1434
|
if (
|
|
819
1435
|
config.rendererWorkload === "pixel-output" &&
|
|
820
|
-
!gpuRendererStrategies.has(config.rendererStrategy)
|
|
1436
|
+
!gpuRendererStrategies.has(config.rendererStrategy) &&
|
|
1437
|
+
(!config.rendererTechnique || !hasMeasuredGpuAlternativeEvidence(config.rendererTechnique))
|
|
821
1438
|
) {
|
|
822
1439
|
errors.push(
|
|
823
|
-
`rendererWorkload "pixel-output"
|
|
1440
|
+
`rendererWorkload "pixel-output" should use rendererStrategy "webgl" or "webgpu", received "${config.rendererStrategy}". Keeping a CPU renderer requires measured WebGL/WebGPU evidence in rendererTechnique.whyNotAlternativeStrategies or performanceRisks.`,
|
|
824
1441
|
);
|
|
825
1442
|
}
|
|
826
1443
|
|
|
@@ -910,11 +1527,17 @@ export function validateToolcraftPerformanceCoverage(
|
|
|
910
1527
|
}
|
|
911
1528
|
}
|
|
912
1529
|
|
|
913
|
-
if (
|
|
1530
|
+
if (
|
|
1531
|
+
scenario.workload &&
|
|
1532
|
+
scenario.interaction !== "media-import" &&
|
|
1533
|
+
!hasMinDefaultMax(scenario.values)
|
|
1534
|
+
) {
|
|
914
1535
|
errors.push(`${scenario.id} workload scenario must include min/default/max values.`);
|
|
915
1536
|
}
|
|
916
1537
|
|
|
917
|
-
|
|
1538
|
+
const scenarioControl = controlsByTarget.get(scenario.target ?? "");
|
|
1539
|
+
errors.push(...getStressFixtureErrors(scenario, scenarioControl));
|
|
1540
|
+
errors.push(...getWorkloadFixtureErrors(config, scenario, scenarioControl));
|
|
918
1541
|
|
|
919
1542
|
if (
|
|
920
1543
|
requiresConcreteUiTarget(scenario.interaction) &&
|
|
@@ -1039,10 +1662,24 @@ export function validateToolcraftPerformanceCoverage(
|
|
|
1039
1662
|
}
|
|
1040
1663
|
}
|
|
1041
1664
|
|
|
1042
|
-
if (schema.canvas.upload
|
|
1043
|
-
|
|
1044
|
-
|
|
1665
|
+
if (schema.canvas.upload) {
|
|
1666
|
+
const mediaImportScenarios = config.scenarios.filter(
|
|
1667
|
+
(scenario) => scenario.interaction === "media-import",
|
|
1045
1668
|
);
|
|
1669
|
+
|
|
1670
|
+
if (mediaImportScenarios.length === 0) {
|
|
1671
|
+
errors.push(
|
|
1672
|
+
"Custom renderers with canvas upload must include a media-import performance scenario.",
|
|
1673
|
+
);
|
|
1674
|
+
} else if (
|
|
1675
|
+
!mediaImportScenarios.some(
|
|
1676
|
+
(scenario) => scenario.workload && scenario.stressFixture?.kind === "media",
|
|
1677
|
+
)
|
|
1678
|
+
) {
|
|
1679
|
+
errors.push(
|
|
1680
|
+
'Custom renderers with canvas upload must include a workload media-import performance scenario with stressFixture.kind "media".',
|
|
1681
|
+
);
|
|
1682
|
+
}
|
|
1046
1683
|
}
|
|
1047
1684
|
|
|
1048
1685
|
if (hasKeyframeTimeline(schema)) {
|