Search Unity

Mono garbage collection, can I get a summary?

Discussion in 'General Discussion' started by jackmott, Feb 13, 2014.

Thread Status:
Not open for further replies.
  1. jpthek9

    jpthek9

    Joined:
    Nov 28, 2013
    Posts:
    944
    I think we we may be exaggerating the practical consequences of the garbage collection.

    Here's a quick test done in the editor:

    Code (CSharp):
    1.  
    2. Test test;
    3. void Start () {
    4.     const int iterations = 10000000;
    5.     for (int i = 0; i < iterations; i++) {
    6.         test = new Test();
    7.     }
    8.     System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
    9.     sw.Start ();
    10.     System.GC.Collect ();
    11.     sw.Stop ();
    12.     Debug.Log (sw.Elapsed);
    13. }
    14. private class Test {
    15. }
    Result: 00:00:00.0041840
    I'm not sure if I'm measuring correctly (please correct me if I'm not) but GC, according to my results, takes less than a micro second to clean up 10,000,000 objects. Of course, more references probably puts more stress on the algorithm so this test may not be accurate of practical scenarios.
     
    Last edited: Oct 10, 2015
    Ironmax likes this.
  2. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    How much garbage were you generating each frame? The profiler will tell you.

    I've had noticable issues with the GC before. So I know it can and does bite. My particular use case was discarding a array of several thousand data points. Game would freeze for a small but noticable period of time each time the array was discarded. Switching to reusing the array instead of discarding it made the problem go away.

    I'd also be curious to see the same test done with destroying on a different frame from Instantiate.
     
  3. jpthek9

    jpthek9

    Joined:
    Nov 28, 2013
    Posts:
    944
    I changed the code for better measurement. I think Destroy may have bypassed the GC.

    For the new test, it was 152 MB.
     
  4. Arowx

    Arowx

    Joined:
    Nov 12, 2009
    Posts:
    8,194
    Thinking about it Unity is a game engine with a GC so shouldn't Unity do all the object pooling and jumping through GC hoops for us behind the scenes?

    Ran a more testing example cubes and spheres with rigodbodies battling in space using bullets with particle trails and explosions. Managed to get a huge 8.2ms GC spike after a couple of seconds.

    The test adds more Units 20 per second, does not do any pooling of objects or particle systems.
     
  5. Ryiah

    Ryiah

    Joined:
    Oct 11, 2012
    Posts:
    21,205
    Is there a one-size fits all solution for object pooling they could implement? If not, they would be better off not doing anything.
     
    Ironmax likes this.
  6. Arowx

    Arowx

    Joined:
    Nov 12, 2009
    Posts:
    8,194
    So if UT implemented a built-in generic object pooling/recycling system that could be used via an InstantiatePooled() call that wouldn't work for the majority of situations?

    Or isPooled bit flag in GameObject that can be set in the Inspector so in scene but re-used prefabs can be recycled.
     
    Last edited: Oct 10, 2015
  7. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    Pooling is not required for every project. Not by a long shot. How the pool is constructed should be crafted for the individual project too. I'm not sure a one size fits all solution could be created.
     
    Dustin-Horne likes this.
  8. Arowx

    Arowx

    Joined:
    Nov 12, 2009
    Posts:
    8,194
    I would be happy with a built in toggleable pooling system that would be good for the majority of cases in Unity.

    Benefits no coding needed by me, also could take advantage of under the hood close to the metal optimisations that I as a script developer cannot access.

    But if your project needs a specific pooling system then you should add one that does what you need.

    Until UT can improve on or replace the not real-time game engine friendly GC with a better system an object pooling system would be a great help and could even be used within the engine to reduce memory allocations.
     
    Last edited: Oct 11, 2015
  9. HiddenMonk

    HiddenMonk

    Joined:
    Dec 19, 2014
    Posts:
    987
    I miss simply using methods that return arrays T.T
    Example
    Code (CSharp):
    1. using System;
    2. using UnityEngine;
    3.  
    4. public class TestGC : MonoBehaviour
    5. {
    6.     float[] floats = new float[3];
    7.  
    8.     void Update()
    9.     {
    10.         ToFloatsWithGarbage(Vector3.one); //52B of garbage
    11.  
    12.         ToFloatsNoGarbageUnsafe(Vector3.one); //0 garbage, but is unsafe to use...
    13.  
    14.         Array.Clear(floats, 0, floats.Length);
    15.         ToFloatsNoGarbageSafe(Vector3.one, ref floats); //0 garbage and safe to use.
    16.     }
    17.  
    18.     float[] ToFloatsWithGarbage(Vector3 vector)
    19.     {
    20.         return new float[] {vector.x, vector.y, vector.z};
    21.     }
    22.  
    23.     float[] tempFloats = new float[3];
    24.     float[] ToFloatsNoGarbageUnsafe(Vector3 vector)
    25.     {
    26.         Array.Clear(tempFloats, 0, tempFloats.Length);
    27.         tempFloats[0] = vector.x;
    28.         tempFloats[1] = vector.y;
    29.         tempFloats[2] = vector.z;
    30.         return tempFloats;
    31.     }
    32.  
    33.     float[] ToFloatsNoGarbageSafe(Vector3 vector, ref float[] floatArray)
    34.     {
    35.         floatArray[0] = vector.x;
    36.         floatArray[1] = vector.y;
    37.         floatArray[2] = vector.z;
    38.         return floatArray;
    39.     }
    40. }

    I tend to go with the Unsafe collection route when I just need the data right then and there, and if I want to store the data I can just copy the values. In fact, I even created a class for unsafe collections to remind me.
    Code (CSharp):
    1.  
    2. using System.Collections.Generic;
    3. using System.Collections.ObjectModel;
    4.  
    5. public class UnsafeCollection<T> : ReadOnlyCollection<T>
    6. {
    7.     public UnsafeCollection(IList<T> list) : base(list) {}
    8. }

    It just makes the code very ugly though in my opinion.
    Would the new garbage collector allow me to avoid this?
     
  10. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    Dropping arrays has always been my biggest garbage hit. A new GC would improve performance. But it wouldn't make the problem go away.
     
  11. jpthek9

    jpthek9

    Joined:
    Nov 28, 2013
    Posts:
    944
    I've never noticed it.
     
    Ironmax likes this.
  12. Ironmax

    Ironmax

    Joined:
    May 12, 2015
    Posts:
    890
    There is no issue with array or 3darray , list or for loops, when your allocating in update, dont be supprised if the gc comes for a vist.
     
  13. Ironmax

    Ironmax

    Joined:
    May 12, 2015
    Posts:
    890
    HiddenMonk:
    If you want to rape your project better do it right without any overhead issue.

    If you brows this forum you see that most experienced developer highly recommend
    object pooling and correct allocation.

    Code (CSharp):
    1.  
    2.  
    3.    List<Vector3> VectorList = new List<Vector3>();  // list
    4.     float[] floats = new float[3]; // Array
    5.  
    6.     void Update()
    7.     {
    8.         VectorGenerator(Vector3.one);
    9.     }
    10.  
    11.     void VectorGenerator(Vector3 vector)
    12.     {
    13.         VectorList.Add (vector);
    14.  
    15.         floats[0] = vector.x;
    16.         floats[1] = vector.y;
    17.         floats[2] = vector.z;
    18.     }
    This will cause zero overhead because you allocated things correctly. Maybe some one from Unity can come and confirm this and close this thread. Since its obvious how easy it is to avoid GC/Overheads.

     
    Last edited: Oct 11, 2015
  14. Sebioff

    Sebioff

    Joined:
    Dec 22, 2013
    Posts:
    218
    Not quite. Your script creates garbage every time the internal array of List needs to be resized due to running out of space (bigger allocations the longer it runs).

     
  15. 00christian00

    00christian00

    Joined:
    Jul 22, 2012
    Posts:
    1,035
    Glad to see this, I was scratching my head as to why it wasn't allocating anything.
     
  16. Ironmax

    Ironmax

    Joined:
    May 12, 2015
    Posts:
    890
    nope.. 12kb is bullshit-. just tested Unity 5.1.2f zero gc..
     
    Last edited: Oct 11, 2015
    jpthek9 likes this.
  17. Sebioff

    Sebioff

    Joined:
    Dec 22, 2013
    Posts:
    218
    It's not ;) And you can easily see that it roughly doubles on every frame where the list needs to resize - that is frame #1, 4, 8, 16, 32, 64....since the internal list array size gets doubled when running out of space.
    Frame #1 is the initial allocation of the list with the default capacity of 4, then on frame #4 the capacity is reached and so on.

    Also, I think you should read this: https://msdn.microsoft.com/en-gb/library/ee787088(v=vs.110).aspx#fundamentals_of_memory
     
    Last edited: Oct 11, 2015
  18. Ironmax

    Ironmax

    Joined:
    May 12, 2015
    Posts:
    890
    No i dont see it every frame like 20 mins now. Not sure how a link to Microsoft about gc (
    .NET Framework 4.6 and 4.5)

    will make it happen.

    Zero overhead. I think its time to close this thread .
     
  19. Arowx

    Arowx

    Joined:
    Nov 12, 2009
    Posts:
    8,194
    I noticed that having the profiler in deep mode showed up GC collection spikes whereas normal mode did not.

    Not sure if this is because it looks deeper into the code or it adds more memory overhead?
     
  20. Ironmax

    Ironmax

    Joined:
    May 12, 2015
    Posts:
    890
    The stack size will increase, but the heap size for reference will stay unchanged, hence why no GC, I saw no difference from running deep profile and normal. GC Allocation stays stable at 0% because the heap is clean

    you can also use struct with array. To only allocate on the stack.. The gc will never be invoked, this is faster than doing heap allocation
     
    Last edited: Oct 11, 2015
  21. Sebioff

    Sebioff

    Joined:
    Dec 22, 2013
    Posts:
    218
    Nope, it's not on every frame (as described above). And yes, it's not a lot indeed, but it's definitely not "zero overhead/never triggers GC".
     
  22. makeshiftwings

    makeshiftwings

    Joined:
    May 28, 2011
    Posts:
    3,350
    You have a list that keeps adding an item every single frame and grows infinitely large. Of course it is going to allocate. Where do you think the memory for the list comes from? If you're not seeing it allocate, you're doing something wrong or you don't have the same code in your project as you do in the post.
     
    angrypenguin and Tautvydas-Zilys like this.
  23. Ironmax

    Ironmax

    Joined:
    May 12, 2015
    Posts:
    890
    I think you lack the understanding of allocating and how STACK and HEAP memory blocks work. Things on the stack does not invoke GC. If the HEAP got infinite large, you should see more than 12k (i still doubt that you see even 12k) could be something strange with 5.2 ( i am using 5.1.2) but i doubt that to.
     
  24. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,620
    What leads you to believe that your list isn't being created (and resized) on the heap? It's a reference type, and reference types go on the heap.
     
  25. Ironmax

    Ironmax

    Joined:
    May 12, 2015
    Posts:
    890
    Because in this case the 2 objects are just allocated on the Heap at start, not every frame. But the stack size is,
    the Stack handle it self very differently than the Heap, Data = Stack, Reference = heap. The reference does not increase.

    The HEAP objects are just a placeholder for the data allocation in this case, and we have 2 allocation on the heap, not a infinite allocation, like we saw in the first example by Hiddenmonk.
     
  26. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,620
    Sure, the "List" object is only allocated once, but as the others have said the array backing it has to get re-allocated every time it gets filled up. You might not be allocating a new List, but the List is allocating new arrays. It has to.

    As @makeshiftwings asked, what do you expect is actually happening when the List's storage is exhausted? If the list is currently big enough to store 100 elements, and you insert a 101st element, what do think happens?

    I'm not sure what you mean by this. In this context I don't know what you mean by "data". Anything in RAM is "data". Value types go on the stack. Reference types go on the heap. Note that a "reference type" is not "a reference".

    Your List is a reference type. It stores data in a backing array (also a reference type even if the items within it are value types) which it resizes by re-allocating it as needed.
     
  27. Tautvydas-Zilys

    Tautvydas-Zilys

    Unity Technologies

    Joined:
    Jul 25, 2013
    Posts:
    10,680
    I guess you didn't look at implementation of List<T>.Add(). It goes something like this:

    Code (csharp):
    1. public void Add(T item)
    2. {
    3.     if (m_Count == m_Items.Length)
    4.     {
    5.         var oldItems = m_Items;
    6.         m_Items = new T[m_Items.Length * 2];
    7.         for (int i = 0; i < oldItems.Length; i++)
    8.             m_Items[i] = oldItems[i];
    9.     }
    10.  
    11.     m_Items[m_Count++] = item;
    12. }
    So yes, it can and will definitely allocate GC memory.
     
    Dustin-Horne likes this.
  28. Ironmax

    Ironmax

    Joined:
    May 12, 2015
    Posts:
    890
    There is no new arrays, please look at the code, only the 3 clusters are changed in the same object array . There is no change in the heap referance object.
     
  29. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,620
    Yes, there is change in the heap reference object. Your reference itself may not change, but it stores references of its own and they do change.

    As I asked:
    Also, @Tautvydas Zilys answered those questions with his code sample. A List uses an array internally, and when the array fills up it makes a new one. That's allocation.
     
    Dustin-Horne likes this.
  30. Ironmax

    Ironmax

    Joined:
    May 12, 2015
    Posts:
    890
    Bulllshit

     
  31. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,620
    Would you like to have a go at explaining how a List works, then?
    Where does that 101st value get stored? It has to go somewhere, there is no magic.
     
  32. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,620
    Also, the Profiler's display doesn't update as fast as Unity renders. It's not that your List isn't allocating, it's that you're not displaying those frames in the Profiler. If you hit pause and go looking for it, you will find that your List is allocating. (It'll be easier to find near the start when those allocations are more frequent.)
     
  33. Ironmax

    Ironmax

    Joined:
    May 12, 2015
    Posts:
    890
    The STACK uses push and pop to store data (yes data value, in this case floats, that is ValueType )
     
  34. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,620
    Yes it does, but that has nothing to do with what's going on here.

     
  35. Ironmax

    Ironmax

    Joined:
    May 12, 2015
    Posts:
    890
    Now your blameing the profiler is wrong? What will be your next excuse? It doesn't show ANY GC allocation , you did not see the overtime graph? if there was any GC allocation it should be there. I am getting abit tired to repeat my self, There is zero job for the GC to deal with this because the heap is unchanged.. If i destroyed the object, that would been a different story. This is the reason why we use object pooling to make the Heap clean.. In my example the heap is clean.
     
  36. snacktime

    snacktime

    Joined:
    Apr 15, 2013
    Posts:
    3,356
  37. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,620
    No, the Profiler is correct. My screenshot above comes from the Profiler. I said you weren't looking at the right frames.

    Anyway, I'm done.
     
    LazloBonin and Ironmax like this.
  38. Ironmax

    Ironmax

    Joined:
    May 12, 2015
    Posts:
    890
    Its allocating 2 objects on the heap at start if that is what you meant, but that has nothing to do with overhead. Have a good day mate. Just trying get your point up correctly :)

    Lets be constructive here, we all agree that GC interaction can be better, i never denied that, even i like to allocate on the stack as much as possible, but what Unity have done with the last version is to cut does GC down to smaller chunks to prevent hicups performance,. That is why i say that there is no issue with GC with Unity. The heap must clean it self at one point, it's unavoidable
     
  39. HiddenMonk

    HiddenMonk

    Joined:
    Dec 19, 2014
    Posts:
    987
    Regardless, I dont really know what your code @Ironmax was meant to demonstrate. Was there something in my example code that I was doing wrong?
    The example I gave was very basic. For example, instead of clearing the array, I could have just overwritten the values and it would have been fine, but I was just using that to demonstrate a point. I guess it would have been more appropriate to use a list instead of a array.

    You talked about using a pool, and while I have used a pool for something else, I usually just have a dedicated list for a certain method that needs to use / return an array that I dont want to allocate garbage. Maybe a global static pool would be better? I would then have to worry about clearing it after I use it, but that would mean I would need to remember to do it elsewhere as I cant do it if I am returning said list. Seems like it can cause issues.

    Dont get me wrong, pooling is very very good, but I am not sure if that is the tool for all cases.

    For example, how would you handle garbage collection if you had something like this
    Code (CSharp):
    1.     public static Vector3[] ConvertToVector3s(float[] values)
    2.     {
    3.         Vector3[] vector3s = new Vector3[values.Length / 3];
    4.  
    5.         int j = 0;
    6.         for(int i = 0; i < vector3s.Length; i++)
    7.         {
    8.             vector3s[i] = new Vector3(values[j], values[j + 1], values[j + 2]);
    9.             j += 3;
    10.         }
    11.  
    12.         return vector3s;
    13.     }

    Lets say for whatever reason you needed to call that method every update, how would you handle it?
    Is doing something like this so wrong? (Note - I know there can probably be threading issues)
    Code (CSharp):
    1.     static List<Vector3> tempVectors = new List<Vector3>();
    2.     static UnsafeCollection<Vector3> unsafeVectors = new UnsafeCollection<Vector3>(tempVectors);
    3.     public static UnsafeCollection<Vector3> ConvertToVector3s(float[] values)
    4.     {
    5.         tempVectors.Clear();
    6.  
    7.         int iterations = values.Length / 3;
    8.  
    9.         int j = 0;
    10.         for(int i = 0; i < iterations; i++)
    11.         {
    12.             tempVectors.Add(new Vector3(values[j], values[j + 1], values[j + 2]));
    13.             j += 3;
    14.         }
    15.  
    16.         return unsafeVectors;
    17.     }
    Where unsafecollections is this..
    Code (CSharp):
    1. using System.Collections.Generic;
    2. using System.Collections.ObjectModel;
    3.  
    4. public class UnsafeCollection<T> : ReadOnlyCollection<T>
    5. {
    6.     public UnsafeCollection(IList<T> list) : base(list) {}
    7. }

    I dont really know what to do otherwise, so If you or anyone has advise, please do let us know =)
     
  40. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,620
    No, that's not what I meant. And there isn't 3kb of that.
     
  41. Ironmax

    Ironmax

    Joined:
    May 12, 2015
    Posts:
    890
    HiddenMonk your allocating on the heap in every frame + on for loop and you ask me for a better code ? :p I am not sure what you want to accomplish with that code, other than really bad performance.

    but i can show you how i do things with Vector objects you can run this code every frame if you like, and without overhead.

    Code (CSharp):
    1.     Vector3 RandomCircle ( Vector3 center ,   float radius  )
    2.     {
    3.         float ang = Random.value * 360;
    4.         Vector3 newRandpos;
    5.         newRandpos.x = center.x + radius * Mathf.Sin(ang * Mathf.Deg2Rad);
    6.         newRandpos.y = center.y;
    7.         newRandpos.z = center.z + radius * Mathf.Cos(ang * Mathf.Deg2Rad);
    8.         return newRandpos;
    9.     }
     
    Last edited: Oct 12, 2015
  42. Sebioff

    Sebioff

    Joined:
    Dec 22, 2013
    Posts:
    218
    What @Tautvydas Zilys posted is roughly the internal implementation of List.Add that shows the capacity doubling behaviour I described.

    Just pause the profiler and look at the frames I listed above.

    Ah! That is good news, didn't know that. Got a link to the changelog for this?
     
    angrypenguin likes this.
  43. HiddenMonk

    HiddenMonk

    Joined:
    Dec 19, 2014
    Posts:
    987
    "Lets say for whatever reason you needed to call that method every update, how would you handle it?"

    It was just example code, I dont have a reason for running that code every frame. Your example code demonstrated nothing that has to do with garbage collection T.T

    Lets say I wanted to load in character models, and I wanted the user to do other things while the models are being loaded in. I use the converttovector3 helper method to deserialize collada files or something. However, uh oh, the garbage is causing spikes, making the user not being able to play the game as his files are being loaded. How would you optimize that method to avoid the garbage spikes? Again, this is just a made up example. I am not actually loading in a bunch of files.

    Also, how am I allocating on the heap every frame with my no garbage example?

    Im curious, where does it say the garbage collector was changed?
     
    angrypenguin likes this.
  44. 00christian00

    00christian00

    Joined:
    Jul 22, 2012
    Posts:
    1,035
    Since this is a thread about advice about avoid GC and the code IronMax doesn't do anything to avoid it, here is the real GC free version :
    Code (CSharp):
    1.  
    2. const int MaxSize=100;
    3. List<Vector3> VectorList = new List<Vector3>(MaxSize);  // list
    4.     float[] floats = new float[3]; // Array
    5.     void Update()
    6.     {
    7.         VectorGenerator(Vector3.one);
    8.     }
    9.     void VectorGenerator(Vector3 vector)
    10.     {
    11. #if UNITY_EDITOR
    12. if(VectorList.Count>=MaxSize)Debug.LogError("VectorList is not big enough, please raise MaxSize");
    13. #endif
    14.         VectorList.Add (vector);
    15.         floats[0] = vector.x;
    16.         floats[1] = vector.y;
    17.         floats[2] = vector.z;
    18.     }
    With MaxSize being the smallest number you know you'll never exceed.
    The check is only done in the editor, so you won't have a performance hit.
    Obviously this code doesn't have much sense because it will stop after 100 frames.

    If you need something to use in the update loop you could use a circular buffer:
    https://en.wikipedia.org/wiki/Circular_buffer
     
  45. superpig

    superpig

    Drink more water! Unity Technologies

    Joined:
    Jan 16, 2011
    Posts:
    4,660
  46. Ironmax

    Ironmax

    Joined:
    May 12, 2015
    Posts:
    890
    Its never going to be a reason to allocate to the heap on every frame... Just because you can drive outside the road doesn't mean you should.

    http://blogs.unity3d.com/2015/05/06/an-introduction-to-ilcpp-internals/
    We’re shipping Unity 5 with libgc, the Boehm-Demers-Weiser garbage collector

    Boehm GC is also distributed with a C string handling library called cords. This is similar to ropes in C++ (strings are trees of small arrays, and they never change), but instead of using reference counting for proper deallocation, it relies on garbage collection to free objects. Cords are good at handling very large texts, modifications to them in the middle, slicing, concatenating, and keeping history of changes (undo/redo functionality).
     
  47. superpig

    superpig

    Drink more water! Unity Technologies

    Joined:
    Jan 16, 2011
    Posts:
    4,660
    Unity's List<T> is here, by the way.

    @Ironmax: it seems like you're mixing up allocation of the List<T> object itself - which is an allocation of about 16 bytes (4 bytes for List<T>._items, 4 bytes for List<T>._size, 4 bytes for List<T>._version, plus I think about 4 bytes of object header) - with the allocation of storage for the list itself, i.e. the array that is assigned to List<T>._items.

    Instead of calling your test method once per frame, try calling it Mathf.Pow(3, Time.frameCount) times per frame, and see if you still have 0 bytes of GC.
     
    Last edited: Oct 12, 2015
    Sebioff and angrypenguin like this.
  48. 00christian00

    00christian00

    Joined:
    Jul 22, 2012
    Posts:
    1,035
    Didn't even know about its existence :D
    Not a big fan of c#, I tend to look online only for the bare minimum I need to code, never bothered to really study it.
    Wrote my first generic method few days ago....
     
  49. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,620
    That looks better, as a theoretical example. (In practice if you were going to use size capping as an approach to limit allocation then in most cases you'd be better off using a the native fixed size array in the first place.)
     
  50. Ironmax

    Ironmax

    Joined:
    May 12, 2015
    Posts:
    890
    Sorry, i still get 0 bytes, and overhead 0 bytes.. The sum of bytes for allocation is the same. I am using Unity 5.1.2f So i cant tell if something changed with the last version with libgc. Maybe you can try with 5.1.2 and get the same result? My only logical explanation, or some one is doing some heavy trolling.
     
Thread Status:
Not open for further replies.