This is a read-only snapshot of the ComputerCraft forums, taken in April 2020.
Rybec's profile picture

Monitors and multiplayer lag

Started by Rybec, 18 January 2014 - 10:00 PM
Rybec #1
Posted 18 January 2014 - 11:00 PM
I remember reading somewhere that if done improperly, drawing to monitors in multiplayer can cause some rather terrible client lag; to the point where some servers either don't allow/heavily restrict monitors or ban people who create laggy ones. I've been working on a project that uses multiple network-attached monitors and would like some advice on how to minimize or eliminate any possible lag. I've been trying to do so myself but would appreciate additional opinions.

So far, each monitor typically only redraws if it is directly interacted with or when the program first launches. I expect this to produce NO lag as a redraw only occurs when a monitor is touched. (I am also using 1x1 montiors, so a redraw is only 150 chars+color data. That's like what, one packet to send out?)

However, some of the screens I can display automatically refresh. In all instances where more than one monitor is being drawn at a time, I sleep for a tick in-between monitors, and refreshes can only occur every two seconds at the fastest. This is my main event loop:

repeat
	event,device,clickX,clickY = os.pullEvent()
	if event == "monitor_touch" then
   	 --irrelevant
	elseif event == "timer" and device == refreshTimer then
		refreshTimer = os.startTimer(updateRate) --timer initially started outside the loop
		updateScreens("refresh", count)
	elseif event == "peripheral" then
   	 --irrelevant
	elseif event == "peripheral_detach" then
   	 --irrelevant
	end
	count = count + 1
until event == "key"

This is the part that checks if a screen needs to be redrawn:
function updateScreens(trigger,ID,mX,mY)
	if trigger == "draw" then
		screen[mon[ID].screen].draw(ID)
	elseif trigger == "refresh" then
		--Redraw any screens that request it
		local count = ID
		for k, v in pairs(mon) do
			if screen[mon[k].screen].refresh ~= nil then --each screen can define it's own refresh to be how many cycles between draws
				if count % screen[mon[k].screen].refresh == 0 then
					screen[mon[k].screen].draw(k)
					sleep(0.05) -- Drawing can cause lag;
				end -- if
			end -- not nil
			sleep(0.05) --sleep even we we don't draw anything; I was having a strange issue where the interface wouldn't see monitor touches
								 on occasion if there wasn't at least one monitor being refreshed. Bandaid? Maybe.
		end -- for
	elseif --other possible draw events removed for brevity and irrelevance
	end -- clear
end -- function()

Will this be enough to prevent multiplayer lag, or are there other steps I should take? Am I being too careful, and I could speed things up? I'm afraid to just go online and test it in the offchance it gets me in trouble. I get no lag in singleplayer, but we all know that doesn't mean anything.
However, while the GAME does not lag adding more monitors does seem to slow down the interface a bit and cause it to sometimes miss clicks; what could I do to make the refresh process more light-weight so it doesn't bog the program down?
theoriginalbit #2
Posted 18 January 2014 - 11:24 PM
Your easiest solution to reduce lag and network traffic is to only render changes that are needed. You have to be aware that each time you make a background colour change, draw characters, or modify anything like the cursor position it sends out a packet to clients that are viewing the computer, as such making use of screen buffers can massively reduce server lag and traffic. Here is an example of a screen buffer.

You should also make sure your program runs only when it needs to, for example a common mistake people make is with checking redstone input, they normally do the following

while true do
  if rs.getInput('back') then
	--# do something
  end
  sleep(0.1)
end
however the better solution is to make use of the 'redstone' event that fires when a change occurs like so

while true do
  os.pullEvent('redstone')
  if rs.getInput('back') then
	--# do something
  end
end
making use of these kinds of techniques to reduce the frequency of your computer running will also reduce server lag.
Edited on 18 January 2014 - 10:25 PM
oeed #3
Posted 19 January 2014 - 12:30 AM
As theoriginalbit said, buffers are the best way. The other thing is to only run your draw function (the thing that draws the buffer) when you actually change something, don't just do it every second.
Rybec #4
Posted 19 January 2014 - 12:51 AM
I must have mis-communicated; most of the screens a monitor can display are static and only change/are drawn when explicitly clicked on by a user. The ones that DO get redrawn regularly will be data readouts; things like energy storage levels, tank screens, maybe a recreation of the vanilla clock item that updates every 20 seconds or so. I may even raise the fastest refresh to 5 seconds, because I can't think of anything that I would REALLY need to update quicker than that.

So if I made a buffer for each monitor, made my changes to those buffers, and then tried to draw ALL the buffers it would not send any commands to the screens for which the data didn't change?


Suppose I should also clarify the potential scale I'm going for here….
These are just test monitors, realistically they would be spread throughout a base and display useful information; not visible to the left are more screens setup to act as elevator controllers; those are NOT drawn to on update, but they are CHECKED each refresh to see if they want to be redrawn (as per code in OP; elevator screen-type does not have a refresh value, but testScreen does) Screens are detected automatically.


I think part of my slowdown is that I'm iterating through the entire list of monitors when I refresh. It would probably be faster to keep a list of which monitors are refreshing to reduce how much table gets iterated through. Comparing runtimes on the screens, it took 1.55 seconds to redraw 14 screens with a 0.05 delay between them. 14*0.05 is only 0.7….

I'll have to look at adapting buffers to work inside my massive tangled mess, try to figure out how to integrate them with my current way of doing things.
theoriginalbit #5
Posted 19 January 2014 - 03:01 AM
The ones that DO get redrawn regularly will be data readouts; things like energy storage levels, tank screens, maybe a recreation of the vanilla clock item that updates every 20 seconds or so.
Data readouts can still be made so only appropriate changes are made, for example say we have a data readout "Total Power: 320 MJ" and then the power increases the next time to 325 MJ, clearing the line and redrawing it is bad, the easiest solution is just to write the new data, so override the 320 with 325, however if you implement a buffer correctly it should just change the 0 to a 5, thats a significant reduction in operations to the monitor.

So if I made a buffer for each monitor, made my changes to those buffers, and then tried to draw ALL the buffers it would not send any commands to the screens for which the data didn't change?
If you were to implement the buffer correctly, yes…

I think part of my slowdown is that I'm iterating through the entire list of monitors when I refresh. It would probably be faster to keep a list of which monitors are refreshing to reduce how much table gets iterated through. Comparing runtimes on the screens, it took 1.55 seconds to redraw 14 screens with a 0.05 delay between them. 14*0.05 is only 0.7….
That is definitely a slow write time, making use of buffers and only rendering needed changes will definitely help speed that up quite a lot. The method I suggest that you use would be to keep record of what monitors display what - assuming that you've groups of monitors displaying the same thing - and a buffer for each of these screens, then iterate through these lists outputting the buffer to each screen as you go.
Edited on 19 January 2014 - 02:04 AM
oeed #6
Posted 19 January 2014 - 06:09 AM
Yes, that seems like an awfully long time. It might be the mods/peripherals that are causing the lag. If you still have issues after adding a buffer then you might want to poll them less.
Rybec #7
Posted 19 January 2014 - 10:17 PM
You're not going to believe this: Most of that "render" time had NOTHING to do with what I was drawing to the screen. I changed things around so each screen has a draw() method and a refresh() method and set up my testscreen so all it did on refresh was write the runtime and program iteration (two setcursorpos and two writes) and it STILL took 1.55 seconds to do all 14 screens….


And then I removed the modulo operation from my refresh loop and it dropped to 0.65, which means all redraws were instant and the only delays were the built-in one tick sleep between each monitor (do I even need to worry about that for optimized screen refreshes?).
I guess modulo is really expensive in LUA. I replaced it with a single divide operation (test = count/screenRefreshRate), then see if it's an integer (if math.floor(test) == test)


Still looking into how best to integrate buffers into my current system; Symmetryc's buffer-str (or something like it) looks like it might be the easiest to integrate, but it's not done yet. I'll probably try to make my own just so I fully understand what it's doing.


I'd also like to investigate the possibility of putting the refresh into a coroutine; as is every time it starts checking for refreshes it becomes blind to all other input. The test setup above means it locks up for .65s every 5 seconds which is probably going to be unnoticable most of the time, but if I can maintain user input while refreshing I can potentially have faster refreshes.
Edited on 19 January 2014 - 09:18 PM
Bomb Bloke #8
Posted 19 January 2014 - 11:32 PM
Now that you mention it, the code extracts you're showing suggest that "count" doesn't naturally reset - if this is the case, wouldn't the division operations get more and more expensive as time goes on?

Depending on what sort of values you've got in screen[mon[k].screen].refresh, it may be feasible to reset count back down to whatever value whenever it reaches whatever value.

A check against the results of a binary AND between the two values may be another option.
theoriginalbit #9
Posted 20 January 2014 - 12:13 AM
Those sleep(0.05) lines in there would also be causing a lot of delay! Lets assume that a monitor doesn't need to update all 14 are just skipped that's 0.7 seconds. Now lets assume they all render, that's 1.4 seconds.

Putting in those sleeps will also not stop lag, nor will it fix you missing touch events; if anything it will be the cause of you missing touch events!

If possible I advise you to post all your code so we can suggest other performance enhancements that can be made :)/>
oeed #10
Posted 20 January 2014 - 12:23 AM
Those sleep(0.05) lines in there would also be causing a lot of delay! Lets assume that a monitor doesn't need to update all 14 are just skipped that's 0.7 seconds. Now lets assume they all render, that's 1.4 seconds.

Putting in those sleeps will also not stop lag, nor will it fix you missing touch events; if anything it will be the cause of you missing touch events!

If possible I advise you to post all your code so we can suggest other performance enhancements that can be made :)/>

Yes, obviously sleeps would be causing a huge delay if you had them.

If you need sleeps (which your probably don't), use sleep(0).
theoriginalbit #11
Posted 20 January 2014 - 12:30 AM
If you need sleeps (which your probably don't), use sleep(0).
Or just remove them all together, a sleep(0) if sleeping isn't needed, why have them at all, all it will do is consume all events, meaning that monitor interaction or timers or anything of the such could be missed!
Also if you do want a 'null yield' you're better to use os.queueEvent('d') coroutine.yield('d') as that will yield, and return your program the quickest, meaning less of a chance to miss things, it still will however have the side effect of clearing the event queue.
Edited on 19 January 2014 - 11:31 PM