File: Engine\CacheScope.cs
Web Access
Project: ..\..\..\src\Deprecated\Engine\Microsoft.Build.Engine.csproj (Microsoft.Build.Engine)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
// THE ASSEMBLY BUILT FROM THIS SOURCE FILE HAS BEEN DEPRECATED FOR YEARS. IT IS BUILT ONLY TO PROVIDE
// BACKWARD COMPATIBILITY FOR API USERS WHO HAVE NOT YET MOVED TO UPDATED APIS. PLEASE DO NOT SEND PULL
// REQUESTS THAT CHANGE THIS FILE WITHOUT FIRST CHECKING WITH THE MAINTAINERS THAT THE FIX IS REQUIRED.
 
using System;
using System.Collections;
using System.Threading;
using Microsoft.Build.BuildEngine.Shared;
using System.Globalization;
 
namespace Microsoft.Build.BuildEngine
{
    /// <summary>
    /// The purpose of this class is to contain a set of cache entries which belong to
    /// a particular scope. The scope is defined as string (project file name) X set of
    /// string ( global properties ). This class is thread safe and can be used from multiple
    /// threads. It is also lock free for multiple readers (via use of Hashtable as
    /// the backing store).
    /// We use a ReaderWriterLock in here so that when a request comes in for multiple
    /// cache entries we guarantee that all the entries come from a consistent view of
    /// the cache at some point in time. Without a lock we might get a write in between
    /// the reads and half of the entries would represent the values before the write and
    /// the rest after. This is unacceptable as at no point in time the cache actually contained
    /// the entries that would be returned without the lock. This is required by the caching APIs
    /// provided to the tasks which support retrieving multiple entries at once.
    /// </summary>
    internal class CacheScope
    {
        #region Constructors
 
        /// <summary>
        /// This constructor creates a scope for a particular name and set of properties
        /// </summary>
        internal CacheScope(string scopeName, BuildPropertyGroup scopeProperties, string scopeToolsVersion)
        {
            // Make certain we don't cache a reference to a Project object, which would defeat
            // the purpose of this cache
            scopeProperties.ClearParentProject();
 
            this.scopeName = scopeName;
            this.scopeToolsVersion = scopeToolsVersion;
            this.scopeProperties = scopeProperties;
            this.cacheContents = new Hashtable(StringComparer.OrdinalIgnoreCase);
        }
        #endregion
 
        #region Properties
        /// <summary>
        /// Set of cached project file properties
        /// </summary>
        internal BuildPropertyGroup ScopeProperties
        {
            get
            {
                return this.scopeProperties;
            }
        }
 
        /// <summary>
        /// Usually the project file name
        /// </summary>
        internal string ScopeName
        {
            get
            {
                return this.scopeName;
            }
        }
 
        /// <summary>
        /// Usually the version of the toolset used
        /// </summary>
        internal string ScopeToolsVersion
        {
            get
            {
                return this.scopeToolsVersion;
            }
        }
        #endregion
 
        #region Methods
 
        /// <summary>
        /// This method adds an entry to the cache in a thread-safe way
        /// </summary>
        internal void AddCacheEntry(CacheEntry cacheEntry)
        {
            cacheScopeReaderWriterLock.AcquireWriterLock(Timeout.Infinite);
 
            try
            {
                if (cacheEntry != null)
                {
                    AddCacheEntryInternal(cacheEntry);
                }
            }
            finally
            {
                cacheScopeReaderWriterLock.ReleaseWriterLock();
            }
        }
 
        /// <summary>
        /// This method adds multiple entries to the cache in a thread-safe way.
        /// </summary>
        /// <param name="cacheEntries"></param>
        internal void AddCacheEntries(CacheEntry[] cacheEntries)
        {
            cacheScopeReaderWriterLock.AcquireWriterLock(Timeout.Infinite);
 
            try
            {
                for (int i = 0; i < cacheEntries.Length; i++)
                {
                    if (cacheEntries[i] != null)
                    {
                        AddCacheEntryInternal(cacheEntries[i]);
                    }
                }
            }
            finally
            {
                cacheScopeReaderWriterLock.ReleaseWriterLock();
            }
        }
 
        /// <summary>
        /// This method adds an entry to the cache, without taking a lock
        /// </summary>
        private void AddCacheEntryInternal(CacheEntry cacheEntry)
        {
            if (!cacheContents.ContainsKey(cacheEntry.Name))
            {
                cacheContents.Add(cacheEntry.Name, cacheEntry);
            }
            else
            {
                CacheEntry existingCacheEntry = (CacheEntry)cacheContents[cacheEntry.Name];
                // Make sure the cache values, if overwritten, stay the same. We do not currently support
                // changing the cached value to something else. This allows us to not have a notification
                // mechanism for changed values and if a node has a cached entry it can assume it's up to date.
                // This can change in the future if we discover a compelling scenario for changing cache values.
                if (!cacheEntry.IsEquivalent(existingCacheEntry))
                {
                    ErrorUtilities.VerifyThrowInvalidOperation(false, "CannotModifyCacheEntryValues");
                }
            }
        }
 
        /// <summary>
        /// This method return the cache entry for a given name. If the cache entry doesn't exist it
        /// return null. This method is thread safe.
        /// </summary>
        internal CacheEntry GetCacheEntry(string name)
        {
            if (cacheContents.ContainsKey(name))
            {
                return (CacheEntry)cacheContents[name];
            }
 
            return null;
        }
 
        /// <summary>
        /// This method returns the requested set of cache entries. This method is thread safe
        /// </summary>
        /// <param name="names"></param>
        /// <returns></returns>
        internal CacheEntry[] GetCacheEntries(string[] names)
        {
            CacheEntry[] results = new CacheEntry[names.Length];
 
            // This is read only, but since we're processing multiple entries we want to present a consistent
            // view of the cache... we don't want a write between our reads
            cacheScopeReaderWriterLock.AcquireReaderLock(Timeout.Infinite);
 
            try
            {
                for (int i = 0; i < names.Length; i++)
                {
                    results[i] = GetCacheEntry(names[i]);
                }
            }
            finally
            {
                cacheScopeReaderWriterLock.ReleaseReaderLock();
            }
 
            return results;
        }
 
        /// <summary>
        /// This method removes an entry from the cache if it exists and does nothing if it doesn't exist
        /// This method is thread safe.
        /// </summary>
        internal void ClearCacheEntry(string name)
        {
            if (cacheContents.ContainsKey(name))
            {
                cacheScopeReaderWriterLock.AcquireWriterLock(Timeout.Infinite);
 
                try
                {
                    cacheContents.Remove(name);
                }
                finally
                {
                    cacheScopeReaderWriterLock.ReleaseWriterLock();
                }
            }
        }
 
        /// <summary>
        /// This method returns true if the cache entry for a given name is present in the cache.
        /// This method is thread safe.
        /// </summary>
        internal bool ContainsCacheEntry(string name)
        {
            return cacheContents.ContainsKey(name);
        }
 
        /// <summary>
        /// This method adds cached results for each target results for which are contained inside
        /// the build result. This method is thread safe.
        /// </summary>
        internal void AddCacheEntryForBuildResults(BuildResult buildResult)
        {
            ErrorUtilities.VerifyThrow(buildResult != null, "Expect a non-null build result");
 
            // Don't cache results if they are marked as uncacheable
            if (!buildResult.UseResultCache)
            {
                return;
            }
 
            cacheScopeReaderWriterLock.AcquireWriterLock(Timeout.Infinite);
 
            try
            {
                if (!ContainsCacheEntry(Constants.defaultTargetCacheName))
                {
                    // If the project file is malformed the build may fail without initializing the initialtargets or
                    // the default targests fields. The retrieval code expects non-null values
                    // so it is necessary to replace null with empty string
                    ErrorUtilities.VerifyThrow(!buildResult.EvaluationResult || (buildResult.InitialTargets != null
                                               && buildResult.DefaultTargets != null),
                                               "Expect initial targets to be non-null for successful builds");
                    string defaultTargets = buildResult.DefaultTargets ?? String.Empty;
                    PropertyCacheEntry defaultTargetsCacheEntry = new PropertyCacheEntry(Constants.defaultTargetCacheName, defaultTargets);
                    AddCacheEntryInternal(defaultTargetsCacheEntry);
 
                    string initialTargets = buildResult.InitialTargets ?? String.Empty;
                    PropertyCacheEntry initialTargetsCacheEntry = new PropertyCacheEntry(Constants.initialTargetCacheName, initialTargets);
                    AddCacheEntryInternal(initialTargetsCacheEntry);
                }
 
                if (!ContainsCacheEntry(Constants.projectIdCacheName))
                {
                    PropertyCacheEntry projectIdCacheEntry = new PropertyCacheEntry(Constants.projectIdCacheName, buildResult.ProjectId.ToString(CultureInfo.InvariantCulture));
                    AddCacheEntryInternal(projectIdCacheEntry);
                }
 
                IDictionary outputsByTargetName = buildResult.OutputsByTarget;
 
                //Create single entry for each target in the request
                foreach (DictionaryEntry entry in buildResult.ResultByTarget)
                {
                    Target.BuildState buildState = (Target.BuildState)entry.Value;
 
                    // Only cache successful and failed targets
                    if ((buildState == Target.BuildState.CompletedSuccessfully) ||
                        (buildState == Target.BuildState.CompletedUnsuccessfully))
                    {
                        BuildItem[] targetOutputs = null;
 
                        // Only cache output items for successful targets
                        if (buildState == Target.BuildState.CompletedSuccessfully)
                        {
                            ErrorUtilities.VerifyThrow(buildResult.OutputsByTarget.Contains(entry.Key),
                                "We must have build results for successful targets");
 
                            BuildItem[] outputItems = (BuildItem[])buildResult.OutputsByTarget[entry.Key];
 
                            // It's essential that we clear out any pointers to the project from the BuildItem;
                            // otherwise the cache will hold onto the project, and not save any memory.
                            if (outputItems != null)
                            {
                                for (int i = 0; i < outputItems.Length; i++)
                                {
                                    outputItems[i] = outputItems[i].VirtualClone(true /* remove references to minimise transitive size */);
                                }
                            }
 
                            targetOutputs = (BuildItem[])buildResult.OutputsByTarget[entry.Key];
                        }
 
                        BuildResultCacheEntry cacheEntry = new BuildResultCacheEntry((string)entry.Key, targetOutputs,
                            buildState == Target.BuildState.CompletedSuccessfully);
 
                        if (Engine.debugMode)
                        {
                            Console.WriteLine("+++Adding cache entry for " + (string)entry.Key + " in " +
                                this.ScopeName + " result: " + (buildState == Target.BuildState.CompletedSuccessfully));
                        }
 
                        AddCacheEntryInternal(cacheEntry);
                    }
                }
            }
            finally
            {
                cacheScopeReaderWriterLock.ReleaseWriterLock();
            }
        }
 
        /// <summary>
        /// Get a cached build result if available for the given request. This method is thread safe.
        /// </summary>
        /// <param name="buildRequest"></param>
        /// <param name="actuallyBuiltTargets"></param>
        /// <returns></returns>
        internal BuildResult GetCachedBuildResult(BuildRequest buildRequest, out ArrayList actuallyBuiltTargets)
        {
            actuallyBuiltTargets = null;
 
            PropertyCacheEntry defaultTargetsCacheEntry, initialTargetsCacheEntry, projectIdCacheEntry;
 
            // No writes here, but since we're reading multiple values we want to get a consistent view of the cache
            cacheScopeReaderWriterLock.AcquireReaderLock(Timeout.Infinite);
 
            try
            {
                defaultTargetsCacheEntry = (PropertyCacheEntry)GetCacheEntry(Constants.defaultTargetCacheName);
                initialTargetsCacheEntry = (PropertyCacheEntry)GetCacheEntry(Constants.initialTargetCacheName);
                projectIdCacheEntry = (PropertyCacheEntry)GetCacheEntry(Constants.projectIdCacheName);
            }
            finally
            {
                cacheScopeReaderWriterLock.ReleaseReaderLock();
            }
 
            // If we ever built anything in this project we must have the default and initial targets.
            if (defaultTargetsCacheEntry == null && initialTargetsCacheEntry == null)
            {
                return null;
            }
 
            ErrorUtilities.VerifyThrow(projectIdCacheEntry != null, "We should always have the projectId cache entry");
 
            ErrorUtilities.VerifyThrow(defaultTargetsCacheEntry != null && initialTargetsCacheEntry != null,
                "We should have both the initial and default targets in the cache");
 
            ArrayList targetsToBuild = new ArrayList(initialTargetsCacheEntry.Value.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries));
 
            if (buildRequest.TargetNames == null || buildRequest.TargetNames.Length == 0)
            {
                targetsToBuild.AddRange(defaultTargetsCacheEntry.Value.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries));
            }
            else
            {
                targetsToBuild.AddRange(buildRequest.TargetNames);
            }
 
            // Create variable to hold the cached outputs
            Hashtable outputsByTargetName = new Hashtable(targetsToBuild.Count);
            Hashtable resultByTarget = new Hashtable(targetsToBuild.Count, StringComparer.OrdinalIgnoreCase);
 
            bool overallSuccess = true;
            bool missingValues = false;
 
            // No writes here, but since we're reading multiple values we want to get a consistent view of the cache
            cacheScopeReaderWriterLock.AcquireReaderLock(Timeout.Infinite);
 
            try
            {
                for (int i = 0; i < targetsToBuild.Count; i++)
                {
                    string targetName = EscapingUtilities.UnescapeAll((string)targetsToBuild[i]);
                    if (ContainsCacheEntry(targetName))
                    {
                        BuildResultCacheEntry cacheEntry = (BuildResultCacheEntry)GetCacheEntry(targetName);
                        overallSuccess = overallSuccess && cacheEntry.BuildResult;
                        resultByTarget[targetName] = (cacheEntry.BuildResult) ?
                            Target.BuildState.CompletedSuccessfully : Target.BuildState.CompletedUnsuccessfully;
 
                        // Restore output items for successful targets
                        if (cacheEntry.BuildResult)
                        {
                            outputsByTargetName[targetName] = cacheEntry.BuildItems;
                        }
                        // We found a failed target - cut the loop short
                        else
                        {
                            break;
                        }
                    }
                    else
                    {
                        missingValues = true;
                        break;
                    }
                }
            }
            finally
            {
                cacheScopeReaderWriterLock.ReleaseReaderLock();
            }
 
            if (missingValues)
            {
                return null;
            }
 
            actuallyBuiltTargets = targetsToBuild;
 
            return new BuildResult(outputsByTargetName, resultByTarget, overallSuccess, buildRequest.HandleId, buildRequest.RequestId,
                int.Parse(projectIdCacheEntry.Value, CultureInfo.InvariantCulture), false /* use results cache */,
                defaultTargetsCacheEntry.Value, initialTargetsCacheEntry.Value, 0, 0, 0);
        }
 
        #endregion
 
        #region Data
        // This is normally the name of the project file to which the cached entries refer
        private string scopeName;
        // The version of the toolset the project uses
        private string scopeToolsVersion;
        // This is normally a set of properties for the project file to which the cached entries refer
        private BuildPropertyGroup scopeProperties;
        // This dictionary contains all the cached items within the current scope
        private Hashtable cacheContents;
        // Synchronization between multiple threads
        private static ReaderWriterLock cacheScopeReaderWriterLock = new ReaderWriterLock();
        #endregion
    }
}