Core

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 Vector each 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 build makes 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, and amxts dev keeps 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.