@kaminos/webgpu-inference-kit 0.1.4 → 0.1.5
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kaminos/webgpu-inference-kit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Composable browser WebGPU inference route contracts, runtime profiles, and scheduler envelopes.",
|
|
@@ -21,6 +21,6 @@
|
|
|
21
21
|
".": "./src/index.js"
|
|
22
22
|
},
|
|
23
23
|
"scripts": {
|
|
24
|
-
"test": "node tests/receipt-contracts.mjs && node tests/tensor-manifest-contracts.mjs && node tests/gpu-environment-contracts.mjs && node tests/browser-device-context-contracts.mjs && node tests/staged-profile-contracts.mjs && node tests/kernel-profile-contracts.mjs && node tests/runtime-profile-contracts.mjs && node tests/scheduler-backpressure-contracts.mjs && node tests/route-schema-contracts.mjs && node tests/route-receipt-helper-contracts.mjs && node tests/route-receipt-consumer-contracts.mjs && node tests/moge-route-contracts.mjs && node tests/sharp-route-contracts.mjs && node tests/kimodo-route-contracts.mjs && node tests/sf3d-route-contracts.mjs && node tests/route-boundary-contracts.mjs"
|
|
24
|
+
"test": "node tests/receipt-contracts.mjs && node tests/tensor-manifest-contracts.mjs && node tests/gpu-environment-contracts.mjs && node tests/browser-device-context-contracts.mjs && node tests/staged-profile-contracts.mjs && node tests/kernel-profile-contracts.mjs && node tests/runtime-profile-contracts.mjs && node tests/scheduler-backpressure-contracts.mjs && node tests/scheduler-verification-receipt-contracts.mjs && node tests/route-schema-contracts.mjs && node tests/route-receipt-helper-contracts.mjs && node tests/route-receipt-consumer-contracts.mjs && node tests/moge-route-contracts.mjs && node tests/sharp-route-contracts.mjs && node tests/kimodo-route-contracts.mjs && node tests/sf3d-route-contracts.mjs && node tests/route-boundary-contracts.mjs"
|
|
25
25
|
}
|
|
26
26
|
}
|
package/src/index.js
CHANGED
|
@@ -49,6 +49,14 @@ export {
|
|
|
49
49
|
WEBGPU_ROUTE_SCHEDULER_SCHEMA,
|
|
50
50
|
} from './scheduler-backpressure.js';
|
|
51
51
|
|
|
52
|
+
export {
|
|
53
|
+
SCHEDULER_EVENT_TRACE_SCHEMA,
|
|
54
|
+
SCHEDULER_VERIFICATION_RECEIPT_SCHEMA,
|
|
55
|
+
classifySchedulerVerificationReceipt,
|
|
56
|
+
createSchedulerVerificationReceipt,
|
|
57
|
+
validateSchedulerVerificationReceipt,
|
|
58
|
+
} from './scheduler-verification-receipt.js';
|
|
59
|
+
|
|
52
60
|
export {
|
|
53
61
|
classifyWebGpuRouteReceiptEvidence,
|
|
54
62
|
classifyWebGpuRouteWorkerResultEvidence,
|
package/src/kernel-profile.js
CHANGED
|
@@ -20,6 +20,77 @@ function parseTime(value) {
|
|
|
20
20
|
return Number.isFinite(ms) ? ms : null;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
function clone(value) {
|
|
24
|
+
return value == null ? value : JSON.parse(JSON.stringify(value));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const FALSE_VERIFIED_SCHEDULER_DOWNGRADES = new Set([
|
|
28
|
+
'yield-events-missing',
|
|
29
|
+
'event-trace-missing',
|
|
30
|
+
'queue-wait-events-missing',
|
|
31
|
+
'boundary-assertion-event-mismatch',
|
|
32
|
+
'requested-boundary-assertion-missing',
|
|
33
|
+
'requested-field-dropped-without-unsupported',
|
|
34
|
+
'boundary-assertions-missing',
|
|
35
|
+
'timing-proxy-only',
|
|
36
|
+
'route-identity-missing',
|
|
37
|
+
'scheduler-envelope-missing',
|
|
38
|
+
'backpressure-envelope-missing',
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
function nestedSchedulerDowngrades(schedulerVerification) {
|
|
42
|
+
return Array.isArray(schedulerVerification?.downgrades)
|
|
43
|
+
? [...schedulerVerification.downgrades]
|
|
44
|
+
: [];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function hasVerifiedBoundaryAssertion(schedulerVerification) {
|
|
48
|
+
return Array.isArray(schedulerVerification?.boundaryAssertions)
|
|
49
|
+
&& schedulerVerification.boundaryAssertions.some(assertion => assertion?.status === 'verified');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function hasEventTraceEvents(schedulerVerification) {
|
|
53
|
+
return Array.isArray(schedulerVerification?.eventTrace?.events)
|
|
54
|
+
&& schedulerVerification.eventTrace.events.length > 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function normalizeNestedSchedulerVerification(schedulerVerification) {
|
|
58
|
+
if (!schedulerVerification) {
|
|
59
|
+
return {
|
|
60
|
+
receipt: null,
|
|
61
|
+
reportedStatus: null,
|
|
62
|
+
status: null,
|
|
63
|
+
classification: null,
|
|
64
|
+
observationClass: null,
|
|
65
|
+
downgrades: [],
|
|
66
|
+
timingAuthority: null,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const receipt = clone(schedulerVerification);
|
|
70
|
+
const downgrades = nestedSchedulerDowngrades(schedulerVerification);
|
|
71
|
+
const reportedStatus = schedulerVerification.status || null;
|
|
72
|
+
const falseVerifiedDowngrade = downgrades.some(downgrade => FALSE_VERIFIED_SCHEDULER_DOWNGRADES.has(downgrade));
|
|
73
|
+
const falseVerifiedShape = reportedStatus === 'verified'
|
|
74
|
+
&& (!hasEventTraceEvents(schedulerVerification) || !hasVerifiedBoundaryAssertion(schedulerVerification));
|
|
75
|
+
const status = reportedStatus === 'verified' && (falseVerifiedDowngrade || falseVerifiedShape)
|
|
76
|
+
? 'scheduler-unverified'
|
|
77
|
+
: reportedStatus;
|
|
78
|
+
if (status !== reportedStatus) {
|
|
79
|
+
receipt.reportedStatus = reportedStatus;
|
|
80
|
+
receipt.status = status;
|
|
81
|
+
if (receipt.classification === 'observed-boundary') receipt.classification = 'config-only';
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
receipt,
|
|
85
|
+
reportedStatus,
|
|
86
|
+
status,
|
|
87
|
+
classification: receipt.classification || null,
|
|
88
|
+
observationClass: receipt.observationClass || null,
|
|
89
|
+
downgrades,
|
|
90
|
+
timingAuthority: receipt.eventTrace?.timingAuthority || null,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
23
94
|
function staleReason(receipt, options) {
|
|
24
95
|
if (!Number.isFinite(options.maxAgeMs)) return null;
|
|
25
96
|
const createdMs = parseTime(receipt.createdAt);
|
|
@@ -119,6 +190,7 @@ export function classifyWebGpuRouteReceiptEvidence(receipt, options = {}) {
|
|
|
119
190
|
const base = baseClassification(receipt, options);
|
|
120
191
|
const scheduler = receipt?.runtime?.scheduler || null;
|
|
121
192
|
const backpressure = receipt?.runtime?.backpressure || null;
|
|
193
|
+
const schedulerVerification = normalizeNestedSchedulerVerification(receipt?.runtime?.schedulerVerification || null);
|
|
122
194
|
return {
|
|
123
195
|
schema: WEBGPU_ROUTE_EVIDENCE_CLASSIFICATION_SCHEMA,
|
|
124
196
|
classification: base.classification,
|
|
@@ -131,7 +203,14 @@ export function classifyWebGpuRouteReceiptEvidence(receipt, options = {}) {
|
|
|
131
203
|
adapterName: receipt?.backend?.adapterName || null,
|
|
132
204
|
timingSource: receipt?.timings?.source || receipt?.timings?.profile?.timingSource || null,
|
|
133
205
|
totalMs: Number.isFinite(receipt?.timings?.totalMs) ? receipt.timings.totalMs : null,
|
|
134
|
-
|
|
206
|
+
schedulerVerification: schedulerVerification.receipt,
|
|
207
|
+
schedulerVerificationState: schedulerVerification.status || scheduler?.verificationState || null,
|
|
208
|
+
schedulerVerificationStatus: schedulerVerification.status,
|
|
209
|
+
schedulerVerificationReportedStatus: schedulerVerification.reportedStatus,
|
|
210
|
+
schedulerVerificationClassification: schedulerVerification.classification,
|
|
211
|
+
schedulerVerificationObservationClass: schedulerVerification.observationClass,
|
|
212
|
+
schedulerVerificationDowngrades: schedulerVerification.downgrades,
|
|
213
|
+
schedulerVerificationEventTraceTimingAuthority: schedulerVerification.timingAuthority,
|
|
135
214
|
schedulerMode: scheduler?.effectiveScheduler?.mode || scheduler?.requestedScheduler?.mode || null,
|
|
136
215
|
schedulerUnsupportedFields: Array.isArray(scheduler?.effectiveScheduler?.unsupportedFields)
|
|
137
216
|
? [...scheduler.effectiveScheduler.unsupportedFields]
|
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
export const SCHEDULER_VERIFICATION_RECEIPT_SCHEMA = 'kaminos.webgpu-scheduler-verification-receipt.v0';
|
|
2
|
+
export const SCHEDULER_EVENT_TRACE_SCHEMA = 'kaminos.webgpu-scheduler-event-trace.v0';
|
|
3
|
+
|
|
4
|
+
const LEGACY_PHASE_FIELD_ALIASES = {
|
|
5
|
+
spnPatch: ['phaseChunkSize.spnPatch', 'spnPatchChunkSize'],
|
|
6
|
+
vitBlock: ['phaseChunkSize.vitBlock', 'vitBlockChunkSize'],
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
function cloneJson(value) {
|
|
10
|
+
return value == null ? value : JSON.parse(JSON.stringify(value));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function uniq(values) {
|
|
14
|
+
return [...new Set(values.filter(Boolean))];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function asObject(value) {
|
|
18
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function legacyDirectKey(key) {
|
|
22
|
+
if (key === 'spnPatch') return 'spnPatchChunkSize';
|
|
23
|
+
if (key === 'vitBlock') return 'vitBlockChunkSize';
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function canonicalPhaseField(key) {
|
|
28
|
+
return `phaseChunkSize.${key}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function knownPhaseKeys(scheduler = {}) {
|
|
32
|
+
const phaseChunkSize = asObject(scheduler.phaseChunkSize);
|
|
33
|
+
return uniq([
|
|
34
|
+
...Object.keys(phaseChunkSize),
|
|
35
|
+
...Object.keys(LEGACY_PHASE_FIELD_ALIASES).filter(key => Number.isFinite(scheduler[legacyDirectKey(key)])),
|
|
36
|
+
]);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function phaseValue(scheduler = {}, key) {
|
|
40
|
+
const phaseChunkSize = asObject(scheduler.phaseChunkSize);
|
|
41
|
+
if (Number.isFinite(phaseChunkSize[key])) return phaseChunkSize[key];
|
|
42
|
+
const directKey = legacyDirectKey(key);
|
|
43
|
+
return directKey && Number.isFinite(scheduler[directKey]) ? scheduler[directKey] : undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function requestedPhaseFields(requestedScheduler = {}) {
|
|
47
|
+
return knownPhaseKeys(requestedScheduler)
|
|
48
|
+
.map(key => {
|
|
49
|
+
const value = phaseValue(requestedScheduler, key);
|
|
50
|
+
return Number.isFinite(value) && value > 0 ? { key, field: canonicalPhaseField(key), value } : null;
|
|
51
|
+
})
|
|
52
|
+
.filter(Boolean);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function unsupportedFieldsFrom(input = {}) {
|
|
56
|
+
const scheduler = asObject(input.scheduler);
|
|
57
|
+
const effectiveScheduler = asObject(scheduler.effectiveScheduler);
|
|
58
|
+
return uniq([
|
|
59
|
+
...(Array.isArray(input.unsupportedFields) ? input.unsupportedFields : []),
|
|
60
|
+
...(Array.isArray(scheduler.unsupportedFields) ? scheduler.unsupportedFields : []),
|
|
61
|
+
...(Array.isArray(effectiveScheduler.unsupportedFields) ? effectiveScheduler.unsupportedFields : []),
|
|
62
|
+
]);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function aliasesForPhaseKey(key) {
|
|
66
|
+
return uniq([
|
|
67
|
+
canonicalPhaseField(key),
|
|
68
|
+
...(LEGACY_PHASE_FIELD_ALIASES[key] || []),
|
|
69
|
+
]);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function unsupportedCovers(fieldKey, unsupportedFields = []) {
|
|
73
|
+
const aliases = aliasesForPhaseKey(fieldKey);
|
|
74
|
+
return unsupportedFields.some(field => aliases.includes(field) || field === fieldKey || field === 'phaseChunkSize');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function eventKindCounts(events = []) {
|
|
78
|
+
return {
|
|
79
|
+
eventCount: events.length,
|
|
80
|
+
queueStartCount: events.filter(event => event?.kind === 'queue-work-done-start').length,
|
|
81
|
+
queueEndCount: events.filter(event => event?.kind === 'queue-work-done-end').length,
|
|
82
|
+
yieldStartCount: events.filter(event => event?.kind === 'js-yield-start').length,
|
|
83
|
+
yieldEndCount: events.filter(event => event?.kind === 'js-yield-end').length,
|
|
84
|
+
chunkCount: events.filter(event => event?.kind === 'chunk-start' || event?.boundary || event?.phase).length,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function normalizeEventTrace(eventTrace = {}) {
|
|
89
|
+
const events = Array.isArray(eventTrace.events) ? cloneJson(eventTrace.events) : [];
|
|
90
|
+
return {
|
|
91
|
+
schema: eventTrace.schema || SCHEDULER_EVENT_TRACE_SCHEMA,
|
|
92
|
+
clock: eventTrace.clock || 'performance.now',
|
|
93
|
+
timingAuthority: eventTrace.timingAuthority || 'not-observed',
|
|
94
|
+
events,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function normalizeFrameTail(frameTail = {}, eventTrace = {}) {
|
|
99
|
+
const evidenceSource = eventTrace.timingAuthority === 'raf-and-queue-proxy'
|
|
100
|
+
? 'raf-and-queue-proxy'
|
|
101
|
+
: (frameTail.evidenceSource || eventTrace.timingAuthority || 'not-observed');
|
|
102
|
+
return {
|
|
103
|
+
evidenceSource,
|
|
104
|
+
disclaimer: frameTail.disclaimer || 'not-gpu-exclusive-or-present-latency',
|
|
105
|
+
rafFps: frameTail.rafFps ?? null,
|
|
106
|
+
frameP95Ms: frameTail.frameP95Ms ?? null,
|
|
107
|
+
queueDoneP95Ms: frameTail.queueDoneP95Ms ?? null,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function legacyBoundaryForPhaseKey(key) {
|
|
112
|
+
if (key === 'spnPatch') return 'spn-patch-chunk';
|
|
113
|
+
if (key === 'vitBlock') return 'vit-block-chunk';
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function eventMatchesBoundary(event = {}, boundary) {
|
|
118
|
+
if (!boundary) return false;
|
|
119
|
+
return event.boundary === boundary
|
|
120
|
+
|| event.phase === boundary
|
|
121
|
+
|| event.stage === boundary
|
|
122
|
+
|| event.name === boundary;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function eventMatchesPhaseKey(event = {}, key) {
|
|
126
|
+
return event.phase === key
|
|
127
|
+
|| event.stage === key
|
|
128
|
+
|| event.boundary === key
|
|
129
|
+
|| event.boundary === `moge-stage:${key}`
|
|
130
|
+
|| event.boundary === `stage:${key}`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function observedBoundaryForPhaseKey(key, events = []) {
|
|
134
|
+
const legacyBoundary = legacyBoundaryForPhaseKey(key);
|
|
135
|
+
if (legacyBoundary) return legacyBoundary;
|
|
136
|
+
const event = events.find(candidate => eventMatchesPhaseKey(candidate, key));
|
|
137
|
+
return event?.boundary || event?.phase || key;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function observedCountForPhaseKey(key, observedBoundary, events = []) {
|
|
141
|
+
return events.filter(event => eventMatchesBoundary(event, observedBoundary) || eventMatchesPhaseKey(event, key)).length;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function derivedAssertionStatus(key, observedCount, unsupported) {
|
|
145
|
+
if (unsupported) return 'unsupported';
|
|
146
|
+
if (observedCount <= 0) return 'unverified';
|
|
147
|
+
return legacyBoundaryForPhaseKey(key) ? 'verified' : 'observed';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function deriveBoundaryAssertions({
|
|
151
|
+
boundaryAssertions,
|
|
152
|
+
requestedScheduler,
|
|
153
|
+
effectiveScheduler,
|
|
154
|
+
unsupportedFields,
|
|
155
|
+
events,
|
|
156
|
+
}) {
|
|
157
|
+
if (Array.isArray(boundaryAssertions) && boundaryAssertions.length) {
|
|
158
|
+
return cloneJson(boundaryAssertions).map(normalizeCallerBoundaryAssertion);
|
|
159
|
+
}
|
|
160
|
+
const counts = eventKindCounts(events);
|
|
161
|
+
return requestedPhaseFields(requestedScheduler).map(({ key, field, value }) => {
|
|
162
|
+
const effective = phaseValue(effectiveScheduler, key);
|
|
163
|
+
const unsupported = unsupportedCovers(key, unsupportedFields);
|
|
164
|
+
const observedBoundary = observedBoundaryForPhaseKey(key, events);
|
|
165
|
+
const observedCount = observedCountForPhaseKey(key, observedBoundary, events);
|
|
166
|
+
return {
|
|
167
|
+
field,
|
|
168
|
+
requested: value,
|
|
169
|
+
effective: Number.isFinite(effective) ? effective : null,
|
|
170
|
+
status: derivedAssertionStatus(key, observedCount, unsupported),
|
|
171
|
+
observedBoundary,
|
|
172
|
+
observedCount,
|
|
173
|
+
expectedMinimumCount: 1,
|
|
174
|
+
observedQueueWaitCount: Math.min(counts.queueStartCount, counts.queueEndCount),
|
|
175
|
+
observedYieldCount: Math.min(counts.yieldStartCount, counts.yieldEndCount),
|
|
176
|
+
unsupportedReason: unsupported ? 'effective scheduler declared this field unsupported' : null,
|
|
177
|
+
};
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function fieldKeyForAssertion(assertion = {}) {
|
|
182
|
+
const field = assertion.field;
|
|
183
|
+
if (typeof field !== 'string') return null;
|
|
184
|
+
for (const [key, aliases] of Object.entries(LEGACY_PHASE_FIELD_ALIASES)) {
|
|
185
|
+
if (aliases.includes(field)) return key;
|
|
186
|
+
}
|
|
187
|
+
if (field.startsWith('phaseChunkSize.')) return field.slice('phaseChunkSize.'.length);
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function fieldBoundaryForAssertion(assertion = {}) {
|
|
192
|
+
const key = fieldKeyForAssertion(assertion);
|
|
193
|
+
return key ? legacyBoundaryForPhaseKey(key) : null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function expectedBoundaryForAssertion(assertion = {}) {
|
|
197
|
+
return fieldBoundaryForAssertion(assertion) || assertion.observedBoundary || null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function assertionBoundaryConflictsWithField(assertion = {}) {
|
|
201
|
+
const fieldBoundary = fieldBoundaryForAssertion(assertion);
|
|
202
|
+
return Boolean(fieldBoundary && assertion.observedBoundary && assertion.observedBoundary !== fieldBoundary);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function assertionHasMatchingEvent(assertion = {}, events = []) {
|
|
206
|
+
const expectedBoundary = expectedBoundaryForAssertion(assertion);
|
|
207
|
+
if (!expectedBoundary) return false;
|
|
208
|
+
return events.some(event => eventMatchesBoundary(event, expectedBoundary));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function assertionObservesRequestedKey(assertion = {}, key, events = []) {
|
|
212
|
+
return (assertion?.status === 'verified' || assertion?.status === 'observed')
|
|
213
|
+
&& fieldKeyForAssertion(assertion) === key
|
|
214
|
+
&& !assertionBoundaryConflictsWithField(assertion)
|
|
215
|
+
&& assertionHasMatchingEvent(assertion, events);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function assertionVerifiesRequestedKey(assertion = {}, key, events = []) {
|
|
219
|
+
return assertion?.status === 'verified'
|
|
220
|
+
&& assertionObservesRequestedKey(assertion, key, events);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function normalizeCallerBoundaryAssertion(assertion = {}) {
|
|
224
|
+
const normalized = { ...assertion };
|
|
225
|
+
const key = fieldKeyForAssertion(normalized);
|
|
226
|
+
if (normalized.status === 'verified' && key && !legacyBoundaryForPhaseKey(key)) {
|
|
227
|
+
normalized.reportedStatus = normalized.reportedStatus || normalized.status;
|
|
228
|
+
normalized.status = 'observed';
|
|
229
|
+
}
|
|
230
|
+
return normalized;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function routeIsPresent(route = {}) {
|
|
234
|
+
return Boolean(route.pipelineId || route.requestedRouteId || route.effectiveRouteId || route.adapterReport?.path);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function waitRequested(scheduler = {}) {
|
|
238
|
+
return Boolean(scheduler.waitForSubmittedWorkDone);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function yieldRequested(scheduler = {}) {
|
|
242
|
+
return Number(scheduler.yieldMs || 0) > 0 || Number(scheduler.gaussianPhaseYieldMs || 0) > 0;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function droppedRequestedFields(requestedScheduler = {}, effectiveScheduler = {}, unsupportedFields = []) {
|
|
246
|
+
return requestedPhaseFields(requestedScheduler)
|
|
247
|
+
.filter(({ key }) => !Number.isFinite(phaseValue(effectiveScheduler, key)) && !unsupportedCovers(key, unsupportedFields))
|
|
248
|
+
.map(({ field }) => field);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function normalizeDowngrades(values) {
|
|
252
|
+
return uniq(values);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function deriveObservationClass({
|
|
256
|
+
status,
|
|
257
|
+
boundaryAssertions,
|
|
258
|
+
falseAuthorityChecks,
|
|
259
|
+
events,
|
|
260
|
+
}) {
|
|
261
|
+
if (status === 'verified') return 'observed-scheduler-boundary';
|
|
262
|
+
if (falseAuthorityChecks.timingProxyOnly) return 'proxy-only';
|
|
263
|
+
if (!events.length) return 'config-only';
|
|
264
|
+
if (boundaryAssertions.some(assertion => assertion?.status === 'observed' || assertion?.status === 'verified')) {
|
|
265
|
+
return 'observed-stage-boundary';
|
|
266
|
+
}
|
|
267
|
+
return 'event-trace-only';
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function createSchedulerVerificationReceipt(input = {}) {
|
|
271
|
+
const route = cloneJson(asObject(input.route));
|
|
272
|
+
const scheduler = cloneJson(asObject(input.scheduler));
|
|
273
|
+
const backpressure = cloneJson(asObject(input.backpressure));
|
|
274
|
+
const requestedScheduler = asObject(scheduler.requestedScheduler);
|
|
275
|
+
const effectiveScheduler = asObject(scheduler.effectiveScheduler);
|
|
276
|
+
const unsupportedFields = unsupportedFieldsFrom({ ...input, scheduler });
|
|
277
|
+
const eventTrace = normalizeEventTrace(input.eventTrace);
|
|
278
|
+
const frameTail = normalizeFrameTail(input.frameTail || {}, eventTrace);
|
|
279
|
+
const events = eventTrace.events;
|
|
280
|
+
const counts = eventKindCounts(events);
|
|
281
|
+
const boundaryAssertions = deriveBoundaryAssertions({
|
|
282
|
+
boundaryAssertions: input.boundaryAssertions,
|
|
283
|
+
requestedScheduler,
|
|
284
|
+
effectiveScheduler,
|
|
285
|
+
unsupportedFields,
|
|
286
|
+
events,
|
|
287
|
+
});
|
|
288
|
+
const downgrades = Array.isArray(input.downgrades) ? [...input.downgrades] : [];
|
|
289
|
+
const falseAuthorityChecks = {
|
|
290
|
+
eventTraceMissing: false,
|
|
291
|
+
verifiedWithoutObservedBoundary: false,
|
|
292
|
+
timingProxyOnly: false,
|
|
293
|
+
queueWaitEventsMissing: false,
|
|
294
|
+
boundaryAssertionEventMismatch: false,
|
|
295
|
+
requestedBoundaryAssertionMissing: false,
|
|
296
|
+
requestedFieldDroppedWithoutUnsupported: false,
|
|
297
|
+
...(asObject(input.falseAuthorityChecks)),
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
if (!routeIsPresent(route)) downgrades.push('route-identity-missing');
|
|
301
|
+
if (!scheduler.schema && !scheduler.requestedScheduler && !scheduler.effectiveScheduler) downgrades.push('scheduler-envelope-missing');
|
|
302
|
+
if (!backpressure.schema && Object.keys(backpressure).length === 0) downgrades.push('backpressure-envelope-missing');
|
|
303
|
+
|
|
304
|
+
const droppedFields = droppedRequestedFields(requestedScheduler, effectiveScheduler, unsupportedFields);
|
|
305
|
+
if (droppedFields.length) {
|
|
306
|
+
downgrades.push('requested-field-dropped-without-unsupported');
|
|
307
|
+
falseAuthorityChecks.requestedFieldDroppedWithoutUnsupported = true;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (!events.length) {
|
|
311
|
+
downgrades.push('event-trace-missing');
|
|
312
|
+
falseAuthorityChecks.eventTraceMissing = true;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const verifiedAssertions = boundaryAssertions.filter(assertion => assertion?.status === 'verified');
|
|
316
|
+
const mismatchedVerifiedAssertions = verifiedAssertions.filter(assertion => (
|
|
317
|
+
assertionBoundaryConflictsWithField(assertion) || !assertionHasMatchingEvent(assertion, events)
|
|
318
|
+
));
|
|
319
|
+
if (mismatchedVerifiedAssertions.length) {
|
|
320
|
+
downgrades.push('boundary-assertion-event-mismatch');
|
|
321
|
+
falseAuthorityChecks.boundaryAssertionEventMismatch = true;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const requestedBoundaryKeys = requestedPhaseFields(requestedScheduler)
|
|
325
|
+
.map(({ key }) => key)
|
|
326
|
+
.filter(key => !unsupportedCovers(key, unsupportedFields));
|
|
327
|
+
const missingRequestedBoundaryAssertions = requestedBoundaryKeys
|
|
328
|
+
.filter(key => !boundaryAssertions.some(assertion => assertionObservesRequestedKey(assertion, key, events)));
|
|
329
|
+
if (missingRequestedBoundaryAssertions.length) {
|
|
330
|
+
downgrades.push('requested-boundary-assertion-missing');
|
|
331
|
+
falseAuthorityChecks.requestedBoundaryAssertionMissing = true;
|
|
332
|
+
}
|
|
333
|
+
if (scheduler.verificationState === 'verified' && !verifiedAssertions.length) {
|
|
334
|
+
downgrades.push('boundary-assertions-missing');
|
|
335
|
+
falseAuthorityChecks.verifiedWithoutObservedBoundary = true;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (eventTrace.timingAuthority === 'raf-and-queue-proxy') {
|
|
339
|
+
downgrades.push('timing-proxy-only');
|
|
340
|
+
falseAuthorityChecks.timingProxyOnly = true;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (waitRequested(requestedScheduler) || waitRequested(effectiveScheduler)) {
|
|
344
|
+
const hasQueuePair = counts.queueStartCount > 0 && counts.queueEndCount > 0;
|
|
345
|
+
if (!hasQueuePair) {
|
|
346
|
+
downgrades.push('queue-wait-events-missing');
|
|
347
|
+
falseAuthorityChecks.queueWaitEventsMissing = true;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (yieldRequested(requestedScheduler) || yieldRequested(effectiveScheduler)) {
|
|
352
|
+
const hasYieldPair = counts.yieldStartCount > 0 && counts.yieldEndCount > 0;
|
|
353
|
+
if (!hasYieldPair) downgrades.push('yield-events-missing');
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const invalid = droppedFields.length > 0 || !routeIsPresent(route);
|
|
357
|
+
const unsupported = unsupportedFields.length > 0 || boundaryAssertions.some(assertion => assertion?.status === 'unsupported');
|
|
358
|
+
const verified = events.length > 0
|
|
359
|
+
&& requestedBoundaryKeys.length > 0
|
|
360
|
+
&& verifiedAssertions.length > 0
|
|
361
|
+
&& requestedBoundaryKeys.every(key => verifiedAssertions.some(assertion => assertionVerifiesRequestedKey(assertion, key, events)))
|
|
362
|
+
&& !falseAuthorityChecks.timingProxyOnly
|
|
363
|
+
&& !falseAuthorityChecks.queueWaitEventsMissing
|
|
364
|
+
&& !falseAuthorityChecks.boundaryAssertionEventMismatch
|
|
365
|
+
&& !falseAuthorityChecks.requestedBoundaryAssertionMissing
|
|
366
|
+
&& !falseAuthorityChecks.requestedFieldDroppedWithoutUnsupported
|
|
367
|
+
&& !downgrades.includes('yield-events-missing');
|
|
368
|
+
|
|
369
|
+
let status = input.status || 'scheduler-unverified';
|
|
370
|
+
if (invalid) status = 'invalid';
|
|
371
|
+
else if (unsupported) status = 'unsupported';
|
|
372
|
+
else if (verified) status = 'verified';
|
|
373
|
+
else status = 'scheduler-unverified';
|
|
374
|
+
|
|
375
|
+
const classification = status === 'verified'
|
|
376
|
+
? 'observed-boundary'
|
|
377
|
+
: (status === 'unsupported'
|
|
378
|
+
? 'unsupported'
|
|
379
|
+
: (status === 'invalid'
|
|
380
|
+
? 'invalid'
|
|
381
|
+
: (falseAuthorityChecks.timingProxyOnly ? 'damage-only' : 'config-only')));
|
|
382
|
+
const observationClass = deriveObservationClass({
|
|
383
|
+
status,
|
|
384
|
+
boundaryAssertions,
|
|
385
|
+
falseAuthorityChecks,
|
|
386
|
+
events,
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
return {
|
|
390
|
+
schema: SCHEDULER_VERIFICATION_RECEIPT_SCHEMA,
|
|
391
|
+
status,
|
|
392
|
+
classification,
|
|
393
|
+
observationClass,
|
|
394
|
+
route,
|
|
395
|
+
scheduler: {
|
|
396
|
+
...scheduler,
|
|
397
|
+
unsupportedFields,
|
|
398
|
+
},
|
|
399
|
+
backpressure,
|
|
400
|
+
eventTrace,
|
|
401
|
+
boundaryAssertions,
|
|
402
|
+
frameTail,
|
|
403
|
+
downgrades: normalizeDowngrades(downgrades),
|
|
404
|
+
falseAuthorityChecks,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export function validateSchedulerVerificationReceipt(receipt = {}) {
|
|
409
|
+
const errors = [];
|
|
410
|
+
if (receipt.schema !== SCHEDULER_VERIFICATION_RECEIPT_SCHEMA) errors.push('schema-mismatch');
|
|
411
|
+
if (!routeIsPresent(receipt.route || {})) errors.push('route-identity-missing');
|
|
412
|
+
if (!receipt.scheduler || typeof receipt.scheduler !== 'object') errors.push('scheduler-envelope-missing');
|
|
413
|
+
if (!receipt.backpressure || typeof receipt.backpressure !== 'object') errors.push('backpressure-envelope-missing');
|
|
414
|
+
if (receipt.status === 'verified') {
|
|
415
|
+
if (!Array.isArray(receipt.eventTrace?.events) || !receipt.eventTrace.events.length) errors.push('verified-without-event-trace');
|
|
416
|
+
if (!Array.isArray(receipt.boundaryAssertions) || !receipt.boundaryAssertions.some(assertion => assertion?.status === 'verified')) {
|
|
417
|
+
errors.push('verified-without-boundary-assertion');
|
|
418
|
+
}
|
|
419
|
+
if (receipt.falseAuthorityChecks?.timingProxyOnly) errors.push('verified-from-proxy-timing');
|
|
420
|
+
if (receipt.falseAuthorityChecks?.queueWaitEventsMissing) errors.push('verified-without-queue-wait-events');
|
|
421
|
+
if (receipt.falseAuthorityChecks?.boundaryAssertionEventMismatch) errors.push('verified-with-mismatched-boundary-assertion');
|
|
422
|
+
if (receipt.falseAuthorityChecks?.requestedBoundaryAssertionMissing) errors.push('verified-without-requested-boundary-assertion');
|
|
423
|
+
if (receipt.falseAuthorityChecks?.requestedFieldDroppedWithoutUnsupported) errors.push('verified-with-dropped-requested-field');
|
|
424
|
+
if (Array.isArray(receipt.downgrades) && receipt.downgrades.includes('yield-events-missing')) {
|
|
425
|
+
errors.push('verified-without-yield-events');
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (receipt.status === 'invalid') errors.push('receipt-invalid');
|
|
429
|
+
return {
|
|
430
|
+
ok: errors.length === 0,
|
|
431
|
+
errors,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export function classifySchedulerVerificationReceipt(receipt = {}) {
|
|
436
|
+
return {
|
|
437
|
+
status: receipt.status || 'unknown',
|
|
438
|
+
classification: receipt.classification || 'unknown',
|
|
439
|
+
observationClass: receipt.observationClass || 'unknown',
|
|
440
|
+
downgrades: Array.isArray(receipt.downgrades) ? receipt.downgrades : [],
|
|
441
|
+
};
|
|
442
|
+
}
|