Aller au contenu

Photo

Technique to prevent performance issues, Serialized Fake Heartbeat


  • Veuillez vous connecter pour répondre
1 réponse à ce sujet

#1
painofdungeoneternal

painofdungeoneternal
  • Members
  • 1 799 messages
This is something i've set up for my CSL library, which is aimed at solving an issue I see even more advanced scripters destroy the performance of a PW or module with, the feared fake heartbeat. But its also just an idea which can be used on it's own and makes heartbeats which won't destroy NWN2's performance. I've been using it for a long time for my drowning in water code, for my counterspelling monitor, the spell cast in stone, and the spell mercy, and was about to use it again for my languages system when i thought others might be able to use it.

Imagine you have a spell which does some stuff to a player every round. This can be anything from doing some damage, to just waiting until said player stops concentrating and you want to make magic wall go poof in a puff of smoke. To do this you use a Fake Heartbeat which really is a special function which uses delay command to repeat itself. Most functions just do their thing and quit, but this one trails with a special command telling itself to repeat after a given interval. And what you get is an endless loop, which unless you give it a way to stop will never stop. The very real issue is if this gets out of control with multiple fake heartbeats starting on the same object, and ensuring it can be stopped when needed. Often the person programming it has not idea his single fake heartbeat is actually repeating over and over, or perhaps it never quits which makes that PW grind to a halt after it runs for a month straight. Here is a very simple spell script to describe this.

void RepeatingSomeEffect( object oPlayer )
{
	//Do my stuff here, imagine this is very involved
	
	DelayCommand( 6.0f, RepeatingSomeEffect( oPlayer ) );
}

//To start this off use the following 
void main()
{
	// do some stuff
	oPlayer = OBJECT_SELF;
	
	// do initial round stuff, note i don't delay it here, that way i only have to code things once
	RepeatingSomeEffect( oPlayer );
}

Notice i have a function at the top which returns void ( void in front ) and which does not return anything, which is required for any function used for a fake heartbeat. The "RepeatingSomeEffect" will just keep repeating forever. Now this is very simple, but unless the player leaves the server or whatever oPlayer is happens to be destroyed it will keep chugging around. ( Any real code would check for player death, to double check the spell effect is still on them, or keep track of remaining rounds, etc, but that is another topic and i can put that in a finished example later ).

The issue is this, if it does this once it's fine. However how do you deal with this if it's done multiple times and there is no real way to prevent it. To a large degree you can fix this up front by checking up front for stacking, which basically means a caster cannot apply 2 at once and to extend the duration they need to wait until the first effect/repeater is done. Even then, if you dispel it via a buddy, and half a second later cast it again, and manage to do it inside that interval you can get the same function running twice. Generally if well done it's minor overhead, but often these things get out of control and you might have hundreds of these running per second, per creature really affecting performance.

The behaviour i want though is for a RepeatingEffect to replace the older one, any previous loops should just stop working after the new loop starts.

To solve this i actually used a technique based on something actually done on webservers, where a completely random number is created as a serial number to avoid the overhead of actually keeping track of any numbers. Normally people think you want a incremented number but this means you have to keep track which is actually a lot of overhead. The idea is that since it's random, the odds are better than 1 in a million, which means it is realistically never going to happen twice in a row, even though every once in a while it might.

The basic idea is this, when the looping heartbeat starts it begins with a -1. The void repeat fucntion is structured in three parts, the beginning is a validation routine, the middle is the meat of the script implementing whatever is desired, and the final part is a delayed repeat which makes the whole thing repeat.

The first pass thru the serial is -1, which makes it know it's just starting and the dominant beat. It validates the current serial and sets a variable on the control object with a random number, and then returns true.

The first round is allowed to run normally doing whatever work is intended.

If the serial is -1, it goes ahead and gets the variable from the control object.

At the end of it, the function actually runs itself, with a delay command, and it passes whatever paramters it wants, but the final parameter is the serial number. This serial number is not retrieved again, it basically floats in the out function being passed as a parameter in a loop, and acting as the identifier for the current heartbeat.

Now at any time, if that serial does not match the variable stored on that control object, you know you have a duplicate. The validation which begins the function sees this, returns false, and the function immediately exits. If it's the same serial number being passed, it knows its unique, however other rules can be added by the scripter such as does it still have the spell effect, did the player die, or perhaps is the player still in combat.

Now what if i want there to be 2 of these at once, i just added a name parameter to the 2 functions which gets combined with the serial number. This allows 2 heartbeats to coexist, while also ensuring only one of a given type is running at once. This is important especially if you have 2 conflicting codebases using this similar technique, or perhaps have one coming from a players spell and another from a obstacle.

Now compare this with the previous code, and look at the changes. You can actually use the following as a template as you mainly have to add more paramters, and edit the do my stuff here. Remember to come up with a unique name for each repeater by changing the "REPEATERNAME" string in the two spots it's located.

// assuming you use CSL Library you can use this include, if not include the 2 functions here
#include "_CSLCore_Magic"

// added iSerial = -1 to the Fake Heartbeat. oPlayer and iSerial are required. In between will be whatever paramters are needed which should be quite a few, i added iRemainingRounds as an example. Note that if you need anything pass it as a paramter, things to get caster level and spell class  for example do not work inside a fake heartbeat
void RepeatingSomeEffect( object oPlayer, int iRemainingRounds = 25, int iSerial = -1 )
{
	//.*** this is the validation portion , note i use a || which means or, you would add more validation checks here to do other things like GetIsObjectValid ***//
	if ( !GetIsObjectValid( oPlayer ) || ( iRemainingRounds < 1 ) || !CSLSerialRepeatCheck( oPlayer, "REPEATERNAME", iSerial ) )
	{
		// either no target or a new monitor is replacing the old
		return;
	}
	
	//.*** this is the Implementation portion ***//
	//Do my stuff here, imagine a lot of cool things here
	
	
	///*** this is the delay portion ***///
	// add this block here, just like this, this fixes the initial round
	if ( iSerial == -1 )
	{
		iSerial = CSLSerialGetCurrentValue( oPlayer, "REPEATERNAME" );
	}
	iRemainingRounds--;
	// added iSerial to ensure it's passing
	DelayCommand( 6.0f, RepeatingSomeEffect( oPlayer, iRemainingRounds, iSerial ) );
}

//To start this off use the following 
void main()
{
	// do some stuff
	oPlayer = OBJECT_SELF;
	
	// do initial round stuff
	// i do not add iSerial here, it is set to -1 in the default value
	DelayCommand( 6.0f, RepeatingSomeEffect( oPlayer ) );
}


Requires 3 helper functions which require the following include from the CSL, the actual functions are below if you just want to use this by itself.
#include "_CSLCore_Magic"

If you use the CSL library these will be updated as people point out bugs or issues.

/**  
* Returns a random integer which can be any possible value, if called multiple times 
* you can pass in the same number so it's set only once
* @param iPreviousValidNumber Used for handling repeated use, it only returns a random number if it starts with -1
* @return integers from 1 to a little over 2.1 billion, the largest possible 32 bit number that exists in NWN2. 
*/
int CSLGetRandomSerialNumber( int iPreviousValidNumber = -1 )
{
	if ( iPreviousValidNumber == -1 )
	{
		return Random( 2147483640 )+1;
	}
	return iPreviousValidNumber;
}

/** 
* Used to ensure a pseudo heartbeat is not repeating, first time effect it is run use -1 as the starting integer
* if a new beat takes over the serials will not match and this will return false
* This allows a new heartbeat to take over when needed.
* @param oControlObject, usually the player but generally what the effect is applied to
* @param sVariableName Unique name which allows multiple serials to run at once
* @param iSerial The current serial number being passed in the heartbeat function to itself
* @see CSLSerialGetCurrentValue 
* @return False means it is a duplicate heartbeat, true means its the main heartbeat
*/
int CSLSerialRepeatCheck( object oControlObject, string sVariableName, int iSerial = -1 )
{
	if ( iSerial == -1 )
	{
		SetLocalInt(oControlObject, "SERIAL_"+sVariableName, CSLGetRandomSerialNumber() );
		
		return TRUE;
	}
	else
	{
		int iCheckSerial = GetLocalInt(oControlObject, "SERIAL_"+sVariableName );
		if ( iCheckSerial != iSerial )
		{
			// duplicate older effect is still in effect
			return FALSE;
		}
	}
	return TRUE;
}

/** 
* Gets the new heartbeat serial number which is in effect
* @param oControlObject, usually the player but generally what the effect is applied to
* @param sVariableName Unique name which allows multiple serials to run at once
* @see CSLSerialRepeatCheck
* @return The current heartbeat which is on the player
*/
int CSLSerialGetCurrentValue( object oControlObject, string sVariableName )
{
	return GetLocalInt(oControlObject, "SERIAL_"+sVariableName );
}



#2
dethia

dethia
  • Members
  • 146 messages
;P this is indeed very helpful, ever since you introduced me to it I use it on all recrusive functions of mine.