Performance Optimization

From Avorion Wiki
Jump to: navigation, search

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 Profiler class.

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:

  1. Polling: Check every frame (or second) whether that something has changed (ie. check for new cargo)
  2. 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 onCargoChanged(...) callback.

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

  • function updateParallelRead(timeStep)
  • function updateParallelSelf(timeStep)

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:

  1. 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.
  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

When 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!

See Also[edit | edit source]