Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use kotlin coroutines instead of raw threads (+ refactorings) #6801

Merged
merged 12 commits into from
May 14, 2022

Conversation

Azzurite
Copy link
Collaborator

@Azzurite Azzurite commented May 13, 2022

So for that [one new feature]™ that I'm currently developing I did all these refactorings & kotlin coroutine addition, but yeah, it makes major sense to split that up.

So here are a bunch of refactorings/smaller improvements and the main thing: inclusion of kotlin coroutines within the project, and switching all usages of raw threads to coroutines which run on a cached thread pool (and coroutines also reuse threads anyway). This alone should vastly reduce our number of threads and thus likely increase performance.

When reviewing this, I would strongly suggest to do it commit-by-commit. I have neatly separated each refactoring/improvement into its own commit and the commit messages should be self-explanatory. I hope each commit compiles by itself, but I might have made mistakes, I split this into commits post-partum.

I tested pretty much all aspects of the game on desktop & android, should all be fine.

Here are some additional comments:

  • cf293e8: This is in preparation for my [one new feature]™ which is basically extracting all multiplayer logic into its own classes instead of it being spread around various Screens
  • fe30b6b: We do have FileNotFound exception handling in the multiplayer logic, it just makes sense to use 404's in the custom server implementation.
  • 7ba4f69: There were a lot of instances of GameSaver.saveGame that did not add a saveCompletionCallback. These swallow all exceptions that happen on saving the game, which are some of the most important exceptions to handle, or at least get reports about from users. So I simply made the default handler throw the exception.
  • 0be0b23 and 39b6c75: This is unused but will be used in my [one new feature]™
  • bd015ec: did you know that I hate unnecessarily large files?
  • 43afe6c: did you know that I hate violations of the single responsibility principle?
  • 719809a: this was one instance where a game was loaded from multiplayer on the main thread - unnecessary.

@Azzurite Azzurite changed the title Use kotlin coroutines instead of raw threads Use kotlin coroutines instead of raw threads (+ refactorings) May 13, 2022
@touhidurrr
Copy link
Contributor

Just interested, what is this coroutines stuff?

@touhidurrr
Copy link
Contributor

Also why do you do

stuff = newStuff {
  // .. moreStuff
  return@newStuff
}

in place of

stuff {
   // .. moreStuff
   return
}

does this change anything?

@Azzurite
Copy link
Collaborator Author

Just interested, what is this coroutines stuff?

kotlin's default way of handling asynchronous programming. https://kotlinlang.org/docs/coroutines-overview.html

does this change anything?

yes

@Azzurite
Copy link
Collaborator Author

Azzurite commented May 14, 2022

Also why do you do

Because that's just how you use coroutines. I don't really want to explain here how they work, it's all explained in the official documentation

I think my implementation was as simple as possible and basically a 1:1 drop in to what we currently do.

@Azzurite
Copy link
Collaborator Author

@touhidurrr
Copy link
Contributor

So, basically you believe that coroutines are a more lightweight form of handling parallel processing and than Java Threads and implementing those for better performance? But since that would take explaining first that these would actually optimize stuff, you are shipping these with your [one new feature]™?
So, I would assume that your [one new feature]™ would be something that can take advantage of coroutines massively. Interested to see what it is.

@Azzurite
Copy link
Collaborator Author

Azzurite commented May 14, 2022

So, basically you believe that coroutines are a more lightweight form of handling parallel processing and than Java Threads and implementing those for better performance?

Yes

But since that would take explaining first that these would actually optimize stuff, you are shipping these with your [one new feature]™?

No, I'm adding coroutines with this PR. Re-read the second paragraph of the PR description. Apart from a few of the refactorings, this PR is completely standalone and does what the title says: "Use kotlin coroutines instead of raw threads". So no, I'm not "shipping coroutines with my new feature", I'm shipping them here, that's what this PR is about.

So, I would assume that your [one new feature]™ would be something that can take advantage of coroutines massively.

Not really massively. But it is making multiplayer updates asynchronous and separates them out into its own module, and coroutines are just better for asynchronous programming than anything based on standard java threads and whatever else java has.

@touhidurrr
Copy link
Contributor

touhidurrr commented May 14, 2022

So, basically you believe that coroutines are a more lightweight form of handling parallel processing and than Java Threads and implementing those for better performance?

Also pardon the sarcasm but coroutines looks like async-await to me. Because this:

fun stuff() = runBlocking {
    launch { someStuff() }
    launch { someMoreStuff() }
}

looks a lot like

async function stuff() {
    await Promise.all([
        someStuff(),
        someMoreStuff()
    ]);
}

Except based on docs, they are trickier and gives you more control over how your processor would actually handle them.
Also everything above is [My Own Take on Coroutines]™.

@touhidurrr
Copy link
Contributor

Also since I am new to kotlin it would be if you had some docs about makeNewErrorScope("Stuff") {} syntax for me.
This looks cool.

@Azzurite
Copy link
Collaborator Author

Azzurite commented May 14, 2022

coroutines looks like async-await to me.

This:

fun stuff() = runBlocking {
    launch { someStuff() }
    launch { someMoreStuff() }
}

has no Javascript equivalent, since Javascript always runs on a single thread. Coroutines can run on a single thread like Javascript async/await, but it depends on what you use as your coroutine scope. The scope is basically "where does our coroutine run". And in this PR, the coroutine scope is always the cached thread pools, i.e. multiple threads.

But also, what your example is actually doing is more alike to

function stuff()  {
    someStuff()
    someMoreStuff()
}

where both functions are async functions.

If you wanted to do something like

async function stuff() {
    await Promise.all([
        someStuff(),
        someMoreStuff()
    ]);
}

in coroutines, it would look like this:

fun stuff() = runBlocking {
    listOf(
        async {
            someStuff()
        },
        async {
            someMoreStuff()
        }
    ).awaitAll()
}

but you'll likely not write code like that with coroutines. In that case you'd rather use flows or channels.

But again, I really don't want to do a tutorial on coroutines here... If there are any specific questions about how this PR is implemented, okay, but I'm not going to further explain coroutines in general (unless you pay me for my time ;)

@Azzurite
Copy link
Collaborator Author

Azzurite commented May 14, 2022

if you had some docs about

trailing lambda: https://kotlinlang.org/docs/lambdas.html#passing-trailing-lambdas

@SomeTroglodyte
Copy link
Collaborator

unless you pay me for my time

🤣 👍 👍

basically you believe

Coroutines are not a Religious matter, they're fact. And they're the way to get Unciv's CPU load to >16% where it's currently stopping even on the heaviest huge-map late-game next-turn. If only I had some ideas how to de-couple e.g. the units so one could automate without knowing whether Schrödinger's cat is alive or dead...

@touhidurrr
Copy link
Contributor

touhidurrr commented May 14, 2022

unless you pay me for my time
🤣 👍 👍

Sigh. That's why I sometime wished for Github to have a realtime chat feature to that we could have such silly chats. Forums are not that good for chatting. And I was bored that time reading docs.
The docs was a little funny, first the author told that we don't have the async await stuff that you are looking for and instead we have coroutines. Then trying to convince how cool they were. Then ending with telling that they are lightweight comparing them with Threads as like the coolness didn't feel enough for them.
And so, I was wondering if turning Threads into coroutines was a common practice in Kotlin or not since azzurite just casually named the pr using coroutines while you ask me how much caching gradle would affect CI runs when I pr it. Sob. Sob.

@SomeTroglodyte
Copy link
Collaborator

So you didn't dare tackle the MusicController. I know it's intimidating, but the threading elements are really only a way to update volume repeatedly (fade) or leave a pause between tracks. And the latest complication is simpler than it looks too - lwjgl3 became less thread-safe than 2 on desktop only so I hijacked the Gdx app loop instead of postRunnable everything. On desktop the only Thread is a recovery delay after a bad crash; but on Android that "fade" ticker is still a Timer. Come to think of it, launchCrashHandling("Music") - should be "MusicDownload".

Looks good, but I'm too tired right now to really switch on brain (hi brain where are you). Later.

@yairm210
Copy link
Owner

How much does this raise the size of the apk?

@Azzurite
Copy link
Collaborator Author

So you didn't dare tackle the MusicController

What do you mean, "dare tackle"? There are no threads being used within MusicController, so why should I change anything, when all I'm doing is changing from threads to coroutines?

@touhidurrr
Copy link
Contributor

How much does this raise the size of the apk?

@yairm210 it is < 0.1 MB from my build tests.
See: https://github.com/touhidurrr/buildUnciv/releases

@Azzurite
Copy link
Collaborator Author

How much does this raise the size of the apk?

This PR raises the size of the APK compared to the base commit of this branch by 20 kB, from 11,685 kB to 11,705 kB

@touhidurrr
Copy link
Contributor

This PR raises the size of the APK compared to the base commit of this branch by 20 kB, from 11,685 kB to 11,705 kB

Just wondering why so little. Maybe because coroutines are built on top of Java Threads or something like that?

@Azzurite
Copy link
Collaborator Author

Azzurite commented May 14, 2022

Probably because this is the coroutine library:

image

@yairm210
Copy link
Owner

😎👍

@yairm210
Copy link
Owner

That was the major reason holding back coroutines however many years ago we tried it, of that's ok then in it goes
I'm not 100% sure about where it would be useful, but having it in opens a lot of options

@yairm210 yairm210 merged commit f8e0f57 into yairm210:master May 14, 2022
@Azzurite Azzurite deleted the refactorings branch May 14, 2022 21:53
@SomeTroglodyte
Copy link
Collaborator

"Jar-Datei" - And I would have guessed you to be a Tango-dancing, Suomi full of "Sisu" (ntbc w/ politics). Go prejudices go! 🥳

@SomeTroglodyte
Copy link
Collaborator

There are no threads being used within MusicController

But a kotlin.concurrent.timer.Timer? Is that integrated well enough w/ the coroutine thread pool to just leave it?

@SomeTroglodyte
Copy link
Collaborator

@Azzurite hey, we've had Music stutter on moderately large loads / nextturns for a while (on Mint at least), and now we're getting [ALSOFT] (EE) Failed to set real-time priority for thread: Operation not permitted (1) console logs. Any idea on how to get music to pre-empt other threads (see kcat/openal-soft#554: maybe set that RTkit RLIMIT_RTTIME for our process though I haven't looked for a useable API for that yet)? I've looked into increasing the buffers instead, but my verdict was something along the line that the Gdx api offers some buffer control, they won't help and the buffers one would really like to increase are hard-coded to 3x 40k.

@touhidurrr
Copy link
Contributor

touhidurrr commented May 15, 2022

and now we're getting [ALSOFT] (EE) Failed to set real-time priority for thread: Operation not permitted (1) console logs

Also just wondering if I am the only one facing this error. Does this work on Windows or Linux amd64 devices? It isn't because I am I was testing on a arm device or is it?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants