Difference between revisions of "Performance (Papyrus)"
imported>Plplecuyer |
imported>Hannibalektr |
||
Line 373: | Line 373: | ||
After profiling, you will end up with logs in your Script folder in the Profiling sub-folder. This will generally be "<User Documents Folder>\My Games\Fallout4\Logs\Script\Profiling". | After profiling, you will end up with logs in your Script folder in the Profiling sub-folder. This will generally be "<User Documents Folder>\My Games\Fallout4\Logs\Script\Profiling". | ||
You can then open these logs using the profile log tool in the | You can then open these logs using the profile log tool in the Tools folder where your CK was installed. | ||
The tool will then give you a screen listing every function that was recorded, and various statistics for that particular function. If you select one of the functions then the right panel will show you the complete call tree of a particular thread that executed that function (you can change which call you view with the dropdown). Using that timing information, you should be able to figure out what is making your script slow, and hopefully figure out how to resolve it. | The tool will then give you a screen listing every function that was recorded, and various statistics for that particular function. If you select one of the functions then the right panel will show you the complete call tree of a particular thread that executed that function (you can change which call you view with the dropdown). Using that timing information, you should be able to figure out what is making your script slow, and hopefully figure out how to resolve it. | ||
=I can't remember all this!= | =I can't remember all this!= | ||
Well, you can always come back here for a review, but also try to get coworkers or friends to look at your scripts. They may be able to spot things you miss, or give you alternate solutions to your issues. They may even give you an idea for a new feature, or how to make your script more reusable. | Well, you can always come back here for a review, but also try to get coworkers or friends to look at your scripts. They may be able to spot things you miss, or give you alternate solutions to your issues. They may even give you an idea for a new feature, or how to make your script more reusable. | ||
[[Category:Papyrus]] | [[Category:Papyrus]] |
Latest revision as of 13:50, 1 April 2016
Overview[edit | edit source]
Papyrus, like other scripting and programming languages, has various ways to accomplish the same task. Also like these other languages, some of these methods are more preferable then others in regards to speed, memory usage, or sometimes both. Unlike most other languages, Papyrus has some special architectural choices that make performance sometimes non-intuitive.
This page is set up to provide some basic guidelines on how to improve your script's performance, memory usage, or both, as well as information on profiling your script to see what parts are slow and could use improvement.
Why should I care?[edit | edit source]
While expensive scripts won't impact your framerate, they will impact each other's performance. The more scripts that are running at the same time, the slower each of them is going to run, in general. Therefore, for the best responsiveness, it is preferred to do your work as quickly as possible and exit.
In terms of persistence, having fewer objects persistent means fewer objects have to stay loaded. The fewer objects that are loaded, the better your framerate will be, and the less memory the game will use. And a better framerate equal better script performance as well.
Scripts are cheap, right?[edit | edit source]
Profiling data from a single function call that called functions on two other objects, one of which was hotly contested. Note the long delays on functions called on OBJ B as they have to wait for other scripts to finish.
<OBJ B - Function 1> - 1790.75ms
- 0.06ms
<queue push> - 1454.16ms
<OBJ B - Function 1> - 0.02ms
- 0.02ms
- 1.47ms
<OBJ B - Function 2> - 0.07ms
- 0.05ms
<OBJ C - Function 1> - 0.07ms
- 0.07ms
- 0.16ms
<queue push> - 333.89ms
<OBJ B - Function 3> - 0.42ms
...
A second set of profiling data from a single function call, this time calling another function 3 times in row. This second function would repeat the same work on the same object, rather then having the data passed in via a parameter.
<Function A> - 7233.80ms
- 0.08ms
<Function B> - 5937.31ms
- 0.03ms
ObjectReference..X - 14.29ms
- 0.02ms
<queue push> - 8.76ms
ObjectReference..GetPositionX - 0.02ms
- 0.02ms
- 5.49ms
- 0.06ms
ObjectReference..Y - 5762.33ms
- 0.04ms
<queue push> - 5760.60ms
ObjectReference..GetPositionY - 0.04ms
- 0.04ms
- 1.66ms
- 0.15ms
<queue push> - 121.20ms
ObjectReference..Z - 33.04ms
- 0.76ms
<queue push> - 20.75ms
ObjectReference..GetPositionZ - 0.02ms
- 0.02ms
- 5.45ms
<queue pop> - 6.06ms
- 6.20ms
math..sqrt - 0.01ms
- 0.01ms
- 0.02ms
- 0.06ms
<Function B> - 161.81ms
Getting X, Y, and Z from the same object, getting the same values, repeating work...
<Function B> - 649.49ms
Getting X, Y, and Z from the same object, getting the same values, repeating work...
...
Speed Improvements[edit | edit source]
Don't re-do work[edit | edit source]
Bad:
if getLinkedRef(myKeyword)
if !someStatus
; Do stuff here
if getDistance(getLinkedRef(myKeyword)) > 64
moveto(getLinkedRef(myKeyword))
endif
else
; Do other stuff here
endif
endif
Better:
ObjectReference linkedRef = getLinkedRef(myKeyword)
if linkedRef
if !someStatus
; Do stuff here
if getDistance(linkedRef) > 64
moveto(linkedRef)
endif
else
; Do other stuff here
endif
endif
If you perform an operation and the result of said operation isn't going to change, don't keep doing it over and over again. Store the result in a variable and re-use the variable where needed. Prefer to store the variable in a function instead of your script to reduce memory costs and reduce persistence.
Don't handle events you don't have to[edit | edit source]
Bad:
EVENT onLoad()
RegisterForHitEvent(self)
endEVENT
EVENT onHit(...)
if PropertyValue == TRUE
DoWork()
endif
endEVENT
Better:
EVENT onLoad()
if PropertyValue == TRUE
RegisterForHitEvent(self)
endif
endEVENT
EVENT onHit(...)
DoWork()
endEVENT
If you are not interested in an event, then don't request it, or don't handle it. Handling an event and checking a condition is slower then checking the condition before asking for the event, or using a script state to route the event to an empty handler.
Fail fast, fail early[edit | edit source]
Bad:
if Function() && OtherFunction() == false
Actor meActor = (self as ObjectReference) as Actor
if meActor
meActor.SetUnconscious(true)
endif
endif
Better:
Actor meActor = (self as ObjectReference) as Actor
if meActor && Function() && OtherFunction() == false
meActor.SetUnconscious(true)
endif
Certain operations, like casting, are faster then others, like function calls. If you test things that are fast early, then if they fail you'll have saved time doing the more expensive checks. Also, if a particular check fails more often then the others, prefer to put it first. In the above example, almost everything passed to the function wasn't an actor, so making the actor check first eliminated several calls to the two functions.
Know your language features and functions[edit | edit source]
Bad:
ObjectReference nextRef = GetLinkedRef()
while nextRef
; do something with nextRef
nextRef = nextRef.GetLinkedRef()
endWhile
Better:
ObjectReference[] refChain = GetLinkedRefChain()
int curIndex = 0
while curIndex < refChain.Length
ObjectReference curRef = refChain[curIndex]
; do something with curRef
curIndex += 1
endWhile
There are some functions in Papyrus that will do what you're trying to do natively, so you don't have to spend time doing it yourself. In the above case, it's faster to call a single function to get an array of all linked refs, and iterate over that array, then to repeatedly call getLinkedRef over and over again. This mostly comes down to knowing what tools are available and where they should be used. Of course, if you don't need all the linked refs, and only want one of them, then don't get them all.
Know your language features and functions (part 2)[edit | edit source]
Bad:
Event OnActivate(...)
if DoStuff
; Do a bunch of things
endIf
EndEvent
Better:
State DoStuff
Event OnActivate(...)
; Do a bunch of things
EndEvent
EndState
Event OnActivate(...)
; Empty!
EndEvent
Similar to the previous point, be aware of what language features are available. In this case, using a state and an empty OnActivate event saves time, because Papyrus can detect empty functions and events and avoid calling them at all. So if you aren't in the "DoStuff" state, no code has to be run. The downside to this approach, of course, is you can only have one script state at a time, so it may not fit all usage cases.
Prefer single point of entry over multiple[edit | edit source]
Bad:
Event OnActivate(...)
ObjectB.FunctionA()
ObjectB.FunctionB()
ObjectB.FunctionC()
EndEvent
Better:
Scriptname ScriptA extends ObjectReference
ScriptB ObjectB
Event OnActivate(...)
ObjectB.DoAllTheStuff()
EndEvent
;------- Separate file -------
Scriptname ScriptB extends ObjectReference
Function DoAllTheStuff()
FunctionA()
FunctionB()
FunctionC()
EndFunction
Because every time you call a function on another script that isn't you or a script you extend (i.e. the "self" variable would be different - even if it's attached to the same form) there is a cost in switching. This cost may be expensive if the object is used by a lot of scripts (i.e. the player). So if you find yourself calling multiple functions in a row on the same object, it might be better to group them into a function on the target object and call that instead. That way the switch cost is only paid once. This may not always be possible, however, especially if the destination script isn't one you own.
Additional Notes[edit | edit source]
Calling a native function usually (but not always) syncs to the framerate of the game. So if you call 30 native functions and the game is running at 30 fps, it will take (roughly) 1 second to get through them all, assuming no other extenuating circumstances. The exception are non-delayed functions, which are dispatched immediately. (Note that some of these may be latent and so may take some time to return, even if they immediately dispatch)
Also, if someone else is busy working on an object, everyone else will have to wait their turn. For example, if 10 scripts all want to ask the player what level they are, they all have to take a number and go in order. As such, if you find yourself making "controller" scripts, you may want to carefully monitor how much they are used to make sure you're not slowing yourself down by talking to them all the time. Note that if you call a function outside your script, or a latent function, the next person waiting to talk to your object will get a chance to run.
Memory and Persistence[edit | edit source]
Prefer events[edit | edit source]
Bad:
While HasDirectLOS(OtherObj)
DoStuff()
Utility.Wait(1)
EndWhile
Better:
bool doingStuff
Function SomeFunction()
doingStuff = true
RegisterForDirectLOSLost(self, OtherObj)
StartTimer(1, DoStuffID)
EndFunction
Event OnLostLOS(...)
doingStuff = false
CancelTimer(DoStuffID)
EndEvent
Event OnTimer(int ID)
if ID == DoStuffID
DoStuff()
if doingStuff
StartTimer(1, DoStuffID)
endIf
endIf
EndEvent
Loops polling for things, or doing things over and over again, consume resources while running. Both memory and processor time. If possible, prefer to use events instead of polling, and repeated timers instead of long-running loops (and make sure you have a way out of the timer loop!). Waiting for events is much easier on the system then constantly asking "are you done yet?"
Prefer function variables over script variables[edit | edit source]
Bad:
ObjectReference linkedRef
Event OnInit()
linkedRef = GetLinkedRef()
EndEvent
Event OnActivate(...)
; Do stuff with linkedRef
EndEvent
Better:
Event OnActivate(...)
ObjectReference linkedRef = GetLinkedRef()
; Do stuff with linkedRef
EndEvent
While the first example may be better for performance - you aren't getting the linked ref every single time you are activated - it is worse on memory and persistence. This is because once you've stuck an object into a variable, that object is now persistent until that variable goes away. If that variable is a member of a script, this may be a very long time (or even never).
Prefer to use a function-local variable instead, even though it will cost you a little time in the function. This is because function-local variables go away at the end of the function, releasing the persistence hold on the object they contain.
If you must make an script variable to hold something, then try to clear it by setting it to None when you no longer need it.
Don't register for events that can't happen[edit | edit source]
Bad:
Event OnInit()
RegisterForHitEvent(self)
EndEvent
Better:
Event OnLoad()
RegisterForHitEvent(self)
EndEvent
Don't register for an event if the event can't realistically occur at the time of registration. In the above example, it doesn't make sense to register for a weapon hit in OnInit, because this object may not even have 3d to be hit. It's better to wait til the object's 3d is loaded first, before asking for hit events.
Circular References[edit | edit source]
Bad:
Scriptname ScriptA extends ObjectReference
ObjectReference ThingIMade
Event OnActivate(...)
ThingIMade = PlaceAtMe(Thingy)
(ThinkIMade as ScriptB).SayHello(self)
EndEvent
;------- In another file -------
Scriptname ScriptB extends ObjectReference
ObjectReference MyParent
Function SayHello(ObjectReference aParent)
MyParent = aParent
EndFunction
... where's the better example? Well that's kind of hard to show, as it will depend on what you're doing. The problem with the above example is that ScriptA has put and object into a variable, persisting it, and then ScriptB put the original object into a variable as well, persisting it! This means neither A nor B will ever go away as long as they are in the variables. Sometimes you can't avoid this, but in general, try not to make "reference loops" like this.
Function Size[edit | edit source]
Very large functions consume more memory then small functions. This is usually due to the variety of variables (visible and hidden) in the function. As such, prefer to break up large functions into a series of calls into smaller functions. This way the original function only needs enough memory to make the calls into the smaller functions, and the smaller functions only need enough memory for themselves - giving it all back when they exit.
Not only that, but grouping your script into small, easy to understand functions will make things easier to read and follow.
When in doubt - a function should probably only do one thing, but do it well. If you find yourself doing multiple things in a function, it might be a good candidate to break into smaller ones, even if the smaller ones are only called once.
Profiling Your Scripts[edit | edit source]
Why should I profile?[edit | edit source]
People are incredibly bad and picking out what part of their script or code is actually slow. This isn't unique to scripters or modders, even seasoned programmers don't trust themselves to figure out what part of their code is slowest. So instead of guessing, we measure. In other words, we profile to find out what part of the code or script takes the longest and why, so we can focus our efforts on the parts where we'll get the most bang for our buck.
Setup[edit | edit source]
Before any of the Papyrus profiling console commands and functions will do anything, you need to edit your ini file to turn profiling on. This is off by default because profiling can slow the script system down slightly. To turn it on, set the "bEnableProfiling" option in the "[Papyrus]" section of your custom ini to 1.
[Papyrus]
bEnableProfiling=1
How to profile[edit | edit source]
There are a few ways to profile, depending on what kind of information you want.
The most wide-ranging option is to turn on global profiling using the TPGP console command. This will log everything the script system does from when it is turned on til when it is turned off. This may be too much information, but if you're tracking down why you have general "script lag" it can at least give you a good starting point to do finer-grained profiling by pointing out hot spots.
The next most common option is to use the StartStackProfiling script command in a particular event or command you want to measure. This will log everything that a particular thread of execution does, once the start command is called.
There are also other, more specialized, commands which vary in usefulness. Most of them are either in the debug script or listed on the papyrus console commands page. The one thing to keep in mind is that a lot of these more specific commands are filtered, so if you say, profile a script, you'll know how long a function takes in that script, but any calls into things that aren't that script won't be measured, so it might be hard to tell if a set stage is taking too long if you're measuring your quest script (for example).
Now what?[edit | edit source]
After profiling, you will end up with logs in your Script folder in the Profiling sub-folder. This will generally be "<User Documents Folder>\My Games\Fallout4\Logs\Script\Profiling".
You can then open these logs using the profile log tool in the Tools folder where your CK was installed.
The tool will then give you a screen listing every function that was recorded, and various statistics for that particular function. If you select one of the functions then the right panel will show you the complete call tree of a particular thread that executed that function (you can change which call you view with the dropdown). Using that timing information, you should be able to figure out what is making your script slow, and hopefully figure out how to resolve it.
I can't remember all this![edit | edit source]
Well, you can always come back here for a review, but also try to get coworkers or friends to look at your scripts. They may be able to spot things you miss, or give you alternate solutions to your issues. They may even give you an idea for a new feature, or how to make your script more reusable.