File: System\Drawing\Internal\SystemColorTracker.cs
Web Access
Project: src\src\System.Drawing.Common\src\System.Drawing.Common.csproj (System.Drawing.Common)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Microsoft.Win32;
 
namespace System.Drawing.Internal;
 
// Keeps track of objects that need to be notified of system color change events.
// Mostly this means maintaining a list of weak references.
internal static class SystemColorTracker
{
    // when I tried the self host, it went over 500 but never over 1000.
    private const int INITIAL_SIZE = 200;
    // If it gets this big, I seriously miscalculated the performance of this object.
    private const int WARNING_SIZE = 100000;
    private const float EXPAND_THRESHOLD = 0.75f;
    private const int EXPAND_FACTOR = 2;
 
    private static WeakReference<ISystemColorTracker>?[] s_list = new WeakReference<ISystemColorTracker>?[INITIAL_SIZE];
    private static int s_count;
    private static bool s_addedTracker;
    private static readonly Lock s_lockObject = new();
 
    internal static void Add(ISystemColorTracker obj)
    {
        lock (s_lockObject)
        {
            Debug.Assert(s_list is not null, "List is null");
            Debug.Assert(s_list.Length > 0, "INITIAL_SIZE was initialized after list");
 
            if (s_list.Length == s_count)
            {
                GarbageCollectList();
            }
 
            if (!s_addedTracker)
            {
                s_addedTracker = true;
                SystemEvents.UserPreferenceChanged += OnUserPreferenceChanged;
            }
 
            // Strictly speaking, we should grab a lock on this class. But since the chances
            // of a problem are so low, the consequences so minimal (something will get accidentally dropped
            // from the list), and the performance of locking so lousy, we'll risk it.
            int index = s_count;
            s_count++;
 
            // COM+ takes forever to Finalize() weak references, so it pays to reuse them.
            if (s_list[index] is not WeakReference<ISystemColorTracker> reference)
            {
                s_list[index] = new(obj);
            }
            else
            {
                Debug.Assert(!reference.TryGetTarget(out _), $"Trying to reuse a weak reference that isn't broken yet: list[{index}], length = {s_list.Length}");
                reference.SetTarget(obj);
            }
        }
    }
 
    private static void CleanOutBrokenLinks()
    {
        // Partition the list -- valid references in the low indices, broken references in the high indices.
        // This is taken straight out of Sedgewick (p. 118 on quicksort).
 
        // Basic idea is to find a broken reference on the left side of the list, and swap it with
        // a valid reference on the right
        int right = s_list.Length - 1;
        int left = 0;
 
        int length = s_list.Length;
 
        // Loop invariant: everything to the left of "left" is a valid reference,
        // and anything to the right of "right" is broken.
        while (true)
        {
            while (left < length && s_list[left]?.TryGetTarget(out _) == true)
                left++;
            while (right >= 0 && s_list[right]?.TryGetTarget(out _) != true)
                right--;
 
            if (left >= right)
            {
                s_count = left;
                break;
            }
 
            (s_list[right], s_list[left]) = (s_list[left], s_list[right]);
 
            left++;
            right--;
        }
 
        Debug.Assert(s_count >= 0 && s_count <= s_list.Length, "count not a legal index into list");
 
#if DEBUG
        // Check loop invariant.
 
        // We'd like to assert that any index < count contains a valid pointer,
        // but since garbage collection can happen at any time, it may have been broken
        // after we partitioned it.
        //
        // for (int i = 0; i < count; i++) {
        //     Debug.Assert(list[i].Target is not null, "Null found on the left side of the list");
        // }
 
        for (int i = s_count; i < s_list.Length; i++)
        {
            Debug.Assert(s_list[i]?.TryGetTarget(out _) == false, "Partitioning didn't work");
        }
#endif
    }
 
    private static void GarbageCollectList()
    {
        CleanOutBrokenLinks();
 
        if (s_count / (float)s_list.Length > EXPAND_THRESHOLD)
        {
            Array.Resize(ref s_list, s_list.Length * EXPAND_FACTOR);
 
            Debug.Assert(s_list.Length < WARNING_SIZE, "SystemColorTracker is using way more memory than expected.");
        }
    }
 
    private static void OnUserPreferenceChanged(object sender, UserPreferenceChangedEventArgs e)
    {
        // Update pens and brushes
        if (e.Category == UserPreferenceCategory.Color)
        {
            for (int i = 0; i < s_count; i++)
            {
                Debug.Assert(s_list[i] is not null, "null value in active part of list");
                if (s_list[i]?.TryGetTarget(out ISystemColorTracker? target) == true)
                {
                    target.OnSystemColorChanged();
                }
            }
        }
    }
}