Scripting Performance – Arma Reforger

From Bohemia Interactive Community
Revision as of 11:03, 18 May 2022 by Lou Montana (talk | contribs) (1 revision imported)
Jump to navigation Jump to search

Performance is an important aspect of scripting, as poor performance can tank the whole gaming experience, if not the game itself.

  • what is tanking perfs
    • crazy requests
    • RAM saturation
    • redundant code operations
  • how to benchmark/identify what slows down things
    • what is acceptable?
    • what is not?


Performance Enemies

Many if not all of the usual performance issues can see some of these questions asked:

  • is it needed?
  • is it needed there?
  • is it needed that much?
  • is it needed immediately?
  • is it needed that frequently?

and the respective solutions to positive replies to these questions would be:

  • don't do it: quite straightforward, can mean to remove the code or to replace it with a simpler code
  • don't do it there: the concerned class might be dealing with responsibilities that are out of its scope
  • don't do it that much: the structure can be rethought in order to have fewer operations to be executed
  • don't do it in one frame: the calculation can be spread over multiple frames and result delayed and cached
  • don't do it that frequently: the code can be set to run every X seconds, result being delayed and cached until the next iteration

Not Needed

Problem

Partially unneeded calculation is done.

Solution

  • Remove the unneeded code
  • split the code to only do needed processing in each case

Example

Original Optimised
// soon™
// soon™

Misplaced

Problem

A calculation/storage is done on every instance whereas calculation could be centralised.

Solution

Move the responsibility and the data to a dedicated class (S from SOLID, Single Responsibility Principle).

Example

Original Optimised
class PerformanceExample : IEntity
{
	protected ref array<ref PotentialTarget> m_aObjects = { /* ... */ }; // big list of objects

	protected array<ref PotentialTarget> GetTargets()
	{
		array <ref PotentialTarget> result = {};
		foreach (PotentialTarget object : m_aObjects)
		{
			if (object.IsEnemyTo(this.GetFaction()))
			{
				result.Insert(object);
			}
		}
		return result;
	}
}
class EnemyManager
{
	protected ref map<Faction, ref array<ref PotentialTarget>> m_mTargetMap;

	void EnemyManager(array<ref PotentialTarget> potentialTargets)
	{
		m_mTargetMap = new map<Faction, ref array<ref PotentialTarget>>();

		array<ref PotentialTarget> factionTargets;
		foreach (Faction faction : Faction.GetAllFactions())
		{
			factionTargets = {};
			foreach (PotentialTarget potentialTarget : potentialTargets)
			{
				if (potentialTarget.IsEnemyTo(faction))
				{
					factionTargets.Insert(potentialTarget);
				}
			}
			m_mTargetMap.Set(faction, factionTargets);
		}
	}

	array<ref PotentialTarget> GetTargets(Faction faction)
	{
		return m_mTargetMap.Get(faction);
	}
}

class PerformanceExample : IEntity
{
	protected ref m_EnemyManager;

	void PerformanceExample(notnull EnemyManager enemyManager)
	{
		m_EnemyManager = enemyManager;
	}

	protected array<ref PotentialTarget> GetTargets()
	{
		return m_EnemyManager.GetTargets(this.GetFaction());
	}
}

Not Spread

Problem

A big array of data is processed at once. The calculation itself is not the issue, the amount of items is.
or
A heavy calculation is done, and only one such operation should happen per frame.

Solution

Spread the calculation over multiple frames.

Example 1

Array size issue

Original Optimised
class PerformanceExample : IEntity
{
	protected ref array<ref AliveOrNotObject> m_aObjects = { /* ... */ }; // big list of objects

	void EOnInit(IEntity owner)
	{
		GetGame().GetCallqueue().CallLater(PrintResult, 250, true);
	}

	int GetNumberOfAliveObjects()
	{
		int result = 0;
		foreach (AliveOrNotObject object : m_aObjects)
		{
			if (object.IsAlive())
			{
				result++;
			}
		}
		return result;
	}

	void PrintResult()
	{
		Print("Alive objects = " + GetNumberOfAliveObjects());
	}
}
class PerformanceExample : IEntity
{
	protected ref array<ref AliveOrNotObject> m_aObjects = { /* ... */ }; // big list of objects
	protected ref array<ref AliveOrNotObject> m_aObjectsToBeProcessed = {};
	protected bool m_bIsCalculating;
	protected int m_iTempResult;
	protected int m_iAliveCountBuffer;

	void EOnInit(IEntity owner)
	{
		Calculate();
		GetGame().GetCallqueue().CallLater(PrintResult, 250, true);
	}

	void EOnFrame(IEntity owner, float timeSlice)
	{
		if (m_bIsCalculating)
		{
			Calculate();
		}

		// other frame things
		// ...
	}

	void Calculate()
	{
		if (m_aObjectsToBeProcessed.Count() < 1)
		{
			m_bIsCalculating = true;
			m_iTempResult = 0;
			m_aObjectsToBeProcessed.Copy(m_aObjects);
		}

		// max 10k items slice
		for (int i = 0, int maxCount = Math.Min(m_aObjectsToBeProcessed.Count(), 10000); i < maxCount; i++)
		{
			AliveOrNotObject object = m_aObjectsToBeProcessed[0];	// always @ index 0
																	// for we remove the first item later
			if (object.IsAlive())
			{
				m_iTempResult++;
			}
			m_aObjectsToBeProcessed.Remove(0);
		}

		if (m_aObjectsToBeProcessed.Count() < 1)
		{
			m_iAliveCountBuffer = m_iTempResult;
			GetGame().GetCallqueue().CallLater(Calculate, 1000); // restart calculation in 1 second
			m_bIsCalculating = false;
		}
	}

	int GetNumberOfAliveObjects()
	{
		return m_iAliveCountBuffer;
	}

	void PrintResult()
	{
		Print("Alive objects = " + GetNumberOfAliveObjects());
	}
}

Example 2

Heavy calculation issue

Original Optimised
class PerformanceExample : IEntity
{
	protected ref array<ref Player> m_aPlayers = { /* ... */ };
	protected ref map<ref Player, bool> m_mCastResult;
	protected BaseWorld world;

	void PerformanceExample()
	{
		m_mCastResult = new map<ref Player, bool>();
		world = GetGame().GetWorld();
	}

	void CheckLineOfSight()
	{
		float result;
		TraceParam traceParam;
		foreach (Player player : m_aPlayers)
		{
			traceParam = new TraceParam();
			traceParam.Start = this.Position();
			traceParam.End = player.Position();
			result = world.TraceMove(traceParam, null); // a Trace is a very expensive method
			m_mCastResult.Set(player, result == 1);
		}
	}
}
class PerformanceExample : IEntity
{
	protected ref array<ref Player> m_aPlayers = { /* ... */ };
	protected ref map<ref Player, bool> m_mCastResult;
	protected BaseWorld world;
	protected int m_iIndex = -1;
	protected bool m_bIsCalculating;

	void PerformanceExample()
	{
		m_mCastResult = new map<ref Player, bool>();
		world = GetGame().GetWorld();
	}

	void EOnFrame(IEntity owner, float timeSlice)
	{
		if (m_bIsCalculating)
		{
			CheckLineOfSight();
		}
	}

	void CheckLineOfSight()
	{
		m_iIndex++;
		m_bIsCalculating = m_iIndex < m_aPlayers.Count();
		if (!m_bIsCalculating)
		{
			m_iIndex = -1;
			return;
		}

		Player player = m_aPlayers[m_iIndex];
		TraceParam traceParam = new TraceParam();
		traceParam.Start = this.Position();
		traceParam.End = player.Position();
		float result = world.TraceMove(traceParam, null); // one Trace per frame
		m_mCastResult.Set(player, result == 1);
	}
}

Not Delayed

Problem

A heavy operation is done frequently.

Solution

Do the heavy operation once, store its result in a member variable and accessible through a getter.

Example

Original Optimised
class PerformanceExample : IEntity
{
	protected ref array<ref AliveOrNotObject> m_aObjects = { /* ... */ }; // big list of objects

	void EOnInit(IEntity owner)
	{
		GetGame().GetCallqueue().CallLater(PrintResult, 250, true);
	}

	int GetNumberOfAliveObjects()
	{
		int result = 0;
		foreach (AliveOrNotObject object : m_aObjects)
		{
			if (object.IsAlive())
			{
				result++;
			}
		}
		return result;
	}

	void PrintResult()
	{
		Print("Alive objects = " + GetNumberOfAliveObjects());
	}
}
class PerformanceExample : IEntity
{
	protected ref array<ref AliveOrNotObject> m_aObjects = { /* ... */ }; // big list of objects
	protected int m_iAliveCountBuffer;

	void EOnInit(IEntity owner)
	{
		GetGame().GetCallqueue().CallLater(Calculate, 1000, true);	// calculate only every second
		GetGame().GetCallqueue().CallLater(PrintResult, 250, true);	// displays result every 1/4 second
	}

	void Calculate()
	{
		int result = 0;
		foreach (AliveOrNotObject object : m_aObjects)
		{
			if (object.IsAlive())
			{
				result++;
			}
		}
		m_iAliveCountBuffer = result;
	}

	int GetNumberOfAliveObjects()
	{
		return m_iAliveCountBuffer;
	}

	void PrintResult()
	{
		Print("Alive objects = " + GetNumberOfAliveObjects());
	}
}


Specific Issues

Big Foreach

Problem

Going through one big list of items in one operation.

Specific Solution

  • use a smaller list by being more precise
  • continue out of the scope if the item is not viable for the operation

High-Frequency Scripts

Problem

OnEachFrame thingies - is it needed?

Specific Solution

  • less resource-hungry calls
  • buffering/caching

High-Performance Request

Problem

Is a 10km raycast really needed?

Specific Solution

  • reconsider the need
  • find a smarter solution


Benchmark

Valid Concerns

  • non time-critical operations that are made in the same frame
  • high RAM usage

No Concerns

  • Normal operations that are required and that one cannot do without (yes, check if the player is alive before performing action, otherwise the feature breaks)
  • one-time required big operation (spawning a base = data loading, no can do anything about it)