Fork me on GitHub
#clojure
<
2021-11-27
>
Jakob Durstberger10:11:13

Morning 👋 Is there anything I need to specifically consider if I want to write a clj lib that works for cljs aswell? The lib will just be a convenience wrapper around an http API, so nothing fancy.

Jakob Durstberger11:11:57

No I haven't yet, thank you

borkdude12:11:26

http in JS typically works with callbacks or promises

Shawn14:11:00

Maybe have src/your-project/clj and /cljs containing platform-specific implementations for e.g. HTTP stuff, then a http.cljc or whatever which uses a reader conditional to import the relevant implementation

emccue19:11:09

Because JVM http can be sync and often wants to be and JS http has to be async, you likely want the convenience layer to either 1. only work with callbacks/promises 2. only care about the specification of requests/response handling and have both mechanisms at the end

Joshua Suskalo16:11:55

Does anybody have experience with precise wait times on the JVM? I'm making a game engine and want to have precise wait times between frames, so I've set the thread priority to max, and currently when I need to wait for a frame time I just make a busy loop. I could use Thread/onSpinWait to mark the threads as lower priority for a bit to allow other threads to do more stuff in these in-between times, but I'm concerned that this will result in imprecise wake times. What I would ideally do is use something like Thread/sleep with an ms wait time that's a little bit under what I actually need (based on a cross-platform margin of error) and then spin (without the Thread/onSpinWait call) until the right time arrives, but on windows the scheduler interrupt time is 10-15ms. Is there a cross-platform way to sleep with ~1-2ms error?

diego.videco17:11:54

I use overtone/at-at for music sequencing. Maybe it could do the job for you. https://github.com/overtone/at-at. I use the one from the overtone/overtone repo, but I suppose it should be the same (although if memory doesn't fail it seems copy pasted from the other repo).

Joshua Suskalo18:11:31

That's pretty cool, but doesn't seem to have a primitive for what I need, since I'm not trying to schedule a task for another thread at a specific time, but instead wait for a specific time on the current thread.

emccue19:11:55

this book definitely covers the different ways to do event loops, though i dont have it handy

emccue19:11:27

I think maybe with a bit more context it might be easier to think of a solution

Joshua Suskalo20:11:06

Yeah, I've read it. solves a different problem. This is about precise waits on threads, the rest of my architecture is more or less done.

Joshua Suskalo20:11:12

So is this actually, I just want to change from spins to enable the cpu to idle a bit more and save on battery life on power constrained devices.

adi02:11:57

@U5NCUG8NR This https://stackoverflow.com/questions/16385049/timers-in-clojure/16385198#16385198 for a game engine should still be relevant. It also has a suggestion with core.async , but if you're not already using that, it may be too much to bring in just for this. Also spitballing here. I wonder if agent + await-for will do the trick. Create an agent and a send that does nothing and never completes, and await-for the timeout you need to block the current thread. https://clojure.github.io/clojure/clojure.core-api.html#clojure.core/await-for

user=> (def foo (agent false)) ;; parks in the agent thread pool
#'user/foo
user=> (send foo (fn [x] (while (false? x) false))) ;; send returns immediately and never completes
#agent[{:status :ready, :val false} 0x51e08aee]
user=> (await-for 1000 foo) ;; blocks current thread for 1000 ms
false
user=> (await-for 10000 foo) ;; the send has still not completed, so we can await-for again anytime later
false

Joshua Suskalo03:11:52

I stand by not thinking the timer api fits. This send await seems interesting though. I might check what the implementation of that is.

Joshua Suskalo03:11:45

The stuff I've been able to find though indicates that sleep on windows and linux for short sleep times (under 10 ms) and max priority threads will have a small error of 1-2ms on average. I'll be doing some testing with this, but I might be able to just work with that. I may end up just sticking with the spin wait though.

👍 1
adi03:11:58

Ya, the "agent spin"---if I may call it that---does not cause thread sleep/wake for the main thread. So that should eliminate wake error. My hunch is that Clojure STM should also be good about scheduling the infinite send on the agent so it doesn't eat CPU.

adi05:11:23

Hm, well the await-for trick does not seem to be any more precise than Thread/sleep, in JVM Clojure. Fancy await-for.

user=> (def foo (agent false))
user=> (send foo (fn [x] (while (false? x) false)))
user=> (criterium/bench (await-for 10 foo))
Evaluation count : 6000 in 60 samples of 100 calls.
             Execution time mean : 10.081844 ms
    Execution time std-deviation : 6.561624 µs
   Execution time lower quantile : 10.076834 ms ( 2.5%)
   Execution time upper quantile : 10.091910 ms (97.5%)
                   Overhead used : 22.636976 ns

Found 2 outliers in 60 samples (3.3333 %)
	low-severe	 2 (3.3333 %)
 Variance from outliers : 1.6389 % Variance is slightly inflated by outliers
nil
Plain o'l Thread/sleep.
user=> (criterium/bench (Thread/sleep 10))
Evaluation count : 6000 in 60 samples of 100 calls.
             Execution time mean : 10.065917 ms
    Execution time std-deviation : 7.949942 µs
   Execution time lower quantile : 10.061151 ms ( 2.5%)
   Execution time upper quantile : 10.092689 ms (97.5%)
                   Overhead used : 22.636976 ns

Found 5 outliers in 60 samples (8.3333 %)
	low-severe	 2 (3.3333 %)
	low-mild	 3 (5.0000 %)
 Variance from outliers : 1.6389 % Variance is slightly inflated by outliers
When in doubt, benchmark. When certain, benchmark anyway 😅

adi06:11:27

@U5NCUG8NR I'm curious what you decided to go with (and why)!

Joshua Suskalo14:11:58

At the moment I've decided to just stick with spin waiting. In the future I think most likely I'll test using the Thread/onSpinWait function until within a few thousand nanos to a millisecond or two of the required time and see how precisely it can hit that mark at different timings on windows and linux. I think on windows I might end up running into quanta issues since they're on the order of 10-15 ms. I might end up finding out that on windows when the JVM sets the interrupt time on windows to 1ms for sleeping that ends up more accurate than spinning.

clojure-spin 1
Joshua Suskalo14:11:53

The reasoning for this is that since I'm doing video game rendering, and I'm working in an immutable language, I have to contend with overhead from the language, and I have to do what I can to make sure that I can fully utilize whatever time I have available to me between frames. So I have to wake at precisely the time to start rendering to make sure that the entire time budget is used.

👍 1
adi15:11:10

Thanks for the explanation... May the Source be with you! lightsaber

adi16:11:31

@U5NCUG8NR, @U053G1AJG pointed out that Overtone uses https://github.com/overtone/at-at/blob/master/src/overtone/at_at.clj#L207 to delay execution of functions. I wonder if that might deliver the desired precision across platforms.

(defn at
  "Schedules fun to be executed at ms-time (in milliseconds).
  Use (now) to get the current time in ms.
...)

Joshua Suskalo16:11:01

Yeah, I was aware of this, but the trouble is that I can't do this sort of thing because I can't allow this to cross thread boundaries. I have a hard constraint that the pause must occur on one thread, and that particular thread must cut in at the exact time. This is because OpenGL and GLFW operations to handle windowing must be on the main thread on macos, and must all be executed from the same thread (regardless of which thread it is) on linux.

Joshua Suskalo16:11:41

I could in theory maybe set it up to have a thread pool with only one thread, but this causes other sorts of issues like making it harder to detect when I've gone over-time due to computation and need to introduce a stutter to catch back up.

adi16:11:18

Aaagh, sticky problem! Thank you for engaging, Joshua. This discussion made me think, and I learned a few things. :spock-hand: 🍻

Joshua Suskalo17:11:02

I have a tendency to find sticky problems 😅

😅 1
gotta_go_fast 1
phronmophobic20:11:24

I'm pretty sure what you want is vsync.

phronmophobic20:11:32

unfortunately, it seems like glfw's vsync is currently broken on mac osx: • https://github.com/glfw/glfw/issues/1834https://github.com/glfw/glfw/issues/1990 Hopefully, they'll fix it relatively soon.

Joshua Suskalo22:11:45

Also not quite right. This waiting occurs on the simulation thread, which cannot wait on vblank. There is a parallel wait on the rendering thread, but it occurs after a vblank and as a result can't wait on vsync without causing a stutter.

Joshua Suskalo22:11:21

The engine already takes vsync into account in several different components. Really I just want a precise wake time.

phronmophobic22:11:47

relying on a precise wakeup time doesn't seem right, but maybe I just don't understand your architecture. I'm not aware of any game engines that do that, but it's totally possible that it's common place.

phronmophobic22:11:56

if you do have an example for how other game engines setup their rendering/simulation generally, I would be interested in what you find.

Joshua Suskalo23:11:37

Yeah, there's lots of examples of this. You can look at these two articles to understand the basic flow: https://gafferongames.com/post/fix_your_timestep/ https://frankforce.com/frame-rate-delta-buffering/

Joshua Suskalo23:11:14

In addition to these two articles, you can look at my diagram that I posted in #clojure-gamedev to see what my frame timing looks like.

Joshua Suskalo23:11:05

My timings are very much more complex than the average game engine because I have separate threads for simulation and rendering, which is only possible without needing barriers because the game state is immutable.

phronmophobic23:11:22

thanks! :thumbsup:

phronmophobic23:11:27

from just a skim, it doesn't seem like either of those articles rely on a precise wake up time. is that right?

phronmophobic23:11:27

> Fortunately there is a simple solution to all of these problems; run vsynced and adjust the delta in advance, correcting for how much time will have passed when the frame actually gets displayed.

Joshua Suskalo23:11:27

They do not because they have the simulation and rendering bound together. The wakeup time is given to them by vsync, and they don't care to sleep to save battery life.

Joshua Suskalo23:11:55

My timing diagram will hopefully illuminate the reasoning a bit more, but for clarity, my simulation thread has no synchronization with the render thread, the vsync doesn't help me, and because it may end up looking at mutable state generated by the render thread while polling events (that is input) I do not want to allow it to run ahead too far. At the same time, I have very precise timings I must meet because the less precise my timings are the smaller my frame budget becomes if I want to never drop frames.

Joshua Suskalo23:11:49

And because I'm working in Clojure, which is slower than other languages I might write games in, I must do everything I can to ensure that the engine doesn't further constrain my frame timing budget.

phronmophobic23:11:42

in theory, you should be able to get some of your time budget back by having easier access to multi-threading

Joshua Suskalo23:11:07

As a result I'm using reducers during parallelizable portions of game code, I'm using spin loops on max-priority threads to prevent the liklihood of wasting the first part of my frame time, and the simulation and render threads are separate which gets me back a lot of performance.

phronmophobic23:11:35

I thought you never had to spin on the render thread because, at least with glfw, calling swapBuffers with vsync on will wait to return until the frame gas been swapped, and then your next frame always starts again as soon as possible

Joshua Suskalo23:11:49

At the moment short of game code exceeding my frame budget the only thing in my engine which could cause stutters are GC pauses and the final step of loading resources if too much work is needed to load resources from memory onto the GPU because of too large or too many resources being loaded at once.

Joshua Suskalo23:11:14

I never have to spin on non-vsynched windows. On vsynched windows I may have to spin if I know I will miss a vblank.

Joshua Suskalo23:11:18

although I think I may be able to simplify it for the render thread. The simulation thread still has this problem regardless.

phronmophobic23:11:35

I thought https://blat-blatnik.github.io/computerBear/making-accurate-sleep-function/ had some interesting ideas which combines system sleep with spin lock. It kinda depends on what kind of sleep resolution you need. I would also check OS specific APIs to see what they have to offer. The linked article mentions an option for Windows. Let me know if you have any clojure games. I'm excited to see them!

phronmophobic23:11:56

Is the only downside of having the simulation thread run up to 1-2 ms "ahead" of the render thread that it might not be synced with user input?

phronmophobic23:11:35

otherwise, it seems like you can just sleep only when the simulation is more than 1ms ahead of the render thread.

phronmophobic23:11:52

what other downsides might there be?

Joshua Suskalo00:12:30

Yeah, that accurate sleep function is more or less what I was thinking of implementing.

Joshua Suskalo00:12:41

And the simulation thread in my engine is actually running a lot more than 1-2 ms ahead of the render thread. It's running about 2-3 frames ahead. However the speed at which it can simulate is thousands of times faster than the vsync for very simple scenes, which means that I must have a sleep or a spin in order to not have it slowly (or sometimes very quickly) run ahead of the renderer.

Joshua Suskalo00:12:36

I don't have any released clojure games, although I plan on making some. At the moment I'm working through a sample game that's just a breakout clone, but I plan on going much more advanced than that in the future.

phronmophobic00:12:05

my question is more or less "is busy wait is necessary?" and if system sleep with 1-2ms precision is sufficient

Joshua Suskalo00:12:34

Waiting is necessary because each simulation step must take at least the simulation step to complete otherwise you get the simulation running ahead of realtime, not just to the point of input lag, but to the point of being seconds to minutes ahead, which is not going to work out for memory most likely, and yeah, is input lag like none other. The system sleep with 1-2ms precision is sufficient (when followed by spinning), I'll probably end up using that, the key thing that I was seeing before is that the 1-2ms precision isn't super consistent across platforms, although it looks like it may be more so than my initial research would indicate.

adi02:12:46

🤯 this thread is blogpost-worthy. Design tradeoffs of waiting alternatives. I can even picture the title... "Wait, what?".

Joshua Suskalo02:12:04

If people think it's worth it, then maybe I should finally pull out my old blog project I never finished and actually make it so I can put some content on it.

👍 1
emccue04:12:32

you and me both man

emccue04:12:48

not that i have content, but i feel the “idea of a blog that never became real”

phronmophobic04:12:33

never too late to start

1
adi06:12:32

> If people think it's worth it, Well, I've come to believe it's better to cast it as "I felt like writing X, and I write to think, so here it is". That's how I managed to https://www.evalapply.org/posts/hello-world/ on the WWW again (after 10+ years of avoiding it, I might add). It's not much, but it's my little corner to do as I please, as fast or slow or regularly or sporadically as I wish. The odds of someone coming to my web home are minuscule. Let alone anyone who shares interests and wants to have a conversation.

Joshua Suskalo15:12:56

I mean that's fair, but I tend to be of a mind that me writing ramblings online is a lot of work for no benefit over me writing ramblings in my org files, unless other people want to hear what I have to say.

adi18:12:11

Haha, yes org-mode! :) That is my chosen "workflow"... Keep adding to one's org files and if I feel there's something useful, publish straight to blog from org. FWIW, this thread has been pretty interesting. Thanks for writing in Slack!