Timer only waiting once in _process() function

Godot Version

4.4.1 stable

Question

Hey ppl!

I created this script for my game ui showing player’s health and the time left before the game ends. I added an ‘await’ statement so that godot will wait for 1 second before reducing 1 from ‘seconds_left’ variable value so that the timer will work accurately. But in this case, the timer is only working for the first iteration. From the second iteration onwards, the seconds are passing very fast.

extends CanvasLayer

var minutes_left: int = 10
var seconds_left: int = 60

func _process(_delta):
	$HealthBarContainer/HealthBar.value = Globals.player_health
	await get_tree().create_timer(1).timeout
	print('ok!')
	seconds_left -= 1
	if seconds_left == 0:
		seconds_left = 60
		minutes_left -= 1
	$TimeCounterContainer/Minutes.text = str(minutes_left)
	$TimeCounterContainer/Seconds.text = '    : ' + str(seconds_left)

You should never do this. Process happens many, many times a second. And your code gets executed a bunch. What you should do instead, is connect the timer’s timeout signal up to a function, and use that function to update the time left of your game. Doing this in process while using a timer completely defeats the purpose of using a timer in the first place.
Also in your case you should use a timer node, instead of just creating a one-off timer.

2 Likes

THenks

ok so can i directly use the timer node’s timeout signal to change the time left in the game instead of connecting it to a new function?

also can u explain why the timer actually waited the first iteration but not the other times?

Because when your game first started, the process function started executing your code multiple times. However, each one of those requests did get awaited, so nothing happened in the first second, but then all those hundreds of process requests continued to execute after their timeout finished one after another. You essentially created a queue of many requests.

1 Like

But wont the process function iterate a seond time after completing the first iteration?

Like, after it executes the last line under the function?

await immediately exits the function with a special ability to continue on the next line when the signal arrives. You should read await as exit_and_continue_when.

It basically splits your function into two sub-functions. The first is executed immediately while the second is called when/if the signal is received.

await is a concurrent construct, which means that it creates new edit:[coroutines] previous:[logical threads] that wait to be executed on the Godot process’s thread pool for gdscript. When there is a free slot in the Godot gdscript thread pool for the _process function it gets executed.

With await get_tree().create_timer(1).timeout Godot makes a system call into the operating system that will return after the 1 second has passed. At the same time Godot creates a edit:[coroutines] previous:[logical threads] that is blocked until the operating system call returns. The operating system will of course ensure that the 1 second timer and Godot’s thread pool for gdscript both have time on the CPU with the granularity of it’s time slice.

Since the other parts of the game, except _process of course, aren’t using Godot’s alloted CPU time for the moment, and since await has already returned the execution that created the operating system 1 second callback, _process will be the called again immediately. That means a basically back to back execution of await get_tree().create_timer(1).timeout. As in the previous paragraph a second operating system call will be made, a new edit:[coroutine] previous:[logical thread] will be created. And you guessed which function continues to be called because the game has nothing else to allot CPU time to: _process. And so on and so forth.

These repeated calls to _process that start an OS timer, create a logical thread that blocks on the timer and relinquish execution to a new call to _process have an end once the first operating system call returns after it’s 1 second has passed. edit:[At that point the number of coroutines executing will stabilize with about as many coroutines created as there are coroutines running the second part of the _process coroutine, substracing from the variables in quick and unintented succession and then exiting.] previous:[At that point new calls to _process and timers returning the execution to their respective logical threads fight for Godot’s CPU time.]

edit:[Discard all of what I said previously in this paragraph.] previous:[If not for the short time frame of 1 second and not for the monumental size of the finite resources in modern physical PCs and operating systems the resources available to Godot would have eventually been exhausted and it would have become slugish and then it would have crashed and mabe even crashed the operating system itself.]

Off-topic rant: I could’nt find a strike through text function in this editing interface. Is there one? There should be.

Who told you this? Almost none of it is correct. No logical threads or any OS calls (except memory allocation) are involved in GDScript’s await. In terms of system resources consumption, a yielded await is pretty much in the ballpark of a regular function call.

Here’s a little fun test that measures how much RAM will individual await consume. Run it and observe “ram eaten per frame” in debugger/monitors:

extends Node

signal eternity

var ram_usage := OS.get_static_memory_usage()
var ram_allocated_this_frame: int

func _ready():
	Performance.add_custom_monitor("ram eaten per frame", func(): return ram_allocated_this_frame)

func _process(_dt):
	ram_allocated_this_frame = OS.get_static_memory_usage() - ram_usage
	ram_usage = OS.get_static_memory_usage()
	await eternity

The baseline cost for storing the coroutine sate appears to be about 1K. Coroutine’s allocated local storage will be added to this. Let’s grant it additional 1K, totaling to 2K per coroutine yield.

If you run at 60 fps and accumulate 1 yield per frame, with 2 gigs of available storage you can stack up awaits for about 4 hours before you run out of memory. No other system resources are consumed. Performance will be unaffected by this. Godot may crash after you reach the RAM limit but chances to crash the OS are almost zero.

So if you create a queue of regularly yielding/awaking coroutines, your code can run indefinitely if the total time between await of the first and the last coroutine in the queue is below those 4 hours. To go back to OP’s code as an example, even if their timer timeout is set to e.g. 2 hours, the program can go on forever without any consequences, except for a lot of RAM usage.

1 Like

Hi normalized,

I believe I covered myself with a significant margin of error in the way I phrased my post. I also believe I didn’t make errors in my statements that cannot be fixed by simply adopting regular gdscript and Godot terminology if my terminology may be confusing to the community in this forum. But with this post I do not wish to enter a gdscript technical debate. Instead I want to say that your response seems to be emotional and therefore detrimental to yourself, myself, the original poster and the entire community in this forum because it just raises anyones blood pressure by reading it.

I appreciate that you took some time to formulate some source code for benchmarking and analysis. I believe with everyones responses, including mine, the original poster has the opportunity to enrich his knowledge about his current issue, game development with Godot and computer engineering and science in general. One nitpick in your approach though: I would measure resource usage like memory and system calls and thread usage at the operating system level as being more reliable.

My 2 cents. Have a fun day everyone reading.

1 Like

Nothing emotional in my post. I’m just stating facts. Your “explanation” is factually incorrect, not just a matter of “terminology”. Besides, terminology is important. “Logical thread” has the precise definition, “coroutine” as well. One is not equivalent to other and shouldn’t just be interchanged willy-nilly.

I’m glad my post raised your blood pressure - and rightfully so. Your explanation is incorrect yet authoritative in tone, almost like… an llm wrote it.

If you want to enter a discussion about finer points of the topic at hand - I’m all for it.

2 Likes

Hi normalized,

Happy to hear you don’t care about anyones blood pressure, including yourself, at all.

I will also refuse to enter a technical discussion when you didn’t actually make opinions statement by statement to the post I have made and dissect where I am wrong. A technical back and forth woul enrich everyone reading. An emotional one would just drain my well being.

Just my 2 cents number 2. Have a fun day everyone reading number 2. I won’t bother replying to anymore posts that are not technical in nature coming from you, normalized.

My 2 cents on this matter: It’s nice that you wanted to help OP understand something, but as normalized pointed out, nearly everything you wrote was factually incorrect. normalized did not go on to berate you. normalized corrected a few statements and provided a script for anyone to run which would show the RAM cost in awaiting many threads.

There is no problem if you come out and say “this is my mental model of how things work, and it might be incorrect but that doesn’t matter for reasons X, Y, and Z”. But instead you presented everything as fact, and when normalized pointed out that this was wrong, you got super defensive instead of taking the opportunity to learn and update your mental model.

It is not everybody else’s job to be nice to you because you can’t regulate your mood. In my opinion, normalized was nice in their response. The only place I can see you possibly getting offended would be the first two sentences, but there isn’t really a nice way to tell someone they’re wrong without adding a bunch of fluff or confusing future readers. In technical forums, people are free to point out when incorrect things are said.

2 Likes

Ok. If you insist. Hold on to your pressur-o-meter :wink:

No it isn’t. It’s a cooperative “construct”. You’re confusing preemptive multitasking with cooperative multitasking. Awaits belong into latter category.

Incorrect. No logical threads are created. All GDScript code runs on a single thread unless the user explicitly creates new threads via script code. Otherwise the communication between nodes in the scene tree would require every single property of every node to maintain a mutex lock. This would be a nightmare to work with. It’d also mess up the order of processing and input event propagation across the scene tree. Much of the scene tree functionality relies on the guarantee of this order.

Incorrect. As stated in the above paragraph, all GDScript code runs in a single thread. Although the engine does offer a mechanism to run specific groups of _process() callbacks in separate threads via Node::process_thread_group, that doesn’t happen by default and most typical users will never need it. Using it requires special care of shared data. Same as explicitly created threads would.

No it doesn’t. First, you’re kinda confusing the await with the signal here. Second, no special calls to OS are made for timers, except perhaps to get the system clock ticks. But that happens every frame anyway, regardless of user timers. Game engines typically handle their own timers internally. They do not depend on system interrupts.

Incorrect. No logical threads are created, let alone blocked ones that wake up on system call returns. The things that happen when await is executed are as follows:

  • The current state of the function is stored in a special object GDScriptFunctionState. This object was even exposed to scripting in Godot 3.x but was removed in 4.x as the coroutine mechanism was redesigned and simplified. Such object is maintained internally nevertheless. The stored state includes the complete local execution context (local variabls etc) as well as the continuation point. That’s precisely the same type and amount of data that’s stored in a stack frame when you call a regular function.

  • The function exits back to its caller. In cooperative multitasking such function is called a coroutine, and the exit that happens upon await statement is called explicit yield. In fact, many coroutine implementations use keyword yield instead of await.

Incorrect again. See above. No such involvement from the operating system. No thread pool for GDScript execution.

No, _process() won’t be called immediately. It will be called when all other tasks that constitute the frame (like; rendering, input processing, physics tick(s), etc…) are done. And if the frame rate is vsynched, there may be considerable idle time before _process() gets called again. You can see exactly what happens each frame in Main::iteration() inside main.cpp in Godot’s source code.

That’s kinda correct, depending on what you mean by “basically back to back”. It will indeed execute each frame and create a new yielded coroutine, but everything I mentioned in the previous paragraph will happen in-between.

Yeah it will happen repeatedly, frame in frame out, but none of the things you list here will occur. Only a coroutine yield will, which - as we already established - is as demanding towards the system as a regular function call.

Here you’re just repeating what you already said. So no need to go through it again. All I can say again is that this is not a correct description of what happens.

The only resource that will get exhausted in the case of perpetually yielding coroutines that never wake up - is RAM. I already addressed this in the post that initially raised your blood pressure :smiley:

1 Like

@normalized : Couldn’t you have just posted your post above in the first place? Instead of hammering on with ‘Almost none of it is correct.’ and ‘almost like… an llm wrote it.’?

No I couldn’t because it requires additional time and effort. You earned it by over-complaining.

Although if I posted that first, you’d still have reasons for complaining I suspect. Notice how many more times the word “incorrect” appears in the second post.

The forum uses standard markdown. You need to switch to the markdown editor instead of rich text editor by clicking on the M/A icon in the top left corner, and then enter the formatting tags manually:

~~strikethrough this~~

strikethrough this