Performance
A plugin is compiled to machine code before the server loads it, so the work it does on its own - loops, arithmetic, arrays, text - is fast. What costs time is crossing between the plugin and the game - a call into AMX Mod X, an event on its way to a listener - and memory a busy function asks for again and again. This page says where the time goes and gives the habits that follow.
Where the time goes
- Your own code is fast. Loops, arithmetic with whole numbers and fractions, arrays and maps run as machine code.
- A call costs more than the code around it. A native, and a call to one
of your own small functions - an array's
[]among them - costs more than the arithmetic beside it. A few hundred a frame go unnoticed; code that reads the game every frame and does little with it spends its time in the calls. - The properties of a player, a weapon or an entity are read where the
game keeps them, with no native in between. A property that is a vector
makes a new
Vectoreach time it is read;get<Field>(target)writes it into one you keep. - An event costs the most: on its way to a listener it takes far longer than a call. An event a round, a kill or a command does not register; one every frame for every player is worth counting.
- Memory is managed for you: what a function makes is freed when nothing holds it, and making it is what costs.
- A plugin
amxts buildmakes keeps no record of the calls that led to an error, which would slow a busy plugin down: it names the error and its place, andamxts devkeeps the whole record (errors).
Writing a fast plugin
The facade before the natives
player.money and the other properties of a player, a weapon or an entity
read and write the game's memory directly. ReAPI's field natives -
get_member, get_entvar and their like - take a ... tail, which a
TypeScript call puts together as it runs, so they cost far more than the
property:
// do
if (player.money >= price) player.money -= price;
// don't
if (get_member(player.id, m_iAccount) >= price) set_member(player.id, m_iAccount, get_member(player.id, m_iAccount) - price);
A plain native - is_user_alive, user_slap - is cheap: call it freely
where the facade has nothing of its own
(natives).
Listen to the class you need
Name the classes in the listener's options, and the server hands the plugin only those events; a check inside the listener runs after the event has already paid its way in:
// do: a player touching a weapon on the ground, nothing else
game.addEventListener("touch", onPickup, { toucher: "player", touched: "weaponbox" });
// don't: every touch of every entity, every frame, to find the same ones
game.addEventListener("touch", (event) => {
if (event.toucher.classname != "player" || event.touched.classname != "weaponbox") return;
onPickup(event);
});
classname does the same for an event about one class of entity
(one class of entity).
Keep frequent events light
Some events come all the time: "frame" every server frame, preThink and
postThink every frame for every player, touch, and on every shot
shoot, traceAttack and primaryAttack. On a full server preThink
comes tens of thousands of times a second, and each time it pays its way to
the listener. Do there only what must happen at that moment, and let a
timer do what can wait:
// do: ten times a second, for everyone at once
setInterval(() => {
for (const player of server.players) checkSpeed(player);
}, 100);
// don't: the same check on every frame of every player
game.addEventListener("preThink", (event) => checkSpeed(event.player));
Do not make objects in a busy loop
Every new object costs time to make and later to free, a Vector or a
text more than most. A vector field such as player.origin makes a new Vector on every read, and
so does every event.player. Where it runs every frame, keep a Vector and
have the field written into it:
// do: two Vectors for the whole plugin
const origin = new Vector();
const velocity = new Vector();
function track(player: Player) {
player.getOrigin(origin);
player.getVelocity(velocity);
const speed = Math.sqrt(velocity.x * velocity.x + velocity.y * velocity.y);
}
// don't: two new Vectors for every player, every frame
function track(player: Player) {
const speed = Math.sqrt(player.velocity.x * player.velocity.x + player.velocity.y * player.velocity.y);
}
Text works the same way: build a HUD line when what it shows changes, not every time it is sent.
Read server.players once
Every read of server.players asks the server again and makes a new list of
new players, and the more players, the more it costs. Read it once, and
reuse it:
// do: one list for the whole loop
const players = server.players;
for (const player of players) {
const enemies = players.filter(other => other.team != player.team);
warn(player, enemies);
}
// don't: a new list for every player
for (const player of server.players) {
const enemies = server.players.filter(other => other.team != player.team);
warn(player, enemies);
}
Events, not polling
A timer that asks every player whether something has happened does that work every time, also when nothing has. An event arrives once, when it happens:
// do: the game says when a player has spawned
game.addEventListener("spawn", (event) => giveKit(event.player), { post: true });
// don't: asking every player, ten times a second, whether he is alive yet
setInterval(() => {
for (const player of server.players) {
if (player.isAlive && !equipped.has(player.id)) giveKit(player);
}
}, 100);
Waiting is await
A web request with fetch or useFetch runs beside
the game: the server goes on while it waits, and the answer comes in a later
frame. A loop that spins until something changes holds the whole server
instead - wait with await, a timer or an event
(async). Files are read and written on the server's own
thread, the promise forms of @amxts/core/fs too:
read a file once, when the plugin starts, rather than in a listener.
Crossing into Pawn
A call to a native - AMX Mod X's, a module's, another plugin's - is cheap.
A native with a ... tail - get_entvar, get_member, ExecuteForward,
formatted text - has its arguments put together as it runs and costs much
more. In a listener that runs every frame, count those; anywhere else, they
do not matter.
Async and promises
A plugin waits the way JavaScript does: async functions, await, Promise. While one function waits - for a web request, for a few seconds - the server and the rest of the plugin go on.
Players and entities
Everything in the game world is an entity: players, weapons, grenades, doors, the bomb. In a plugin an entity is an object, and its data are properties you read and assign: