1. We've introduced thread tags, search within a thread and similar thread search. Read more here.
    Dismiss Notice
  2. We've released our first Timeline Experimental Preview, our new tool for creating cutscenes and more! To check it out click here.
    Dismiss Notice
  3. All Unity Pro perpetual license customers: your special offer to subscribe is ready! Click here.
    Dismiss Notice
  4. Unity 5.5 is now released.
    Dismiss Notice
  5. Check out all the fixes for 5.5 in patch releases 1 & 2.
    Dismiss Notice
  6. Unity 5.6 beta is now available for download.
    Dismiss Notice
  7. Get prepared for the Tizen Mobile App Incentive Program! Read more about the upcoming program here.
    Dismiss Notice
  8. Enter the Microsoft Developer Challenge for a chance to win prizes. Read more about it here.
    Dismiss Notice

C# Coroutine WaitForSeconds Garbage Collection tip

Discussion in 'Scripting' started by WarpB, Jan 27, 2014.

  1. WarpB

    WarpB

    Joined:
    Nov 12, 2013
    Messages:
    14
    It's a mouthful of a title but it should be good for searches :)

    Ok, so I'm sure many veteran C#'ers know this already, but everywhere I look, when ever people are talking about Coroutines in C# I see this...

    Code (csharp):
    1.  
    2. IEnumerator myAwesomeCoroutine()
    3. {
    4.     while (true)
    5.     {
    6.         doAwesomeStuff();
    7.         yield return new WaitForSeconds(waitTime);
    8.     }
    9. }
    10.  
    What I wanted to point out was that using "yield return new WaitForSeconds()" causes a guaranteed 21 bytes of garbage allocation every time due to the "new" part (compared to the standard coroutine 9 bytes you would get with a "yield return null").

    To avoid this, simply set up your wait times in advance...

    Code (csharp):
    1.  
    2. WaitForSeconds shortWait = new WaitForSeconds(0.1f);
    3. WaitForSeconds longWait = new WaitForSeconds(5.0f);
    4.  
    5. IEnumerator myEvenAwesomerCoroutine()
    6. {
    7.     while (true)
    8.     {
    9.         if (iNeedToDoStuffFast)
    10.         {
    11.             doAwesomeStuffReallyFast();
    12.             yield return shortWait;
    13.         }
    14.         else{
    15.             dontDoMuch();
    16.             yield return longWait;
    17.         }
    18.     }
    19. }
    20.  
    Now your coroutine will only cause the bare minimum 9 bytes GC allocation each time it is called (not including other allocations you might be causing through your code of course!).

    Those bytes all add up! :)
     
  2. Glader

    Glader

    Joined:
    Aug 19, 2013
    Messages:
    296
    I do the same.

    However, someone who isn't aware that caching objects, instead of constantly instantiating copies, is better for GC isn't going to know to search about garbage collection tips for coroutines I'd bet =/
     
  3. WarpB

    WarpB

    Joined:
    Nov 12, 2013
    Messages:
    14
    Could be right there, I was just assuming searching for "coroutine" or "garbage collection" might just put in in the list and be useful to someone who didn't even realise they wanted to know it :)
     
  4. softwizz

    softwizz

    Joined:
    Mar 12, 2011
    Messages:
    794
    If I reply to this it will be easy to find in future.

    This seems like a good tip that I may implement
     
  5. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Messages:
    9,212
    Yeah, I had a go at reusing the co-routine objects, and since it didn't give me problems I wondered why it wasn't the common solution.
     
  6. jackmott

    jackmott

    Joined:
    Jan 5, 2014
    Messages:
    167
    Are you guys often running into cases where dropping 10-15 bytes in these cases is a substantive performance issue?
    Mobile?
     
  7. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Messages:
    9,212
    The performance issue isn't the allocation itself, it's the garbage collection that allocations eventually trigger.
     
    Mycroft likes this.
  8. Fuzzy_Slippers

    Fuzzy_Slippers

    Joined:
    Dec 21, 2012
    Messages:
    438
    Thanks for the tip. I pool everything but for whatever reason I never thought about pooling WaitForSeconds
     
  9. jackmott

    jackmott

    Joined:
    Jan 5, 2014
    Messages:
    167
    Ahh, I see, you are aiming to never have the GC happen at all (or nearly so)
     
  10. Errorsatz

    Errorsatz

    Joined:
    Aug 8, 2012
    Messages:
    381
    Useful tip, thanks! Hadn't thought about it, but it makes sense.
     
  11. thinkyhead_

    thinkyhead_

    Joined:
    Jan 5, 2014
    Messages:
    3
    The only problem I can foresee with using pre-instantiated WaitForSeconds objects is if you tried to use one in more than one coroutine at the same time. It may have a way to handle that case, but my guess is it would cause timing to get screwed up in both coroutines.
     
    Mycroft likes this.
  12. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Messages:
    2,132
    The list of thing to be done to prevent garbage is rather long.

    - Don't use Invoke or StartCoroutine with a string.
    - Don't use GUILayout and flag your GUI MonoBehaviour to prevent the 800bytes/frames of GUI garbage from occurring. (http://docs.unity3d.com/ScriptReference/MonoBehaviour-useGUILayout.html)
    - Don't use GameObject.Tag or GameObject.Name
    - Never use GetComponent on Update, and cache it if possible
    - Don't use foreach

    etc... I think I have a page long of those somewhere.
     
  13. alexzzzz

    alexzzzz

    Joined:
    Nov 20, 2010
    Messages:
    1,224
    You shouldn't take it as a dogma though. There's always a choice - frequent and quick garbage collections vs rare and long ones.
     
  14. Tochas

    Tochas

    Joined:
    Nov 24, 2012
    Messages:
    17
    You can even cache them globally!
    tested it with a few thousand objects sharing the YieldInstructions and worked flawlessly

    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4.  
    5. public static class Yielders {
    6.  
    7.     static Dictionary<float, WaitForSeconds> _timeInterval = new Dictionary<float, WaitForSeconds>(100);
    8.  
    9.     static WaitForEndOfFrame _endOfFrame = new WaitForEndOfFrame();
    10.     public static WaitForEndOfFrame EndOfFrame {
    11.         get{ return _endOfFrame;}
    12.     }
    13.  
    14.     static WaitForFixedUpdate _fixedUpdate = new WaitForFixedUpdate();
    15.     public static WaitForFixedUpdate FixedUpdate{
    16.         get{ return _fixedUpdate; }
    17.     }
    18.  
    19.     public static WaitForSeconds Get(float seconds){
    20.         if(!_timeInterval.ContainsKey(seconds))
    21.             _timeInterval.Add(seconds, new WaitForSeconds(seconds));
    22.         return _timeInterval[seconds];
    23.     }
    24.    
    25. }
     
  15. Matthew-Schell

    Matthew-Schell

    Unity Technologies

    Joined:
    Oct 1, 2014
    Messages:
    215
    Bumping for awesomeness. I just got rid of a bunch of un-necessary allocations using this.
     
  16. Crayz

    Crayz

    Joined:
    Mar 17, 2014
    Messages:
    106
    Speaking of GC allocation and optimization, is it a big deal if there's a small amount of allocations such as 4kb/frame? Is it necessary to put in the effort to optimize?

    How about a ~50kb allocation maybe once every couple seconds?
     
  17. LaneFox

    LaneFox

    Joined:
    Jun 29, 2011
    Messages:
    3,661
    lol wtf this works?

    *scurries away to push buttons*
     
  18. Tochas

    Tochas

    Joined:
    Nov 24, 2012
    Messages:
    17
    As I could gather the YieldInstructions (WaitForSeconds, WaitForEndOfFrame and WaitForFixedUpdate) are handled by the coroutines just as a delimiter or conditional break. each coroutine must be handling internally the wait logic especially the elapsedTime (WaitForSeconds).

    Therefore creating a new WaitForSeconds(1.0f) object at t=0 and then creating another new WaitForSeconds(1.0f) at t=1 causes the same effect as sharing the object in a cached value.
    Hence they are not bound to the gameobject itself nor time of creation it is safe to share them between objects.

    I have implemented it in my current project using coroutines for almost everything (AI loops, bullets, explosions, slow-mo effects, UI updates and such), the shared Yielders have not caused any bug or issue and have reduced the GC Alloc considerably.

    @Crayz each shared/cached YieldInstruction will save just a few bytes (not a big deal), in the other hand creating them once they are needed will cause problems as Coroutines can be used heavily by multiple objects on multiple consequent frames and then released (once the coroutine has continued execution) the continuous allocation of YieldInstructions will trigger the Garbage Collector to clean the HEAP memory, THIS is the real issue here, GC is expensive and most of the time the only reason to have FPS drops or game update glitches.
     
  19. jctz

    jctz

    Joined:
    Aug 14, 2013
    Messages:
    47
    I'd like to do this but before I do, where would this allocation appear in the profiler?
     
  20. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Messages:
    2,132
    In the garbage collection columns?
     
  21. zwickarr

    zwickarr

    Joined:
    Nov 20, 2012
    Messages:
    27
    Do you have an example using this? thanks
     
  22. RiokuTheSlayer

    RiokuTheSlayer

    Joined:
    Aug 22, 2013
    Messages:
    356
    Did not know about this. I'll probably be doing it from now on!

    Does this work with WaitForEndOfFrame, as well?
     
  23. Tochas

    Tochas

    Joined:
    Nov 24, 2012
    Messages:
    17
    Sure!

    Here is one of the classes I use In my current project. It is a bullet tracer

    at the function

    Code (CSharp):
    1. IEnumerator updateTrail(Bullet bullet)
    you will see I call

    Code (CSharp):
    1. yield return Yielders.Get(this.updateinterval);
    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections;
    3. using Vectrosity;
    4.  
    5. public class BulletTrace : PooledGameObject {
    6.  
    7.     [SerializeField]
    8.     private float updateinterval = 0.04f;
    9.  
    10.     public Color CriticalHitColor = Color.red;
    11.     public Color OnHitColor = Color.white;
    12.  
    13.     [SerializeField]
    14.     private int MaxSegments = 10;
    15.  
    16.     private Vector3[] points;
    17.  
    18.     [SerializeField]
    19.     private Material Material;
    20.  
    21.     [SerializeField]
    22.     private float WidthI = 1f;
    23.  
    24.     [SerializeField]
    25.     private float WidthF = 4f;
    26.  
    27.     private float[] widths;
    28.  
    29.     private VectorLine Line;
    30.  
    31.     protected override void Awake ()
    32.     {
    33.         base.Awake ();
    34.         this.points = new Vector3[this.MaxSegments];
    35.         this.Line = new VectorLine("Trace", points, this.Material, 0.0f, LineType.Continuous);
    36.         this.Line.smoothWidth = true;
    37.         //this.Line.smoothColor = true;
    38.         this.widths = new float[this.Line.GetSegmentNumber()];
    39.         this.Line.SetWidths(this.widths);
    40.  
    41.     }
    42.  
    43.     protected override void OnDestroy ()
    44.     {
    45.         VectorLine.Destroy(ref this.Line);
    46.         base.OnDestroy ();
    47.     }
    48.  
    49.     public void TraceBullet(Bullet bullet){
    50.         this.StartCoroutine(this.updateTrail(bullet));
    51.     }
    52.  
    53.     void OnEnable(){
    54.         this.Line.Draw3DAuto();
    55.     }
    56.  
    57.     private void ResetLine(){
    58.         this.Line.ZeroPoints();
    59.         this.Line.maxDrawIndex = this.points.Length;
    60.         for(int i=0; i< this.widths.Length; i++)
    61.             this.widths[i] = 0.0f;
    62.     }
    63.  
    64.     protected override void OnRestore ()
    65.     {
    66.         this.ResetLine();
    67.         base.OnRestore ();
    68.     }
    69.  
    70.     IEnumerator updateTrail(Bullet bullet){
    71.  
    72.         Vector3 pos = bullet.CurrentTransform.position;
    73.         Vector3 lastPos = pos;
    74.  
    75.         Color color = bullet.isCritical? this.CriticalHitColor: bullet.TraceColor;
    76.  
    77.         int numHits = bullet.Hits;
    78.  
    79.         int updates = 0;
    80.         int pointIdx = 0;
    81.  
    82.         int i =0;
    83.  
    84.         float wDelta = 0.0f;
    85.         this.Line.maxDrawIndex = 0;
    86.  
    87.         int segments = Mathf.Min(bullet.TraceSegments, this.MaxSegments);
    88.  
    89.         while(Application.isPlaying){
    90.             this.Line.SetColor(color);
    91.             yield return Yielders.Get(this.updateinterval);
    92.             if(!bullet.gameObject.activeInHierarchy)
    93.                 break;
    94.             pos = bullet.CurrentTransform.position;
    95.  
    96.             if(pointIdx >= segments){
    97.                 for(i=1; i < this.points.Length; i++)
    98.                     this.points[i -1] = this.points[i];
    99.                 pointIdx = segments -1;
    100.             }
    101.             this.points[pointIdx] = pos;
    102.             this.Line.maxDrawIndex = pointIdx -1;
    103.  
    104.             wDelta = (this.WidthF - this.WidthI) / (float)segments;
    105.             for(i=0; i< pointIdx -1; i++)
    106.                 this.widths[i] = this.WidthI + (wDelta * i);
    107.             this.Line.SetWidths(this.widths);
    108.  
    109.                 pointIdx++;
    110.  
    111.             lastPos = pos;
    112.  
    113.             if(bullet.Hits > numHits){
    114.                 color = MathS.ColorLerp(bullet.TraceColor, this.OnHitColor, ((float)(updates % 4)) / 3.0f);
    115.             }
    116.             updates++;
    117.         }
    118.         while(Application.isPlaying){
    119.             color.a -= color.a * 0.05f;
    120.             this.Line.SetColor(color);
    121.             if(color.a <= 0.05f){
    122.                 this.ResetLine();
    123.                 break;
    124.             }
    125.         }
    126.         this.Restore();
    127.     }
    128. }
     
    chelnok and zwickarr like this.
  24. Tochas

    Tochas

    Joined:
    Nov 24, 2012
    Messages:
    17
    Yes it does

    This is an example from a Explosion script I have.

    When the blast function is triggered, a trigger collider is enabled, it will cache everything within the trigger for 2 fixed update loops and then apply damage to all of them.

    Code (CSharp):
    1.         if(this._collidersInside.Contains(other))
    2.             return;
    3.         this._collidersInside.Add(other);
    4.     }
    5.  
    6.     void OnTriggerStay(Collider other){
    7.         if(this._collidersInside.Contains(other))
    8.             return;
    9.         this._collidersInside.Add(other);
    10.     }
    11.  
    12.     void OnTriggerExit(Collider other){
    13.         this._collidersInside.Remove(other);
    14.     }
    15.  
    16.     private IEnumerator blast(){
    17.         this._collidersInside.Clear();
    18.         this.CachedCollider.enabled = true;
    19.         yield return Yielders.FixedUpdate;
    20.         yield return Yielders.FixedUpdate;
    21.         yield return Yielders.EndOfFrame;
    22.         for(int i=0; i< this._collidersInside.Count; i++){
    23.             HittableObject hitObj = this._collidersInside[i].GetComponent<HittableObject>();
    24.             if(hitObj == null)
    25.                 continue;
    26.             #if UNITY_EDITOR
    27.             Logger.DebugLog("default",this, "OnExplosion({0})", hitObj.name);
    28.             #endif
    29.             hitObj.OnExplosion(this);
    30.         }
    31.         this.CachedCollider.enabled = false;
    32.         this._collidersInside.Clear();
    33.         this.IsBlastFinished = true;
    34.         this.Restore();
    35.     }
     
  25. neonblitzer

    neonblitzer

    Joined:
    Aug 5, 2013
    Messages:
    11
    What about something like:

    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections;
    3.  
    4. public static class Waiter {
    5.     public static YieldInstruction Wait(float waitTime) {
    6.         return StartCoroutine(WaitRoutine(waitTime));
    7.     }
    8.  
    9.     static IEnumerator WaitRoutine(float waitTime) {
    10.         float endWaitTime = Time.time + waitTime;
    11.         while (Time.time < endWaitTime) {
    12.             yield return null;
    13.         }
    14.     }
    15. }
    Code (CSharp):
    1. yield return Waiter.Wait(5f);
    No need for a dictionary, no garbage? Am I wrong?
     
    Last edited: Aug 7, 2015
  26. BTStone

    BTStone

    Joined:
    Mar 10, 2012
    Messages:
    964
    Uh, very nice. Thanks for the tip!
     
  27. Tochas

    Tochas

    Joined:
    Nov 24, 2012
    Messages:
    17
    Sorry for the late response,

    Well I wouldn't concern so much about the dictionary, if it is handled correctly It can even store a few hundred items and really don't cause much trouble for fetching values.

    http://www.dotnetperls.com/dictionary-size
    http://www.dotnetperls.com/dictionary
    http://www.dotnetperls.com/dictionary-optimization

    I took the time to make some tests, profile them and record the results.
    The project can be downloaded from
    https://dl.dropboxusercontent.com/u/72963294/Unity/CoroutineTest.unitypackage

    This was tested with Unity 4.3 (yes kind of old but... reasons!)

    The Setup
    [​IMG]

    The scene is simple, it has a camera and 12 spawners, this will instantiate prefabs that use Coroutines to increase the load and catch alloc and cpu spikes while profiling.

    Each Spawner has a CoroutineUser prefab and will create 100 of them. (Totaling in 1200 CoroutineUser objects)

    [​IMG]

    To create a few dynamism within the test there are three prefabs
    [​IMG] [​IMG] [​IMG]
    Each Coroutine User has a sprite component attached and will change its color every "Delay" seconds.

    Code (CSharp):
    1. //#define USE_YIELDERS
    2. //#define USE_WAITER
    3. using UnityEngine;
    4. using System.Collections;
    5.  
    6. public class CoroutineUser : MonoBehaviour {
    7.  
    8.     private SpriteRenderer Renderer;
    9.  
    10.     public float offset = 0.0f;
    11.     public float delay = 5.0f;
    12.  
    13.     int idx = 0;
    14.     private Color[] Colors = new Color[]{ Color.white, Color.red, Color.green, Color.blue};
    15.  
    16.     public Transform CurrentTransform {get; private set;}
    17.  
    18.     void Awake(){
    19.         this.Renderer = this.GetComponent<SpriteRenderer>();
    20.         this.CurrentTransform = this.transform;
    21.     }
    22.  
    23.     IEnumerator Start(){
    24.         if(this.offset > 0.0f)
    25.             #if USE_YIELDERS && !USE_WAITER
    26.             yield return Yielders.Get(this.offset);
    27.             #endif
    28.             #if !USE_YIELDERS && USE_WAITER
    29.             yield return Waiter.Wait(this, this.offset);
    30.             #endif
    31.             #if !USE_YIELDERS && !USE_WAITER
    32.             yield return new WaitForSeconds(this.offset);
    33.             #endif
    34.         while(Application.isPlaying){
    35.             #if USE_YIELDERS && !USE_WAITER
    36.             yield return Yielders.Get(this.delay);
    37.             #endif
    38.             #if !USE_YIELDERS && USE_WAITER
    39.             yield return Waiter.Wait(this, this.delay);
    40.             #endif
    41.             #if !USE_YIELDERS && !USE_WAITER
    42.             yield return new WaitForSeconds(this.delay);
    43.             #endif
    44.             this.idx = (this.idx + 1) % this.Colors.Length;
    45.             this.Renderer.color = this.Colors[this.idx];
    46.         }
    47.     }
    48.  
    49. }
    With the use of preprocessor symbols we can change the test to be run, thus only changing the lines corresponding with the "Wait" part of the process.

    The Test
    When run, the screen will be "mostly" covered by the same sprite, starting all with white tint and every "x" seconds a row will switch colors.
    [​IMG]

    For best results I do encourage making a "deep profile"
    [​IMG]

    Note: I do recommend to disable VSync to be able to spot more easily the cpu spikes at the profiler.
    Edit->Project->Quality

    The Results

    new WaitForSeconds(this.Delay);
    Creating new Yielders will cause allocs on the frame the instruction was created, and it will be disposed right after the yield statement has finished thus generating GC calls every once in a while.

    [​IMG]
    As can be seen in the profiler window, the 1200 calls to CoroutineUser.Start.Iterator.MoveNext() will cause allocs of 14.1KB and 1200 WaitForSeconds..ctor() can be seen within the stack hierarchy.

    Cache YieldInstruction objects
    Caching the YieldInstructions within a static dictionary will cause allocs the first frame the Yielder is needed and subsequently will reuse those objects.
    Having a strong reference to those objects will keep them away from the GC and can be purge at will ensuring GC calls to be made in safe zones, like before/after Application.LoadLevelAsync for example.

    [​IMG]
    In the profiler window can be seen the 1200 calls to CoroutineUser.Start.Iterator.MoveNext() and the 0KB allocs.
    also no .ctor() can be seen within the stack hierarchy.

    Nested StartCoroutine
    Starting a new "nested" Coroutine within a Coroutine (Coroutineception!) as powerful as it can be is a double edged sword and must be used carefully. This will create a new Coroutine object and all the overhead Unity needs to do to handle a new Coroutine.

    [​IMG]

    As can be seen at this profiler window, each frame the 1200 CoroutineUser objects need to wait will cause allocs of 57.4KB, Iterator<WaitRoutine>..ctor() can be seen within the stack hierarchy.
    due to the size of the allocs this method will cause GC calls to be more often.

    Conclusion
    The standard new WaitForSeconds() method it is not so bad, causing about 0.01175KB of garbage per YieldInstruction.

    Caching YieldInstructions as exposed at the begining of the thread can help reduce the GC load.
    Code (CSharp):
    1. WaitForSeconds shortWait = new WaitForSeconds(0.1f);
    2. WaitForSeconds longWait = new WaitForSeconds(5.0f);
    3. IEnumerator myEvenAwesomerCoroutine()
    4. {
    5.     while (true)
    6.     {
    7.         if (iNeedToDoStuffFast)
    8.         {
    9.             doAwesomeStuffReallyFast();
    10.             yield return shortWait;
    11.         }
    12.         else{
    13.             dontDoMuch();
    14.             yield return longWait;
    15.         }
    16.     }
    17. }
    In "not-so-"extreme environments saving a few KB here and there can be helpful, so caching them globally is appropriate as multiple objects can reuse the same YieldInstruction. Helpful for custom particles and particle engines, spawned objects like bullets, explosions, enemies, making tweens. Proper handling of the bucket used to cache them is required.
    In this case a dictionary do not pose an issue as the key is a float and do not cause issues mostly caused by string keys.
    the dictionary size can be constrained and optimized further like, only allowing a certain float precision as the key, self purging stale (unused) objects and so on.
     
  28. supremegrandruler

    supremegrandruler

    Joined:
    Jul 19, 2014
    Messages:
    890
    So what's the take on neoblitzer's suggestion?
     
  29. Tochas

    Tochas

    Joined:
    Nov 24, 2012
    Messages:
    17
    neoblitzer is proposing to use nested Coroutines.

    Waiter.Wait will create a new Coroutine pointing to WaitRoutine method.
    The WaitRoutine per se do not cause any allocs but creating a hole new Coroutine resulted in less performance that only creating a new YieldInstruction.
    With the tests that I did that method resulted in about four times more memory allocations than the standard 'new WaitForSeconds()'. I wouldn't recommend to use that for only waiting or pausing the execution of a procedure.
     
  30. khan-amil

    khan-amil

    Joined:
    Mar 29, 2012
    Messages:
    128
    Thanks "@GFX47" for pointing me to this thread. Not sure it'll do a lot of difference for our project, but still good find!
     
  31. Azmar

    Azmar

    Joined:
    Feb 23, 2015
    Messages:
    243
    This thread is insanely useful! Is there no way we can cache WaitForSeconds as a private/public variable and be able to change the float at any time without the "new"? Or is dictionary the only real possible way of storing an insane amount of possibilities? The way I have it set up is the user can cast a spell and an animation may go off 0.5sec or 0.6 sec etc...and I don't wanna limit it by hard setting it.
     
  32. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Messages:
    3,956
    It's not a public property, but it's there underneath.

    It's a private field called 'm_Seconds'.

    I reflect it out when copying a WaitForSeconds to my WaitForDuration class:
    Code (csharp):
    1.  
    2.         /// <summary>
    3.         /// Create a WaitForDuration from a WaitForSeconds object as a pooled object.
    4.         ///
    5.         /// NOTE - This retrieves a pooled WaitForDuration that should be used only once. It should be immediately yielded and not used again.
    6.         /// </summary>
    7.         /// <param name="wait"></param>
    8.         /// <param name="returnNullIfZero"></param>
    9.         /// <returns></returns>
    10.         public static WaitForDuration FromWaitForSeconds(WaitForSeconds wait, bool returnNullIfZero = true)
    11.         {
    12.             var dur = ConvertUtil.ToSingle(DynamicUtil.GetValue(wait, "m_Seconds"));
    13.             if (returnNullIfZero && dur <= 0f)
    14.             {
    15.                 return null;
    16.             }
    17.             else
    18.             {
    19.                 var w = _pool.GetInstance();
    20.                 w.Reset(dur, SPTime.Normal);
    21.                 return w;
    22.             }
    23.         }
    24.  
    Found here:
    https://github.com/lordofduct/space...er/SpacepuppyBase/IRadicalYieldInstruction.cs

    Noting that my custom yield statements only work when using my custom RadicalCoroutine:
    https://github.com/lordofduct/space...lob/master/SpacepuppyBase/RadicalCoroutine.cs

    And in it, I support easy pooling of yield instructions, even having it so that a yield instruction can implement an interface called 'IPooledYieldInstruction' that calls 'Dispose' on it when the coroutine is done with it. And the Dispose method returns it to the pool.

    Code (csharp):
    1.  
    2.         #region Static Interface
    3.  
    4.         private static com.spacepuppy.Collections.ObjectCachePool<PooledWaitForDuration> _pool = new com.spacepuppy.Collections.ObjectCachePool<PooledWaitForDuration>(-1, () => new PooledWaitForDuration());
    5.         private class PooledWaitForDuration : WaitForDuration, IPooledYieldInstruction
    6.         {
    7.  
    8.             void IDisposable.Dispose()
    9.             {
    10.                 this.Dispose();
    11.                 _pool.Release(this);
    12.             }
    13.            
    14.         }
    15.  
    16.         /// <summary>
    17.         /// Create a WaitForDuration in seconds as a pooled object.
    18.         ///
    19.         /// NOTE - This retrieves a pooled WaitForDuration that should be used only once. It should be immediately yielded and not used again.
    20.         /// </summary>
    21.         /// <param name="seconds"></param>
    22.         /// <param name="supplier"></param>
    23.         /// <returns></returns>
    24.         public static WaitForDuration Seconds(float seconds, ITimeSupplier supplier = null)
    25.         {
    26.             var w = _pool.GetInstance();
    27.             w.Reset(seconds, supplier);
    28.             return w;
    29.         }
    30.  
    31.         /// <summary>
    32.         /// Create a WaitForDuration from a WaitForSeconds object as a pooled object.
    33.         ///
    34.         /// NOTE - This retrieves a pooled WaitForDuration that should be used only once. It should be immediately yielded and not used again.
    35.         /// </summary>
    36.         /// <param name="wait"></param>
    37.         /// <param name="returnNullIfZero"></param>
    38.         /// <returns></returns>
    39.         public static WaitForDuration FromWaitForSeconds(WaitForSeconds wait, bool returnNullIfZero = true)
    40.         {
    41.             var dur = ConvertUtil.ToSingle(DynamicUtil.GetValue(wait, "m_Seconds"));
    42.             if (returnNullIfZero && dur <= 0f)
    43.             {
    44.                 return null;
    45.             }
    46.             else
    47.             {
    48.                 var w = _pool.GetInstance();
    49.                 w.Reset(dur, SPTime.Normal);
    50.                 return w;
    51.             }
    52.         }
    53.  
    54.         /// <summary>
    55.         /// Create a WaitForDuration from a SPTimePeriod as a pooled object.
    56.         ///
    57.         /// NOTE - This retrieves a pooled WaitForDuration that should be used only once. It should be immediately yielded and not used again.
    58.         /// </summary>
    59.         /// <param name="period"></param>
    60.         /// <returns></returns>
    61.         public static WaitForDuration Period(SPTimePeriod period)
    62.         {
    63.             var w = _pool.GetInstance();
    64.             w.Reset((float)period.Seconds, period.TimeSupplier);
    65.             return w;
    66.         }
    67.  
    68.         #endregion
    69.  
     
    Last edited: Oct 23, 2015
  33. Justin-Wasilenko

    Justin-Wasilenko

    Joined:
    Mar 10, 2015
    Messages:
    46
    I too am interest in this. For example in this script:

    Code (CSharp):
    1.   public float lifetime;
    2.  
    3.   void Start ()
    4.   {
    5.     gameObject.SetActive(true);
    6.   }
    7.  
    8.   void OnEnable ()
    9.   {
    10.     StartCoroutine(LateCall());
    11.   }
    12.  
    13.  
    14.  
    15.   IEnumerator LateCall()
    16.   {
    17.     yield return new WaitForSeconds(lifetime);
    18.     gameObject.SetActive(false);
    19.   }
    Would be really handy... But I haven't found a simple way to implement that.
     
  34. Azmar

    Azmar

    Joined:
    Feb 23, 2015
    Messages:
    243
    I figured something like this would be a simple and quick implementation:

    Code (CSharp):
    1. private WaitForSeconds[] waitForSeconds = new WaitForSeconds[10];
    2.  
    3.     // Use this for initialization
    4.     void Start () {
    5.         waitForSeconds[0] = new WaitForSeconds (0f);
    6.         waitForSeconds[1] = new WaitForSeconds (0.1f);
    7.         waitForSeconds[2] = new WaitForSeconds (0.2f);
    8.         waitForSeconds[3] = new WaitForSeconds (0.3f);
    9.         waitForSeconds[4] = new WaitForSeconds (0.4f);
    10.         StartCoroutine(LateCall(3));
    11.  
    12.     }
    13.  
    14.     IEnumerator LateCall(int slot){
    15.         Debug.Log ("Wait for Slot time: " + slot);
    16.         yield return waitForSeconds[slot];
    17.     }
     
  35. Tochas

    Tochas

    Joined:
    Nov 24, 2012
    Messages:
    17
    If the intervals you use are limited to a handful of values you could have a more read-friendly version by having them as static objects in a static helper class something like

    Code (CSharp):
    1.  
    2. public static class YieldHelper{
    3.     public static WaitForSeconds ZeroSeconds = new WaitForSeconds(0.0f);
    4.     public static WaitForSeconds PointOneSeconds = new WaitForSeconds(0.1f);
    5.     public static WaitForSeconds PointTwoSeconds = new WaitForSeconds(0.2f);
    6.     public static WaitForSeconds PointThreeSeconds = new WaitForSeconds(0.3f);
    7. }
    8.  
    9. public static class SomeOtherClass{
    10.     private IEnumerator doStuffInCoroutine(){
    11.         //do stuff
    12.         yield return YieldHelper.PointTwoSeconds;
    13.         //do more stuff  
    14.     }
    15. }
    16.  
    Still I think the use of the Dictionary<> provides readability and flexibility while properly caching and reusing WaitForSeconds objects with no or very small cost.
     
    Matthew-Schell likes this.
  36. tikhiy

    tikhiy

    Joined:
    Aug 14, 2012
    Messages:
    2
    Hey @Tochas, your code is great, but it will produce garbage every time you use that public method. That's because dictionary with a key of ValueType will box the value to perform comparison / hash code calculation while scanning the hashtable.

    You should implement IEqualityComparer<float> and pass it to your dictionary to avoid that GC.

    Also it is always better to use TryGetValue() method instead of dict.Contains(key) + dict[key] since it performs what you want with a single 'pass' through hashtable...

    So this should look like:

    Code (csharp):
    1.  
    2.     using UnityEngine;
    3.     using System.Collections;
    4.     using System.Collections.Generic;
    5.     public static class Yielders {
    6.         class FloatComparer : IEqualityComparer<float> {
    7.             bool IEqualityComparer<float>.Equals (float x, float y) {
    8.                 return x == y;
    9.             }
    10.             int IEqualityComparer<float>.GetHashCode (float obj) {
    11.                 return obj.GetHashCode();
    12.             }
    13.         }
    14.         static Dictionary<float, WaitForSeconds> _timeInterval = new Dictionary<float, WaitForSeconds>(100, new FloatComparer());
    15.      
    16.         static WaitForEndOfFrame _endOfFrame = new WaitForEndOfFrame();
    17.         public static WaitForEndOfFrame EndOfFrame {
    18.             get{ return _endOfFrame;}
    19.         }
    20.      
    21.         static WaitForFixedUpdate _fixedUpdate = new WaitForFixedUpdate();
    22.         public static WaitForFixedUpdate FixedUpdate {
    23.             get{ return _fixedUpdate; }
    24.         }
    25.      
    26.         public static WaitForSeconds Get(float seconds) {
    27.             WaitForSeconds wfs;
    28.             if(!_timeInterval.TryGetValue(seconds, out wfs))
    29.                 _timeInterval.Add(seconds, wfs = new WaitForSeconds(seconds));
    30.             return wfs;
    31.         }
    32.     }
    33.  
     
  37. Tochas

    Tochas

    Joined:
    Nov 24, 2012
    Messages:
    17
    Hi @tikhiy, yes you are right, c# is boxing the float key. Thank you to pointing that out!
    I will run some tests with this updated code. Thanks!

    just for reference, MSDN boxing and unboxing
     
    idurvesh likes this.
  38. tinyant

    tinyant

    Joined:
    Aug 28, 2015
    Messages:
    12
  39. Zorkman

    Zorkman

    Joined:
    Mar 24, 2015
    Messages:
    4
    Hi Guys. I must say I loved your pooled YieldInstructions and I've used them a lot. Thank you! However, I can't seem to make it work with custom YieldInstructions, like this one:

    Code (CSharp):
    1. public class WaitForSecondsRealTime : CustomYieldInstruction
    2. {
    3.     private float waitTime;
    4.  
    5.     public override bool keepWaiting
    6.     {
    7.         get{ return Time.realtimeSinceStartup < waitTime; }
    8.     }
    9.  
    10.     public WaitForSecondsRealTime(float time)
    11.     {
    12.         waitTime = Time.realtimeSinceStartup + time;
    13.     }
    14. }
    What happens here is that it works the first time, but that's because the time was correct when it was initialized. Of course next time I try to use it the waitTime isn't updated and the YieldInstruction completes immediately. But this does not happen with the original WaitForSeconds(). Does anyone here have an idea on how to implement the same behavior for Custom YieldInstructions? I know you can have more control by making WaitForSecondsRealTime inherit from IEnumerator instead but I couldn't get that one working either.
     
  40. ericbegue

    ericbegue

    Joined:
    May 31, 2013
    Messages:
    1,191
    Hi guys,

    In case you are looking for an alternative to coroutines to implement some delayed stuffs, have a look at Panda BT. It's a tool based of Behaviour Tree I'm maintaining. It is coroutines-free and the core engine is optimized for GC allocations.

    With this tool you can describe some sequence of tasks as follow:
    Code (CSharp):
    1. tree "Root"
    2.     sequence
    3.         DoSomeStuffs
    4.         Wait 1.0
    5.         DoOtherStuffs
    6.         Wait 2.0
    7.         DoSomeMoreStuffs
    8.        
    DoSomeStuffs, DoOtherStuffs and DoSomeMoreStuffs are custom methods that get called at the right time.
    The "Wait" task does not generate any GC allocation after initialization.

    More info here:
    http://www.pandabehaviour.com/
     
    lordofduct likes this.
  41. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Messages:
    3,956
    HAHAHAHAHAHAHAHAHAHAHAHHA

    I love you eric.
     
  42. ericbegue

    ericbegue

    Joined:
    May 31, 2013
    Messages:
    1,191
    Yeah, I love me too.
     
  43. SubZeroGaming

    SubZeroGaming

    Joined:
    Mar 4, 2013
    Messages:
    1,009

    That's enough evidence for me. Interesting concept. I'll be experimenting with this.
     
  44. Vedrit

    Vedrit

    Joined:
    Feb 8, 2013
    Messages:
    415
    For those of us not in the know, how is GC normally handled for coroutines? Is it collected on a regular basis? When the coroutine ends?
     
  45. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Messages:
    3,956
    GC works the same way it does with everything else.

    All object generated in the Coroutine will be cleared out when GC runs and no references to any of the objects exist.

    The Coroutine token, and the IEnumerator created when starting the Coroutine will have references dropped when it's done... unless you store a reference for longer of course. If you maintain a reference then it will stay in memory for longer (of course the unity unmanaged side will have dropped any reference, and the coroutine will do nothing anymore).
     
  46. shcipwa

    shcipwa

    Joined:
    Jan 18, 2014
    Messages:
    33
    Just stumbled across this thread after finding one of the strangest occurrences I've seen. Some code running out of any possible order.

    We re-use a set of WaitForSeconds globally throughout our entire game, and after 6 months of battletesting over hundreds of thousands of players, this has only reared its head a few hundred times.

    I don't currently have a repro project so can't confirm if this is actually the case, but I would warn against re-use of this kind until there is an official word from someone who knows the inner workings of the coroutine system.

    For reference what I am experiencing looks to be this:

    Code (csharp):
    1.  
    2. for (var i = 0; i < 1000; i++)
    3. {
    4.     if (session.IsLoaded)
    5.     {
    6.         break;
    7.     }
    8.  
    9.     yield return SlowUpdateConst.Wait1s;
    10. }
    11.  
    12. if (!session.IsLoaded)
    13. {
    14.     DisconnectPlayer("Structures Taking too long to load");
    15.     yield break;
    16. }
    17.  
    18. yield return SlowUpdateConst.Wait1s;
    19.  
    20. SpawnPlayer(...);
    21.  
    When this rare bug happens,
    1. The player has connected and spawned (the last line here is the only possible place they can be spawned).
    2. The "sending structures to client" message has only be outputted once meaning we don't have a duplicate method call.
    3. Player is kicked for taking too long to load structures

    I have managed to reproduce this behind a firewall with only one player connected to the server, no chance of the wrong player being kicked.

    It appears that another piece of code is yielding the SlowUpdateConst.Wait1s, and instead of returning to the original yield location, the piece of code above is resumed from the middle of the method.

    I will report back when I have more information, but would advise caution here
     
  47. idurvesh

    idurvesh

    Joined:
    Jun 9, 2014
    Messages:
    448
  48. mrliioadin

    mrliioadin

    Joined:
    Jul 13, 2013
    Messages:
    20
    So I am newbie enough that I didn't know about this issue until I basically ran my PC out of memory by misusing Coroutines all with yield return new waitforseconds(). I googled a bit and this thread popped up. Really good bridge into intermediate coding for those of us who are self-taught. The title very much did help get to the right place.