Object Pooling, Part 6 - Growing and Shrinking

This is the sixth article in my mini series about object pooling. Be sure to read part 1, part 2, part 3, part 4 and part 5 first. It took me a while to find the time for the next article, but here it is. Now we’re finally going to deal with the topic of growing and shrinking the pool. One of the first questions we have to ask when it comes to this is, when are we going to do it? We have one mechanism for growing the pool implemented already, which takes place when, during a call to the GetObject method, no unused object can be found in the pool. But is this the right place to think about growing, really? And what about shrinking? I think it’s a good idea to keep these things out of the way of “normal call flow”. Meaning, if possible, a client’s call to get an object from the pool shouldn’t be delayed by management work. So I have implemented the mechanism with the help of a timer: a regular check is executed to find whether the number of used objects in the pool is too high or too low and growing or shrinking is done accordingly. To classify the number of used objects as “too high” or “too low”, I’ve introduced two properties HighWaterMark and LowWaterMark. These are percentage values: if the percentage of used objects in the pool is higher than HighWaterMark, the pool is considered too small and vice versa. To prevent the pool size from being scaled up and down wildly in certain scenarios, I have also introduced threshold values – so the percentage has to be found to be too high at more than one check in a row, for example, for the pool to be grown. Actually, the threshold is more useful in the shrinking than the growing case, but it’s the same principle. So, here’s the code for these changes. The next article will introduce a sample program to test all the functionality that’s implemented so far and I’ll post the source code for the pool and the sample with it. So if you haven’t been typing in all this code yourself, stay tuned for the next installment in the series!

class Pool<T> {
  // ...

  private Pool( ) {
    pool = new List<Slot>( );
    retryWaitTime = 500;
    poolExtensionBatchSize = 10;
    poolExtensionBatchSizeIsPercent = true;
    maxGetObjectTries = 5;
    outOfObjectsBehaviour = OutOfObjectsBehaviour.ExtendPool;
    delayObjectCreation = false;
    maxPoolSize = 100;
    lowWaterMark = 20;
    highWaterMark = 80;
    resizingWaitTime = 2000;
    growThreshold = 0;
    shrinkThreshold = 3;
  }

  Timer resizingTimer;
  object resizingTimerLock = new object( );

  private bool useTimerBasedResizing;
  /// <summary>
  /// Gets or sets a value indicating whether the pool checks for low and high
  /// water situations automatically and regularly. Also see ResizingWaitTime.
  /// </summary>
  public bool UseTimerBasedResizing {
    get {
      return useTimerBasedResizing;
    }
    set {
      if (useTimerBasedResizing != value) {
        useTimerBasedResizing = value;
        lock (resizingTimerLock) {
          if (useTimerBasedResizing == false && resizingTimer != null)
            resizingTimer.Dispose( );
          else if (useTimerBasedResizing == true)
            resizingTimer = new Timer(new TimerCallback(ResizingTimerCallback),
              null, resizingWaitTime, Timeout.Infinite);
        }
      }
    }
  }

  private int resizingWaitTime;
  /// <summary>
  /// Gets or sets a value that indicates the time between to checks for low and
  /// high water situations. Also see UseTimerBasedResizing.
  /// </summary>
  public int ResizingWaitTime {
    get {
      return resizingWaitTime;
    }
    set {
      if (resizingWaitTime != value) {
        resizingWaitTime = value;
        lock (resizingTimerLock) {
          if (resizingTimer != null)
            resizingTimer.Change(resizingWaitTime, Timeout.Infinite);
        }
      }
    }
  }

  private int highWaterMark;
  /// <summary>
  /// Gets or sets a value indicating the percentage of used objects in the pool
  /// that must be exceeded for the pool to be grown automatically.
  /// </summary>
  public int HighWaterMark {
    get {
      return highWaterMark;
    }
    set {
      if (highWaterMark != value) {
        highWaterMark = value;
      }
    }
  }

  private int lowWaterMark;
  /// <summary>
  /// Gets or sets a value that the percentage of used objects in the pool
  /// must fall below for the pool to be shrunken automatically.
  /// </summary>
  public int LowWaterMark {
    get {
      return lowWaterMark;
    }
    set {
      if (lowWaterMark != value) {
        lowWaterMark = value;
      }
    }
  }

  private int shrinkThreshold;
  /// <summary>
  /// Gets or sets a value indicating the number of times the pool must
  /// be judged too big before it is shrunken.
  /// </summary>
  public int ShrinkThreshold {
    get {
      return shrinkThreshold;
    }
    set {
      if (shrinkThreshold != value) {
        shrinkThreshold = value;
      }
    }
  }

  private int growThreshold;
  /// <summary>
  /// Gets or sets a value indicating the number of times the pool must
  /// be judged too small before it is grown.
  /// </summary>
  public int GrowThreshold {
    get {
      return growThreshold;
    }
    set {
      if (growThreshold != value) {
        growThreshold = value;
      }
    }
  }

  Slot FindUnusedSlot( ) {
    return pool.Find(delegate(Slot slot) {
      return !slot.InUse;
    });
  }

  /// <summary>
  /// Shrinks the pool size by count elements, at most. The method removes
  /// only unused pool objects, as long as it finds them.
  /// </summary>
  public void ShrinkPoolBy(int count) {
    if (count  0 && unusedSlot != null);
    }
  }

  /// Returns the number of used objects in the pool.
  ///
  public int GetInUseCount( ) {
    int count = 0;
    pool.ForEach(delegate(Slot slot) {
      if (slot.InUse)
      count++;
    });
    return count;
  }

  /// <summary>
  /// Returns the percentage of used objects in the pool.
  /// </summary>
  public int GetFillLevel( ) {
    return GetInUseCount( ) / pool.Count * 100;
  }

  int fillLevelFoundTooLow;
  int fillLevelFoundTooHigh;

  void ResizingTimerCallback(object state) {
    int fillLevel = GetFillLevel( );

    if (fillLevel  highWaterMark) {
      fillLevelFoundTooHigh++;
      fillLevelFoundTooLow = 0;
    }
    else
      fillLevelFoundTooHigh = fillLevelFoundTooLow = 0;

    if (fillLevelFoundTooLow > shrinkThreshold) {
      ShrinkPoolBy(lowWaterMark - fillLevel);
      fillLevelFoundTooLow = 0;
    }
    else if (fillLevelFoundTooHigh > growThreshold) {
      ExtendPoolBy(fillLevel - highWaterMark);
      fillLevelFoundTooHigh = 0;
    }

    lock (resizingTimerLock)
      if (resizingTimer != null)
        resizingTimer.Change(resizingWaitTime, Timeout.Infinite);
  }

  // ...
}

Sorry, this blog does not support comments.

I used various blog hosting services since this blog was established in 2005, but unfortunately they turned out to be unreliable in the long term and comment threads were lost in unavoidable transitions. At this time I don't want to enable third-party services for comments since it has become obvious in recent years that these providers invariably monetize information about their visitors and users.

Please use the links in the page footer to get in touch with me. I'm available for conversations on Keybase, Matrix, Mastodon or Twitter, as well as via email.