File: WeakStringCache.Locking.cs
Web Access
Project: ..\..\..\src\StringTools\StringTools.csproj (Microsoft.NET.StringTools.net35)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
 
namespace Microsoft.NET.StringTools
{
    /// <summary>
    /// Implements the WeakStringCache functionality on .NET Framework 3.5 where ConcurrentDictionary is not available.
    /// </summary>
    internal sealed partial class WeakStringCache : IDisposable
    {
        private readonly Dictionary<int, StringWeakHandle> _stringsByHashCode;
 
        public WeakStringCache()
        {
            _stringsByHashCode = new Dictionary<int, StringWeakHandle>(_initialCapacity);
        }
 
        /// <summary>
        /// Main entrypoint of this cache. Tries to look up a string that matches the given internable. If it succeeds, returns
        /// the string and sets cacheHit to true. If the string is not found, calls ExpensiveConvertToString on the internable,
        /// adds the resulting string to the cache, and returns it, setting cacheHit to false.
        /// </summary>
        /// <param name="internable">The internable describing the string we're looking for.</param>
        /// <param name="cacheHit">Whether the entry was already in the cache.</param>
        /// <returns>A string matching the given internable.</returns>
        public string GetOrCreateEntry(ref InternableString internable, out bool cacheHit)
        {
            int hashCode = internable.GetHashCode();
 
            StringWeakHandle handle;
            string? result;
            bool addingNewHandle = false;
 
            lock (_stringsByHashCode)
            {
                if (_stringsByHashCode.TryGetValue(hashCode, out handle))
                {
                    result = handle.GetString(ref internable);
                    if (result != null)
                    {
                        cacheHit = true;
                        return result;
                    }
                }
                else
                {
                    handle = new StringWeakHandle();
                    addingNewHandle = true;
                }
 
                // We don't have the string in the cache - create it.
                result = internable.ExpensiveConvertToString();
 
                // Set the handle to reference the new string.
                handle.SetString(result);
 
                if (addingNewHandle)
                {
                    // Prevent the dictionary from growing forever with GC handles that don't reference live strings anymore.
                    if (_stringsByHashCode.Count >= _scavengeThreshold)
                    {
                        // Get rid of unused handles.
                        ScavengeNoLock();
                        // And do this again when the number of handles reaches double the current after-scavenge number.
                        _scavengeThreshold = _stringsByHashCode.Count * 2;
                    }
                }
                _stringsByHashCode[hashCode] = handle;
            }
 
            cacheHit = false;
            return result;
        }
 
        /// <summary>
        /// Iterates over the cache and removes unused GC handles, i.e. handles that don't reference live strings.
        /// This is expensive so try to call such that the cost is amortized to O(1) per GetOrCreateEntry() invocation.
        /// Assumes the lock is taken by the caller.
        /// </summary>
        private void ScavengeNoLock()
        {
            List<int>? keysToRemove = null;
            foreach (KeyValuePair<int, StringWeakHandle> entry in _stringsByHashCode)
            {
                if (!entry.Value.IsUsed)
                {
                    entry.Value.Free();
                    keysToRemove ??= new List<int>();
                    keysToRemove.Add(entry.Key);
                }
            }
            if (keysToRemove != null)
            {
                for (int i = 0; i < keysToRemove.Count; i++)
                {
                    _stringsByHashCode.Remove(keysToRemove[i]);
                }
            }
        }
 
        /// <summary>
        /// Public version of ScavengeUnderLock() which takes the lock.
        /// </summary>
        public void Scavenge()
        {
            lock (_stringsByHashCode)
            {
                ScavengeNoLock();
            }
        }
 
        /// <summary>
        /// Returns internal debug counters calculated based on the current state of the cache.
        /// </summary>
        public DebugInfo GetDebugInfo()
        {
            lock (_stringsByHashCode)
            {
                return GetDebugInfoImpl();
            }
        }
    }
}