File: WorkloadSuggestionFinder.cs
Web Access
Project: ..\..\..\src\Resolvers\Microsoft.NET.Sdk.WorkloadManifestReader\Microsoft.NET.Sdk.WorkloadManifestReader.csproj (Microsoft.NET.Sdk.WorkloadManifestReader)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
namespace Microsoft.NET.Sdk.WorkloadManifestReader
{
    internal class WorkloadSuggestionFinder
    {
        public WorkloadSuggestionFinder(HashSet<WorkloadPackId> installedPacks, HashSet<WorkloadPackId> requestedPacks, IEnumerable<(WorkloadId id, HashSet<WorkloadPackId> expandedPacks)> expandedWorkloads)
        {
            FindPartialSuggestionsAndSimpleCompleteSuggestions(
                requestedPacks, expandedWorkloads,
                out List<WorkloadSuggestionCandidate> partialSuggestions,
                out HashSet<WorkloadSuggestionCandidate> completeSuggestions);
 
            foreach (var suggestion in GatherUniqueCompletePermutedSuggestions(partialSuggestions))
            {
                completeSuggestions.Add(suggestion);
            }
 
            UnsortedSuggestions = completeSuggestions.Select(
                c =>
                {
                    var installedCount = c.Packs.Count(p => installedPacks.Contains(p));
                    var extraPacks = c.Packs.Count - installedCount - requestedPacks.Count;
                    return new WorkloadSuggestion(c.Workloads, extraPacks);
                })
                .ToList();
 
            if (UnsortedSuggestions.Count == 0)
            {
                throw new ArgumentException("requestedPacks may only contain packs that exist in expandedWorkloads", "requestedPacks");
            }
        }
 
        /// <summary>
        /// Search the list of expanded workloads for workloads that are "simple" complete suggestions themselves and workloads that could be part of a more complex complete suggestion.
        /// </summary>
        /// <param name="requestedPacks">The packs that a complete suggestion must include</param>
        /// <param name="expandedWorkloads">The full set of workloads, flattened to include the packs in the workloads they extend</param>
        /// <param name="partialSuggestions">Workloads that contain one or more of the required packs and could be combined with another workload to make a complete suggestion</param>
        /// <param name="completeSuggestions">Workloads that contain all the requested packs and therefore are inherently complete suggestions</param>
        internal static void FindPartialSuggestionsAndSimpleCompleteSuggestions(
            HashSet<WorkloadPackId> requestedPacks,
            IEnumerable<(WorkloadId id, HashSet<WorkloadPackId> expandedPacks)> expandedWorkloads,
            out List<WorkloadSuggestionCandidate> partialSuggestions,
            out HashSet<WorkloadSuggestionCandidate> completeSuggestions)
        {
            partialSuggestions = new List<WorkloadSuggestionCandidate>();
            completeSuggestions = new HashSet<WorkloadSuggestionCandidate>();
            foreach (var workload in expandedWorkloads)
            {
                if (workload.expandedPacks.Any(e => requestedPacks.Contains(e)))
                {
                    var unsatisfied = new HashSet<WorkloadPackId>(requestedPacks.Where(p => !workload.expandedPacks.Contains(p)));
                    var suggestion = new WorkloadSuggestionCandidate(new HashSet<WorkloadId>() { workload.id }, workload.expandedPacks, unsatisfied);
                    if (suggestion.IsComplete)
                    {
                        completeSuggestions.Add(suggestion);
                    }
                    else
                    {
                        partialSuggestions.Add(suggestion);
                    }
                }
            }
        }
 
        /// <summary>
        /// Finds complete suggestions by permutationally combining partial suggestions.
        /// </summary>
        /// <param name="partialSuggestions">List of partial suggestions to permutationally combine</param>
        internal static HashSet<WorkloadSuggestionCandidate> GatherUniqueCompletePermutedSuggestions(List<WorkloadSuggestionCandidate> partialSuggestions)
        {
            var completeSuggestions = new HashSet<WorkloadSuggestionCandidate>();
 
            foreach (var root in partialSuggestions)
            {
                GatherCompletePermutedSuggestions(root, partialSuggestions, completeSuggestions);
            }
 
            return FilterRedundantSuggestions(completeSuggestions);
        }
 
        /// <summary>
        /// Recursively explores a branching tree from the root, adding branches that would reduce the number of unsatisfied packs, and recording any fully satisfied solutions found.
        /// This is intended to only be called by <see cref="GatherUniqueCompletePermutedSuggestions"/>.
        /// </summary>
        /// <remarks>
        /// Some of these solutions may contain redundancies, i.e. workloads that are not necessary for it to be a complete solution. These should be filtered out using
        /// <see cref="FilterRedundantSuggestions"/> before using them.
        /// </remarks>
        /// <param name="root">A partial suggestion candidate that is the base for this permutation</param>
        /// <param name="branches">Partial suggestion candidates that can be added to the root to make it more complete</param>
        /// <param name="completeSuggestions">A collection to which to add any discovered complete solutions</param>
        static void GatherCompletePermutedSuggestions(WorkloadSuggestionCandidate root, List<WorkloadSuggestionCandidate> branches, HashSet<WorkloadSuggestionCandidate> completeSuggestions)
        {
            foreach (var branch in branches)
            {
                //skip branches identical to ones that have already already been taken
                //there's probably a more efficient way to do this but this is easy to reason about
                if (root.Workloads.IsSupersetOf(branch.Workloads))
                {
                    continue;
                }
 
                //skip branches that don't reduce the number of missing packs
                //the branch may be a more optimal solution, but this will be handled elsewhere in the permutation space where it is treated as a root
                if (!root.UnsatisfiedPacks.Overlaps(branch.Packs))
                {
                    continue;
                }
 
                //construct the new condidate by combining the root and the branch
                var combinedIds = new HashSet<WorkloadId>(root.Workloads);
                combinedIds.UnionWith(branch.Workloads);
                var combinedPacks = new HashSet<WorkloadPackId>(root.Packs);
                combinedPacks.UnionWith(branch.Packs);
                var stillMissing = new HashSet<WorkloadPackId>(root.UnsatisfiedPacks);
                stillMissing.ExceptWith(branch.Packs);
                var candidate = new WorkloadSuggestionCandidate(combinedIds, combinedPacks, stillMissing);
 
                //if the candidate contains all the requested packs, it's complete. else, recurse to try adding more branches to it.
                if (candidate.IsComplete)
                {
                    completeSuggestions.Add(candidate);
                }
                else
                {
                    GatherCompletePermutedSuggestions(candidate, branches, completeSuggestions);
                }
            }
        }
 
        /// <summary>
        /// Returns a new set with redundant suggestions removed from it, i.e. suggestions that are a superset of another of the suggestions.
        /// </summary>
        internal static HashSet<WorkloadSuggestionCandidate> FilterRedundantSuggestions(HashSet<WorkloadSuggestionCandidate> completeSuggestions)
        {
            var filtered = new HashSet<WorkloadSuggestionCandidate>();
 
            foreach (var suggestion in completeSuggestions)
            {
                bool isSupersetOfAny = false;
                foreach (var other in completeSuggestions)
                {
                    if (suggestion.Workloads.IsProperSupersetOf(other.Workloads))
                    {
                        isSupersetOfAny = true;
                    }
                }
                if (!isSupersetOfAny)
                {
                    filtered.Add(suggestion);
                }
            }
 
            return filtered;
        }
 
        public ICollection<WorkloadSuggestion> UnsortedSuggestions { get; }
 
        /// <summary>
        /// Finds the best value from a list of values according to one or more custom comparators. The comparators are an ordered fallback list - the second comparator is only checked when values are equal according to the first comparator, and so on.
        /// </summary>
        private static T FindBest<T>(IEnumerable<T> values, params Comparison<T>[] comparators)
        {
            T best = values.First();
 
            foreach (T val in values.Skip(1))
            {
                foreach (Comparison<T> c in comparators)
                {
                    var cmp = c(val, best);
                    if (cmp > 0)
                    {
                        best = val;
                        break;
                    }
                    else if (cmp < 0)
                    {
                        break;
                    }
                }
            }
            return best;
        }
 
        internal static WorkloadSuggestion GetBestSuggestion(ICollection<WorkloadSuggestion> suggestions) => FindBest(
                suggestions,
                (x, y) => ContainsExperimental(y.Workloads) - ContainsExperimental(x.Workloads),
                (x, y) => y.ExtraPacks - x.ExtraPacks,
                (x, y) => y.Workloads.Count - x.Workloads.Count);
 
        /// <summary>
        /// Gets the suggestion with the lowest number of extra packs and lowest number of workload IDs.
        /// </summary>
        public WorkloadSuggestion GetBestSuggestion() => GetBestSuggestion(UnsortedSuggestions);
 
        private static int ContainsExperimental(HashSet<WorkloadId> set) => set.Any(w => w.ToString().Contains("experimental")) ? 1 : 0;
 
        /// <summary>
        /// A partial or complete suggestion for workloads to install, annotated with which requested packs it does not satisfy
        /// </summary>
        internal class WorkloadSuggestionCandidate : IEquatable<WorkloadSuggestionCandidate>
        {
            public WorkloadSuggestionCandidate(HashSet<WorkloadId> id, HashSet<WorkloadPackId> packs, HashSet<WorkloadPackId> unsatisfiedPacks)
            {
                Packs = packs;
                UnsatisfiedPacks = unsatisfiedPacks;
                Workloads = id;
            }
 
            public HashSet<WorkloadId> Workloads { get; }
            public HashSet<WorkloadPackId> Packs { get; }
            public HashSet<WorkloadPackId> UnsatisfiedPacks { get; }
            public bool IsComplete => UnsatisfiedPacks.Count == 0;
 
            public bool Equals(WorkloadSuggestionCandidate? other) => other != null && Workloads.SetEquals(other.Workloads);
 
            public override int GetHashCode()
            {
                int hashcode = 0;
                foreach (var id in Workloads)
                {
                    hashcode ^= id.GetHashCode();
                }
                return hashcode;
            }
        }
 
        /// <summary>
        /// A suggestion of one or more workloads to be installed to satisfy missing packs.
        /// </summary>
        public class WorkloadSuggestion
        {
            public WorkloadSuggestion(HashSet<WorkloadId> workloads, int extraPacks)
            {
                Workloads = workloads;
                ExtraPacks = extraPacks;
            }
 
            /// <summary>
            /// The workload IDs that comprise this suggestion
            /// </summary>
            public HashSet<WorkloadId> Workloads { get; internal set; }
 
            /// <summary>
            /// How many additional (and potentionally unnecessary) packs this suggestion will result in installing
            /// </summary>
            public int ExtraPacks { get; internal set; }
        }
    }
}