Sclang Clocks, precision and limits

Hey everybody!

I’m currently working on a project where i want to record incoming OSC data into an array.
Right now everything happens on the language side of supercollider.
Via a =OSCdef= or =OSCFunc= i receive the OSC data, which is then saved into an variable.

Via a =Task= i write this variable into an =Array=. The Task loops the writing process and it’s writing speed is controlled by a delta time value inside the loop. This works right now.

[...]
	// make OSCReceiver
	OSCReceiver: OSCFunc({ |msg|
	  inVal = msg[1]
	},
	  oscPath
	),

	// make write task
	writeTask: Task({
	  loop {
		idx = idx % size;
		dict[\data].put(idx, inVal);
		dict[\delta].wait;
		idx = idx + 1;
	  }
	})
[...]

But i was wondering, how many times per second the OSCFunc is able to receive a new OSC message. Since this is on the language side, i assumed that this is happening on the =SystemClock=. But then i did read a little and now i’m not sure anymore.
// Edit: Also, what are the limits of a loop inside a Task? How many times per second is a loop happening (besides a =delta.wait= inside the loop). And how is the receiving of an OSC message synced with the execution of one task loop?

I try to compare this to the process in Csound, where everything is bound to the kontrol rate and audio rate, where audio rate is the sampling rate and the kontrol rate is equivalent to =1 / (vector-size / SR)=. Receiving OSC data in Csound is bound the k-rate, which limits the speed of receiving but makes it also controlable.
How is this managed in SuperCollider?

OSC messages are dispatched as soon as the network thread can grab the global interpreter lock. The main scheduler thread releases the lock when there are no more routines to run for the current logical system time, so it goes to sleep. This is a chance for pending OSC messages to be dispatched.

Side note: ideally, the network thread would not try to directly pass the OSC message to the OSC handlers. Instead, it should put the messages on a queue that is regularly polled in the interpreter loop. This would make sure that the network thread never has to wait for the interpreter, minimizing the risk of packet loss.

BTW, all of the above also applies to MIDI input.

A loop without wait times just runs as fast as possible. If you write an infinite loop, the interpreter will hang :slight_smile:

This made me curious about the boundary condition for packet loss, for local transmission.

So I made a Pd patch that sends back an OSC message n times.

And a script in SC:

n = NetAddr("127.0.0.1", 9000);

(
x = 0;
OSCdef(\x, { x = x + 1; x.postln }, '/sendback');

fork {
	n.sendMsg(\go, \reply, 1);
	bench { 1e8.do { 1+1 } }
}
)

1e8.do blocks the interpreter thread for between 2 and 3 seconds on my machine. The Pd patch sends back immediately, so all of those messages will be received while the interpreter is busy.

For n > 8738, it only receives 8738 messages consistently (though I’m not doing anything here to identify which messages are being lost),

So it looks to me like packet loss is extremely rare. You would have to keep the interpreter busy for long periods of time, and flood the language with a huge number of messages in quicker succession than is probably useful for real-world applications.

hjh

The number of messages does not really matter, it’s about the number of bytes. A [ \go, \reply, 1 ] message has 20 bytes. If we also consider the IPv4 header (20 bytes) and UDP header (8 bytes), we get the following:

(20 + 20 + 8) * 8738 = 419424 bytes

That’s more than enough buffer space for most applications, but it’s entirely possible to run into problems, e.g. when streaming larger amounts of data over UDP.

My point is that the network thread should only be responsible for receiving the network packets. The general idea is to receive packets as quickly as possible, store them on a queue and only process them on the main interpreter thread. If the queue is unbounded, no packets will ever be dropped as long as the network thread can drain the socket buffer faster than the sender is able to fill it.

(Note that we have recently increased the default socket buffer sizes, which significantly reduced the risk of packet loss, but it’s still possible to lose data when the interpreter happens to block long enough.)

Agreed that this would be better – I just thought it would be useful to know where the boundary is, under which conditions.

hjh

I wonder how dependant this is on sclang’s performance? It might be good to think about generating some statistics about how much time the mutex stays locked for.

It’s a general problem. The mutex simply stays locked until there are no more Routines to run. Even the fastest scripting language can block for seconds if you do lots of work in a single function call. I don’t think a performance analysis would provide much insight.

Thank you all for clarifying! It’s quiet interesting to see how SC is working under the hood and helps developing a better understanding on how to design better SC programms!

So, this is not, how it happens right now in SC but would be the ideal design?

Thanks a lot for testing! This is also quiet interesting!

To clarify: “rare” because I believe it’s uncommon for SC code to block the interpreter for hundreds of milliseconds at a time (a complex pattern might block for a few ms at once) and concurrently try to receive over 8000 OSC messages within that interpreter-lock time window (an extremely high rate of transmission). It could happen but based on usage patterns observed here over a long time, I don’t think many people run into that case. (I’ve never seen messages get dropped when transmitted to/from the local machine, because my transmission rate is much lower than that, and I’m not blocking for very long.)

And spacechild1 is right that there’s a software design pattern that completely solves this bad case, and SC isn’t using it presently.

hjh

Practical example: loading and processing a large JSON file or building a complex GUI.

But yes, you have to be very unlucky to receive a large amount of data over UDP at exactly that time :slight_smile: