Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

IObjectPool missing Count properties

Discussion in 'Scripting' started by ShawnFeatherly, Jan 31, 2022.

  1. ShawnFeatherly

    ShawnFeatherly

    Joined:
    Feb 22, 2013
    Posts:
    57
    I don't understand why `CountActive` and `CountAll` are not available on `IObjectPool`. ( https://docs.unity3d.com/2021.1/Documentation/ScriptReference/Pool.IObjectPool_1.html )

    From using `IObjectPool` in my own scripts it seems that the only reason they're not available is because there is not an implementation for `CountActive` or `CountAll` in `LinkedPool<>`. Yet, `LinkedPool<>` functionally could and should include those 2 properties. Especially since `IObjectPool` does already include `CountInactive`.

    I initially thought those properties missing were a bug, yet the bug I filed was closed as working as intended. I could be missing a reason those properties shouldn't be there. If not, is this forum the best place to make the suggestion to the team that worked on `IObjectPool` that they add the `CountActive` and `CountAll` properties?
     
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,710
    This sounds like an API design argument.

    Another argument could also be made that says users of a given pool should know nothing about counts or capacity, on the basis that it doesn't enhance their ability to use the pooling API.

    If I needed a pool with a count of some kind, I would first a) review why I thought that was an important thing for my pool to have, and b) if it really was the best engineering solution, then I would write my own pool.

    Disclaimer: I don't have a position one way or another.
     
  3. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,379
    Because that would assume tracking the active members of the pool vs the inactive.

    This would have various issues...

    1) it means increased memory since we need to store the active objects somewhere for tracking.

    2) what happens if Release is never called on the active object? What does this state count as? It's no longer in use, but it was never released for reuse? Can it be destroyed? Can it be garbage collected? If it can be than the IObjectPool must maintain this active reference as a WeakReference so its eligible for collection. But what sanitizes the pool of WeakReferences that are now dead? What accounts for this overhead?

    3) Of course we could just not store a reference to the active, and just have an int that counts them on get/release. But then this number can get wildly out of sync if again you're not releasing your objects that you retrieve from the pool. Thusly making the CountActive inaccurate/untrustworthy and basically useless.

    4) And what use is this? Most of the pool's you can see in that link are things like recycled collections. What is the use of knowing how many active objects are out there? The only times I can see needing that is say you repurposed IObjectPool for say GameObject pooling (say like a bullet pool). If you need this functionality just define your own interface:

    Code (csharp):
    1. public interface ITrackedObjectPool<T> : IObjectPool<T>
    2. {
    3.     int CountActive { get; }
    4.     int CountAll { get; }
    5.  
    6.     //Remove completely from tracking
    7.     void Purge(T obj);
    8. }
    Then in your bullet script you can just make sure that OnDestroy it purges itself from the pool.

    This extra purge is necessary to avoid the whole "active entry that's technically not active but is still being tracked as active" gotcha that IObjectPool would have if CountActive existed.

    ...

    Think of it this way...

    Why doesn't IEnumerable have a Count property?
    https://docs.microsoft.com/en-us/dotnet/api/system.collections.ienumerable?view=net-6.0

    Well because not all IEnumerable's necessarily have a knowable count (say you're enumerating a data stream of entries of which you haven't reached the end).

    But if you do have a Count, you now have a ICollection:
    https://docs.microsoft.com/en-us/dotnet/api/system.collections.icollection?view=net-6.0

    ICollection assumes you can add/remove/contains elements, and also have a Count. But there's no indexable access since not all collections are indexed... for example a HashSet.

    If it is indexable, well now you have an IList:
    https://docs.microsoft.com/en-us/dotnet/api/system.collections.ilist?view=net-6.0

    ...

    The interface is designed to describe the problem it is solving. And in the case of IObjectPool the problem its solving is getting/releasing pooled objects. We don't really need to know how many are active. Honestly we don't really need to know the InactiveCount, but since it falls out of the system (if it's pooled you have to know the inactive count), and that can be useful to understaning the overhead that the pool might be costing.
     
    Last edited: Jan 31, 2022
    VolodymyrBS and Kurt-Dekker like this.
  4. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,379
    I also want to say... this is the first I'm learning about IObjectPool and the various colleciton pools like ListPool:
    https://docs.unity3d.com/2021.1/Documentation/ScriptReference/Pool.ListPool_1.html

    I'm still on 2020 and not 2021 or 2022, which explain why I haven't seen it.

    But I like that Unity is adding features that I've had to hand write for years, and I presume others have as well.

    Example... (all of these are in my version 4 of SP, but originate with code I wrote over 10 years ago when I first started with Unity. My framework is my personal code base that I carry with me to all my projects)

    ICachePool: (pretty much IObjectPool, note I don't have any Count properties for the reasons @Kurt-Dekker brought up. They're not really necessary)
    https://github.com/lordofduct/space...py.core/Runtime/src/Collections/ICachePool.cs

    And TempList:
    https://github.com/lordofduct/space...uppy.core/Runtime/src/Collections/TempList.cs

    And all sorts of TempCollections:
    https://github.com/lordofduct/space...ore/Runtime/src/Collections/TempCollection.cs
    https://github.com/lordofduct/space...k/com.spacepuppy.core/Runtime/src/Collections

    To go back to the necessity of count vs my version here which doesn't have it.

    Unity defines a 'Clear' method on their IObjectPool interface. This accounts for the usefulness of InactiveCount, you're clearing the Inactive objects. Basically you can monitor that inactive count and clear them out if it gets to large to reduce overhead. Unity assumes this a critical enough aspect of pool's to bake it into the interface.

    Personally I didn't.

    I wanted to account for the possibility of both self-managed and unmanaged pools.

    I could create a pool that maxes out at N inactive instances and not caching anything past that. This pool wouldn't necessarily have a "clear" method since the pool doesn't necessarily want to every be cleared. Maybe there's some reason to it like it doesn't logically make sense.

    Example... lets discuss an existing pooling system like ThreadPool's and Tasks (which don't use either of these interfaces, but are pools, and so I want to use to demonstrate a real world pool that doesn't have an InactiveCount or Clear analogues). A simple ThreadPool (there are more complex threadpools) will usually consist of N threads ready to go at a moments notice... all N threads are tracked and you'll never have more than N threads. A call to 'Get' accepts the threads entrypoint callback, and it'll pick one of the currently unused threads, and returns a 'Task' object as a token for this work being done (note a real .net Task may not even actually have a thread since Task's don't necessarily need threading, they just represent asynchronous actions). But what if a thread doesn't exist? Well then the callback is queued until one of the threads is released and it picks the nest callback in the queue.

    The point is, this only ever has N threads. No more, no less. Clearing it doesn't make any sense.

    I personally boil my interfaces down to what is necessary to represent the contract. A pool like IObjectPool/ICachePool has one major job of getting/releasing reusable instances of some type. Extra information is specific to concrete implementations and if necessary to be abstracted as an interface can be done so as an inheritance form the base interface (see: ITrackedObjectPool, or my ICollection example).

    But that's all API design choices.

    There's no real "correct" answer here.

    It's what the person designing it considers appropriate. And Unity considered Clear/InactiveCount appropriate to their design. I did not. And both of us considered ActiveCount/CountAll unecessary.
     
  5. Lurking-Ninja

    Lurking-Ninja

    Joined:
    Jan 20, 2015
    Posts:
    9,903
    Keep in mind that these APIs aren't really made for users. They were originally written for internal purposes for Unity Engineers. They just probably cleaned them up a bit and made them public at one point. So having or not having any aspect of these APIs may be affected by the fact that they were internal tools before they got published.
     
    lordofduct likes this.
  6. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,379
    I'm not really making this argument from an internal/external perspective.

    The principles that come into defining an API at this level don't really change just cause it's internal/external. The fucntionality of 'Clear/InactiveCount' is identical in both scenarios. You're clearing your inactive objects, so you need an InactiveCount to know if you need to Clear or not.

    The only thing that really changes is defining that use to the user which is easier internally than externallly.
     
  7. Lurking-Ninja

    Lurking-Ninja

    Joined:
    Jan 20, 2015
    Posts:
    9,903
    Right I ended up addressing only you. Sorry, I intended my comment generally everyone in this thread. Just to remind people that this API is somewhat ad-hoc, based on what they needed at the time rather than a concise thought-process what a Unity user will need in reality. So please, disregard that I addressed you specifically. And obviously you're right.
     
    lordofduct likes this.
  8. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,379
    I see, you're adding another point to the answer to OP.

    Agreed, design also comes down to what the creator "needs" for themselves.

    Just cause an interface was released publicly doesn't mean it was designed with the public in mind.
     
  9. ShawnFeatherly

    ShawnFeatherly

    Joined:
    Feb 22, 2013
    Posts:
    57
    Thank you for all the help on understanding this!

    Great succinct point, thanks!

    It seems intended that an object is active until Release() is called or the object itself is destroyed. Isn't it good to have something to help make it apparent the pool is being used incorrectly? I do not think a limbo state needs to be accounted for. I don't get how this is different than a single element in an array no longer being used. It still takes up space.

    To be able to switch between ObjectPool and LinkedPool. I wanted to use CountAll to help document which object is which in a pool, ` current.name = "poolBullet#" + bulletPool.CountAll;`. I can see how this is specialized and not a good reason to have it in the API.

    IEnumerable doesn't have a count property because the expression it holds can be evaluated later. Active objects in a pool have already been created and evaluated.

    It sounds like the main concern is Release() not being called. That seems like misuse of a pool.
     
  10. koirat

    koirat

    Joined:
    Jul 7, 2012
    Posts:
    2,008
    I did not know Unity3d had ListPool<T>.
    Unfortunately it is lacking important feature and if this feature is crucial to you I would suggest making your own ListPool.

    It is basically this, ListPool<T>.Get() don't have an option to specify estimated items count.
    This means that It returns lists at "random" so when you need list for 10 objects it can return list for 10K objects. And when you need 10K objects it might return list for 10 objects and list will have to grow.
     
    lordofduct likes this.
  11. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,379
    Now the logic of the pool must also be enhanced to track improper use. The complexity has increased. And an interface doesn't do any implementation, so this would force anyone implmenting the interface to do this leg work.

    Yes, it's specialized, so therefore doesn't need to be in the interface. Nothing is stopping you from adding it yourself.

    But interfaces should represent the generalized/abstract contract for accessing its state, not necessarily how its state is to be maintained.

    Not necessarily.

    First off yes... the enumerable count could be evaluated later. In most cases. Maybe it can't... like say an enumerable of the fibonacci sequence which is infinitely long... but you could be like "(new FibonacciSequence()).Take(100)" to get the first 100.

    And as for active objects in a pool have already been created and evaluated. But so what... being created doesn't necessitate a count. McDonald's has created and evaluated every cheeseburger it has made (that's just a matter of definition)... but it doesn't necessarily have an accurate count of all of them.

    Sure, but the contract needs to account for misuse.

    And it's not necessarily a misuse. What if I want to consume an object from a pool and never release it? That's a potential use case. It all depends on what I'm pooling.

    Back to the accounting for misuse. Why even have an IEnumerable just cause Count can be evaluated later? Why not just have an ICollection interface and all the underlying implementation decide what to do on Count even if it means it's inaccurate?

    Cause I can think of a reason... what's the point then? What's the point of returning an object as some polymorphic type if the members of that type don't behave predictably?
     
    Last edited: Jan 31, 2022
    Kurt-Dekker likes this.
  12. ShawnFeatherly

    ShawnFeatherly

    Joined:
    Feb 22, 2013
    Posts:
    57
    Thanks for these extra notes! Lots to ponder.
     
  13. koirat

    koirat

    Joined:
    Jul 7, 2012
    Posts:
    2,008
    If you are still not sure why IEnumerable don't have a Count check out the "yield" keyword in c#.

    Now pooling should not keep track of objects, what is important to understand is that pooling is not a collection.
    It is usually implemented by the use of collection but it does not have to be, it is classified as creation pattern.
     
    lordofduct likes this.