Main Menu

Tips On Safe Resource Management

Started by kevin, April 03, 2013, 05:52:59 PM

Previous topic - Next topic

kevin




Tips On Safe Resource Management:

Document Revision: V0.02  (5th,April,2013)



Q. What is this ?  


      Answer:  This is just a few tips / arranged into an FAQ to help new programmers understand resource management in PlayBASIC better.

      Whenever we develop a program, be it in PlayBASIC or other wise, our program is going to be managing many different type of resources,  ranging from simple variables, strings, arrays through to media resources like Images, Sprites and Sounds to just name a few.     Some of these things are managed internally by the language, while others such as sprites/images we become responsible for.    This can catch new programmers out at times, where the programmer forgets to delete a resource they're no longer using.  Which In programming terms is generally referred to as a memory leak.

     
Note: These are in no particular order




Q.  Why does it matter ? Doesn't PlayBASIC clean up all the memory when a program exits ?



      Answer:  Yes, PlayBASIC does indeed delete all memory resources and media when your program quits.   We're not really concerned about what occurs after exit here, rather the issue is making sure our game isn't infinitely consuming memory while it's running.   For the sake of an example, we'll imagine we've written a program that has 10 levels.  At the start of each level the media loading routine loads a couple of images into memory via LoadNewImage() , storing the returned Image Media indexes in a variables.      

       Eg.

PlayBASIC Code: [Select]
Load_Level_Media:

BadGuy1 = LoadNewImage(Path$+"BadGuy1.bmp")
BadGuy2 = LoadNewImage(Path$+"BadGuy2.bmp")

etc
etc

return




    On the surface this might seem like a pretty innocent bit of code and there's really nothing wrong with it, provided that before we call this subroutine multiple times (like at the start of each new level)  we're  deleting old media at some point.   Otherwise each time we run that section of code, it's going to load the Badguy images and  store the new image indexes in the Integer Variables BadGuy1 and BadGuy2.     What's the problem then ? - Well, since they're integer variables  and the contained Image Indexes still exist,  then the code will simply overwrite those values (they won't be deleted for you) with the new ones from the load new image function.   So the program would be forgetting about the old versions.    

     Here's a simplified simulation of what's happening over time in our game, here's it's just condensed into a do loop that loads an image, draws is and then repeat this.  

PlayBASIC Code: [Select]
   ; Let this program run for 5 seconds
EndTime=Timer()+5000

; limit the FPS so, it's run crazy fast
Setfps 10

; ---------------- MAIN LOOP -------
Do
; ---------------------------------

; clear screen
Cls


; simulate our load image
MyIMage =Load_New_Image()

; draw our image
DrawImage MyImage,300,300,false


; display number of images in memory
print "Images In memory:"+str$(Count_Used_Images())

print "My Image Index:"+Str$(MyImage)

Sync

; ---------------------------------
loop Timer()>EndTime
; ---------------------------------


End



; -----------------------------------------------------------------------
; ->> LOAD IMAGE SIMULATION <<-------------------------------------------
; -----------------------------------------------------------------------

Function Load_New_Image()
ThisIMage =NewImage(32,32)
RenderToImage ThisIMage
Cls rndrgb()
rendertoscreen
EndFunction ThisIMage


; -----------------------------------------------------------------------
; ->> Count USED IMAGES <<-------------------------------------------
; -----------------------------------------------------------------------

Function Count_Used_Images()
For lp=1 to GetImageQuantity()
Count+= GetIMageStatus(lp)
next
EndFunction Count





      The code works, but we;re leaking images due to the fault logic in our program.    One solution is that  after we've done using our image we'll delete it .  So in you insert the line DeleteImage MyImage after the draw image line in the example above, the problem is no more.

PlayBASIC Code: [Select]
      ; simulate our load image    
MyIMage =Load_New_Image()

; draw our image
DrawImage MyImage,300,300,false

; Delete Image from memory now. Remember this won't clear MyImage back to zero!
DeleteImage MyImage





      Another approach might be to only load the image when our variable (MyImage) is this example is zero.   Now assuming your variables are global (they're not in this example), this would mean the image data is held in memory indefinitely   So the program only ever reloads the image when our identifier variable is zero.  

      We can do this by placing our  load inside an IF statement, a bit like this.

PlayBASIC Code: [Select]
      if MyIMage=0
; simulate our load image
MyIMage =Load_New_Image()
endif

; draw our image
DrawImage MyImage,300,300,false




         



Q.  Can using DIM / REDIM cause memory leaks ?


      Answer: Yes & No.    If you're dimensioning an array multiple times, like in a loop say, then NO,  the PB VM (runtime) will happily manage this memory for you.  So it'll delete the old arrays data and allocate some new memory for the array.    Problems can occur when we start using Typed Arrays.    While the runtime will happily clean up your types, what it can't do for you, is free any media you have conceptually associated with a particular field in a type.    

      To demostrate this, we'll simulate a fairly standard type characters in a game could use.   Where each character has a hitcounter and a Sprite index which it'd use for it's visualization.  If we fill an array some of random characters data allocated each character a sprite to use, then dim the array again and count the number of 'used' sprites in memory, the answer might come as a shock.


PlayBASIC Code: [Select]
   ; ------------------------------------------
; clear the screen to a bluely colour
; ------------------------------------------
Cls $203040


; ------------------------------------------
; define our game character structure
; ------------------------------------------
Type tGameCharacter
SpriteIndex
HitCounter
EndType


; ------------------------------------------
; dim an empty array of game characters
; ------------------------------------------
print "Dimension Objects Array"
DIm Objects(10) as tGameCharacter


; ------------------------------------------
; Fill the array in with some random data
; to represent what might be a real game
; ------------------------------------------
print "Filling Game Characters Array"
For lp =0 to 10
Objects(lp)=New tGameCharacter
x#=rnd(800)
y#=rnd(800)
Objects(lp).SpriteIndex=NewSprite(X#,y#,0)
Objects(lp).HitCounter =rndrange(100,200)
next

SHow_Game_Characters(200,100)

SHow_Sprites_Used()


print "Press Any Key"

Sync
waitkey
waitnokey


Cls $203040


; --------------------------------------------------------------------
; Redim this array.. This will destroy it's contents, but won't
; delete the SPRITES that have been created..
; --------------------------------------------------------------------

Dim Objects(10) as tGameCharacter

SHow_Game_Characters(200,100)

SHow_Sprites_Used()

print "Press Any Key"



Sync
WaitKey




Function SHow_Game_Characters(Xpos,Ypos)
yh=GetTExtHeight("|")
For lp=0 to GetArrayElements(Objects())
s$ =" Objects("+digits$(lp,2)+") ="

if Objects(lp)
Spr =Objects(lp).SpriteIndex
Hits =Objects(lp).HitCounter
s$+="Sprite #="+digits$(Spr,4)+" "
s$+="Hits #="+digits$(Hits,4)
else
s$+=" Empty"
endif

Text Xpos,Ypos,s$
ypos+=yh

next

EndFunction

Function SHow_Sprites_Used()
For lp=1 to GetSpriteQuantity()
Count+=GetSpriteStatus(lp)
next
print "Sprites IN Memory:"+str$(Count)
EndFunction


     

      If you run the code you'll notice that after the array is dimensioned the second time, PB deletes the types from the array for us, but the number of sprites in memory remains the same.   The reason for this is as you may already guessed, is that our 'SpriteIndex' field within our tGameCharacter type, is just an Integer.    Even though the field  it's called 'SpriteIndex' which might make sense to us humans,  PB has no idea what we're using each field to represent and therefore can't release the Sprite Media for us.    
   
      This particular is example might be a tad extreme,  but there many subtle variations of this theme that can appear in a programs logic if your not careful.




Q.  I think my program is leaking resources, how can I check ?...


      Answer:  Well, the modern versions of the PlayBASIC debugger actually have a memory usages tool build into it.   The tool does an audit of the PB runtimes current memory usage, then dumps this data to the debugger console as text.    The state tab in the debugger has a quick overview of this information also.  So you can check the number of used media types in most of the main command sets and well as viewing the runtimes current memory usage.

      Example of the State panel.  


[Command Sets]   <<<<< HERE WE CAN GET A QUICK LOOK AT WHAT RESOURCES ARE BEING USED

Banks In Use: [ 1 ]  of  [ 1024 ]
Camera In Use: [ 0 ]  of  [ 16 ]
Files In Use: [ 0 ]  of  [ 64 ]
Fonts In Use: [ 2 ]  of  [ 64 ]
Images In Use: [ 1 ]  of  [ 1500 ]
Shapes In Use: [ 0 ]  of  [ 500 ]
Sprites In Use: [ 0 ]  of  [ 1500 ]
Maps In Use: [ 0 ]  of  [ 128 ]

[Current Surface Details]

Surface: 1 [IMAGE]
Type: 2 [FX]
Size: 320,200
ViewPort: 0,0 - 320,200

[Font Details]

Font: 1
Name: Courier
Type: 1  (Windows GDI)

[Graphics Ink]

Current Ink: $FFFFFFFF A=255 R=255 G=255 B=255
Ink Mode: 1
Cursor Pos: X0, Y0
Cursor Margin: X:0

[Scene Details]

Pos: 0
Size: 499999 Bytes
ZDepth: 0.0
CamVis: %1111111111111111

[RunTime]

VM Cache: [ 424 ]  of  [ 1000 ]   <<<<<<< THIS IS NUMBER MEMORY BANKS A program is using at any one time!  
Stack Size: 0



    The VM Cache field shows us the number of chunks of the memory a program is using from a the current size of the memory pool.   If the program runs out of memory, PB expands the pool automatically for us.




Q.  What's the recommended approach for dealing with dynamic resource creation / deletion.


      Answer: When writing programs, we recommend setting out the code in such a way, where all the main behaviors required can be a set of Functions &  Psubs.   For things like the creating and deleting characters, you'd have a set of functions that perform these actions.   This has a number of benefits, from greatly simplifying our program (in particular as it gets larger), to making the detection of potential issues much easier.  

       How does using functions it make fault detection easier ? - Well, if our program isn't working the way we expect, then we know exactly where to look.  Since all the code that performs the various behaviors in our program would be in one place, such as inside the  creation, deletion, movement, collision, animation etc etc functions.  



PlayBASIC Code: [Select]
     Player=NewCharacter(100,200)

BadGuy=NewCharacter(400,400)

; Display the number sprites created
SHow_Sprites_Used()

; call out function that checks if two charcters are close
if CharactersWithinDistance(PLayer,BadGuy,100)
print "This dudes are within 100 pixels"
endif



; delete the characters
DEleteCHaracter PLayer
DEleteCHaracter BAdGuy



; Now Display the number sprites that exist..
SHow_Sprites_Used()

Sync
waitkey


end





;---------------------------------------------------------------------
;---------------------------------------------------------------------
;--EXAMPLE CHARACTER CONTROL FUNCTIONS ----------------------------------
;---------------------------------------------------------------------
;---------------------------------------------------------------------


Type tGameCharacter
x#,y#
Sprite
EndType

DIm Objects(0) as tGameCharacter


;---------------------------------------------------------------------
;--NEW CHARACTER -----------------------------------------------------
;---------------------------------------------------------------------

Psub NewCharacter(X#,y#)

if GetArraySTatus(Objects())=False
DIm Objects(1) as tGameCharacter
endif

Index=GetFreeCell(Objects())

Objects(Index) = New tGameCharacter

Objects(Index).X=X#
Objects(Index).y=y#

Objects(Index).Sprite=NewSprite(x#,y#,0)

EndPsub Index


;---------------------------------------------------------------------
;--DELETE CHARACTER --------------------------------------------------
;---------------------------------------------------------------------

Psub DeleteCharacter(Index)

if Objects(index)

; Get this characters sprite index
Spr=Objects(Index).Sprite

; check if sprite exists
if Spr
if GetSpriteStatus(Spr)
; if it did, now we delete it
DeleteSprite Spr
endif
endif

; now we can delete this type.
Objects(Index)=NUll
endif

EndPsub Index



;---------------------------------------------------------------------
;--Check If Characters within a distance -----------------------------
;---------------------------------------------------------------------

Psub CharactersWithinDistance(ThisDude,ThatDude,Radius=10)
result =false
if Objects(ThisDude)
if Objects(ThatDude)

x1#=Objects(ThisDude).X
y1#=Objects(ThisDude).Y

x2#=Objects(ThatDude).X
y2#=Objects(ThatDude).Y
result= (Radius <=GetDistance2D(x1#,y1#,x2#,y2#))

endif
endif
EndPsub result




Function SHow_Sprites_Used()
For lp=1 to GetSpriteQuantity()
Count+=GetSpriteStatus(lp)
next
print "Sprites IN Memory:"+str$(Count)
EndFunction


       

 
        So what we're doing is building an abstraction layer using functions, some people call this an Game Engine.   It's a deceptively powerful model, which allows us is hides the bulk of low level code inside our high level function set.  So our game/program is calling our custom interface, rather than poking around underneath.   So when somethings not working the way we want, we just locate the function can tweak it as need be.   We don't have to run through the program and make multiple changes or corrections.  Which only leads to more problems in the long run.  







Q.  My program creates/loads all it's media resources once at the start, do i need to worry about leaking resources then ?..


      Answer: No, there's unlikely to be any media leaks in these types of programs.  But it's still possible to leak memory with fault logic in your program.    One that comes to mind is inappropriate use of GoSUB.  

       Every  time a gosub is made, this statement places the return information onto the PB runtime stack.    When we hit a RETURN, this statement grabs this information off the stack and balance is maintained.   But if we fail to return for some reason, then this data will build up on the stack over time.  Resulting in stack overflow error.    

       You see view the size of PB runtime stack in the Debugger under the state tab, it's at the bottom called  Stack Size: -  The size will naturally fluctuate as the program runs, but If you notice that over time the stack size is slowly increasing, then you might have a some faulty logic in the program.    

      The following snippet shows such a fault logic (found in a number of real world programs) just in a condensed form.    

PlayBASIC Code: [Select]
RestartGame:

; Load media and stuff here

Print "Loading Media"
Sync
wait 250

X=0

setfps 30
do
Cls 0

X=Mod(X+1,800)

Circle X,200,100,true

Gosub Check_For_Restart

print "Playing Game"
print "Press Space to Restart"

Sync
loop



Check_For_Restart:

if spaceKey()=true then Goto RestartGame


return





       Cut'n'Paste this example into PlayBASIC and run it in debug mode F7,  if you stop it and look at the stack size, it'll be fluctuating around zero.  But when you press SPACE to restart the game,  which is within the subroutine, the GOSUB statement never reaches the RETURN, leaving data on the stack.    Force it restart a bunch of times the stack progressively grows in size.   So whats bound to happen is that the game will eventually die from stack over.   Which can be hard to track down.