UnrealNetImgui icon indicating copy to clipboard operation
UnrealNetImgui copied to clipboard

poor performance attached to unreal editor by default

Open jswigart opened this issue 1 year ago • 26 comments

By default, Unreal Editor runs with this option enabled

image

This option, when the editor doesn't have focus, causes the editor to nearly stop updating. This will occur any time your NetImgui server app has focus. As a consequence, the frame events that UnrealNetImgui gets drawn to drop to near nothing, making interaction with the UI within the server app unusable.

Some possible fixes

Conditionally disable this option when NetImgui::IsConnected, like so

UEditorPerformanceSettings* Settings = GetMutableDefault<UEditorPerformanceSettings>();
Settings->bThrottleCPUWhenNotForeground = false;

The downside of this option is that it would basically keep this option disabled constantly, as just having the server running is almost always going to be connected. Also turning of this option keeps the editor eating a lot of CPU, as it attempts to run normally, causing your GPU/cooling to kick on as it normally does under load.

Another option potentially, rather than draw via FCoreDelegates::OnEndFrame, consider use a standalone timer callback, to fully decouple the 'framerate' of the editor, with the updaterate to netimgui, so NetImgui doesn't entirely circumvent the savings that are part of the bThrottleCPUWhenNotForeground option, and doesn't need to hijack the setting.

Perhaps the more notable bug in this situation is that the interaction with the widgets in the server window(clicks, etc) is so unreliable under this throttled situation?

jswigart avatar Aug 21 '24 21:08 jswigart

Thank you. I have been meaning to explore accessing the CPUThrottle option in my next release, but the idea of not relying on FrameEnd might have some value in editor

sammyfreg avatar Aug 21 '24 22:08 sammyfreg

I think there is something else going on, even with a timer based tick for editor, the click responsiveness in the server app is pretty bad.

image

Is there something inherent to this system that breaks down when decoupled from the framerate?

jswigart avatar Aug 21 '24 22:08 jswigart

Seems like an issue with the server app itself. Is it capturing/caching the mouse click events in between updates so that it doesn't miss them? I have to long click to reliably trigger stuff. Quick clicking doesn't do anything much of the time.

jswigart avatar Aug 21 '24 22:08 jswigart

I have confirmed that the timer approach calls Update at the expected rate. Tried up to 60 hz, but still the server interactivity is poor. Also confirmed that the OnEndFrame, drops to 3 fps when the editor doesn't have focus

OnEndFrame image

Timer image

I don't get why the interactivity is still flaky on a 60hz timer tick.

jswigart avatar Aug 21 '24 23:08 jswigart

It should not be the NetImguiServer side, since it process content up to the framerate specified in its config (30fps by default) and this only arise when CPUThrottle is on in Unreal Editor. Maybe the communication thread gets throttled too somehow?

See if EndFrame() in 'NetImgui_Api.cpp' gets called at 30/60fps (should be with your timer callback change) and then check in CommunicationsHost() in 'NetImgui_Client.cpp' which runs in a separate thread to exchange with the Server.

sammyfreg avatar Aug 21 '24 23:08 sammyfreg

Looks like EndFrame starvation is still occuring

image

In/Out is the Communications_Incoming and Communications_Outgoing

jswigart avatar Aug 22 '24 01:08 jswigart

The top sections are the EndFrame counts when the app has focus, the ones below are when it doesn't have focus

image

jswigart avatar Aug 22 '24 01:08 jswigart

Looks like the issue is related to NETIMGUI_FRAMESKIP_ENABLED, passing 0 to NetImgui::NewFrame seems to fix it

This option is tied to mbValidDrawFrame and this is set by ProcessInputData. Does that mean if there is no input captured in the frame, it's not a valid draw frame and so it won't send with that option?

I could see how the app would not have input state if it doesn't have focus, but I don't understand why that would prevent the draw frame.

image

jswigart avatar Aug 22 '24 02:08 jswigart

This InputData is received from the NetImguiServer, not Unreal. When there's no pending input, it skip drawing content in the editor/game to preserve CPU since it will be discarded. With frameskip disabled, it makes sures there's always a valid imgui context to draw into, but the content is discarded, never sent to the server.

Check that Communications_Incoming_Input() is called every ~33ms (or whatever fps you set on the server). This is called on the communication thread, when receiving data from the server. The server never change its speed, after receiving a draw command from the game, it waits until 33ms has passed to send a new input back to the game, triggering a new draw. If we are not receiving it at that rate, the only thing left I can think of at the moment, is that the Epic TCP/IP com library also gets slowed down when CPU throttle is active?

Without having a new Input received every 33ms, then Communications_Outgoing_Frame should also not be having a pending drawcommand send back to the server either at 33ms.

sammyfreg avatar Aug 22 '24 02:08 sammyfreg

Incoming_Input rate is steady

image

The hasNewInput in ProcessInputData is what drops

jswigart avatar Aug 22 '24 02:08 jswigart

Interesting. Thank you for your investigation, I will take a look today.

sammyfreg avatar Aug 22 '24 13:08 sammyfreg

I'm not sure why the Incoming_Input actually doubles in rate when the editor doesn't have focus while the HasNewInput drops to 3-4, but I guess that shows the network thread isn't being starved on the unreal side.

Based on my understanding, this means the input messages double in rate when the server window has focus, but it makes no sense as to why hasNewInput would drop, bc it looks like those are tied together. Incoming_Input sets client.mPendingInputIn every call, and that's the basis for hasNewInput, so as far as I can tell, those numbers should match.

Only thing possible it looks like to me would be if a bunch of input events are coming in and just stomping each other before one can be processed. Input isn't processed as a queue, so if multiple come in they would just stomp each other.

Confirmed, lots of input stomping

image

Didn't you say there should be some sort of 2 way lock stepping thing or did I misunderstand that?

jswigart avatar Aug 22 '24 13:08 jswigart

I'm currently setuping myself to test this, but the question is why ProcessInputData is still called at a slower rate if it doesn't have time to process input data.

sammyfreg avatar Aug 22 '24 13:08 sammyfreg

The ProcessInputData call rate stays steady at 60

Only thing I can figure is that the network buffer is filling up with a bunch of inputs, and since data is pulled out of the socket until there is nothing left in Communications_Incoming, we get a ton of input message stompage in between frames of ProcessInputData. Any reason not to push these into a queue instead?

jswigart avatar Aug 22 '24 13:08 jswigart

Simplicity :)

The received input are bitmask letting us know if a key is up or down. I'm not sure what would happen to suddenly insert 10 frames of key input events into 1. But it might be worth it after all.

That said, it is not normal that ProcessInputData() and Communications_Incoming_Input() are both called at a high rate but the processed input isn't. Your number of Incoming_Input is with valid data received and assigned?

sammyfreg avatar Aug 22 '24 14:08 sammyfreg

Well, ProcessInputData is gated by FNetImguiModule::Update calling NewFrame, called by the 60 hz timer in this case(even slower without the timer when not focused), while the comm thread runs freely. That inherently creates a race condition unless there is some sort of network level message exchange that keeps the systems in some sort of lockstep.

jswigart avatar Aug 22 '24 14:08 jswigart

Look how much faster the comm thread runs than the game thread

image

jswigart avatar Aug 22 '24 15:08 jswigart

Yes, it seems like I will have to improve the input reception to buffer them until processed.

I'm currently looking at the slow frame rate.

sammyfreg avatar Aug 22 '24 15:08 sammyfreg

It seems to me a queue is the right approach, in principal. Otherwise any sort of stomping or input dropping will cause unreliable input in the server app. However, the big worry with queuing is what if the app can't keep up with the input rate?

Clearly we're getting more input right now through the comms than it can process in the main loop, so it's possible the EnQueue count will outpace the Dequeue count, unless you while(Dequeue) each frame in the client side to clear the buffer each frame. That sounds proper to me, as you need to process each one through the full set of ImGui draw pipeline to pick up on all the UI state changes each individual input causes, but you don't want to send a render frame back to the server for all of them, just the last one

jswigart avatar Aug 22 '24 15:08 jswigart

I believe I found the issue for the low responsiveness with the Timer method. When unfocused, the callback timing is not steady at all. Overall seems to match the requested speed, but when inspecting the Elapsed time between call, I see about 10 quick call, then a long pause (timing in ms):

| [0] | 0.000600000028 | float
  | [1] | 0.000500000024 | float
  | [2] | 0.000600000028 | float
  | [3] | 332.980194 | float
  | [4] | 0.188800007 | float
  | [5] | 0.000799999980 | float
  | [6] | 0.000300000014 | float
  | [7] | 0.000199999995 | float
  | [8] | 9.99999975e-05 | float
  | [9] | 0.000199999995 | float
  | [10] | 0.000199999995 | float
  | [11] | 0.000199999995 | float
  | [12] | 0.000199999995 | float
  | [13] | 333.130402 | float
  | [14] | 0.564000010 | float
  | [15] | 0.00200000009 | float

So, it appears something else should be used if available, or just disabling the CPU optim whiled connected. There's still the problem of better input enqueuing, but it is not the main issue observed here.

Note: Looking at the Engine loop a little closer, I do not believe there's a way around disabling the CPUThrottle and keeping things on the GameThread.

sammyfreg avatar Aug 22 '24 20:08 sammyfreg

I suppose that's coming from UEditorEngine::ShouldThrottleCPUUsage

I had initially only seen the first reference to bThrottleCPUWhenNotForeground which pauses the viewport

Looks like they have a hook to get around that, see ShouldDisableCPUThrottlingDelegates

This can check if Netimgui is connected.

This will allow the CPU throttle to bypass without missing out on the viewport render throttle.

jswigart avatar Aug 22 '24 21:08 jswigart

That probably gets rid of the need to use the timer also?

jswigart avatar Aug 22 '24 21:08 jswigart

Yes, since the timer won't work. I was pulling my hair trying to understand how I could have 30fps in the Update, 60fps in the reception of input and still have a bunch of null pending input :)

sammyfreg avatar Aug 22 '24 21:08 sammyfreg

This simple code addition (executed after the engine has been properly init) does the trick. I will include it in the next release. Thank you for the pointers.

#if WITH_EDITOR
	GEditor->ShouldDisableCPUThrottlingDelegates.Add(
		UEditorEngine::FShouldDisableCPUThrottling::CreateLambda([](){ return NetImgui::IsConnected();})
	);
#endif

sammyfreg avatar Aug 22 '24 22:08 sammyfreg

Where are you calling that from? In my project the GEditor isn't initialized yet(module load order) if called from FNetImguiModule::StartupModule

For the sake of avoiding these issues, you might want to set that GEditor delegate inside of a FCoreDelegates::OnPostEngineInit

#if WITH_EDITOR
	FCoreDelegates::OnPostEngineInit.AddLambda([]
	{
		GEditor->ShouldDisableCPUThrottlingDelegates.Add(
			UEditorEngine::FShouldDisableCPUThrottling::CreateLambda([](){ return NetImgui::IsConnected();}));
	});
#endif

jswigart avatar Aug 22 '24 22:08 jswigart

Yes, I moved some plugin init that included this new throttle callback, to a new method attached to the PostEngineInit delegate.

sammyfreg avatar Aug 22 '24 23:08 sammyfreg