diff --git a/src/EditorFeatures/Core/EditorFeatures.csproj b/src/EditorFeatures/Core/EditorFeatures.csproj
index a76cdd24561225a3825b500602058fbc1f0224c9..1d281adf49a56afec46a31c3180841b9a6790711 100644
--- a/src/EditorFeatures/Core/EditorFeatures.csproj
+++ b/src/EditorFeatures/Core/EditorFeatures.csproj
@@ -53,22 +53,36 @@
true
- false
+
+ false
+
true
- false
+
+ false
+
- false
+
+ false
+
- false
+
+ false
+
- false
- false
- false
+
+ false
+
+
+ false
+
+
+ false
+
@@ -651,8 +665,8 @@
-
-
+
+
diff --git a/src/EditorFeatures/Core/Implementation/Workspaces/ProjectCacheService.SimpleMRUCache.cs b/src/EditorFeatures/Core/Implementation/Workspaces/ProjectCacheService.SimpleMRUCache.cs
new file mode 100644
index 0000000000000000000000000000000000000000..680f736105eaa72911c34ef0ad24fcafa0ed2fde
--- /dev/null
+++ b/src/EditorFeatures/Core/Implementation/Workspaces/ProjectCacheService.SimpleMRUCache.cs
@@ -0,0 +1,134 @@
+// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.Host;
+using Microsoft.CodeAnalysis.Shared.TestHooks;
+using Microsoft.CodeAnalysis.SolutionCrawler;
+using Roslyn.Utilities;
+
+namespace Microsoft.CodeAnalysis.Editor.Implementation.Workspaces
+{
+ internal partial class ProjectCacheService : IProjectCacheHostService
+ {
+ private class SimpleMRUCache
+ {
+ private const int CacheSize = 3;
+
+ private readonly Node[] nodes = new Node[CacheSize];
+
+ public bool Empty
+ {
+ get
+ {
+ for (var i = 0; i < nodes.Length; i++)
+ {
+ if (nodes[i].Data != null)
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ }
+
+ public void Touch(object instance)
+ {
+ var oldIndex = -1;
+ var oldTime = DateTime.UtcNow;
+
+ for (var i = 0; i < nodes.Length; i++)
+ {
+ if (instance == nodes[i].Data)
+ {
+ nodes[i].LastTouched = DateTime.UtcNow;
+ return;
+ }
+
+ if (oldTime >= nodes[i].LastTouched)
+ {
+ oldTime = nodes[i].LastTouched;
+ oldIndex = i;
+ }
+ }
+
+ Contract.Requires(oldIndex >= 0);
+ nodes[oldIndex] = new Node(instance, DateTime.UtcNow);
+ }
+
+ public void ClearExpiredItems(DateTime expirationTime)
+ {
+ for (var i = 0; i < nodes.Length; i++)
+ {
+ if (nodes[i].Data != null && nodes[i].LastTouched < expirationTime)
+ {
+ nodes[i] = default(Node);
+ }
+ }
+ }
+
+ public void Clear()
+ {
+ Array.Clear(nodes, 0, nodes.Length);
+ }
+
+ private struct Node
+ {
+ public readonly object Data;
+ public DateTime LastTouched;
+
+ public Node(object data, DateTime lastTouched)
+ {
+ Data = data;
+ LastTouched = lastTouched;
+ }
+ }
+ }
+
+ private class ImplicitCacheMonitor : IdleProcessor
+ {
+ private readonly ProjectCacheService _owner;
+ private readonly SemaphoreSlim _gate;
+
+ public ImplicitCacheMonitor(ProjectCacheService owner, int backOffTimeSpanInMS) :
+ base(AggregateAsynchronousOperationListener.CreateEmptyListener(),
+ backOffTimeSpanInMS,
+ CancellationToken.None)
+ {
+ _owner = owner;
+ _gate = new SemaphoreSlim(0);
+
+ Start();
+ }
+
+ protected override Task ExecuteAsync()
+ {
+ _owner.ClearExpiredImplicitCache(DateTime.UtcNow - TimeSpan.FromMilliseconds(BackOffTimeSpanInMS));
+
+ return SpecializedTasks.EmptyTask;
+ }
+
+ public void Touch()
+ {
+ UpdateLastAccessTime();
+
+ if (_gate.CurrentCount == 0)
+ {
+ _gate.Release();
+ }
+ }
+
+ protected override Task WaitAsync(CancellationToken cancellationToken)
+ {
+ if (_owner.IsImplicitCacheEmpty)
+ {
+ return _gate.WaitAsync(cancellationToken);
+ }
+
+ return SpecializedTasks.EmptyTask;
+ }
+ }
+ }
+}
diff --git a/src/EditorFeatures/Core/Implementation/Workspaces/ProjectCacheService.cs b/src/EditorFeatures/Core/Implementation/Workspaces/ProjectCacheService.cs
new file mode 100644
index 0000000000000000000000000000000000000000..30b10df08f3085fa301b54ebd6f8978800be4db8
--- /dev/null
+++ b/src/EditorFeatures/Core/Implementation/Workspaces/ProjectCacheService.cs
@@ -0,0 +1,195 @@
+// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Runtime.CompilerServices;
+using Microsoft.CodeAnalysis.Host;
+using Microsoft.CodeAnalysis.Host.Mef;
+
+namespace Microsoft.CodeAnalysis.Editor.Implementation.Workspaces
+{
+ internal partial class ProjectCacheService : IProjectCacheHostService
+ {
+ internal const int ImplicitCacheSize = 3;
+
+ private readonly object _gate = new object();
+
+ private readonly Workspace _workspace;
+ private readonly Dictionary _activeCaches = new Dictionary();
+
+ private readonly SimpleMRUCache _implicitCache = new SimpleMRUCache();
+ private readonly ImplicitCacheMonitor _implicitCacheMonitor;
+
+ public ProjectCacheService(Workspace workspace, int implicitCacheTimeout, bool forceCleanup = false)
+ {
+ _workspace = workspace;
+
+ // forceCleanup is for testing
+ if (workspace?.Kind == WorkspaceKind.Host || forceCleanup)
+ {
+ // monitor implicit cache for host
+ _implicitCacheMonitor = new ImplicitCacheMonitor(this, implicitCacheTimeout);
+ }
+ }
+
+ public bool IsImplicitCacheEmpty
+ {
+ get
+ {
+ lock (_gate)
+ {
+ return _implicitCache.Empty;
+ }
+ }
+ }
+
+ public void ClearImplicitCache()
+ {
+ lock (_gate)
+ {
+ _implicitCache.Clear();
+ }
+ }
+
+ public void ClearExpiredImplicitCache(DateTime expirationTime)
+ {
+ lock (_gate)
+ {
+ _implicitCache.ClearExpiredItems(expirationTime);
+ }
+ }
+
+ public IDisposable EnableCaching(ProjectId key)
+ {
+ lock (_gate)
+ {
+ Cache cache;
+ if (!_activeCaches.TryGetValue(key, out cache))
+ {
+ cache = new Cache(this, key);
+ _activeCaches.Add(key, cache);
+ }
+
+ cache.Count++;
+ return cache;
+ }
+ }
+
+ public T CacheObjectIfCachingEnabledForKey(ProjectId key, object owner, T instance) where T : class
+ {
+ lock (_gate)
+ {
+ Cache cache;
+ if (_activeCaches.TryGetValue(key, out cache))
+ {
+ cache.CreateStrongReference(owner, instance);
+ }
+ else if (!PartOfP2PReferences(key))
+ {
+ _implicitCache.Touch(instance);
+ _implicitCacheMonitor?.Touch();
+ }
+
+ return instance;
+ }
+ }
+
+ private bool PartOfP2PReferences(ProjectId key)
+ {
+ if (_activeCaches.Count == 0 || _workspace == null)
+ {
+ return false;
+ }
+
+ var solution = _workspace.CurrentSolution;
+ var graph = solution.GetProjectDependencyGraph();
+
+ foreach (var projectId in _activeCaches.Keys)
+ {
+ // this should be cheap. graph is cached everytime project reference is updated.
+ var p2pReferences = (ImmutableHashSet)graph.GetProjectsThatThisProjectTransitivelyDependsOn(projectId);
+ if (p2pReferences.Contains(key))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public T CacheObjectIfCachingEnabledForKey(ProjectId key, ICachedObjectOwner owner, T instance) where T : class
+ {
+ lock (_gate)
+ {
+ Cache cache;
+ if (owner.CachedObject == null && _activeCaches.TryGetValue(key, out cache))
+ {
+ owner.CachedObject = instance;
+ cache.CreateOwnerEntry(owner);
+ }
+
+ return instance;
+ }
+ }
+
+ private void DisableCaching(ProjectId key, Cache cache)
+ {
+ lock (_gate)
+ {
+ cache.Count--;
+ if (cache.Count == 0)
+ {
+ _activeCaches.Remove(key);
+ cache.FreeOwnerEntries();
+ }
+ }
+ }
+
+ private class Cache : IDisposable
+ {
+ internal int Count;
+ private readonly ProjectCacheService _cacheService;
+ private readonly ProjectId _key;
+ private readonly ConditionalWeakTable
- false
- false
- false
+
+ false
+
+
+ false
+
+
+ false
+
- false
- false
- false
- false
- false
+
+ false
+
+
+ false
+
+
+ false
+
+
+ false
+
+
+ false
+
- false
+
+ false
+
- false
- false
+
+ false
+
+
+ false
+
- false
- false
- false
- false
- false
- false
+
+ false
+
+
+ false
+
+
+ false
+
+
+ false
+
+
+ false
+
+
+ false
+
True
@@ -259,14 +294,30 @@
True
- false
- false
- false
- false
- false
- false
- false
- false
+
+ false
+
+
+ false
+
+
+ false
+
+
+ false
+
+
+ false
+
+
+ false
+
+
+ false
+
+
+ false
+
@@ -281,9 +332,15 @@
- false
- false
- false
+
+ false
+
+
+ false
+
+
+ false
+
False