- 1 Before Optimizing
- 2 Avoid too many new API class instantiations
- 3 Callbacks over Polling
- 4 Don't define functions you don't need
- 5 Use a low update frequency
- 6 Use Predefined Parallel Functions
- 7 Execute performance-heavy code asynchronically
- 8 Use move Functions
- 9 See Also
Before Optimizing[edit | edit source]
If your mod or some scripts in it have very bad performance, you have a few options on how to rework your code to improve performance.
- Note: You should always write good code that is easy to maintain. Easily maintainable code is easier to optimize.
- Note: Optimize only once you realize you have to optimize: As Donald Knuth said it: Premature Optimization is the root of all evil.
General lua optimizations[edit | edit source]
Most importantly: All general lua optimization tips are also true for all scripts in Avorion. Check out this link for a lot of useful information on how to write well performing lua code.
All of the following performance optimizations are tailored to Avorion and the Avorion Scripting API.
Profiling[edit | edit source]
Once you realize that for some unknown reason, your code is performing poorly, use the
Profiler class to find out where it's performing poorly. Check out the Scripting API for more information on the
Avoid too many new API class instantiations[edit | edit source]
Every time you create an instance of a Scripting API class, for example with
Entity(id), Avorion does a few things in the background before it finally creates the instance of the class. Among those is can be memory allocations (internally done with new(), but Avorion tries to do those as little as possible using memory pools), lookups of the Entity in the Sector, potential locking due to its multithreaded nature, and many more.
So one, very easy way to improve performance of your scripts would be to avoid new instantiations of classes as much as (reasonably) possible.
For example, take this code, where we want to add cargo to a ship:
-- bad, creates a new reference to Entity in each loop function addGoods(id, goodsToAdd, amount) for _, good in pairs(goodsToAdd) do Entity(id):addCargo(good, amount) end end
-- better, creates a single reference to entity and reuses it in each loop function addGoods(id, goodsToAdd, amount) local entity = Entity(id) for _, good in pairs(goodsToAdd) do entity:addCargo(good, amount) end end
Callbacks over Polling[edit | edit source]
Whenever you want to react to something that happens in the game (say a ship got some new cargo), you have two options for detecting that something happened:
- Polling: Check every frame (or second) whether that something has changed (ie. check for new cargo)
- Callbacks: Have the game call a very specific function when that something has changed
The second option is vastly superior because you can avoid unnecessary execution of script code. So whenever you want to check for, say, new cargo, use the
Check out the Callbacks Section of the Scripting API for more information on callbacks and a complete listing of all callbacks.
-- bad function update(timeStep) -- it is possible to make this a lot more sophisticated, but this is mainly for demonstration purposes if CargoBay():findCargos("Energy Cell") then myCargoChangedFunction() end end function myCargoChangedFunction() print ("cargo changed!") end
-- better function initialize() Entity():registerCallback("onCargoChanged", "myCargoChangedFunction") end -- Avorion also tells us the index of the object that the callback happened to, the delta change and the good that changed function myCargoChangedFunction(objectIndex, delta, good) print ("cargo changed!") end
Not only is the second variant easier to read, but callbacks usually also provide a lot of relevant information by themselves.
Don't define functions you don't need[edit | edit source]
The predefined function templates in the Scripting API can be nice to have, but make sure that you remove all unused functions that you may have copy-pasted.
When Avorion calls lua functions, it carries some overhead, and it should be avoided as much as possible. When the function isn't defined, then Avorion won't even try to call the function.
Note: When the function isn't defined when Avorion first tries to call it, Avorion remembers that and will never again try to call that function. So adding it later will not work and it won't be called by Avorion.
-- bad function MyScript.interactionPossible(player, option) return false end function MyScript.initialize() Entity():setTitle("Super-Duper-Entity") end function MyScript.update(timeStep) end function MyScript.updateServer(timeStep) end function MyScript.updateClient(timeStep) end
-- better -- leave out all functions that are unused anyways function MyScript.initialize() Entity():setTitle("Super-Duper-Entity") end
Use a low update frequency[edit | edit source]
Unless your script needs very high accuracy and must run in real time, you should define a low update frequency for your script in
getUpdateInterval(). For example, a script that may start an event every 30 minutes, doesn't need to update every frame, or even every second. You can have it update only once every 10 minutes and it will still be just as accurate as when you update it every second or every frame even.
-- bad function MyScript.getUpdateInterval() return 1 -- unnecessary, high update rate end function MyScript.update(timeStep) MyScript.timePassed = (MyScript.timePassed or 0) + 1 if MyScript.timePassed >= 60 * 30 then -- do something end end
-- better function MyScript.getUpdateInterval() return 60 * 10 -- low update rate that will still work with the below code without problems end function MyScript.update(timeStep) -- timeStep contains the amount of time passed since the last tick MyScript.timePassed = (MyScript.timePassed or 0) + timeStep if MyScript.timePassed >= 60 * 30 then -- do something end end
Or even better: Only update every 30 minutes and leave the time counter out altogether.
-- even better function MyScript.getUpdateInterval() return 60 * 30 -- low update rate that will still work with the below code without problems end function MyScript.update(timeStep) -- in this variant, we can leave the time check out entirely! -- do something end
Use Predefined Parallel Functions[edit | edit source]
Avorion provides functionality to have your scripts updated in parallel (instead of one-by-one). This may come in handy, provided your scripts don't have to write-access other entities during their update step.
Using these functions
you can have your script be updated in parallel, instead of one after another. This only works for the server.
Note: See the documentation of these functions in the Scripting API for more information.
The regular execution order for script updates is completely sequential. Scripts can't just be updated in parallel, because of issues like race conditions when accessing other entities. So what Avorion does, is that it call every script update method of every script that needs to be updated one after another, instead of in multiple threads.
There are several cases though, where you don't have to access other entities during the update loop (think of a factory script for example, which only needs to access its own entity to read, remove and add cargo). Updates like that could easily be done in parallel.
When you define the above functions, Avorion will do additional update steps, where it executes those particular functions in parallel, in 2 phases:
- For all entities that have it defined in their scripts,
updateParallelRead()is called in parallel.
- Here, all entities have read-access to all entities, but cannot write to themselves or any other entities. This phase is meant for gathering information that can then be used in phase 2.
- For all entities that have it defined in their scripts,
updateParallelSelf()is called in parallel.
- Here, all entities have read-write-access to themselves, and only themselves. This phase is meant for doing changes to oneself.
-- this function is executed one after another for all entities in the sector function MyEntityScript.update(timeStep) ... end -- this function is executed one after another for all entities in the sector function MyEntityScript.updateServer(timeStep) ... end -- this function is executed in parallel together will all other scripts that have it defined -- we have read-only-access to the entire sector function MyEntityScript.updateParallelRead(timeStep) local s = Sector() -- returns nil, we don't have write-access to the sector local s = ReadOnlySector() -- this works local e = Entity() -- returns nil, we don't have write-access to the entity local e = ReadOnlyEntity() -- this works local other = ReadOnlyEntity(someId) -- this also works ... end -- this function is executed in parallel together will all other scripts that have it defined -- we only have access to our own entity, but we have read-write-access function MyEntityScript.updateParallelSelf(timeStep) local s = Sector() -- returns nil, we don't have write- nor read access to the sector local s = ReadOnlySector() -- returns nil, we don't have write- nor read access to the sector local e = Entity() -- this works, we have read-write-access to our own entity local e = ReadOnlyEntity() -- this works as well local other = ReadOnlyEntity(someId) -- returns nil, we don't have any access to any other entities than ourself ... end
Note: Factories are updated like this! Check out the data/scripts/entity/merchants/factory.lua script for a working example on how this works.
Execute performance-heavy code asynchronically[edit | edit source]
Note: This is a rather advanced topic and should only be done by coders who know their way around multithreading and asynchronous execution of code.
Sometimes there is no way around executing code that just takes some time. But instead of blocking the updates of the server, you can move that code into an
async() call, which will execute the code in a separate thread!
Take this code for example:
function generateAsync(faction, seed, volume, styleName, material) local code = [[ package.path = package.path .. ";data/scripts/lib/?.lua" package.path = package.path .. ";data/scripts/?.lua" local PlanGenerator = include ("plangenerator") function run(styleName, seed, volume, material, factionIndex) local faction = Faction(factionIndex) if not volume then volume = Balancing_GetSectorShipVolume(faction:getHomeSectorCoordinates()); local deviation = Balancing_GetShipVolumeDeviation(); volume = volume * deviation end local style = PlanGenerator.selectShipStyle(faction, styleName) local plan = GeneratePlanFromStyle(style, Seed(seed), volume, 2000, 1, material) PlanGenerator.transformToFreighter(plan, faction) return plan end ]] async("onFreighterPlanFinished", code, styleName, seed, volume, material, faction.index) end function onFreighterPlanFinished(plan) -- receives all return values of the code above as arguments -- now we do something with the plan end
generateAsync() is called, it creates a string of code, that is then used for the
async() call. Upon finishing, the given callback function (
onFreighterPlanFinished) is called and all return values of the
run() function of the code string are given to it as arguments.
Note: If you want to learn more on how to execute async code, you should take a look at the plangenerator.lua file in your AvorionInstallation/data/scripts/lib/ folder.
Use move Functions[edit | edit source]
Note: This is a rather advanced topic and should only be done by programmers who have already heard of move semantics.
Some classes of the Avorion Scripting API have special
move() functions, that move internal data around, instead of copying it. This is useful, when you want to use large amounts of data (like a
BlockPlan in a new place, like a new Entity.
Take the following code excerpt from
stationfounder.lua for example:
-- create the station -- get plan of ship local plan = ship:getPlan() -- this creates a copy of the plan -> slow! local crew = ship.crew -- create station local desc = StationDescriptor() desc.factionIndex = ship.factionIndex desc:setPlan(plan) -- this creates another copy of the plan -> slow! desc.position = ship.position desc.name = ship.name ship.name = "" local station = Sector():createEntity(desc)
-- create the station -- get plan of ship local plan = ship:getMovePlan() -- moves the internal data out of the ship -> only reassigns a few pointers -> fast! -- now, the plan of the ship is only a single block, the actual plan is inside the local variable plan! local crew = ship.crew -- create station local desc = StationDescriptor() desc.factionIndex = ship.factionIndex desc:setMovePlan(plan) -- moves the internal data from the 'plan' variable into the descriptor -- 'plan' is still a valid variable, but it's an empty plan now, all data is in 'desc' desc.position = ship.position desc.name = ship.name ship.name = "" local station = Sector():createEntity(desc)
Moving data around is a fast and easy way of getting large amounts of data from one location to another, just keep in mind that, when you do this, you also modify the source object!