Skip to content

Commit b6dfbb6

Browse files
authored
Reduce resident memory for VS tokenization and other caches (#4590)
* improve tokenizer memory performance * add comments * use Memory Cache with sliding window for tokeniztion information * use MemoryCache
1 parent 75e435e commit b6dfbb6

File tree

14 files changed

+243
-147
lines changed

14 files changed

+243
-147
lines changed

src/fsharp/service/ServiceDeclarationLists.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -736,7 +736,7 @@ type FSharpMethodGroup( name: string, unsortedMethods: FSharpMethodGroupItem[] )
736736
// BUG 413009 : [ParameterInfo] takes about 3 seconds to move from one overload parameter to another
737737
// cache allows to avoid recomputing parameterinfo for the same item
738738
#if !FX_NO_WEAKTABLE
739-
static let methodOverloadsCache = System.Runtime.CompilerServices.ConditionalWeakTable()
739+
static let methodOverloadsCache = System.Runtime.CompilerServices.ConditionalWeakTable<ItemWithInst, FSharpMethodGroupItem[]>()
740740
#endif
741741

742742
let methods =

src/fsharp/service/ServiceLexing.fs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ module FSharpTokenTag =
7777
let STRUCT = tagOfToken STRUCT
7878
let CLASS = tagOfToken CLASS
7979
let TRY = tagOfToken TRY
80+
let NEW = tagOfToken NEW
81+
let WITH = tagOfToken WITH
82+
let OWITH = tagOfToken OWITH
8083

8184

8285
/// This corresponds to a token categorization originally used in Visual Studio 2003.

src/fsharp/service/ServiceLexing.fsi

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,12 @@ module FSharpTokenTag =
182182
val CLASS : int
183183
/// Indicates the token is keyword `try`
184184
val TRY : int
185+
/// Indicates the token is keyword `with`
186+
val WITH : int
187+
/// Indicates the token is keyword `with` in #light
188+
val OWITH : int
189+
/// Indicates the token is keyword `new`
190+
val NEW : int
185191

186192
/// Information about a particular token from the tokenizer
187193
type FSharpTokenInfo =

vsintegration/src/FSharp.Editor/CodeFix/ImplementInterfaceCodeFixProvider.fs

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ type internal InterfaceState =
2222
{ InterfaceData: InterfaceData
2323
EndPosOfWith: pos option
2424
AppendBracketAt: int option
25-
Tokens: FSharpTokenInfo list }
25+
Tokens: Tokenizer.SavedTokenInfo[] }
2626

2727
[<ExportCodeFixProvider(FSharpConstants.FSharpLanguageName, Name = "ImplementInterface"); Shared>]
2828
type internal FSharpImplementInterfaceCodeFixProvider
@@ -36,15 +36,15 @@ type internal FSharpImplementInterfaceCodeFixProvider
3636
let checker = checkerProvider.Checker
3737
static let userOpName = "ImplementInterfaceCodeFixProvider"
3838

39-
let queryInterfaceState appendBracketAt (pos: pos) tokens (ast: Ast.ParsedInput) =
39+
let queryInterfaceState appendBracketAt (pos: pos) (tokens: Tokenizer.SavedTokenInfo[]) (ast: Ast.ParsedInput) =
4040
asyncMaybe {
4141
let line = pos.Line - 1
4242
let column = pos.Column
4343
let! iface = InterfaceStubGenerator.tryFindInterfaceDeclaration pos ast
4444
let endPosOfWidth =
4545
tokens
46-
|> List.tryPick (fun (t: FSharpTokenInfo) ->
47-
if t.CharClass = FSharpTokenCharKind.Keyword && t.LeftColumn >= column && t.TokenName = "WITH" then
46+
|> Array.tryPick (fun (t: Tokenizer.SavedTokenInfo) ->
47+
if t.Tag = FSharpTokenTag.WITH || t.Tag = FSharpTokenTag.OWITH then
4848
Some (Pos.fromZ line (t.RightColumn + 1))
4949
else None)
5050
let appendBracketAt =
@@ -70,8 +70,8 @@ type internal FSharpImplementInterfaceCodeFixProvider
7070
getLineIdent lineStr + indentSize
7171
| InterfaceData.ObjExpr _ as iface ->
7272
state.Tokens
73-
|> List.tryPick (fun (t: FSharpTokenInfo) ->
74-
if t.CharClass = FSharpTokenCharKind.Keyword && t.TokenName = "NEW" then
73+
|> Array.tryPick (fun (t: Tokenizer.SavedTokenInfo) ->
74+
if t.Tag = FSharpTokenTag.NEW then
7575
Some (t.LeftColumn + indentSize)
7676
else None)
7777
// There is no reference point, we indent the content at the start column of the interface
@@ -149,18 +149,18 @@ type internal FSharpImplementInterfaceCodeFixProvider
149149
// That's why we tokenize the line and try to find the last successive identifier token
150150
let tokens = Tokenizer.tokenizeLine(context.Document.Id, sourceText, context.Span.Start, context.Document.FilePath, defines)
151151
let startLeftColumn = context.Span.Start - textLine.Start
152-
let rec tryFindIdentifierToken acc tokens =
153-
match tokens with
154-
| t :: remainingTokens when t.LeftColumn < startLeftColumn ->
152+
let rec tryFindIdentifierToken acc i =
153+
if i >= tokens.Length then acc else
154+
match tokens.[i] with
155+
| t when t.LeftColumn < startLeftColumn ->
155156
// Skip all the tokens starting before the context
156-
tryFindIdentifierToken acc remainingTokens
157-
| t :: remainingTokens when t.Tag = FSharpTokenTag.Identifier ->
158-
tryFindIdentifierToken (Some t) remainingTokens
159-
| t :: remainingTokens when t.Tag = FSharpTokenTag.DOT || Option.isNone acc ->
160-
tryFindIdentifierToken acc remainingTokens
161-
| _ :: _
162-
| [] -> acc
163-
let! token = tryFindIdentifierToken None tokens
157+
tryFindIdentifierToken acc (i+1)
158+
| t when t.Tag = FSharpTokenTag.Identifier ->
159+
tryFindIdentifierToken (Some t) (i+1)
160+
| t when t.Tag = FSharpTokenTag.DOT || Option.isNone acc ->
161+
tryFindIdentifierToken acc (i+1)
162+
| _ -> acc
163+
let! token = tryFindIdentifierToken None 0
164164
let fixupPosition = textLine.Start + token.RightColumn
165165
let interfacePos = Pos.fromZ textLine.LineNumber token.RightColumn
166166
// We rely on the observation that the lastChar of the context should be '}' if that character is present

vsintegration/src/FSharp.Editor/Common/Extensions.fs

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -147,27 +147,22 @@ module Option =
147147
else
148148
None
149149

150+
[<RequireQualifiedAccess>]
151+
module Seq =
152+
open System.Collections.Immutable
153+
154+
let toImmutableArray (xs: seq<'a>) : ImmutableArray<'a> = xs.ToImmutableArray()
150155

151156
[<RequireQualifiedAccess>]
152-
module List =
153-
let foldi (folder : 'State -> int -> 'T -> 'State) (state : 'State) (xs : 'T list) =
157+
module Array =
158+
let foldi (folder : 'State -> int -> 'T -> 'State) (state : 'State) (xs : 'T[]) =
154159
let mutable state = state
155160
let mutable i = 0
156161
for x in xs do
157162
state <- folder state i x
158163
i <- i + 1
159164
state
160165

161-
162-
[<RequireQualifiedAccess>]
163-
module Seq =
164-
open System.Collections.Immutable
165-
166-
let toImmutableArray (xs: seq<'a>) : ImmutableArray<'a> = xs.ToImmutableArray()
167-
168-
169-
[<RequireQualifiedAccess>]
170-
module Array =
171166
/// Optimized arrays equality. ~100x faster than `array1 = array2` on strings.
172167
/// ~2x faster for floats
173168
/// ~0.8x slower for ints
@@ -178,12 +173,12 @@ module Array =
178173
| null, _ | _, null -> false
179174
| _ when xs.Length <> ys.Length -> false
180175
| _ ->
181-
let mutable break' = false
176+
let mutable stop = false
182177
let mutable i = 0
183178
let mutable result = true
184-
while i < xs.Length && not break' do
179+
while i < xs.Length && not stop do
185180
if xs.[i] <> ys.[i] then
186-
break' <- true
181+
stop <- true
187182
result <- false
188183
i <- i + 1
189184
result

vsintegration/src/FSharp.Editor/Completion/CompletionProvider.fs

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ open Microsoft.VisualStudio.Shell.Interop
2020
open Microsoft.FSharp.Compiler
2121
open Microsoft.FSharp.Compiler.Range
2222
open Microsoft.FSharp.Compiler.SourceCodeServices
23+
open System.Runtime.Caching
2324

2425
type internal FSharpCompletionProvider
2526
(
@@ -33,7 +34,8 @@ type internal FSharpCompletionProvider
3334
inherit CompletionProvider()
3435

3536
static let userOpName = "CompletionProvider"
36-
static let declarationItemsCache = ConditionalWeakTable<string, FSharpDeclarationListItem>()
37+
// Save the backing data in a memory cache held in a sliding window
38+
static let declarationItemsCache = new MemoryCache("FSharp.Editor." + userOpName)
3739
static let [<Literal>] NameInCodePropName = "NameInCode"
3840
static let [<Literal>] FullNamePropName = "FullName"
3941
static let [<Literal>] IsExtensionMemberPropName = "IsExtensionMember"
@@ -138,15 +140,15 @@ type internal FSharpCompletionProvider
138140

139141
let maxHints = if mruItems.Values.Count = 0 then 0 else Seq.max mruItems.Values
140142

141-
sortedDeclItems |> Array.iteri (fun number declItem ->
142-
let glyph = Tokenizer.FSharpGlyphToRoslynGlyph (declItem.Glyph, declItem.Accessibility)
143+
sortedDeclItems |> Array.iteri (fun number declarationItem ->
144+
let glyph = Tokenizer.FSharpGlyphToRoslynGlyph (declarationItem.Glyph, declarationItem.Accessibility)
143145
let name =
144-
match declItem.NamespaceToOpen with
145-
| Some namespaceToOpen -> sprintf "%s (open %s)" declItem.Name namespaceToOpen
146-
| _ -> declItem.Name
146+
match declarationItem.NamespaceToOpen with
147+
| Some namespaceToOpen -> sprintf "%s (open %s)" declarationItem.Name namespaceToOpen
148+
| _ -> declarationItem.Name
147149

148150
let filterText =
149-
match declItem.NamespaceToOpen, declItem.Name.Split '.' with
151+
match declarationItem.NamespaceToOpen, declarationItem.Name.Split '.' with
150152
// There is no namespace to open and the item name does not contain dots, so we don't need to pass special FilterText to Roslyn.
151153
| None, [|_|] -> null
152154
// Either we have a namespace to open ("DateTime (open System)") or item name contains dots ("Array.map"), or both.
@@ -155,39 +157,37 @@ type internal FSharpCompletionProvider
155157

156158
let completionItem =
157159
CommonCompletionItem.Create(name, glyph = Nullable glyph, rules = getRules(), filterText = filterText)
158-
.AddProperty(FullNamePropName, declItem.FullName)
160+
.AddProperty(FullNamePropName, declarationItem.FullName)
159161

160162
let completionItem =
161-
match declItem.Kind with
163+
match declarationItem.Kind with
162164
| CompletionItemKind.Method (isExtension = true) ->
163165
completionItem.AddProperty(IsExtensionMemberPropName, "")
164166
| _ -> completionItem
165167

166168
let completionItem =
167-
if name <> declItem.NameInCode then
168-
completionItem.AddProperty(NameInCodePropName, declItem.NameInCode)
169+
if name <> declarationItem.NameInCode then
170+
completionItem.AddProperty(NameInCodePropName, declarationItem.NameInCode)
169171
else completionItem
170172

171173
let completionItem =
172-
match declItem.NamespaceToOpen with
174+
match declarationItem.NamespaceToOpen with
173175
| Some ns -> completionItem.AddProperty(NamespaceToOpenPropName, ns)
174176
| None -> completionItem
175177

176178
let priority =
177-
match mruItems.TryGetValue declItem.FullName with
179+
match mruItems.TryGetValue declarationItem.FullName with
178180
| true, hints -> maxHints - hints
179181
| _ -> number + maxHints + 1
180182

181183
let sortText = sprintf "%06d" priority
182184

183-
//#if DEBUG
184-
//Logging.Logging.logInfof "***** %s => %s" name sortText
185-
//#endif
186-
187185
let completionItem = completionItem.WithSortText(sortText)
188186

189-
declarationItemsCache.Remove(completionItem.DisplayText) |> ignore // clear out stale entries if they exist
190-
declarationItemsCache.Add(completionItem.DisplayText, declItem)
187+
let key = completionItem.DisplayText
188+
let cacheItem = CacheItem(key, declarationItem)
189+
let policy = CacheItemPolicy(SlidingExpiration=DefaultTuning.PerDocumentSavedDataSlidingWindow)
190+
declarationItemsCache.Set(cacheItem, policy)
191191
results.Add(completionItem))
192192

193193
if results.Count > 0 && not declarations.IsForType && not declarations.IsError && List.isEmpty partialName.QualifyingIdents then
@@ -234,15 +234,15 @@ type internal FSharpCompletionProvider
234234

235235
override this.GetDescriptionAsync(_: Document, completionItem: Completion.CompletionItem, cancellationToken: CancellationToken): Task<CompletionDescription> =
236236
async {
237-
let exists, declarationItem = declarationItemsCache.TryGetValue(completionItem.DisplayText)
238-
if exists then
237+
match declarationItemsCache.Get(completionItem.DisplayText) with
238+
| :? FSharpDeclarationListItem as declarationItem ->
239239
let! description = declarationItem.StructuredDescriptionTextAsync
240240
let documentation = List()
241241
let collector = RoslynHelpers.CollectTaggedText documentation
242242
// mix main description and xmldoc by using one collector
243243
XmlDocumentation.BuildDataTipText(documentationBuilder, collector, collector, collector, collector, collector, description)
244244
return CompletionDescription.Create(documentation.ToImmutableArray())
245-
else
245+
| _ ->
246246
return CompletionDescription.Empty
247247
} |> RoslynHelpers.StartAsyncAsTask cancellationToken
248248

vsintegration/src/FSharp.Editor/Diagnostics/SimplifyNameDiagnosticAnalyzer.fs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@ open Microsoft.CodeAnalysis.Diagnostics
1414
open Microsoft.FSharp.Compiler
1515
open Microsoft.FSharp.Compiler.Range
1616
open Microsoft.FSharp.Compiler.SourceCodeServices
17+
open System.Runtime.Caching
1718

1819
type private TextVersionHash = int
20+
type private PerDocumentSavedData = { Hash: int; Diagnostics: ImmutableArray<Diagnostic> }
1921

2022
[<DiagnosticAnalyzer(FSharpConstants.FSharpLanguageName)>]
2123
type internal SimplifyNameDiagnosticAnalyzer() =
@@ -25,7 +27,7 @@ type internal SimplifyNameDiagnosticAnalyzer() =
2527
let getProjectInfoManager (document: Document) = document.Project.Solution.Workspace.Services.GetService<FSharpCheckerWorkspaceService>().FSharpProjectOptionsManager
2628
let getChecker (document: Document) = document.Project.Solution.Workspace.Services.GetService<FSharpCheckerWorkspaceService>().Checker
2729
let getPlidLength (plid: string list) = (plid |> List.sumBy String.length) + plid.Length
28-
static let cache = ConditionalWeakTable<DocumentId, TextVersionHash * ImmutableArray<Diagnostic>>()
30+
static let cache = new MemoryCache("FSharp.Editor." + userOpName)
2931
// Make sure only one document is being analyzed at a time, to be nice
3032
static let guard = new SemaphoreSlim(1)
3133

@@ -54,8 +56,9 @@ type internal SimplifyNameDiagnosticAnalyzer() =
5456
let textVersionHash = textVersion.GetHashCode()
5557
let! _ = guard.WaitAsync(cancellationToken) |> Async.AwaitTask |> liftAsync
5658
try
57-
match cache.TryGetValue document.Id with
58-
| true, (oldTextVersionHash, diagnostics) when oldTextVersionHash = textVersionHash -> return diagnostics
59+
let key = document.Id.ToString()
60+
match cache.Get(key) with
61+
| :? PerDocumentSavedData as data when data.Hash = textVersionHash -> return data.Diagnostics
5962
| _ ->
6063
let! sourceText = document.GetTextAsync()
6164
let checker = getChecker document
@@ -116,8 +119,11 @@ type internal SimplifyNameDiagnosticAnalyzer() =
116119
properties = (dict [SimplifyNameDiagnosticAnalyzer.LongIdentPropertyKey, relativeName]).ToImmutableDictionary()))
117120

118121
let diagnostics = result.ToImmutableArray()
119-
cache.Remove(document.Id) |> ignore
120-
cache.Add(document.Id, (textVersionHash, diagnostics))
122+
cache.Remove(key) |> ignore
123+
let data = { Hash = textVersionHash; Diagnostics=diagnostics }
124+
let cacheItem = CacheItem(key, data)
125+
let policy = CacheItemPolicy(SlidingExpiration=DefaultTuning.PerDocumentSavedDataSlidingWindow)
126+
cache.Set(cacheItem, policy)
121127
return diagnostics
122128
finally guard.Release() |> ignore
123129
}

vsintegration/src/FSharp.Editor/DocComments/XMLDocumentation.fs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
namespace Microsoft.VisualStudio.FSharp.Editor
44

55
open System
6+
open System.Runtime.CompilerServices
7+
open System.Runtime.Caching
68
open System.Text.RegularExpressions
79
open Internal.Utilities.Collections
810
open EnvDTE
@@ -413,6 +415,6 @@ module internal XmlDocumentation =
413415
let BuildMethodParamText(documentationProvider, xmlCollector, xml, paramName) =
414416
AppendXmlComment(documentationProvider, TextSanitizingCollector(xmlCollector), TextSanitizingCollector(xmlCollector), xml, false, true, Some paramName)
415417

416-
let documentationBuilderCache = System.Runtime.CompilerServices.ConditionalWeakTable<IVsXMLMemberIndexService, IDocumentationBuilder>()
418+
let documentationBuilderCache = ConditionalWeakTable<IVsXMLMemberIndexService, IDocumentationBuilder>()
417419
let CreateDocumentationBuilder(xmlIndexService: IVsXMLMemberIndexService, dte: DTE) =
418420
documentationBuilderCache.GetValue(xmlIndexService,(fun _ -> Provider(xmlIndexService, dte) :> IDocumentationBuilder))

vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@
109109
<Reference Include="System.ComponentModel.Composition" />
110110
<Reference Include="System.Drawing" />
111111
<Reference Include="System.Windows.Forms" />
112+
<Reference Include="System.Runtime.Caching" />
112113
<Reference Include="System.Xaml" />
113114
<Reference Include="System.Xml" />
114115
<Reference Include="System.Xml.Linq" />

vsintegration/src/FSharp.Editor/Formatting/EditorFormattingService.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ type internal FSharpEditorFormattingService
4444

4545
let! firstMeaningfulToken =
4646
tokens
47-
|> List.tryFind (fun x ->
47+
|> Array.tryFind (fun x ->
4848
x.Tag <> FSharpTokenTag.WHITESPACE &&
4949
x.Tag <> FSharpTokenTag.COMMENT &&
5050
x.Tag <> FSharpTokenTag.LINE_COMMENT)

0 commit comments

Comments
 (0)