Skip to content

Commit 98571dc

Browse files
authored
Change E2 time quota to be per player (#3558)
* Add per player time quota for E2 I think this would be beneficial for servers, because servers care more about the overall load from a player than about their specific chip. Currently, players can simply spread the load across multiple chips to bypass the quota limit, but this won't eliminate it; it will only increase the overhead * Use CurTime for it * Count the execution cycle in ticks for accuracy * Don't round this * Use selfTbl * Fix typo * Use the total load from all E2s of one player, also calculating the average load (code from Redox, slightly modified) * Code styling/small optimization * Typo fix/use tabs * Fix one more typo * Optimizations * One more small optimization * Do some cleanup * Don't use median * Remove now unused * Kill chips until the cpu time drops to the maximum threshold * Fix typo * Use OOP style add new e2 functions * Don't chips subtable
1 parent db99507 commit 98571dc

File tree

4 files changed

+132
-44
lines changed

4 files changed

+132
-44
lines changed

lua/entities/gmod_wire_expression2/core/core.lua

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,29 @@ e2function number entity:cpuUsage()
210210
return this.context.timebench
211211
end
212212

213+
__e2setcost(100) -- approximation
214+
215+
[nodiscard]
216+
e2function number totalCpuUsage()
217+
local owner = self.player
218+
if not IsValid(owner) then return self.timebench end
219+
220+
return E2Lib.PlayerChips[owner]:getTotalTime()
221+
end
222+
223+
[nodiscard]
224+
e2function number entity:totalCpuUsage()
225+
if not IsValid(this) or not this:IsPlayer() then return 0 end
226+
227+
-- To avoid creating new table
228+
local chips = rawget(E2Lib.PlayerChips, this)
229+
if not chips then return 0 end
230+
231+
return chips:getTotalTime()
232+
end
233+
234+
__e2setcost(1)
235+
213236
--- If used as a while loop condition, stabilizes the expression around <maxexceed> hardquota used.
214237
[nodiscard]
215238
e2function number perf()

lua/entities/gmod_wire_expression2/init.lua

Lines changed: 106 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,12 @@ function ENT:Initialize()
7575
self.error = true
7676
self:UpdateOverlay(true)
7777
self:SetColor(Color(255, 0, 0, self:GetColor().a))
78+
79+
local owner = self.player
80+
81+
if IsValid(owner) then
82+
E2Lib.PlayerChips[owner]:add(self)
83+
end
7884
end
7985

8086
function ENT:OnRestore()
@@ -287,14 +293,90 @@ function ENT:Think()
287293
context.prf = 0
288294
context.time = 0
289295

290-
if e2_timequota > 0 and context.timebench > e2_timequota then
291-
self:Error("Expression 2 (" .. selfTbl.name .. "): time quota exceeded", "time quota exceeded")
292-
self:PCallHook("destruct")
296+
return true
297+
end
298+
299+
local PlayerChips = {}
300+
PlayerChips.__index = PlayerChips
301+
302+
function PlayerChips:new()
303+
return setmetatable({}, self)
304+
end
305+
306+
function PlayerChips:getTotalTime()
307+
local total_time = 0
308+
309+
for _, chip in ipairs(self) do
310+
local tab = chip:GetTable()
311+
if tab.error then continue end
312+
313+
local context = tab.context
314+
if not context then continue end
315+
316+
total_time = total_time + context.timebench
293317
end
294318

295-
return true
319+
return total_time
320+
end
321+
322+
function PlayerChips:findMaxTimeChip()
323+
local max_chip, max_time = nil, 0
324+
325+
for _, chip in ipairs(self) do
326+
local tab = chip:GetTable()
327+
if tab.error then continue end
328+
329+
local context = tab.context
330+
if not context then continue end
331+
332+
if context.timebench > max_time then
333+
max_time = context.timebench
334+
max_chip = chip
335+
end
336+
end
337+
338+
return max_chip, max_time
339+
end
340+
341+
function PlayerChips:checkCpuTime()
342+
local total_time = self:getTotalTime()
343+
344+
while total_time > e2_timequota do
345+
local max_chip, max_time = self:findMaxTimeChip()
346+
if max_chip then
347+
total_time = total_time - max_time
348+
max_chip:Error("Expression 2 (" .. max_chip.name .. "): Per-player time quota exceeded", "per-player time quota exceeded")
349+
max_chip:Destruct()
350+
else
351+
-- It shouldn't happen, but if something breaks, it will prevent an infinity loop
352+
break
353+
end
354+
end
355+
end
356+
357+
function PlayerChips:add(chip)
358+
table.insert(self, chip)
296359
end
297360

361+
function PlayerChips:remove(remove_chip)
362+
for index, chip in ipairs(self) do
363+
if remove_chip == chip then
364+
table.remove(self, index)
365+
break
366+
end
367+
end
368+
end
369+
370+
E2Lib.PlayerChips = setmetatable({}, {__index = function(self, ply) local chips = PlayerChips:new() self[ply] = chips return chips end})
371+
372+
hook.Add("Think", "E2_Think", function()
373+
if e2_timequota > 0 then
374+
for ply, chips in pairs(E2Lib.PlayerChips) do
375+
chips:checkCpuTime()
376+
end
377+
end
378+
end)
379+
298380
local CallHook = wire_expression2_CallHook
299381
function ENT:CallHook(hookname, ...)
300382
local context = self.context
@@ -308,6 +390,16 @@ function ENT:OnRemove()
308390
self:Destruct()
309391
end
310392

393+
local owner = self.player
394+
if not IsValid(owner) then return end
395+
396+
local chips = E2Lib.PlayerChips[owner]
397+
chips:remove(self)
398+
399+
if #chips == 0 then
400+
E2Lib.PlayerChips[owner] = nil
401+
end
402+
311403
BaseClass.OnRemove(self)
312404
end
313405

@@ -718,52 +810,23 @@ end
718810
--[[
719811
Player Disconnection Magic
720812
--]]
721-
local cvar = CreateConVar("wire_expression2_pause_on_disconnect", 0, 0, "Decides if chips should pause execution on their owner's disconnect.\n0 = no, 1 = yes, 2 = non-admins only.")
722-
-- This is a global function so it can be overwritten for greater control over whose chips are frozenated
723-
function wire_expression2_ShouldFreezeChip(ply)
724-
return not ply:IsAdmin()
725-
end
813+
hook.Add("PlayerDisconnected", "Wire_Expression2_Player_Disconnected", function(ply)
814+
E2Lib.PlayerChips[ply] = nil
726815

727-
-- It uses EntityRemoved because PlayerDisconnected doesn't catch all disconnects.
728-
hook.Add("EntityRemoved", "Wire_Expression2_Player_Disconnected", function(ent)
729-
if (not (ent and ent:IsPlayer())) then
730-
return
731-
end
732-
local ret = cvar:GetInt()
733-
if (ret == 0 or (ret == 2 and not wire_expression2_ShouldFreezeChip(ent))) then
734-
return
735-
end
736816
for _, v in ipairs(ents.FindByClass("gmod_wire_expression2")) do
737-
if (v.player == ent) then
738-
v:SetOverlayText(v.name .. "\n(Owner disconnected.)")
739-
local oldColor = v:GetColor()
740-
v:SetColor(Color(255, 0, 0, v:GetColor().a))
741-
v.disconnectPaused = oldColor
742-
v.error = true
817+
if v.player == ply and not v.error then
818+
v:Error("Owner disconnected")
819+
v:Destruct()
743820
end
744821
end
745822
end)
746823

747824
hook.Add("PlayerAuthed", "Wire_Expression2_Player_Authed", function(ply, sid, uid)
748825
for _, ent in ipairs(ents.FindByClass("gmod_wire_expression2")) do
749-
if ent.uid == uid and ent.context then
750-
ent.context.player = ply
751-
ent.player = ply
826+
if ent.uid == uid then
827+
E2Lib.PlayerChips[ply]:add(ent)
752828
ent:SetNWEntity("player", ply)
753-
ent:SetPlayer(ply)
754-
755-
if ent.disconnectPaused then
756-
ent:SetColor(ent.disconnectPaused)
757-
ent:SetRenderMode(ent:GetColor().a == 255 and RENDERMODE_NORMAL or RENDERMODE_TRANSALPHA)
758-
ent.error = false
759-
ent.disconnectPaused = nil
760-
ent:SetOverlayText(ent.name)
761-
end
762-
end
763-
end
764-
for _, ent in ipairs(ents.FindByClass("gmod_wire_hologram")) do
765-
if ent.steamid == sid then
766-
ent:SetPlayer(ply)
829+
ent.player = ply
767830
end
768831
end
769832
end)
@@ -781,10 +844,10 @@ function MakeWireExpression2(player, Pos, Ang, model, buffer, name, inputs, outp
781844
self:SetModel(model)
782845
self:SetAngles(Ang)
783846
self:SetPos(Pos)
784-
self:Spawn()
785847
self:SetPlayer(player)
786-
self.player = player
787848
self:SetNWEntity("player", player)
849+
self.player = player
850+
self:Spawn()
788851

789852
if isstring( buffer ) then -- if someone dupes an E2 with compile errors, then all these values will be invalid
790853
buffer = string.Replace(string.Replace(buffer, string.char(163), "\""), string.char(128), "\n")

lua/entities/gmod_wire_expression2/shared.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ CreateConVar("wire_expression2_unlimited", "0", {FCVAR_REPLICATED})
1212
CreateConVar("wire_expression2_quotasoft", "10000", {FCVAR_REPLICATED})
1313
CreateConVar("wire_expression2_quotahard", "100000", {FCVAR_REPLICATED})
1414
CreateConVar("wire_expression2_quotatick", "25000", {FCVAR_REPLICATED})
15-
CreateConVar("wire_expression2_quotatime", "-1", {FCVAR_REPLICATED}, "Time in (ms) the e2 can consume before killing (-1 is infinite)")
15+
CreateConVar("wire_expression2_quotatime", "-1", {FCVAR_REPLICATED}, "Time in (ms) that all E2s of one player can consume before killing (-1 is infinite)")
1616

1717
include("core/e2lib.lua")
1818
include("base/debug.lua")

lua/wire/client/e2descriptions.lua

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -939,6 +939,8 @@ E2Helper.Descriptions["setName(e:s)"] = "Set the name of another E2 or component
939939
E2Helper.Descriptions["setOverlayText(s)"] = "Set the overlay text of the E2"
940940
E2Helper.Descriptions["cpuUsage()"] = "Returns the average time per tick the server spends running this E2, in seconds (multiply it by 1000000 to get the same value as is displayed on the E2 overlay)"
941941
E2Helper.Descriptions["cpuUsage(e:)"] = "Returns the average time per tick the server spends running the specified E2, in seconds (multiply it by 1000000 to get the same value as is displayed on the E2 overlay)"
942+
E2Helper.Descriptions["totalCpuUsage()"] = "Returns the average time per tick the server spends running all yours E2s, in seconds (multiply it by 1000000 to get the same value as is displayed on the E2 overlay)"
943+
E2Helper.Descriptions["totalCpuUsage(e:)"] = "Returns the average time per tick the server spends running specified player E2s, in seconds (multiply it by 1000000 to get the same value as is displayed on the E2 overlay)"
942944
E2Helper.Descriptions["error(s)"] = "Shuts down the E2 with specified script error message"
943945
E2Helper.Descriptions["assert(n)"] = "If the argument is 0, shut down the E2 with an error message"
944946
E2Helper.Descriptions["assert(ns)"] = "If the first argument is 0, shut down the E2 with the given error message string"

0 commit comments

Comments
 (0)