File: Evaluation\ProjectRootElementCache.cs
Web Access
Project: ..\..\..\src\Build\Microsoft.Build.csproj (Microsoft.Build)
// 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.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Xml;
using Microsoft.Build.Collections;
using Microsoft.Build.Construction;
using Microsoft.Build.Framework;
using Microsoft.Build.Internal;
using Microsoft.Build.Shared;
using ErrorUtilities = Microsoft.Build.Shared.ErrorUtilities;
using OutOfProcNode = Microsoft.Build.Execution.OutOfProcNode;
 
#nullable disable
 
namespace Microsoft.Build.Evaluation
{
    /// <summary>
    /// Maintains a cache of all loaded ProjectRootElement's for design time purposes.
    /// Weak references are held to add added ProjectRootElement's.
    /// Strong references are held to a limited number of added ProjectRootElement's.
    ///
    /// 1. Loads of a ProjectRootElement will share any existing loaded ProjectRootElement, rather
    /// than loading and parsing a new one. This is the case whether the ProjectRootElement
    /// is loaded directly or imported.
    ///
    /// 2. For design time, only a weak reference needs to be held, because all users have a strong reference.
    ///
    /// 3. Because all loads of a ProjectRootElement consult this cache, they can be assured that any
    /// entries in this cache are up to date. For example, if a ProjectRootElement is modified and saved,
    /// the cached ProjectRootElement will be the loaded one that was saved, so it will be up to date.
    ///
    /// 4. If, after a project has been loaded, an external app changes the project file content on disk, it is
    /// important that a subsequent load of that project does not return stale ProjectRootElement. To avoid this, the
    /// timestamp of the file on disk is compared to the timestamp of the file at the time that the ProjectRootElement loaded it.
    ///
    /// 5. For build time, some strong references need to be held, as otherwise the ProjectRootElement's for reuseable
    /// imports will be collected, and time will be wasted reparsing them. However we do not want to hold strong references
    /// to all ProjectRootElement's, consuming memory without end. So a simple priority queue is used. All Adds and Gets boost their
    /// entry to the top. As the queue gets too big, low priority entries are dropped.
    ///
    /// No guesses are made at which files are more interesting to cache, beyond the most-recently-used list. For example, ".targets" files
    /// or imported files are not treated specially, as this is a potentially unreliable heuristic. Besides, caching a project file itself could
    /// be useful, if for example you want to build it twice with different sets of properties.
    ///
    /// Because of the strongly typed list, some ProjectRootElement's will be held onto indefinitely. This is an acceptable price to pay for
    /// being able to provide a commonly used ProjectRootElement immediately it's needed. It is mitigated by the list being finite and small, and
    /// because we allow ProjectCollection.UnloadAllProjects to hint to us to clear the list.
    ///
    /// Implicit references are those which were loaded as a result of a build, and not explicitly loaded through, for instance, the project
    /// collection.
    ///
    /// </summary>
    internal class ProjectRootElementCache : ProjectRootElementCacheBase
    {
        /// <summary>
        /// The maximum number of entries to keep strong references to.
        /// This has to be strong enough to make sure that key .targets files aren't pushed
        /// off by transient loads of non-reusable files like .user files.
        /// </summary>
        /// <remarks>
        /// Made this as large as 200 because ASP.NET Core (6.0) projects have
        /// something like 80-90 imports. This was observed to give a noticeable
        /// performance improvement compared to a mid-17.0 MSBuild with the old
        /// value of 50.
        ///
        /// If this number is increased much higher, the datastructure may
        /// need to be changed from a linked list, since it's currently O(n).
        /// </remarks>
        private static readonly int s_maximumStrongCacheSize =
            int.TryParse(Environment.GetEnvironmentVariable("MSBUILDPROJECTROOTELEMENTCACHESIZE"), out int cacheSize) ? cacheSize : 200;
 
        /// <summary>
        /// Whether the cache should log activity to the Debug.Out stream
        /// </summary>
        private static bool s_debugLogCacheActivity = Environment.GetEnvironmentVariable("MSBUILDDEBUGXMLCACHE") == "1";
 
        /// <summary>
        /// Whether the cache should check file content for cache entry invalidation.
        /// </summary>
        /// <remarks>
        /// Value shall be true only in case of testing. Outside QA tests it shall be false.
        /// </remarks>
        private static bool s_сheckFileContent = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("MSBUILDCACHECHECKFILECONTENT"));
 
#if DEBUG
        /// <summary>
        /// A simple IDisposable struct implementing the holder/guard pattern over the Get reentrancy counter.
        /// </summary>
        private struct ReentrancyGuard : IDisposable
        {
            /// <summary>
            /// Number of entries into Get function of the ProjectRootElementCache.
            /// Shall be always 0 or 1. Reentrance to the Get function (value > 1) could lead to race condition.
            /// </summary>
            [ThreadStatic]
            private static int s_getEntriesNumber = 0;
 
            public ReentrancyGuard()
            {
                s_getEntriesNumber++;
                ErrorUtilities.VerifyThrow(
                    s_getEntriesNumber == 1,
                    "Reentrance to the ProjectRootElementCache.Get function detected.");
            }
 
            public void Dispose()
            {
                s_getEntriesNumber--;
            }
        }
#endif
 
        /// <summary>
        /// The map of weakly-held ProjectRootElement's
        /// </summary>
        /// <remarks>
        /// Be sure that the string keys are strongly held, or unpredictable bad
        /// behavior will ensue.
        /// </remarks>
        private WeakValueDictionary<string, ProjectRootElement> _weakCache;
 
        /// <summary>
        /// Lock objects keyed by project file path.
        /// </summary>
        private ConcurrentDictionary<string, object> _fileLoadLocks;
 
        /// <summary>
        /// The list of strongly-held ProjectRootElement's
        /// </summary>
        private LinkedList<ProjectRootElement> _strongCache;
 
        /// <summary>
        /// Whether the cache should check the timestamp of the file on disk
        /// whenever it is requested, and update with the latest content of that
        /// file if it has changed.
        /// </summary>
        private bool _autoReloadFromDisk;
 
        /// <summary>
        /// Locking object for this shared cache
        /// </summary>
        private object _locker = new object();
 
        /// <summary>
        /// Creates an empty cache.
        /// </summary>
        internal ProjectRootElementCache(bool autoReloadFromDisk, bool loadProjectsReadOnly = false)
        {
            DebugTraceCache("Constructing with autoreload from disk: ", autoReloadFromDisk);
 
            _weakCache = new WeakValueDictionary<string, ProjectRootElement>(StringComparer.OrdinalIgnoreCase);
            _strongCache = new LinkedList<ProjectRootElement>();
            _fileLoadLocks = new ConcurrentDictionary<string, object>(StringComparer.OrdinalIgnoreCase);
            _autoReloadFromDisk = autoReloadFromDisk;
            LoadProjectsReadOnly = loadProjectsReadOnly;
        }
 
 
        /// <summary>
        /// Returns true if given cache entry exists and is outdated.
        /// </summary>
        private bool IsInvalidEntry(string projectFile, ProjectRootElement projectRootElement)
        {
            // When we do not _autoReloadFromDisk we expect that cached value is always valid.
            // Usually lifespan of cache is expected to be build duration (process will terminate after build).
            if (projectRootElement == null || !_autoReloadFromDisk)
            {
                return false;
            }
 
            // If the project file is non modifiable, assume it is up to date and consider the cached value valid.
            if (!Traits.Instance.EscapeHatches.AlwaysDoImmutableFilesUpToDateCheck && FileClassifier.Shared.IsNonModifiable(projectFile))
            {
                return false;
            }
 
            FileInfo fileInfo = FileUtilities.GetFileInfoNoThrow(projectFile);
 
            // If the file doesn't exist on disk, go ahead and use the cached version.
            // It's an in-memory project that hasn't been saved yet.
            if (fileInfo == null)
            {
                return false;
            }
 
            if (fileInfo.LastWriteTime != projectRootElement.LastWriteTimeWhenRead)
            {
                // File was changed on disk by external means. Cached version is no longer valid.
                // We could throw here or ignore the problem, but it is a common and reasonable pattern to change a file
                // externally and load a new project over it to see the new content. So we dump it from the cache
                // to force a load from disk. There might then exist more than one ProjectRootElement with the same path,
                // but clients ought not get themselves into such a state - and unless they save them to disk,
                // it may not be a problem.
                return true;
            }
            else if (s_сheckFileContent)
            {
                // QA tests run too fast for the timestamp check to work. This environment variable is for their
                // use: it checks the file content as well as the timestamp. That's better than completely disabling
                // the cache as we get test coverage of the rest of the cache code.
                XmlDocument document = new XmlDocument();
                document.PreserveWhitespace = projectRootElement.XmlDocument.PreserveWhitespace;
 
                using (var xtr = XmlReaderExtension.Create(projectRootElement.FullPath, projectRootElement.ProjectRootElementCache.LoadProjectsReadOnly))
                {
                    document.Load(xtr.Reader);
                }
 
                string diskContent = document.OuterXml;
                string cacheContent = projectRootElement.XmlDocument.OuterXml;
 
                if (diskContent != cacheContent)
                {
                    return true;
                }
            }
 
            return false;
        }
 
        /// <summary>
        /// Returns an existing ProjectRootElement for the specified file path, if any.
        /// If none exists, calls the provided delegate to load one, and adds that to the cache.
        /// The reason that it calls back to do this is so that the cache is locked between determining
        /// that the entry does not exist and adding the entry.
        ///
        /// If <see cref="_autoReloadFromDisk"/> was set to true, and the file on disk has changed since it was cached,
        /// it will be reloaded before being returned.
        ///
        /// Thread safe.
        /// </summary>
        /// <remarks>
        /// Never needs to consult the strong cache as well, since if the item is in there, it will
        /// not have left the weak cache.
        /// If item is found, boosts it to the top of the strong cache.
        /// </remarks>
        /// <param name="projectFile">The project file which contains the ProjectRootElement.  Must be a full path.</param>
        /// <param name="loadProjectRootElement">The delegate to use to load if necessary. May be null. Must not update the cache.</param>
        /// <param name="isExplicitlyLoaded"><code>true</code> if the project is explicitly loaded, otherwise <code>false</code>.</param>
        /// <param name="preserveFormatting"><code>true</code> to the project was loaded with the formated preserved, otherwise <code>false</code>.</param>
        /// <returns>The ProjectRootElement instance if one exists.  Null otherwise.</returns>
        internal override ProjectRootElement Get(string projectFile, OpenProjectRootElement loadProjectRootElement, bool isExplicitlyLoaded,
            bool? preserveFormatting)
        {
#if DEBUG
            // Verify that loadProjectRootElement delegate does not call ProjectRootElementCache.Get().
            using var reentrancyGuard = new ReentrancyGuard();
 
            // Verify that we never call this with _locker held, as that would create a lock ordering inversion with the per-file lock.
            ErrorUtilities.VerifyThrow(
                !System.Threading.Monitor.IsEntered(_locker),
                "Detected lock ordering inversion in ProjectRootElementCache.");
#endif
            // Should already have been canonicalized
            ErrorUtilities.VerifyThrowInternalRooted(projectFile);
 
            // First try getting the ProjectRootElement from the cache.
            ProjectRootElement projectRootElement = GetOrLoad(projectFile, loadProjectRootElement: null, isExplicitlyLoaded, preserveFormatting);
 
            if (projectRootElement != null || loadProjectRootElement == null)
            {
                // If we found it or no load callback was specified, we are done.
                return projectRootElement;
            }
 
            try
            {
                // We are about to load. Take a per-file lock to prevent multiple threads from duplicating the work multiple times.
                object perFileLock = _fileLoadLocks.GetOrAdd(projectFile, () => new object());
                lock (perFileLock)
                {
                    // Call GetOrLoad again, this time with the OpenProjectRootElement callback.
                    return GetOrLoad(projectFile, loadProjectRootElement, isExplicitlyLoaded, preserveFormatting);
                }
            }
            finally
            {
                // Remove the lock object as we have otherwise no good way of preventing _fileLoadLocks from growing unboundedly.
                // If another thread is inside the lock, we effectively create a race condition where someone else may enter
                // GetOrLoad. This is OK because this fine-grained locking is just a perf optimization, and we have either loaded
                // the ProjectRootElement by now, or it is an error condition where perf is not critical.
                _fileLoadLocks.TryRemove(projectFile, out _);
            }
        }
 
        /// <summary>
        /// A helper used by <see cref="Get"/>.
        /// </summary>
        private ProjectRootElement GetOrLoad(string projectFile, OpenProjectRootElement loadProjectRootElement, bool isExplicitlyLoaded,
            bool? preserveFormatting)
        {
            ProjectRootElement projectRootElement;
            lock (_locker)
            {
                _weakCache.TryGetValue(projectFile, out projectRootElement);
 
                if (projectRootElement != null)
                {
                    BoostEntryInStrongCache(projectRootElement);
 
                    // An implicit load will never reset the explicit flag.
                    if (isExplicitlyLoaded)
                    {
                        projectRootElement.MarkAsExplicitlyLoaded();
                    }
                }
                else
                {
                    DebugTraceCache("Not found in cache: ", projectFile);
                }
 
                if (preserveFormatting != null && projectRootElement != null && projectRootElement.XmlDocument.PreserveWhitespace != preserveFormatting)
                {
                    // Cached project doesn't match preserveFormatting setting, so reload it
                    projectRootElement.Reload(true, preserveFormatting);
                }
            }
 
            bool projectRootElementIsInvalid = IsInvalidEntry(projectFile, projectRootElement);
            if (projectRootElementIsInvalid)
            {
                DebugTraceCache("Not satisfied from cache: ", projectFile);
                ForgetEntryIfExists(projectRootElement);
            }
 
            if (loadProjectRootElement == null)
            {
                if (projectRootElement == null || projectRootElementIsInvalid)
                {
                    return null;
                }
                else
                {
                    DebugTraceCache("Satisfied from XML cache: ", projectFile);
                    return projectRootElement;
                }
            }
 
            // Use openProjectRootElement to reload the element if the cache element does not exist or need to be reloaded.
            if (projectRootElement == null || projectRootElementIsInvalid)
            {
                projectRootElement = loadProjectRootElement(projectFile, this);
                ErrorUtilities.VerifyThrowInternalNull(projectRootElement, "projectRootElement");
                ErrorUtilities.VerifyThrow(
                    projectRootElement.FullPath.Equals(projectFile, StringComparison.OrdinalIgnoreCase),
                    "Got project back with incorrect path. Expected path: {0}, received path: {1}.",
                    projectFile,
                    projectRootElement.FullPath);
 
                // An implicit load will never reset the explicit flag.
                if (isExplicitlyLoaded)
                {
                    projectRootElement.MarkAsExplicitlyLoaded();
                }
 
                // Update cache element.
                // It is unlikely, but it might be that while without the lock, the projectRootElement in cache was updated by another thread.
                // And here its entry will be replaced with the loaded projectRootElement. This is fine:
                // if loaded projectRootElement is out of date (so, it changed since the time we loaded it), it will be updated the next time some thread calls Get function.
                AddEntry(projectRootElement);
            }
            else
            {
                DebugTraceCache("Satisfied from XML cache: ", projectFile);
            }
 
            return projectRootElement;
        }
 
        /// <summary>
        /// Add an entry to the cache.
        /// </summary>
        internal override void AddEntry(ProjectRootElement projectRootElement)
        {
            lock (_locker)
            {
                RenameEntryInternal(null, projectRootElement);
 
                RaiseProjectRootElementAddedToCacheEvent(projectRootElement);
            }
        }
 
        /// <summary>
        /// Rename an entry in the cache.
        /// Entry must already be in the cache.
        /// </summary>
        internal override void RenameEntry(string oldFullPath, ProjectRootElement projectRootElement)
        {
            lock (_locker)
            {
                ErrorUtilities.VerifyThrowArgumentLength(oldFullPath, nameof(oldFullPath));
                RenameEntryInternal(oldFullPath, projectRootElement);
            }
        }
 
        /// <summary>
        /// Returns any a ProjectRootElement in the cache with the provided full path,
        /// otherwise null.
        /// </summary>
        internal override ProjectRootElement TryGet(string projectFile)
        {
            return TryGet(projectFile, preserveFormatting: null);
        }
 
        /// <summary>
        /// Returns any a ProjectRootElement in the cache with the provided full path,
        /// otherwise null.
        /// </summary>
        internal override ProjectRootElement TryGet(string projectFile, bool? preserveFormatting)
        {
            ProjectRootElement result = Get(
                projectFile,
                loadProjectRootElement: null, // no delegate to load it
                isExplicitlyLoaded: false, // Since we are not creating a PRE this can be true or false
                preserveFormatting: preserveFormatting);
 
            return result;
        }
 
        /// <summary>
        /// Discards strong references held by the cache.
        /// </summary>
        /// <remarks>
        /// The weak cache is never cleared, as we need it to guarantee that the appdomain never
        /// has two ProjectRootElement's for a particular file. Attempts to clear out the weak cache
        /// resulted in this guarantee being broken and subtle bugs popping up everywhere.
        /// </remarks>
        internal override void DiscardStrongReferences()
        {
            lock (_locker)
            {
                DebugTraceCache("Clearing strong refs: ", _strongCache.Count);
 
                LinkedList<ProjectRootElement> oldStrongCache = _strongCache;
                _strongCache = new LinkedList<ProjectRootElement>();
 
                foreach (ProjectRootElement projectRootElement in oldStrongCache)
                {
                    RaiseProjectRootElementRemovedFromStrongCache(projectRootElement);
                }
 
                // A scavenge of the weak cache is probably not worth it as
                // the GC would have had to run immediately after the line above.
            }
        }
 
        /// <summary>
        /// Clears out the cache.
        /// Called when all projects are unloaded and possibly when a build is done.
        /// </summary>
        internal override void Clear()
        {
            lock (_locker)
            {
                if (Traits.Instance.EscapeHatches.AlwaysDoImmutableFilesUpToDateCheck)
                {
                    LinkedList<ProjectRootElement> oldStrongCache = _strongCache;
                    _weakCache = new WeakValueDictionary<string, ProjectRootElement>(StringComparer.OrdinalIgnoreCase);
                    _strongCache = new LinkedList<ProjectRootElement>();
 
                    foreach (ProjectRootElement projectRootElement in oldStrongCache)
                    {
                        RaiseProjectRootElementRemovedFromStrongCache(projectRootElement);
                    }
                }
                else
                {
                    // Manually iterate through LinkedList so we can remove items during this iteration
                    for (var listNode = _strongCache.First; listNode != null;)
                    {
                        var nextNode = listNode.Next;
 
                        ProjectRootElement projectRootElement = listNode.Value;
                        // Do not remove cache of files from immutable locations.
                        // Those are mostly SDK project files and will be most probably needed in next builds.
                        if (!FileClassifier.Shared.IsNonModifiable(projectRootElement.FullPath))
                        {
                            _weakCache.Remove(projectRootElement.FullPath);
                            _strongCache.Remove(listNode);
                            RaiseProjectRootElementRemovedFromStrongCache(projectRootElement);
                        }
 
                        listNode = nextNode;
                    }
 
                    // From weak list remove all which is not in strong list anymore
                    IList<string> toBeRemovedFromWeakRefs = _weakCache.Keys.Except(_strongCache.Select(i => i.FullPath)).ToList();
                    foreach (string victim in toBeRemovedFromWeakRefs)
                    {
                        _weakCache.Remove(victim);
                    }
                    _weakCache.Scavenge();
                }
            }
        }
 
        /// <summary>
        /// Discard any entries (weak and strong) which do not have the explicitlyLoaded flag set.
        /// </summary>
        internal override void DiscardImplicitReferences()
        {
            if (_autoReloadFromDisk)
            {
                // no need to clear it, as auto reload properly invalidates caches if changed.
                return;
            }
 
            lock (_locker)
            {
                // Make a new Weak cache only with items that have been explicitly loaded, this will be a small number, there will most likely
                // be many items which were not explicitly loaded (ie p2p references).
                WeakValueDictionary<string, ProjectRootElement> oldWeakCache = _weakCache;
                _weakCache = new WeakValueDictionary<string, ProjectRootElement>(StringComparer.OrdinalIgnoreCase);
 
                LinkedList<ProjectRootElement> oldStrongCache = _strongCache;
                _strongCache = new LinkedList<ProjectRootElement>();
 
                foreach (KeyValuePair<string, ProjectRootElement> kvp in oldWeakCache)
                {
                    if (kvp.Value is null)
                    {
                        continue;
                    }
 
                    if (kvp.Value.IsExplicitlyLoaded)
                    {
                        _weakCache[kvp.Key] = kvp.Value;
                    }
 
                    if (oldStrongCache.Contains(kvp.Value))
                    {
                        if (kvp.Value.IsExplicitlyLoaded)
                        {
                            _strongCache.AddFirst(kvp.Value);
                        }
                        else
                        {
                            RaiseProjectRootElementRemovedFromStrongCache(kvp.Value);
                        }
                    }
                }
            }
        }
 
        /// <summary>
        /// Forces a removal of a project root element from the weak cache if it is present.
        /// </summary>
        /// <param name="projectRootElement">The project root element to remove.</param>
        /// <remarks>
        /// No exception is thrown if this project root element is in use by currently loaded projects
        /// by this method.  The calling method must know that this is a safe operation.
        /// There may of course be strong references to the project root element from customer code.
        /// The assumption is that when they instruct the project collection to unload it, which
        /// leads to this being called, they are releasing their strong references too (or it doesn't matter)
        /// </remarks>
        internal override void DiscardAnyWeakReference(ProjectRootElement projectRootElement)
        {
            ErrorUtilities.VerifyThrowArgumentNull(projectRootElement, nameof(projectRootElement));
 
            // A PRE may be unnamed if it was only used in memory.
            if (projectRootElement.FullPath != null)
            {
                lock (_locker)
                {
                    _weakCache.Remove(projectRootElement.FullPath);
                }
            }
        }
 
        /// <summary>
        /// Add or rename an entry in the cache.
        /// Old full path may be null iff it was not already in the cache.
        /// </summary>
        /// <remarks>
        /// Must be called within the cache lock.
        /// </remarks>
        private void RenameEntryInternal(string oldFullPathIfAny, ProjectRootElement projectRootElement)
        {
            ErrorUtilities.VerifyThrowInternalNull(projectRootElement.FullPath, "FullPath");
 
            if (oldFullPathIfAny != null)
            {
                ErrorUtilities.VerifyThrowInternalRooted(oldFullPathIfAny);
                ErrorUtilities.VerifyThrow(_weakCache[oldFullPathIfAny] == projectRootElement, "Should already be present");
                _weakCache.Remove(oldFullPathIfAny);
            }
 
            // There may already be a ProjectRootElement in the cache with the new name. In this case we cannot throw an exception;
            // we must merely replace it. This is because it may be an unrooted entry
            // (and thus gone from the client's point of view) that merely remains
            // in the cache because we still have a reference to it from our strong cache.
            // Another possibility is that there are two, unrelated, un-saved, in-memory projects that were given the same path.
            // Replacing the cache entry does not in itself cause a problem -- if there are any actual users of the old
            // entry they will not be affected. There would then exist more than one ProjectRootElement with the same path,
            // but clients ought not get themselves into such a state - and unless they save them to disk,
            // it may not be a problem. Replacing also doesn't cause a problem for the strong cache,
            // as it is never consulted by us, but it is reasonable for us to remove the old entry in that case.
            ProjectRootElement existingWeakEntry;
            _weakCache.TryGetValue(projectRootElement.FullPath, out existingWeakEntry);
 
            if (existingWeakEntry != null && !object.ReferenceEquals(existingWeakEntry, projectRootElement))
            {
                _strongCache.Remove(existingWeakEntry);
                RaiseProjectRootElementRemovedFromStrongCache(existingWeakEntry);
            }
 
            DebugTraceCache("Adding: ", projectRootElement.FullPath);
            _weakCache[projectRootElement.FullPath] = projectRootElement;
 
            BoostEntryInStrongCache(projectRootElement);
        }
 
        /// <summary>
        /// Update the strong cache.
        /// If the item is already a member of the list, move it to the top.
        /// Otherwise, just add it to the top.
        /// If the list is too large, remove an entry from the bottom.
        /// </summary>
        /// <remarks>
        /// Must be called within the cache lock.
        /// If the size of strong cache gets large, this needs a faster data structure
        /// than a linked list. It's currently O(n).
        /// </remarks>
        private void BoostEntryInStrongCache(ProjectRootElement projectRootElement)
        {
            LinkedListNode<ProjectRootElement> node = _strongCache.First;
 
            while (node != null)
            {
                if (Object.ReferenceEquals(node.Value, projectRootElement))
                {
                    // DebugTraceCache("Boosting: ", projectRootElement.FullPath);
                    _strongCache.Remove(node);
                    _strongCache.AddFirst(node);
 
                    return;
                }
 
                node = node.Next;
            }
 
            _strongCache.AddFirst(projectRootElement);
 
            if (_strongCache.Count > s_maximumStrongCacheSize)
            {
                node = _strongCache.Last;
 
                DebugTraceCache("Shedding: ", node.Value.FullPath);
                _strongCache.Remove(node);
                RaiseProjectRootElementRemovedFromStrongCache(node.Value);
            }
        }
 
        /// <summary>
        /// Completely remove an entry from this cache
        /// </summary>
        /// <remarks>
        /// Must be called within the cache lock.
        /// </remarks>
        private void ForgetEntry(ProjectRootElement projectRootElement)
        {
            DebugTraceCache("Forgetting: ", projectRootElement.FullPath);
 
            _weakCache.Remove(projectRootElement.FullPath);
 
            LinkedListNode<ProjectRootElement> strongCacheEntry = _strongCache.Find(projectRootElement);
            if (strongCacheEntry != null)
            {
                _strongCache.Remove(strongCacheEntry);
                RaiseProjectRootElementRemovedFromStrongCache(strongCacheEntry.Value);
            }
 
            DebugTraceCache("Out of date dropped from XML cache: ", projectRootElement.FullPath);
        }
 
        /// <summary>
        /// Completely remove an entry from this cache if it exists.
        /// </summary>
        private void ForgetEntryIfExists(ProjectRootElement projectRootElement)
        {
            lock (_locker)
            {
                if (_weakCache.TryGetValue(projectRootElement.FullPath, out var cached) && cached == projectRootElement)
                {
                    ForgetEntry(projectRootElement);
                }
            }
        }
 
        /// <summary>
        /// Write debugging messages to the Debug.Out stream.
        /// </summary>
        private void DebugTraceCache(string message, bool param1)
        {
            if (s_debugLogCacheActivity)
            {
                DebugTraceCache(message, Convert.ToString(param1, CultureInfo.InvariantCulture));
            }
        }
 
        /// <summary>
        /// Write debugging messages to the Debug.Out stream.
        /// </summary>
        private void DebugTraceCache(string message, int param1)
        {
            if (s_debugLogCacheActivity)
            {
                DebugTraceCache(message, Convert.ToString(param1, CultureInfo.InvariantCulture));
            }
        }
 
        /// <summary>
        /// Write debugging messages to the Debug.Out stream.
        /// </summary>
        private void DebugTraceCache(string message, string param1)
        {
            if (s_debugLogCacheActivity)
            {
                string prefix = OutOfProcNode.IsOutOfProcNode ? "C" : "P";
                Trace.WriteLine(prefix + " " + Process.GetCurrentProcess().Id + " | " + message + param1);
            }
        }
    }
}