#pragma warning disable CA1822
#nullable disable
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// define TRACE_LEAKS to get additional diagnostics that can lead to the leak sources. note: it will
// make everything about 2-3x slower
//
// #define TRACE_LEAKS
// define DETECT_LEAKS to detect possible leaks
// #if DEBUG
// #define DETECT_LEAKS //for now always enable DETECT_LEAKS in debug.
// #endif
using System.Diagnostics;
#if DETECT_LEAKS
using System.Runtime.CompilerServices;
#endif
namespace Jint.Pooling;
///
/// Generic implementation of object pooling pattern with predefined pool size limit. The main
/// purpose is that limited number of frequently used objects can be kept in the pool for
/// further recycling.
///
/// Notes:
/// 1) it is not the goal to keep all returned objects. Pool is not meant for storage. If there
/// is no space in the pool, extra returned objects will be dropped.
///
/// 2) it is implied that if object was obtained from a pool, the caller will return it back in
/// a relatively short time. Keeping checked out objects for long durations is ok, but
/// reduces usefulness of pooling. Just new up your own.
///
/// Not returning objects to the pool in not detrimental to the pool's work, but is a bad practice.
/// Rationale:
/// If there is no intent for reusing the object, do not use pool - just use "new".
///
internal sealed class ObjectPool where T : class
{
[DebuggerDisplay("{Value,nq}")]
private struct Element
{
internal T Value;
}
///
/// Not using System.Func{T} because this file is linked into the (debugger) Formatter,
/// which does not have that type (since it compiles against .NET 2.0).
///
internal delegate T Factory();
// Storage for the pool objects. The first item is stored in a dedicated field because we
// expect to be able to satisfy most requests from it.
private T _firstItem;
private readonly Element[] _items;
private int _currentSlowPooledItems;
// factory is stored for the lifetime of the pool. We will call this only when pool needs to
// expand. compared to "new T()", Func gives more flexibility to implementers and faster
// than "new T()".
private readonly Factory _factory;
#if DETECT_LEAKS
private static readonly ConditionalWeakTable leakTrackers = new ConditionalWeakTable();
private class LeakTracker : IDisposable
{
private volatile bool disposed;
#if TRACE_LEAKS
internal volatile object Trace = null;
#endif
public void Dispose()
{
disposed = true;
GC.SuppressFinalize(this);
}
private string GetTrace()
{
#if TRACE_LEAKS
return Trace == null ? "" : Trace.ToString();
#else
return "Leak tracing information is disabled. Define TRACE_LEAKS on ObjectPool`1.cs to get more info \n";
#endif
}
~LeakTracker()
{
if (!this.disposed && !Environment.HasShutdownStarted)
{
var trace = GetTrace();
// If you are seeing this message it means that object has been allocated from the pool
// and has not been returned back. This is not critical, but turns pool into rather
// inefficient kind of "new".
Debug.WriteLine($"TRACEOBJECTPOOLLEAKS_BEGIN\nPool detected potential leaking of {typeof(T)}. \n Location of the leak: \n {GetTrace()} TRACEOBJECTPOOLLEAKS_END");
}
}
}
#endif
internal ObjectPool(Factory factory)
: this(factory, Environment.ProcessorCount * 2)
{ }
internal ObjectPool(Factory factory, int size)
{
Debug.Assert(size >= 1);
_factory = factory;
_items = new Element[size - 1];
}
private T CreateInstance()
{
var inst = _factory();
return inst;
}
///
/// Produces an instance.
///
///
/// Search strategy is a simple linear probing which is chosen for it cache-friendliness.
/// Note that Free will try to store recycled objects close to the start thus statistically
/// reducing how far we will typically search.
///
internal T Allocate()
{
// PERF: Examine the first element. If that fails, AllocateSlow will look at the remaining elements.
// Note that the initial read is optimistically not synchronized. That is intentional.
// We will interlock only when we have a candidate. in a worst case we may miss some
// recently returned objects. Not a big deal.
T inst = _firstItem;
if (inst is not null)
{
_firstItem = null;
return inst;
}
inst = AllocateSlow();
#if DETECT_LEAKS
var tracker = new LeakTracker();
leakTrackers.Add(inst, tracker);
#if TRACE_LEAKS
var frame = CaptureStackTrace();
tracker.Trace = frame;
#endif
#endif
return inst;
}
private T AllocateSlow()
{
if (_currentSlowPooledItems <= 0)
return CreateInstance();
var items = _items;
for (int i = 0; i < items.Length; i++)
{
T inst = items[i].Value;
if (inst is not null)
{
_currentSlowPooledItems--;
items[i].Value = null;
return inst;
}
}
return CreateInstance();
}
///
/// Returns objects to the pool.
///
///
/// Search strategy is a simple linear probing which is chosen for it cache-friendliness.
/// Note that Free will try to store recycled objects close to the start thus statistically
/// reducing how far we will typically search in Allocate.
///
internal void Free(T obj)
{
Validate(obj);
ForgetTrackedObject(obj);
if (_firstItem is null)
{
// Intentionally not using interlocked here.
// In a worst case scenario two objects may be stored into same slot.
// It is very unlikely to happen and will only mean that one of the objects will get collected.
_firstItem = obj;
}
else
{
FreeSlow(obj);
}
}
private void FreeSlow(T obj)
{
var items = _items;
if (_currentSlowPooledItems >= items.Length)
return;
for (int i = 0; i < items.Length; i++)
{
if (ReferenceEquals(items[i].Value, null))
{
// Intentionally not using interlocked here.
// In a worst case scenario two objects may be stored into same slot.
// It is very unlikely to happen and will only mean that one of the objects will get collected.
_currentSlowPooledItems++;
items[i].Value = obj;
break;
}
}
}
///
/// Removes an object from leak tracking.
///
/// This is called when an object is returned to the pool. It may also be explicitly
/// called if an object allocated from the pool is intentionally not being returned
/// to the pool. This can be of use with pooled arrays if the consumer wants to
/// return a larger array to the pool than was originally allocated.
///
[Conditional("DEBUG")]
internal void ForgetTrackedObject(T old, T replacement = null)
{
#if DETECT_LEAKS
LeakTracker tracker;
if (leakTrackers.TryGetValue(old, out tracker))
{
tracker.Dispose();
leakTrackers.Remove(old);
}
else
{
var trace = CaptureStackTrace();
Debug.WriteLine($"TRACEOBJECTPOOLLEAKS_BEGIN\nObject of type {typeof(T)} was freed, but was not from pool. \n Callstack: \n {trace} TRACEOBJECTPOOLLEAKS_END");
}
if (replacement != null)
{
tracker = new LeakTracker();
leakTrackers.Add(replacement, tracker);
}
#endif
}
#if DETECT_LEAKS
private static Lazy _stackTraceType = new Lazy(() => Type.GetType("System.Diagnostics.StackTrace"));
private static object CaptureStackTrace()
{
return Activator.CreateInstance(_stackTraceType.Value);
}
#endif
[Conditional("DEBUG")]
private void Validate(object obj)
{
Debug.Assert(obj != null, "freeing null?");
Debug.Assert(_firstItem != obj, "freeing twice?");
var items = _items;
for (int i = 0; i < items.Length; i++)
{
var value = items[i].Value;
if (value is null)
{
return;
}
Debug.Assert(value != obj, "freeing twice?");
}
}
}