Skip to content

Improve player system memory usage and performance#1216

Open
CaseIRL wants to merge 3 commits intoqbcore-framework:mainfrom
CaseIRL:main
Open

Improve player system memory usage and performance#1216
CaseIRL wants to merge 3 commits intoqbcore-framework:mainfrom
CaseIRL:main

Conversation

@CaseIRL
Copy link
Copy Markdown

@CaseIRL CaseIRL commented Feb 19, 2026

Description

Rewrites player.lua to use a class-based prototype pattern with shared methods via __index and weak-keyed private state to reduce memory usage and improve performance at scale.

All existing scripts continue to work without any changes needed, QBCore.Functions.GetPlayer() acts as an API layer. Each GetPlayer call still creates closures for the proxy but these are short-lived rather than permanently stored on the player object still a net improvement over the original.

  • player.lua: Virtually a full rewrite to class methods
  • functions.lua: New helper added, all player lookups updated
  • events.lua: Internal player handling cleaned up

Mock Object Benchmark

32 players | Old: 87.99 KB | New: 43.43 KB | Saved: 44.56 KB
64 players | Old: 188.37 KB | New: 86.87 KB | Saved: 101.50 KB
128 players | Old: 376.77 KB | New: 173.77 KB | Saved: 203.00 KB
256 players | Old: 753.64 KB | New: 347.64 KB | Saved: 406.00 KB
512 players | Old: 1507.39 KB | New: 695.39 KB | Saved: 812.00 KB
1024 players | Old: 3014.92 KB | New: 1390.92 KB | Saved: 1624.00 KB
2048 players | Old: 6030.92 KB | New: 2782.92 KB | Saved: 3248.00 KB

Checklist

  • I have personally loaded this code into an updated qbcore project and checked all of its functionality.
  • My code fits the style guidelines.
  • My PR fits the contribution guidelines.

Rewrites player.lua to use a class-based prototype pattern with shared methods via __index and weak-keyed private state to reduce memory usage and improve performance at scale.

All existing scripts continue to work without any changes needed, QBCore.Functions.GetPlayer() acts as an API layer.
Each GetPlayer call still creates closures for the proxy but these are short-lived rather than permanently stored on the player object still a net improvement
over the original.

- player.lua: Virtually a full rewrite to class methods
- functions.lua: New helper added, all player lookups updated
- events.lua: Internal player handling cleaned up
Title says it all.
I forgot to swap the Player.Functions calls in paycheck interval to call class methods.
@CaseIRL
Copy link
Copy Markdown
Author

CaseIRL commented Feb 19, 2026

I forgot to do the PaycheckInterval in this

function PaycheckInterval()
    if not next(QBCore.Players) then
        SetTimeout(QBCore.Config.Money.PayCheckTimeOut * (60 * 1000), PaycheckInterval) -- Prevent paychecks from stopping forever once 0 players
        return
    end
    for _, Player in pairs(QBCore.Players) do
        if not Player then return end
        local payment = QBShared.Jobs[Player.PlayerData.job.name]['grades'][tostring(Player.PlayerData.job.grade.level)].payment
        if not payment then payment = Player.PlayerData.job.payment end
        if Player.PlayerData.job and payment > 0 and (QBShared.Jobs[Player.PlayerData.job.name].offDutyPay or Player.PlayerData.job.onduty) then
            if QBCore.Config.Money.PayCheckSociety then
                local account = exports['qb-banking']:GetAccountBalance(Player.PlayerData.job.name)
                if account ~= 0 then
                    if account < payment then
                        TriggerClientEvent('QBCore:Notify', Player.PlayerData.source, Lang:t('error.company_too_poor'), 'error')
                    else
                        Player:AddMoney('bank', payment, 'paycheck')
                        exports['qb-banking']:RemoveMoney(Player.PlayerData.job.name, payment, 'Employee Paycheck')
                        TriggerClientEvent('QBCore:Notify', Player.PlayerData.source, Lang:t('info.received_paycheck', { value = payment }))
                    end
                else
                    Player:AddMoney('bank', payment, 'paycheck')
                    TriggerClientEvent('QBCore:Notify', Player.PlayerData.source, Lang:t('info.received_paycheck', { value = payment }))
                end
            else
                Player:AddMoney('bank', payment, 'paycheck')
                TriggerClientEvent('QBCore:Notify', Player.PlayerData.source, Lang:t('info.received_paycheck', { value = payment }))
            end
        end
    end
    SetTimeout(QBCore.Config.Money.PayCheckTimeOut * (60 * 1000), PaycheckInterval)
end

@Qwerty1Verified Qwerty1Verified self-requested a review March 21, 2026 23:38
Comment thread server/functions.lua Outdated
}

for name, fn in pairs(player:GetAllExtraMethods()) do
functions[name] = function(...) return fn(player, ...) end
Copy link
Copy Markdown
Member

@Qwerty1Verified Qwerty1Verified Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe I noticed an issue with this whilst testing. Extra methods are defined by resources that call AddMethod, so they typically won't need the player object passed as the first parameter. An example you can test with is GetItemByName from qb-inventory.

Could you test this out on your end and confirm? Thank you

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For sure, was mostly done pretty quick to be honest as a test if it was even do-able, I am still running the modified version on my dev server atm and i did have to make one or 2 other changes I never pushed back here. Only just noticing this though at 4:50am for me so bare with, i'll take a note of it now, and have a test after sleep ❤️

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No worries. Thank you for working on these changes 😃

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

Your not wrong, thats something i have updated in my version yes, bare with me I have a few things to do today but ill give it all a proper once over again make sure theres nothing else i forgot about :)

Copy link
Copy Markdown
Member

@Qwerty1Verified Qwerty1Verified left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apologies for the review gap.

I think with these we could save on even more memory, as well as fix a potential future issue on internal calls via the metamethod.

Feel free to let me know what you find 😄

Comment thread server/player.lua
Comment on lines +19 to +27
local self = setmetatable({}, {
__index = function(t, key)
local priv = Private[t]
if priv and priv.extra_methods[key] then
return function(...) return priv.extra_methods[key](t, ...) end
end
return QBPlayer[key]
end
})
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we could even move the metatable and closure creation out of here, to re-use the same metatable and closures per player instance. It saves extra allocations per player, and saves on garbage collection.

Also, the closure on line 23 may possibly give us the same issue we had previously where it's passing itself into external "extra_methods". From what I can tell, the only path that this would at any time occur on is internal player object calls, which isn't entirely the intended path anyways, and likely wouldn't work with this call flow unless . syntax is used.

It may be best to eliminate the closure if there's no other need for it, and just return the handler.

Comment thread server/player.lua
__index = function(t, key)
local priv = Private[t]
if priv and priv.extra_methods[key] then
return function(...) return priv.extra_methods[key](t, ...) end
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return function(...) return priv.extra_methods[key](t, ...) end
return priv.extra_methods[key]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would allow for the same style of call external to occur internally, rather than having either:
Player:Test(arg1) - function(player, player, arg1)
or
Player.Test(arg1) - function(player, arg1)

It could be used as:
Player.Test(arg1) - function(arg1)

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.

2 participants