supersonic-scsynth 0.4.0 → 0.6.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": "20251118-093603",
4
- "buildTime": "2025-11-18T09:36:03Z",
5
- "gitHash": "d43c4be"
3
+ "buildId": "20251121-192655",
4
+ "buildTime": "2025-11-21T19:26:55Z",
5
+ "gitHash": "467357e0e8"
6
6
  }
Binary file
@@ -26,6 +26,10 @@ var bufferConstants = null;
26
26
  // Control indices (calculated after init)
27
27
  var CONTROL_INDICES = {};
28
28
 
29
+ // Metrics view (for writing stats to SAB)
30
+ var metricsView = null;
31
+ var METRICS_INDICES = {};
32
+
29
33
  // Worker state
30
34
  var running = false;
31
35
 
@@ -36,14 +40,6 @@ function debugWorkerLog() {
36
40
  }
37
41
  }
38
42
 
39
- // Statistics
40
- var stats = {
41
- messagesReceived: 0,
42
- wakeups: 0,
43
- timeouts: 0,
44
- bytesRead: 0
45
- };
46
-
47
43
  /**
48
44
  * Initialize ring buffer access
49
45
  */
@@ -60,6 +56,17 @@ function initRingBuffer(buffer, base, constants) {
60
56
  DEBUG_HEAD: (ringBufferBase + bufferConstants.CONTROL_START + 16) / 4,
61
57
  DEBUG_TAIL: (ringBufferBase + bufferConstants.CONTROL_START + 20) / 4
62
58
  };
59
+
60
+ // Initialize metrics view (Debug metrics are at offsets 23-26 in the metrics array)
61
+ var metricsBase = ringBufferBase + bufferConstants.METRICS_START;
62
+ metricsView = new Uint32Array(sharedBuffer, metricsBase, bufferConstants.METRICS_SIZE / 4);
63
+
64
+ METRICS_INDICES = {
65
+ MESSAGES_RECEIVED: 23,
66
+ WAKEUPS: 24,
67
+ TIMEOUTS: 25,
68
+ BYTES_READ: 26
69
+ };
63
70
  }
64
71
 
65
72
  /**
@@ -137,13 +144,13 @@ function readDebugMessages() {
137
144
  // Move to next message
138
145
  currentTail = (currentTail + length) % bufferConstants.DEBUG_BUFFER_SIZE;
139
146
  messagesRead++;
140
- stats.messagesReceived++;
147
+ if (metricsView) Atomics.add(metricsView, METRICS_INDICES.MESSAGES_RECEIVED, 1);
141
148
  }
142
149
 
143
150
  // Update tail pointer (consume messages)
144
151
  if (messagesRead > 0) {
145
152
  Atomics.store(atomicView, CONTROL_INDICES.DEBUG_TAIL, currentTail);
146
- stats.bytesRead += messagesRead;
153
+ if (metricsView) Atomics.add(metricsView, METRICS_INDICES.BYTES_READ, messagesRead);
147
154
  }
148
155
 
149
156
  return messages.length > 0 ? messages : null;
@@ -166,9 +173,9 @@ function waitLoop() {
166
173
 
167
174
  if (result === 'ok' || result === 'not-equal') {
168
175
  // We were notified or value changed!
169
- stats.wakeups++;
176
+ if (metricsView) Atomics.add(metricsView, METRICS_INDICES.WAKEUPS, 1);
170
177
  } else if (result === 'timed-out') {
171
- stats.timeouts++;
178
+ if (metricsView) Atomics.add(metricsView, METRICS_INDICES.TIMEOUTS, 1);
172
179
  continue; // Check running flag
173
180
  }
174
181
  }
@@ -180,13 +187,7 @@ function waitLoop() {
180
187
  // Send to main thread
181
188
  self.postMessage({
182
189
  type: 'debug',
183
- messages: messages,
184
- stats: {
185
- wakeups: stats.wakeups,
186
- timeouts: stats.timeouts,
187
- messagesReceived: stats.messagesReceived,
188
- bytesRead: stats.bytesRead
189
- }
190
+ messages: messages
190
191
  });
191
192
  }
192
193
 
@@ -265,13 +266,6 @@ self.addEventListener('message', function(event) {
265
266
  clear();
266
267
  break;
267
268
 
268
- case 'getStats':
269
- self.postMessage({
270
- type: 'stats',
271
- stats: stats
272
- });
273
- break;
274
-
275
269
  default:
276
270
  console.warn('[DebugWorker] Unknown message type:', data.type);
277
271
  }
@@ -26,6 +26,10 @@ var bufferConstants = null;
26
26
  // Control indices (calculated after init)
27
27
  var CONTROL_INDICES = {};
28
28
 
29
+ // Metrics view (for writing stats to SAB)
30
+ var metricsView = null;
31
+ var METRICS_INDICES = {};
32
+
29
33
  // Worker state
30
34
  var running = false;
31
35
 
@@ -36,14 +40,8 @@ function oscInLog() {
36
40
  }
37
41
  }
38
42
 
39
- // Statistics
40
- var stats = {
41
- messagesReceived: 0,
42
- lastSequenceReceived: -1,
43
- droppedMessages: 0,
44
- wakeups: 0,
45
- timeouts: 0
46
- };
43
+ // Sequence tracking for dropped message detection
44
+ var lastSequenceReceived = -1;
47
45
 
48
46
  /**
49
47
  * Initialize ring buffer access
@@ -61,6 +59,17 @@ function initRingBuffer(buffer, base, constants) {
61
59
  OUT_HEAD: (ringBufferBase + bufferConstants.CONTROL_START + 8) / 4,
62
60
  OUT_TAIL: (ringBufferBase + bufferConstants.CONTROL_START + 12) / 4
63
61
  };
62
+
63
+ // Initialize metrics view (OSC In metrics are at offsets 19-22 in the metrics array)
64
+ var metricsBase = ringBufferBase + bufferConstants.METRICS_START;
65
+ metricsView = new Uint32Array(sharedBuffer, metricsBase, bufferConstants.METRICS_SIZE / 4);
66
+
67
+ METRICS_INDICES = {
68
+ MESSAGES_RECEIVED: 19,
69
+ DROPPED_MESSAGES: 20,
70
+ WAKEUPS: 21,
71
+ TIMEOUTS: 22
72
+ };
64
73
  }
65
74
 
66
75
  /**
@@ -100,7 +109,7 @@ function readMessages() {
100
109
 
101
110
  if (magic !== bufferConstants.MESSAGE_MAGIC) {
102
111
  console.error('[OSCInWorker] Corrupted message at position', currentTail);
103
- stats.droppedMessages++;
112
+ if (metricsView) Atomics.add(metricsView, METRICS_INDICES.DROPPED_MESSAGES, 1);
104
113
  // Skip this byte and continue
105
114
  currentTail = (currentTail + 1) % bufferConstants.OUT_BUFFER_SIZE;
106
115
  continue;
@@ -113,23 +122,23 @@ function readMessages() {
113
122
  // Validate message length
114
123
  if (length < bufferConstants.MESSAGE_HEADER_SIZE || length > bufferConstants.OUT_BUFFER_SIZE) {
115
124
  console.error('[OSCInWorker] Invalid message length:', length);
116
- stats.droppedMessages++;
125
+ if (metricsView) Atomics.add(metricsView, METRICS_INDICES.DROPPED_MESSAGES, 1);
117
126
  currentTail = (currentTail + 1) % bufferConstants.OUT_BUFFER_SIZE;
118
127
  continue;
119
128
  }
120
129
 
121
130
  // Check for dropped messages via sequence
122
- if (stats.lastSequenceReceived >= 0) {
123
- var expectedSeq = (stats.lastSequenceReceived + 1) & 0xFFFFFFFF;
131
+ if (lastSequenceReceived >= 0) {
132
+ var expectedSeq = (lastSequenceReceived + 1) & 0xFFFFFFFF;
124
133
  if (sequence !== expectedSeq) {
125
134
  var dropped = (sequence - expectedSeq + 0x100000000) & 0xFFFFFFFF;
126
135
  if (dropped < 1000) { // Sanity check
127
136
  console.warn('[OSCInWorker] Detected', dropped, 'dropped messages (expected seq', expectedSeq, 'got', sequence, ')');
128
- stats.droppedMessages += dropped;
137
+ if (metricsView) Atomics.add(metricsView, METRICS_INDICES.DROPPED_MESSAGES, dropped);
129
138
  }
130
139
  }
131
140
  }
132
- stats.lastSequenceReceived = sequence;
141
+ lastSequenceReceived = sequence;
133
142
 
134
143
  // Read payload (OSC binary data) - now contiguous due to padding
135
144
  var payloadLength = length - bufferConstants.MESSAGE_HEADER_SIZE;
@@ -149,7 +158,7 @@ function readMessages() {
149
158
  // Move to next message
150
159
  currentTail = (currentTail + length) % bufferConstants.OUT_BUFFER_SIZE;
151
160
  messagesRead++;
152
- stats.messagesReceived++;
161
+ if (metricsView) Atomics.add(metricsView, METRICS_INDICES.MESSAGES_RECEIVED, 1);
153
162
  }
154
163
 
155
164
  // Update tail pointer (consume messages)
@@ -177,9 +186,9 @@ function waitLoop() {
177
186
 
178
187
  if (result === 'ok' || result === 'not-equal') {
179
188
  // We were notified or value changed!
180
- stats.wakeups++;
189
+ if (metricsView) Atomics.add(metricsView, METRICS_INDICES.WAKEUPS, 1);
181
190
  } else if (result === 'timed-out') {
182
- stats.timeouts++;
191
+ if (metricsView) Atomics.add(metricsView, METRICS_INDICES.TIMEOUTS, 1);
183
192
  continue; // Check running flag
184
193
  }
185
194
  }
@@ -191,13 +200,7 @@ function waitLoop() {
191
200
  // Send to main thread
192
201
  self.postMessage({
193
202
  type: 'messages',
194
- messages: messages,
195
- stats: {
196
- wakeups: stats.wakeups,
197
- timeouts: stats.timeouts,
198
- messagesReceived: stats.messagesReceived,
199
- droppedMessages: stats.droppedMessages
200
- }
203
+ messages: messages
201
204
  });
202
205
  }
203
206
 
@@ -261,13 +264,6 @@ self.addEventListener('message', function(event) {
261
264
  stop();
262
265
  break;
263
266
 
264
- case 'getStats':
265
- self.postMessage({
266
- type: 'stats',
267
- stats: stats
268
- });
269
- break;
270
-
271
267
  default:
272
268
  console.warn('[OSCInWorker] Unknown message type:', data.type);
273
269
  }
@@ -17,6 +17,10 @@ var uint8View = null;
17
17
  // Ring buffer control indices
18
18
  var CONTROL_INDICES = {};
19
19
 
20
+ // Metrics view (for writing stats to SAB)
21
+ var metricsView = null;
22
+ var METRICS_INDICES = {};
23
+
20
24
  // Priority queue implemented as binary min-heap
21
25
  // Entries: { ntpTime, seq, editorId, runTag, oscData }
22
26
  var eventHeap = [];
@@ -24,33 +28,14 @@ var periodicTimer = null; // Single periodic timer (25ms interval)
24
28
  var sequenceCounter = 0;
25
29
  var isDispatching = false; // Prevent reentrancy into dispatch loop
26
30
 
31
+ // Message sequence counter for ring buffer writes (NOT a metric - used for message headers)
32
+ var outgoingMessageSeq = 0;
33
+
27
34
  // Retry queue for failed writes
28
35
  var retryQueue = [];
29
36
  var MAX_RETRY_QUEUE_SIZE = 100;
30
37
  var MAX_RETRIES_PER_MESSAGE = 5;
31
38
 
32
- // Statistics
33
- var stats = {
34
- bundlesScheduled: 0,
35
- bundlesWritten: 0,
36
- bundlesDropped: 0,
37
- bufferOverruns: 0,
38
- eventsPending: 0,
39
- maxEventsPending: 0,
40
- eventsCancelled: 0,
41
- totalDispatches: 0,
42
- totalLateDispatchMs: 0,
43
- maxLateDispatchMs: 0,
44
- totalSendTasks: 0,
45
- totalSendProcessMs: 0,
46
- maxSendProcessMs: 0,
47
- messagesRetried: 0,
48
- retriesSucceeded: 0,
49
- retriesFailed: 0,
50
- retryQueueSize: 0,
51
- maxRetryQueueSize: 0
52
- };
53
-
54
39
  // Timing constants
55
40
  var NTP_EPOCH_OFFSET = 2208988800; // Seconds from 1900-01-01 to 1970-01-01
56
41
  var POLL_INTERVAL_MS = 25; // Check every 25ms
@@ -127,7 +112,44 @@ function initSharedBuffer() {
127
112
  IN_TAIL: (ringBufferBase + bufferConstants.CONTROL_START + 4) / 4
128
113
  };
129
114
 
130
- console.log('[PreScheduler] SharedArrayBuffer initialized with direct ring buffer writing');
115
+ // Initialize metrics view (OSC Out metrics are at offsets 7-12 in the metrics array)
116
+ var metricsBase = ringBufferBase + bufferConstants.METRICS_START;
117
+ metricsView = new Uint32Array(sharedBuffer, metricsBase, bufferConstants.METRICS_SIZE / 4);
118
+
119
+ METRICS_INDICES = {
120
+ EVENTS_PENDING: 7,
121
+ MAX_EVENTS_PENDING: 8,
122
+ BUNDLES_WRITTEN: 9,
123
+ BUNDLES_DROPPED: 10,
124
+ RETRIES_SUCCEEDED: 11,
125
+ RETRIES_FAILED: 12,
126
+ BUNDLES_SCHEDULED: 13,
127
+ EVENTS_CANCELLED: 14,
128
+ TOTAL_DISPATCHES: 15,
129
+ MESSAGES_RETRIED: 16,
130
+ RETRY_QUEUE_SIZE: 17,
131
+ RETRY_QUEUE_MAX: 18
132
+ };
133
+
134
+ console.log('[PreScheduler] SharedArrayBuffer initialized with direct ring buffer writing and metrics');
135
+ }
136
+
137
+ /**
138
+ * Write metrics to SharedArrayBuffer
139
+ * Increments use Atomics.add() for thread safety, stores use Atomics.store()
140
+ */
141
+ function updateMetrics() {
142
+ if (!metricsView) return;
143
+
144
+ // Update current values (use Atomics.store for absolute values)
145
+ Atomics.store(metricsView, METRICS_INDICES.EVENTS_PENDING, eventHeap.length);
146
+
147
+ // Update max if current exceeds it
148
+ var currentPending = eventHeap.length;
149
+ var currentMax = Atomics.load(metricsView, METRICS_INDICES.MAX_EVENTS_PENDING);
150
+ if (currentPending > currentMax) {
151
+ Atomics.store(metricsView, METRICS_INDICES.MAX_EVENTS_PENDING, currentPending);
152
+ }
131
153
  }
132
154
 
133
155
  /**
@@ -138,7 +160,7 @@ function initSharedBuffer() {
138
160
  function writeToRingBuffer(oscMessage, isRetry) {
139
161
  if (!sharedBuffer || !atomicView) {
140
162
  console.error('[PreScheduler] Not initialized for ring buffer writing');
141
- stats.bundlesDropped++;
163
+ if (metricsView) Atomics.add(metricsView, METRICS_INDICES.BUNDLES_DROPPED, 1);
142
164
  return false;
143
165
  }
144
166
 
@@ -148,7 +170,7 @@ function writeToRingBuffer(oscMessage, isRetry) {
148
170
  // Check if message fits in buffer at all
149
171
  if (totalSize > bufferConstants.IN_BUFFER_SIZE - bufferConstants.MESSAGE_HEADER_SIZE) {
150
172
  console.error('[PreScheduler] Message too large:', totalSize);
151
- stats.bundlesDropped++;
173
+ if (metricsView) Atomics.add(metricsView, METRICS_INDICES.BUNDLES_DROPPED, 1);
152
174
  return false;
153
175
  }
154
176
 
@@ -161,11 +183,10 @@ function writeToRingBuffer(oscMessage, isRetry) {
161
183
 
162
184
  if (available < totalSize) {
163
185
  // Buffer full - return false so caller can queue for retry
164
- stats.bufferOverruns++;
165
186
  if (!isRetry) {
166
187
  // Only increment bundlesDropped on initial attempt
167
188
  // Retries increment different counters
168
- stats.bundlesDropped++;
189
+ if (metricsView) Atomics.add(metricsView, METRICS_INDICES.BUNDLES_DROPPED, 1);
169
190
  console.warn('[PreScheduler] Ring buffer full, message will be queued for retry');
170
191
  }
171
192
  return false;
@@ -183,7 +204,7 @@ function writeToRingBuffer(oscMessage, isRetry) {
183
204
  var headerView = new DataView(headerBytes.buffer);
184
205
  headerView.setUint32(0, bufferConstants.MESSAGE_MAGIC, true);
185
206
  headerView.setUint32(4, totalSize, true);
186
- headerView.setUint32(8, stats.bundlesWritten, true);
207
+ headerView.setUint32(8, outgoingMessageSeq, true);
187
208
  headerView.setUint32(12, 0, true);
188
209
 
189
210
  var writePos1 = ringBufferBase + bufferConstants.IN_BUFFER_START + head;
@@ -214,7 +235,7 @@ function writeToRingBuffer(oscMessage, isRetry) {
214
235
  // Write header
215
236
  dataView.setUint32(writePos, bufferConstants.MESSAGE_MAGIC, true);
216
237
  dataView.setUint32(writePos + 4, totalSize, true);
217
- dataView.setUint32(writePos + 8, stats.bundlesWritten, true);
238
+ dataView.setUint32(writePos + 8, outgoingMessageSeq, true);
218
239
  dataView.setUint32(writePos + 12, 0, true);
219
240
 
220
241
  // Write payload
@@ -222,8 +243,8 @@ function writeToRingBuffer(oscMessage, isRetry) {
222
243
  }
223
244
 
224
245
  // Diagnostic: Log first few writes
225
- if (stats.bundlesWritten < 5) {
226
- schedulerLog('[PreScheduler] Write:', 'seq=' + stats.bundlesWritten,
246
+ if (outgoingMessageSeq < 5) {
247
+ schedulerLog('[PreScheduler] Write:', 'seq=' + outgoingMessageSeq,
227
248
  'pos=' + head, 'size=' + totalSize, 'newHead=' + ((head + totalSize) % bufferConstants.IN_BUFFER_SIZE));
228
249
  }
229
250
 
@@ -236,7 +257,11 @@ function writeToRingBuffer(oscMessage, isRetry) {
236
257
  var newHead = (head + totalSize) % bufferConstants.IN_BUFFER_SIZE;
237
258
  Atomics.store(atomicView, CONTROL_INDICES.IN_HEAD, newHead);
238
259
 
239
- stats.bundlesWritten++;
260
+ // Increment sequence counter for next message
261
+ outgoingMessageSeq = (outgoingMessageSeq + 1) & 0xFFFFFFFF;
262
+
263
+ // Update SAB metrics
264
+ if (metricsView) Atomics.add(metricsView, METRICS_INDICES.BUNDLES_WRITTEN, 1);
240
265
  return true;
241
266
  }
242
267
 
@@ -246,7 +271,7 @@ function writeToRingBuffer(oscMessage, isRetry) {
246
271
  function queueForRetry(oscData, context) {
247
272
  if (retryQueue.length >= MAX_RETRY_QUEUE_SIZE) {
248
273
  console.error('[PreScheduler] Retry queue full, dropping message permanently');
249
- stats.retriesFailed++;
274
+ if (metricsView) Atomics.add(metricsView, METRICS_INDICES.RETRIES_FAILED, 1);
250
275
  return;
251
276
  }
252
277
 
@@ -257,9 +282,13 @@ function queueForRetry(oscData, context) {
257
282
  queuedAt: performance.now()
258
283
  });
259
284
 
260
- stats.retryQueueSize = retryQueue.length;
261
- if (stats.retryQueueSize > stats.maxRetryQueueSize) {
262
- stats.maxRetryQueueSize = stats.retryQueueSize;
285
+ // Update SAB metrics
286
+ if (metricsView) {
287
+ Atomics.store(metricsView, METRICS_INDICES.RETRY_QUEUE_SIZE, retryQueue.length);
288
+ var currentMax = Atomics.load(metricsView, METRICS_INDICES.RETRY_QUEUE_MAX);
289
+ if (retryQueue.length > currentMax) {
290
+ Atomics.store(metricsView, METRICS_INDICES.RETRY_QUEUE_MAX, retryQueue.length);
291
+ }
263
292
  }
264
293
 
265
294
  schedulerLog('[PreScheduler] Queued message for retry:', context, 'queue size:', retryQueue.length);
@@ -284,24 +313,28 @@ function processRetryQueue() {
284
313
  if (success) {
285
314
  // Success - remove from queue
286
315
  retryQueue.splice(i, 1);
287
- stats.retriesSucceeded++;
288
- stats.messagesRetried++;
289
- stats.retryQueueSize = retryQueue.length;
316
+ if (metricsView) {
317
+ Atomics.add(metricsView, METRICS_INDICES.RETRIES_SUCCEEDED, 1);
318
+ Atomics.add(metricsView, METRICS_INDICES.MESSAGES_RETRIED, 1);
319
+ Atomics.store(metricsView, METRICS_INDICES.RETRY_QUEUE_SIZE, retryQueue.length);
320
+ }
290
321
  schedulerLog('[PreScheduler] Retry succeeded for:', item.context,
291
322
  'after', item.retryCount + 1, 'attempts');
292
323
  // Don't increment i - we removed an item
293
324
  } else {
294
325
  // Failed - increment retry count
295
326
  item.retryCount++;
296
- stats.messagesRetried++;
327
+ if (metricsView) Atomics.add(metricsView, METRICS_INDICES.MESSAGES_RETRIED, 1);
297
328
 
298
329
  if (item.retryCount >= MAX_RETRIES_PER_MESSAGE) {
299
330
  // Give up on this message
300
331
  console.error('[PreScheduler] Giving up on message after',
301
332
  MAX_RETRIES_PER_MESSAGE, 'retries:', item.context);
302
333
  retryQueue.splice(i, 1);
303
- stats.retriesFailed++;
304
- stats.retryQueueSize = retryQueue.length;
334
+ if (metricsView) {
335
+ Atomics.add(metricsView, METRICS_INDICES.RETRIES_FAILED, 1);
336
+ Atomics.store(metricsView, METRICS_INDICES.RETRY_QUEUE_SIZE, retryQueue.length);
337
+ }
305
338
  // Don't increment i - we removed an item
306
339
  } else {
307
340
  // Keep in queue, try again next cycle
@@ -343,17 +376,14 @@ function scheduleEvent(oscData, editorId, runTag) {
343
376
 
344
377
  heapPush(event);
345
378
 
346
- stats.bundlesScheduled++;
347
- stats.eventsPending = eventHeap.length;
348
- if (stats.eventsPending > stats.maxEventsPending) {
349
- stats.maxEventsPending = stats.eventsPending;
350
- }
379
+ if (metricsView) Atomics.add(metricsView, METRICS_INDICES.BUNDLES_SCHEDULED, 1);
380
+ updateMetrics(); // Update SAB with current queue depth and peak
351
381
 
352
382
  schedulerLog('[PreScheduler] Scheduled bundle:',
353
383
  'NTP=' + ntpTime.toFixed(3),
354
384
  'current=' + currentNTP.toFixed(3),
355
385
  'wait=' + (timeUntilExec * 1000).toFixed(1) + 'ms',
356
- 'pending=' + stats.eventsPending);
386
+ 'pending=' + eventHeap.length);
357
387
  }
358
388
 
359
389
  function heapPush(event) {
@@ -469,16 +499,16 @@ function checkAndDispatch() {
469
499
  if (nextEvent.ntpTime <= lookaheadTime) {
470
500
  // Ready to dispatch
471
501
  heapPop();
472
- stats.eventsPending = eventHeap.length;
502
+ updateMetrics(); // Update SAB with current queue depth
473
503
 
474
504
  var timeUntilExec = nextEvent.ntpTime - currentNTP;
475
- stats.totalDispatches++;
505
+ if (metricsView) Atomics.add(metricsView, METRICS_INDICES.TOTAL_DISPATCHES, 1);
476
506
 
477
507
  schedulerLog('[PreScheduler] Dispatching bundle:',
478
508
  'NTP=' + nextEvent.ntpTime.toFixed(3),
479
509
  'current=' + currentNTP.toFixed(3),
480
510
  'early=' + (timeUntilExec * 1000).toFixed(1) + 'ms',
481
- 'remaining=' + stats.eventsPending);
511
+ 'remaining=' + eventHeap.length);
482
512
 
483
513
  var success = writeToRingBuffer(nextEvent.oscData, false);
484
514
  if (!success) {
@@ -524,8 +554,8 @@ function cancelBy(predicate) {
524
554
  if (removed > 0) {
525
555
  eventHeap = remaining;
526
556
  heapify();
527
- stats.eventsCancelled += removed;
528
- stats.eventsPending = eventHeap.length;
557
+ if (metricsView) Atomics.add(metricsView, METRICS_INDICES.EVENTS_CANCELLED, removed);
558
+ updateMetrics(); // Update SAB with current queue depth
529
559
  console.log('[PreScheduler] Cancelled ' + removed + ' events, ' + eventHeap.length + ' remaining');
530
560
  }
531
561
  }
@@ -553,9 +583,9 @@ function cancelAllTags() {
553
583
  return;
554
584
  }
555
585
  var cancelled = eventHeap.length;
556
- stats.eventsCancelled += cancelled;
586
+ if (metricsView) Atomics.add(metricsView, METRICS_INDICES.EVENTS_CANCELLED, cancelled);
557
587
  eventHeap = [];
558
- stats.eventsPending = 0;
588
+ updateMetrics(); // Update SAB (sets eventsPending to 0)
559
589
  console.log('[PreScheduler] Cancelled all ' + cancelled + ' events');
560
590
  // Note: Periodic timer continues running (it will just find empty queue)
561
591
  }
@@ -633,22 +663,13 @@ self.addEventListener('message', function(event) {
633
663
  break;
634
664
 
635
665
  case 'send':
636
- var sendStart = performance.now();
637
-
638
- // New NTP-based scheduling: extract NTP from bundle
666
+ // NTP-based scheduling: extract NTP from bundle
639
667
  // scheduleEvent() will dispatch immediately if not a bundle
640
668
  scheduleEvent(
641
669
  data.oscData,
642
670
  data.editorId || 0,
643
671
  data.runTag || ''
644
672
  );
645
-
646
- var sendDuration = performance.now() - sendStart;
647
- stats.totalSendTasks++;
648
- stats.totalSendProcessMs += sendDuration;
649
- if (sendDuration > stats.maxSendProcessMs) {
650
- stats.maxSendProcessMs = sendDuration;
651
- }
652
673
  break;
653
674
 
654
675
  case 'sendImmediate':
@@ -669,32 +690,6 @@ self.addEventListener('message', function(event) {
669
690
  cancelAllTags();
670
691
  break;
671
692
 
672
- case 'getStats':
673
- self.postMessage({
674
- type: 'stats',
675
- stats: {
676
- bundlesScheduled: stats.bundlesScheduled,
677
- bundlesWritten: stats.bundlesWritten,
678
- bundlesDropped: stats.bundlesDropped,
679
- bufferOverruns: stats.bufferOverruns,
680
- eventsPending: stats.eventsPending,
681
- maxEventsPending: stats.maxEventsPending,
682
- eventsCancelled: stats.eventsCancelled,
683
- totalDispatches: stats.totalDispatches,
684
- totalLateDispatchMs: stats.totalLateDispatchMs,
685
- maxLateDispatchMs: stats.maxLateDispatchMs,
686
- totalSendTasks: stats.totalSendTasks,
687
- totalSendProcessMs: stats.totalSendProcessMs,
688
- maxSendProcessMs: stats.maxSendProcessMs,
689
- messagesRetried: stats.messagesRetried,
690
- retriesSucceeded: stats.retriesSucceeded,
691
- retriesFailed: stats.retriesFailed,
692
- retryQueueSize: stats.retryQueueSize,
693
- maxRetryQueueSize: stats.maxRetryQueueSize
694
- }
695
- });
696
- break;
697
-
698
693
  default:
699
694
  console.warn('[OSCPreSchedulerWorker] Unknown message type:', data.type);
700
695
  }