supersonic-scsynth 0.1.9 → 0.2.0

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "wasmFile": "scsynth-nrt.wasm",
3
- "buildId": "20251105-102519",
4
- "buildTime": "2025-11-05T10:25:19Z",
5
- "gitHash": "4ea9bb5"
3
+ "buildId": "20251112-224002",
4
+ "buildTime": "2025-11-12T22:40:02Z",
5
+ "gitHash": "82a71b5"
6
6
  }
Binary file
@@ -29,6 +29,13 @@ var CONTROL_INDICES = {};
29
29
  // Worker state
30
30
  var running = false;
31
31
 
32
+ var DEBUG_DEBUGWORKER_LOGS = false;
33
+ function debugWorkerLog() {
34
+ if (DEBUG_DEBUGWORKER_LOGS) {
35
+ console.log.apply(console, arguments);
36
+ }
37
+ }
38
+
32
39
  // Statistics
33
40
  var stats = {
34
41
  messagesReceived: 0,
@@ -69,12 +76,18 @@ function readDebugMessages() {
69
76
  var messages = [];
70
77
  var currentTail = tail;
71
78
  var messagesRead = 0;
72
- var maxMessages = 10; // Process up to 10 messages per wake
79
+ var maxMessages = 1000; // Process up to 1000 messages per wake
73
80
 
74
81
  while (currentTail !== head && messagesRead < maxMessages) {
82
+ var bytesToEnd = bufferConstants.DEBUG_BUFFER_SIZE - currentTail;
83
+ if (bytesToEnd < bufferConstants.MESSAGE_HEADER_SIZE) {
84
+ currentTail = 0;
85
+ continue;
86
+ }
87
+
75
88
  var readPos = ringBufferBase + bufferConstants.DEBUG_BUFFER_START + currentTail;
76
89
 
77
- // Read message header (now always contiguous due to padding)
90
+ // Read message header (now contiguous or wrapped)
78
91
  var magic = dataView.getUint32(readPos, true);
79
92
 
80
93
  // Check for padding marker - skip to beginning
@@ -271,4 +284,4 @@ self.onmessage = function(event) {
271
284
  }
272
285
  };
273
286
 
274
- console.log('[DebugWorker] Script loaded');
287
+ debugWorkerLog('[DebugWorker] Script loaded');
@@ -29,6 +29,13 @@ var CONTROL_INDICES = {};
29
29
  // Worker state
30
30
  var running = false;
31
31
 
32
+ var DEBUG_OSCIN_LOGS = false;
33
+ function oscInLog() {
34
+ if (DEBUG_OSCIN_LOGS) {
35
+ console.log.apply(console, arguments);
36
+ }
37
+ }
38
+
32
39
  // Statistics
33
40
  var stats = {
34
41
  messagesReceived: 0,
@@ -74,9 +81,15 @@ function readMessages() {
74
81
  var maxMessages = 100;
75
82
 
76
83
  while (currentTail !== head && messagesRead < maxMessages) {
84
+ var bytesToEnd = bufferConstants.OUT_BUFFER_SIZE - currentTail;
85
+ if (bytesToEnd < bufferConstants.MESSAGE_HEADER_SIZE) {
86
+ currentTail = 0;
87
+ continue;
88
+ }
89
+
77
90
  var readPos = ringBufferBase + bufferConstants.OUT_BUFFER_START + currentTail;
78
91
 
79
- // Read message header (now always contiguous due to padding)
92
+ // Read message header (now contiguous or wrapped)
80
93
  var magic = dataView.getUint32(readPos, true);
81
94
 
82
95
  // Check for padding marker - skip to beginning
@@ -267,4 +280,4 @@ self.onmessage = function(event) {
267
280
  }
268
281
  };
269
282
 
270
- console.log('[OSCInWorker] Script loaded');
283
+ oscInLog('[OSCInWorker] Script loaded');
@@ -0,0 +1,575 @@
1
+ /*
2
+ SuperSonic - OSC Pre-Scheduler Worker
3
+ Ports the Bleep pre-scheduler design:
4
+ - Single priority queue of future bundles/events
5
+ - One timer driving dispatch (no per-event setTimeout storm)
6
+ - Tag-based cancellation to drop pending runs before they hit WASM
7
+ */
8
+
9
+ // Shared memory for ring buffer writing
10
+ var sharedBuffer = null;
11
+ var ringBufferBase = null;
12
+ var bufferConstants = null;
13
+ var atomicView = null;
14
+ var dataView = null;
15
+ var uint8View = null;
16
+
17
+ // Ring buffer control indices
18
+ var CONTROL_INDICES = {};
19
+
20
+ // Priority queue implemented as binary min-heap
21
+ // Entries: { ntpTime, seq, editorId, runTag, oscData }
22
+ var eventHeap = [];
23
+ var periodicTimer = null; // Single periodic timer (25ms interval)
24
+ var sequenceCounter = 0;
25
+ var isDispatching = false; // Prevent reentrancy into dispatch loop
26
+
27
+ // Statistics
28
+ var stats = {
29
+ bundlesScheduled: 0,
30
+ bundlesWritten: 0,
31
+ bundlesDropped: 0,
32
+ bufferOverruns: 0,
33
+ eventsPending: 0,
34
+ maxEventsPending: 0,
35
+ eventsCancelled: 0,
36
+ totalDispatches: 0,
37
+ totalLateDispatchMs: 0,
38
+ maxLateDispatchMs: 0,
39
+ totalSendTasks: 0,
40
+ totalSendProcessMs: 0,
41
+ maxSendProcessMs: 0
42
+ };
43
+
44
+ // Timing constants
45
+ var NTP_EPOCH_OFFSET = 2208988800; // Seconds from 1900-01-01 to 1970-01-01
46
+ var POLL_INTERVAL_MS = 25; // Check every 25ms
47
+ var LOOKAHEAD_S = 0.100; // 100ms lookahead window
48
+
49
+ function schedulerLog() {
50
+ // Toggle to true for verbose diagnostics
51
+ var DEBUG = true; // Enable for debugging
52
+ if (DEBUG) {
53
+ console.log.apply(console, arguments);
54
+ }
55
+ }
56
+
57
+ // ============================================================================
58
+ // NTP TIME HELPERS
59
+ // ============================================================================
60
+
61
+ /**
62
+ * Get current NTP time from system clock
63
+ *
64
+ * Bundles contain full NTP timestamps. We just need to compare them against
65
+ * current NTP time (system clock) to know when to dispatch.
66
+ *
67
+ * AudioContext timing, drift correction, etc. are handled by the C++ side.
68
+ * The prescheduler only needs to know "what time is it now in NTP?"
69
+ */
70
+ function getCurrentNTP() {
71
+ // Convert current system time to NTP
72
+ var perfTimeMs = performance.timeOrigin + performance.now();
73
+ return (perfTimeMs / 1000) + NTP_EPOCH_OFFSET;
74
+ }
75
+
76
+ /**
77
+ * Extract NTP timestamp from OSC bundle
78
+ * Returns NTP time in seconds (double), or null if not a bundle
79
+ */
80
+ function extractNTPFromBundle(oscData) {
81
+ if (oscData.length >= 16 && oscData[0] === 0x23) { // '#bundle'
82
+ var view = new DataView(oscData.buffer, oscData.byteOffset);
83
+ var ntpSeconds = view.getUint32(8, false);
84
+ var ntpFraction = view.getUint32(12, false);
85
+ return ntpSeconds + ntpFraction / 0x100000000;
86
+ }
87
+ return null;
88
+ }
89
+
90
+ /**
91
+ * Legacy wrapper for backwards compatibility
92
+ */
93
+ function getBundleTimestamp(oscMessage) {
94
+ return extractNTPFromBundle(oscMessage);
95
+ }
96
+
97
+ // ============================================================================
98
+ // SHARED ARRAY BUFFER ACCESS
99
+ // ============================================================================
100
+
101
+ /**
102
+ * Initialize ring buffer access for writing directly to SharedArrayBuffer
103
+ */
104
+ function initSharedBuffer() {
105
+ if (!sharedBuffer || !bufferConstants) {
106
+ console.error('[PreScheduler] Cannot init - missing buffer or constants');
107
+ return;
108
+ }
109
+
110
+ atomicView = new Int32Array(sharedBuffer);
111
+ dataView = new DataView(sharedBuffer);
112
+ uint8View = new Uint8Array(sharedBuffer);
113
+
114
+ // Calculate control indices for ring buffer
115
+ CONTROL_INDICES = {
116
+ IN_HEAD: (ringBufferBase + bufferConstants.CONTROL_START + 0) / 4,
117
+ IN_TAIL: (ringBufferBase + bufferConstants.CONTROL_START + 4) / 4
118
+ };
119
+
120
+ console.log('[PreScheduler] SharedArrayBuffer initialized with direct ring buffer writing');
121
+ }
122
+
123
+ /**
124
+ * Write OSC message directly to ring buffer (replaces MessagePort to writer worker)
125
+ * This is now the ONLY place that writes to the ring buffer
126
+ */
127
+ function writeToRingBuffer(oscMessage) {
128
+ if (!sharedBuffer || !atomicView) {
129
+ console.error('[PreScheduler] Not initialized for ring buffer writing');
130
+ stats.bundlesDropped++;
131
+ return false;
132
+ }
133
+
134
+ var payloadSize = oscMessage.length;
135
+ var totalSize = bufferConstants.MESSAGE_HEADER_SIZE + payloadSize;
136
+
137
+ // Check if message fits in buffer at all
138
+ if (totalSize > bufferConstants.IN_BUFFER_SIZE - bufferConstants.MESSAGE_HEADER_SIZE) {
139
+ console.error('[PreScheduler] Message too large:', totalSize);
140
+ stats.bundlesDropped++;
141
+ return false;
142
+ }
143
+
144
+ // Try to write (non-blocking, single attempt)
145
+ var head = Atomics.load(atomicView, CONTROL_INDICES.IN_HEAD);
146
+ var tail = Atomics.load(atomicView, CONTROL_INDICES.IN_TAIL);
147
+
148
+ // Calculate available space
149
+ var available = (bufferConstants.IN_BUFFER_SIZE - 1 - head + tail) % bufferConstants.IN_BUFFER_SIZE;
150
+
151
+ if (available < totalSize) {
152
+ // Buffer full - drop message (prescheduler should not block)
153
+ stats.bufferOverruns++;
154
+ stats.bundlesDropped++;
155
+ console.warn('[PreScheduler] Ring buffer full, dropping message');
156
+ return false;
157
+ }
158
+
159
+ // Check if message fits contiguously
160
+ var spaceToEnd = bufferConstants.IN_BUFFER_SIZE - head;
161
+
162
+ if (totalSize > spaceToEnd) {
163
+ if (spaceToEnd >= bufferConstants.MESSAGE_HEADER_SIZE) {
164
+ // Write padding marker and wrap
165
+ var paddingPos = ringBufferBase + bufferConstants.IN_BUFFER_START + head;
166
+ dataView.setUint32(paddingPos, bufferConstants.PADDING_MAGIC, true);
167
+ dataView.setUint32(paddingPos + 4, 0, true);
168
+ dataView.setUint32(paddingPos + 8, 0, true);
169
+ dataView.setUint32(paddingPos + 12, 0, true);
170
+ } else if (spaceToEnd > 0) {
171
+ // Not enough room for a padding header - clear remaining bytes
172
+ var padStart = ringBufferBase + bufferConstants.IN_BUFFER_START + head;
173
+ for (var i = 0; i < spaceToEnd; i++) {
174
+ uint8View[padStart + i] = 0;
175
+ }
176
+ }
177
+
178
+ // Wrap to beginning
179
+ head = 0;
180
+ }
181
+
182
+ // Write message
183
+ var writePos = ringBufferBase + bufferConstants.IN_BUFFER_START + head;
184
+
185
+ // Write header
186
+ dataView.setUint32(writePos, bufferConstants.MESSAGE_MAGIC, true);
187
+ dataView.setUint32(writePos + 4, totalSize, true);
188
+ dataView.setUint32(writePos + 8, stats.bundlesWritten, true); // sequence
189
+ dataView.setUint32(writePos + 12, 0, true); // padding
190
+
191
+ // Write payload
192
+ uint8View.set(oscMessage, writePos + bufferConstants.MESSAGE_HEADER_SIZE);
193
+
194
+ // Update head pointer (publish message)
195
+ var newHead = (head + totalSize) % bufferConstants.IN_BUFFER_SIZE;
196
+ Atomics.store(atomicView, CONTROL_INDICES.IN_HEAD, newHead);
197
+
198
+ stats.bundlesWritten++;
199
+ return true;
200
+ }
201
+
202
+ /**
203
+ * Schedule an OSC bundle by its NTP timestamp
204
+ * Non-bundles or bundles without timestamps are dispatched immediately
205
+ */
206
+ function scheduleEvent(oscData, editorId, runTag) {
207
+ var ntpTime = extractNTPFromBundle(oscData);
208
+
209
+ if (ntpTime === null) {
210
+ // Not a bundle - dispatch immediately to ring buffer
211
+ schedulerLog('[PreScheduler] Non-bundle message, dispatching immediately');
212
+ writeToRingBuffer(oscData);
213
+ return;
214
+ }
215
+
216
+ var currentNTP = getCurrentNTP();
217
+ var timeUntilExec = ntpTime - currentNTP;
218
+
219
+ // Create event with NTP timestamp
220
+ var event = {
221
+ ntpTime: ntpTime,
222
+ seq: sequenceCounter++,
223
+ editorId: editorId || 0,
224
+ runTag: runTag || '',
225
+ oscData: oscData
226
+ };
227
+
228
+ heapPush(event);
229
+
230
+ stats.bundlesScheduled++;
231
+ stats.eventsPending = eventHeap.length;
232
+ if (stats.eventsPending > stats.maxEventsPending) {
233
+ stats.maxEventsPending = stats.eventsPending;
234
+ }
235
+
236
+ schedulerLog('[PreScheduler] Scheduled bundle:',
237
+ 'NTP=' + ntpTime.toFixed(3),
238
+ 'current=' + currentNTP.toFixed(3),
239
+ 'wait=' + (timeUntilExec * 1000).toFixed(1) + 'ms',
240
+ 'pending=' + stats.eventsPending);
241
+ }
242
+
243
+ function heapPush(event) {
244
+ eventHeap.push(event);
245
+ siftUp(eventHeap.length - 1);
246
+ }
247
+
248
+ function heapPeek() {
249
+ return eventHeap.length > 0 ? eventHeap[0] : null;
250
+ }
251
+
252
+ function heapPop() {
253
+ if (eventHeap.length === 0) {
254
+ return null;
255
+ }
256
+ var top = eventHeap[0];
257
+ var last = eventHeap.pop();
258
+ if (eventHeap.length > 0) {
259
+ eventHeap[0] = last;
260
+ siftDown(0);
261
+ }
262
+ return top;
263
+ }
264
+
265
+ function siftUp(index) {
266
+ while (index > 0) {
267
+ var parent = Math.floor((index - 1) / 2);
268
+ if (compareEvents(eventHeap[index], eventHeap[parent]) >= 0) {
269
+ break;
270
+ }
271
+ swap(index, parent);
272
+ index = parent;
273
+ }
274
+ }
275
+
276
+ function siftDown(index) {
277
+ var length = eventHeap.length;
278
+ while (true) {
279
+ var left = 2 * index + 1;
280
+ var right = 2 * index + 2;
281
+ var smallest = index;
282
+
283
+ if (left < length && compareEvents(eventHeap[left], eventHeap[smallest]) < 0) {
284
+ smallest = left;
285
+ }
286
+ if (right < length && compareEvents(eventHeap[right], eventHeap[smallest]) < 0) {
287
+ smallest = right;
288
+ }
289
+ if (smallest === index) {
290
+ break;
291
+ }
292
+ swap(index, smallest);
293
+ index = smallest;
294
+ }
295
+ }
296
+
297
+ function compareEvents(a, b) {
298
+ if (a.ntpTime === b.ntpTime) {
299
+ return a.seq - b.seq;
300
+ }
301
+ return a.ntpTime - b.ntpTime;
302
+ }
303
+
304
+ function swap(i, j) {
305
+ var tmp = eventHeap[i];
306
+ eventHeap[i] = eventHeap[j];
307
+ eventHeap[j] = tmp;
308
+ }
309
+
310
+ /**
311
+ * Start periodic polling (called once on init)
312
+ */
313
+ function startPeriodicPolling() {
314
+ if (periodicTimer !== null) {
315
+ console.warn('[PreScheduler] Polling already started');
316
+ return;
317
+ }
318
+
319
+ console.log('[PreScheduler] Starting periodic polling (every ' + POLL_INTERVAL_MS + 'ms)');
320
+ checkAndDispatch(); // Start immediately
321
+ }
322
+
323
+ /**
324
+ * Stop periodic polling
325
+ */
326
+ function stopPeriodicPolling() {
327
+ if (periodicTimer !== null) {
328
+ clearTimeout(periodicTimer);
329
+ periodicTimer = null;
330
+ console.log('[PreScheduler] Stopped periodic polling');
331
+ }
332
+ }
333
+
334
+ /**
335
+ * Periodic check and dispatch function
336
+ * Uses NTP timestamps and global offset for drift-free timing
337
+ */
338
+ function checkAndDispatch() {
339
+ isDispatching = true;
340
+
341
+ var currentNTP = getCurrentNTP();
342
+ var lookaheadTime = currentNTP + LOOKAHEAD_S;
343
+ var dispatchCount = 0;
344
+ var dispatchStart = performance.now();
345
+
346
+ // Dispatch all bundles that are ready
347
+ while (eventHeap.length > 0) {
348
+ var nextEvent = heapPeek();
349
+
350
+ if (nextEvent.ntpTime <= lookaheadTime) {
351
+ // Ready to dispatch
352
+ heapPop();
353
+ stats.eventsPending = eventHeap.length;
354
+
355
+ var timeUntilExec = nextEvent.ntpTime - currentNTP;
356
+ stats.totalDispatches++;
357
+
358
+ schedulerLog('[PreScheduler] Dispatching bundle:',
359
+ 'NTP=' + nextEvent.ntpTime.toFixed(3),
360
+ 'current=' + currentNTP.toFixed(3),
361
+ 'early=' + (timeUntilExec * 1000).toFixed(1) + 'ms',
362
+ 'remaining=' + stats.eventsPending);
363
+
364
+ writeToRingBuffer(nextEvent.oscData);
365
+ dispatchCount++;
366
+ } else {
367
+ // Rest aren't ready yet (heap is sorted)
368
+ break;
369
+ }
370
+ }
371
+
372
+ if (dispatchCount > 0 || eventHeap.length > 0) {
373
+ schedulerLog('[PreScheduler] Dispatch cycle complete:',
374
+ 'dispatched=' + dispatchCount,
375
+ 'pending=' + eventHeap.length);
376
+ }
377
+
378
+ isDispatching = false;
379
+
380
+ // Reschedule for next check (fixed interval)
381
+ periodicTimer = setTimeout(checkAndDispatch, POLL_INTERVAL_MS);
382
+ }
383
+
384
+ function cancelBy(predicate) {
385
+ if (eventHeap.length === 0) {
386
+ return;
387
+ }
388
+
389
+ var before = eventHeap.length;
390
+ var remaining = [];
391
+
392
+ for (var i = 0; i < eventHeap.length; i++) {
393
+ var event = eventHeap[i];
394
+ if (!predicate(event)) {
395
+ remaining.push(event);
396
+ }
397
+ }
398
+
399
+ var removed = before - remaining.length;
400
+ if (removed > 0) {
401
+ eventHeap = remaining;
402
+ heapify();
403
+ stats.eventsCancelled += removed;
404
+ stats.eventsPending = eventHeap.length;
405
+ console.log('[PreScheduler] Cancelled ' + removed + ' events, ' + eventHeap.length + ' remaining');
406
+ }
407
+ }
408
+
409
+ function heapify() {
410
+ for (var i = Math.floor(eventHeap.length / 2) - 1; i >= 0; i--) {
411
+ siftDown(i);
412
+ }
413
+ }
414
+
415
+ function cancelEditorTag(editorId, runTag) {
416
+ cancelBy(function(event) {
417
+ return event.editorId === editorId && event.runTag === runTag;
418
+ });
419
+ }
420
+
421
+ function cancelEditor(editorId) {
422
+ cancelBy(function(event) {
423
+ return event.editorId === editorId;
424
+ });
425
+ }
426
+
427
+ function cancelAllTags() {
428
+ if (eventHeap.length === 0) {
429
+ return;
430
+ }
431
+ var cancelled = eventHeap.length;
432
+ stats.eventsCancelled += cancelled;
433
+ eventHeap = [];
434
+ stats.eventsPending = 0;
435
+ console.log('[PreScheduler] Cancelled all ' + cancelled + ' events');
436
+ // Note: Periodic timer continues running (it will just find empty queue)
437
+ }
438
+
439
+ // Helpers reused from legacy worker for immediate send
440
+ function isBundle(data) {
441
+ if (!data || data.length < 8) {
442
+ return false;
443
+ }
444
+ return data[0] === 0x23 && data[1] === 0x62 && data[2] === 0x75 && data[3] === 0x6e &&
445
+ data[4] === 0x64 && data[5] === 0x6c && data[6] === 0x65 && data[7] === 0x00;
446
+ }
447
+
448
+ function extractMessagesFromBundle(data) {
449
+ var messages = [];
450
+ var view = new DataView(data.buffer, data.byteOffset, data.byteLength);
451
+ var offset = 16; // skip "#bundle\0" + timetag
452
+
453
+ while (offset < data.length) {
454
+ var messageSize = view.getInt32(offset, false);
455
+ offset += 4;
456
+
457
+ if (messageSize <= 0 || offset + messageSize > data.length) {
458
+ break;
459
+ }
460
+
461
+ var messageData = data.slice(offset, offset + messageSize);
462
+ messages.push(messageData);
463
+ offset += messageSize;
464
+
465
+ while (offset % 4 !== 0 && offset < data.length) {
466
+ offset++;
467
+ }
468
+ }
469
+
470
+ return messages;
471
+ }
472
+
473
+ function processImmediate(oscData) {
474
+ if (isBundle(oscData)) {
475
+ var messages = extractMessagesFromBundle(oscData);
476
+ for (var i = 0; i < messages.length; i++) {
477
+ writeToRingBuffer(messages[i]);
478
+ }
479
+ } else {
480
+ writeToRingBuffer(oscData);
481
+ }
482
+ }
483
+
484
+ // Message handling
485
+ self.onmessage = function(event) {
486
+ var data = event.data;
487
+
488
+ try {
489
+ switch (data.type) {
490
+ case 'init':
491
+ sharedBuffer = data.sharedBuffer;
492
+ ringBufferBase = data.ringBufferBase;
493
+ bufferConstants = data.bufferConstants;
494
+
495
+ // Initialize SharedArrayBuffer views (including offset)
496
+ initSharedBuffer();
497
+
498
+ // Start periodic polling
499
+ startPeriodicPolling();
500
+
501
+ schedulerLog('[OSCPreSchedulerWorker] Initialized with NTP-based scheduling and direct ring buffer writing');
502
+ self.postMessage({ type: 'initialized' });
503
+ break;
504
+
505
+ case 'send':
506
+ var sendStart = performance.now();
507
+
508
+ // New NTP-based scheduling: extract NTP from bundle
509
+ // scheduleEvent() will dispatch immediately if not a bundle
510
+ scheduleEvent(
511
+ data.oscData,
512
+ data.editorId || 0,
513
+ data.runTag || ''
514
+ );
515
+
516
+ var sendDuration = performance.now() - sendStart;
517
+ stats.totalSendTasks++;
518
+ stats.totalSendProcessMs += sendDuration;
519
+ if (sendDuration > stats.maxSendProcessMs) {
520
+ stats.maxSendProcessMs = sendDuration;
521
+ }
522
+ break;
523
+
524
+ case 'sendImmediate':
525
+ processImmediate(data.oscData);
526
+ break;
527
+
528
+ case 'cancelEditorTag':
529
+ if (data.runTag !== undefined && data.runTag !== null && data.runTag !== '') {
530
+ cancelEditorTag(data.editorId || 0, data.runTag);
531
+ }
532
+ break;
533
+
534
+ case 'cancelEditor':
535
+ cancelEditor(data.editorId || 0);
536
+ break;
537
+
538
+ case 'cancelAll':
539
+ cancelAllTags();
540
+ break;
541
+
542
+ case 'getStats':
543
+ self.postMessage({
544
+ type: 'stats',
545
+ stats: {
546
+ bundlesScheduled: stats.bundlesScheduled,
547
+ bundlesWritten: stats.bundlesWritten,
548
+ bundlesDropped: stats.bundlesDropped,
549
+ bufferOverruns: stats.bufferOverruns,
550
+ eventsPending: stats.eventsPending,
551
+ maxEventsPending: stats.maxEventsPending,
552
+ eventsCancelled: stats.eventsCancelled,
553
+ totalDispatches: stats.totalDispatches,
554
+ totalLateDispatchMs: stats.totalLateDispatchMs,
555
+ maxLateDispatchMs: stats.maxLateDispatchMs,
556
+ totalSendTasks: stats.totalSendTasks,
557
+ totalSendProcessMs: stats.totalSendProcessMs,
558
+ maxSendProcessMs: stats.maxSendProcessMs
559
+ }
560
+ });
561
+ break;
562
+
563
+ default:
564
+ console.warn('[OSCPreSchedulerWorker] Unknown message type:', data.type);
565
+ }
566
+ } catch (error) {
567
+ console.error('[OSCPreSchedulerWorker] Error:', error);
568
+ self.postMessage({
569
+ type: 'error',
570
+ error: error.message
571
+ });
572
+ }
573
+ };
574
+
575
+ schedulerLog('[OSCPreSchedulerWorker] Script loaded');