Skip to content

Commit

Permalink
AdaptiveLSPServer improvements: Autocomplete Speed, Typechecking Prog…
Browse files Browse the repository at this point in the history
…ress, Memory Usage (#1036)
  • Loading branch information
TheAngryByrd authored Jan 30, 2023
1 parent ddda6b7 commit 63e5bbd
Show file tree
Hide file tree
Showing 28 changed files with 1,214 additions and 591 deletions.
13 changes: 12 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
"args": [
"--debug",
"--filter",
"FSAC.lsp.${input:loader}.${input:testName}"
"FSAC.lsp.${input:loader}.${input:lsp-server}.${input:testName}"
]
}
],
Expand All @@ -103,6 +103,17 @@
"default": "WorkspaceLoader",
"type": "pickString"
},

{
"id": "lsp-server",
"description": "The lsp serrver",
"options": [
"FSharpLspServer",
"AdaptiveLspServer"
],
"default": "FSharpLspServer",
"type": "pickString"
},
{
"id": "testName",
"description": "the name of the test as provided to `testCase`",
Expand Down
2 changes: 1 addition & 1 deletion build.cmd
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
dotnet tool restore
dotnet run --project build -t ReleaseArchive
dotnet run --project ./build/build.fsproj -- -t %*
4 changes: 1 addition & 3 deletions build/Program.fs
Original file line number Diff line number Diff line change
Expand Up @@ -228,9 +228,7 @@ let main args =
init ((args |> List.ofArray))

try
match args with
| [| target |] -> Target.runOrDefaultWithArguments target
| _ -> Target.runOrDefaultWithArguments "Test"
Target.runOrDefaultWithArguments "ReleaseArchive"

0
with e ->
Expand Down
2 changes: 2 additions & 0 deletions paket.dependencies
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ nuget System.CommandLine prerelease
nuget FSharp.Data.Adaptive
nuget Microsoft.NET.Test.Sdk
nuget Dotnet.ReproducibleBuilds copy_local:true
nuget NuGet.Frameworks copy_local:false

nuget Microsoft.NETFramework.ReferenceAssemblies
nuget Ionide.KeepAChangelog.Tasks copy_local: true
Expand All @@ -50,6 +51,7 @@ nuget YoloDev.Expecto.TestSdk
nuget AltCover
nuget GitHubActionsTestLogger
nuget Ionide.LanguageServerProtocol
nuget Microsoft.Extensions.Caching.Memory

group Build
source https://api.nuget.org/v3/index.json
Expand Down
28 changes: 25 additions & 3 deletions paket.lock
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ NUGET
System.Collections.Immutable (>= 5.0)
System.Reflection.Metadata (>= 5.0)
Ionide.KeepAChangelog.Tasks (0.1.8) - copy_local: true
Ionide.LanguageServerProtocol (0.4.10)
Ionide.LanguageServerProtocol (0.4.11)
FSharp.Core (>= 6.0)
Newtonsoft.Json (>= 13.0.1)
StreamJsonRpc (>= 2.10.44)
Expand Down Expand Up @@ -176,12 +176,33 @@ NUGET
System.Text.Encoding.CodePages (>= 4.0.1) - restriction: || (&& (== net7.0) (< net6.0)) (== netstandard2.0)
Microsoft.CodeCoverage (17.3) - restriction: || (== net6.0) (== net7.0) (&& (== netstandard2.0) (>= net45)) (&& (== netstandard2.0) (>= netcoreapp1.0))
Microsoft.DotNet.PlatformAbstractions (3.1.6) - restriction: || (== net6.0) (== net7.0) (&& (== netstandard2.0) (>= net5.0))
Microsoft.Extensions.Caching.Abstractions (6.0)
Microsoft.Extensions.Primitives (>= 6.0)
Microsoft.Extensions.Caching.Memory (6.0.1)
Microsoft.Extensions.Caching.Abstractions (>= 6.0)
Microsoft.Extensions.DependencyInjection.Abstractions (>= 6.0)
Microsoft.Extensions.Logging.Abstractions (>= 6.0)
Microsoft.Extensions.Options (>= 6.0)
Microsoft.Extensions.Primitives (>= 6.0)
Microsoft.Extensions.DependencyInjection.Abstractions (6.0)
Microsoft.Bcl.AsyncInterfaces (>= 6.0) - restriction: || (&& (== net6.0) (>= net461)) (&& (== net6.0) (< netstandard2.1)) (&& (== net7.0) (>= net461)) (&& (== net7.0) (< netstandard2.1)) (== netstandard2.0)
System.Threading.Tasks.Extensions (>= 4.5.4) - restriction: || (&& (== net6.0) (>= net461)) (&& (== net6.0) (< netstandard2.1)) (&& (== net7.0) (>= net461)) (&& (== net7.0) (< netstandard2.1)) (== netstandard2.0)
Microsoft.Extensions.DependencyModel (6.0) - restriction: || (== net6.0) (== net7.0) (&& (== netstandard2.0) (>= net5.0))
System.Buffers (>= 4.5.1)
System.Memory (>= 4.5.4)
System.Runtime.CompilerServices.Unsafe (>= 6.0)
System.Text.Encodings.Web (>= 6.0)
System.Text.Json (>= 6.0)
Microsoft.Extensions.Logging.Abstractions (6.0.2)
System.Buffers (>= 4.5.1) - restriction: || (&& (== net6.0) (>= net461)) (&& (== net7.0) (>= net461)) (&& (== net7.0) (< net6.0)) (== netstandard2.0)
System.Memory (>= 4.5.4) - restriction: || (&& (== net6.0) (>= net461)) (&& (== net7.0) (>= net461)) (&& (== net7.0) (< net6.0)) (== netstandard2.0)
Microsoft.Extensions.Options (6.0)
Microsoft.Extensions.DependencyInjection.Abstractions (>= 6.0)
Microsoft.Extensions.Primitives (>= 6.0)
System.ComponentModel.Annotations (>= 5.0) - restriction: || (&& (== net6.0) (< netstandard2.1)) (&& (== net7.0) (< netstandard2.1)) (== netstandard2.0)
Microsoft.Extensions.Primitives (6.0)
System.Memory (>= 4.5.4) - restriction: || (&& (== net6.0) (>= net461)) (&& (== net6.0) (< netcoreapp3.1)) (&& (== net7.0) (>= net461)) (&& (== net7.0) (< netcoreapp3.1)) (== netstandard2.0)
System.Runtime.CompilerServices.Unsafe (>= 6.0)
Microsoft.NET.StringTools (17.3.1) - copy_local: false
System.Memory (>= 4.5.5)
System.Runtime.CompilerServices.Unsafe (>= 6.0)
Expand Down Expand Up @@ -235,8 +256,8 @@ NUGET
Microsoft.VisualStudio.Validation (>= 16.10.26)
System.IO.Pipelines (>= 5.0.1)
System.Runtime.CompilerServices.Unsafe (>= 5.0)
Newtonsoft.Json (13.0.1)
NuGet.Frameworks (6.3)
Newtonsoft.Json (13.0.2)
NuGet.Frameworks (6.3) - copy_local: false
runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.3)
runtime.debian.9-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.3)
runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.3)
Expand Down Expand Up @@ -319,6 +340,7 @@ NUGET
System.Runtime.CompilerServices.Unsafe (>= 6.0)
System.CommandLine (2.0.0-beta4.22272.1)
System.Memory (>= 4.5.4) - restriction: || (&& (== net7.0) (< net6.0)) (== netstandard2.0)
System.ComponentModel.Annotations (5.0) - restriction: || (&& (== net6.0) (< netstandard2.1)) (&& (== net7.0) (< netstandard2.1)) (== netstandard2.0)
System.Configuration.ConfigurationManager (6.0)
System.Security.Cryptography.ProtectedData (>= 6.0)
System.Security.Permissions (>= 6.0)
Expand Down
2 changes: 1 addition & 1 deletion src/FsAutoComplete.Core/CodeGeneration.fs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/// Original code from VisualFSharpPowerTools project: https://github.com/fsprojects/VisualFSharpPowerTools/blob/master/src/FSharp.Editing/CodeGeneration/CodeGeneration.fs
// Original code from VisualFSharpPowerTools project: https://github.com/fsprojects/VisualFSharpPowerTools/blob/master/src/FSharp.Editing/CodeGeneration/CodeGeneration.fs
namespace FsAutoComplete

open System
Expand Down
71 changes: 36 additions & 35 deletions src/FsAutoComplete.Core/Commands.fs
Original file line number Diff line number Diff line change
Expand Up @@ -713,11 +713,9 @@ module Commands =

let getSymbolUsesInProjects (symbol, projects: FSharpProjectOptions list, onFound) =
projects
|> List.map (fun p ->
asyncResult {
for file in p.SourceFiles do
do! findReferencesInFile (file, symbol, p, onFound)
})
|> List.collect (fun p ->
[ for file in p.SourceFiles do
yield findReferencesInFile (file, symbol, p, onFound) ])
|> Async.Parallel
|> Async.map (Array.toList >> FsToolkit.ErrorHandling.List.sequenceResultM)

Expand Down Expand Up @@ -770,9 +768,9 @@ module Commands =
let symbolRange = symbol.DefinitionRange.NormalizeDriveLetterCasing()
let symbolFile = symbolRange.TaggedFileName

let symbolFileText =
let! symbolFileText =
tryGetFileSource (symbolFile)
|> Result.fold id (fun e -> failwith $"Unable to get file source for file '{symbolFile}'")
|> Result.mapError (fun e -> e + $"Unable to get file source for file '{symbolFile}'")

let! symbolText = symbolFileText.[symbolRange]
// |> Result.fold id (fun e -> failwith "Unable to get text for initial symbol use")
Expand All @@ -790,37 +788,40 @@ module Commands =
|> List.distinctBy (fun x -> x.ProjectFileName)

let onFound (symbolUseRange: range) =
async {
asyncResult {
let symbolUseRange = symbolUseRange.NormalizeDriveLetterCasing()
let symbolFile = symbolUseRange.TaggedFileName
let targetText = tryGetFileSource (symbolFile)

match targetText with
| Error e -> ()
| Ok sourceText ->
let sourceSpan =
sourceText.[symbolUseRange]
|> Result.fold id (fun e -> failwith "Unable to get text for symbol use")

// There are two kinds of ranges we get back:
// * ranges that exactly match the short name of the symbol
// * ranges that are longer than the short name of the symbol,
// typically because we're talking about some kind of fully-qualified usage
// For the latter, we need to adjust the reported range to just be the portion
// of the fully-qualfied text that is the symbol name.
if sourceSpan = symbolText then
symbolUseRanges.Add symbolUseRange
else
match sourceSpan.IndexOf(symbolText) with
| -1 -> ()
| n ->
if sourceSpan.Length >= n + symbolText.Length then
let startPos = symbolUseRange.Start.IncColumn n
let endPos = symbolUseRange.Start.IncColumn(n + symbolText.Length)

let actualUseRange = Range.mkRange symbolUseRange.FileName startPos endPos
symbolUseRanges.Add actualUseRange
let! sourceText = tryGetFileSource (symbolFile)


let! sourceSpan =
sourceText.[symbolUseRange]
|> Result.mapError (fun e -> e + "Unable to get text for symbol use")

// There are two kinds of ranges we get back:
// * ranges that exactly match the short name of the symbol
// * ranges that are longer than the short name of the symbol,
// typically because we're talking about some kind of fully-qualified usage
// For the latter, we need to adjust the reported range to just be the portion
// of the fully-qualfied text that is the symbol name.
if sourceSpan = symbolText then
symbolUseRanges.Add symbolUseRange
else
match sourceSpan.IndexOf(symbolText) with
| -1 -> ()
| n ->
if sourceSpan.Length >= n + symbolText.Length then
let startPos = symbolUseRange.Start.IncColumn n
let endPos = symbolUseRange.Start.IncColumn(n + symbolText.Length)

let actualUseRange = Range.mkRange symbolUseRange.FileName startPos endPos
symbolUseRanges.Add actualUseRange
}
|> Async.map (fun x ->
match x with
| Ok () -> ()
| Error e ->
commandsLogger.info (Log.setMessage "OnFound failed: {errpr}" >> Log.addContextDestructured "error" e))

let! _ = getSymbolUsesInProjects (symbol, projects, onFound)

Expand Down
57 changes: 52 additions & 5 deletions src/FsAutoComplete.Core/CompilerServiceInterface.fs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ open Ionide.ProjInfo.ProjectSystem
open FSharp.UMX
open FSharp.Compiler.EditorServices
open FSharp.Compiler.Symbols
open Microsoft.Extensions.Caching.Memory
open System
open FsToolkit.ErrorHandling



type Version = int

Expand All @@ -24,8 +29,18 @@ type FSharpCompilerServiceChecker(hasAnalyzers) =
keepAllBackgroundSymbolUses = true
)



let entityCache = EntityCache()

// This is used to hold previous check results for autocompletion.
// We can't seem to rely on the checker for previous cached versions
let memoryCache () =
new MemoryCache(MemoryCacheOptions(SizeLimit = Nullable<_>(2000L)))

let mutable lastCheckResults: IMemoryCache = memoryCache ()


let checkerLogger = LogProvider.getLoggerByName "Checker"
let optsLogger = LogProvider.getLoggerByName "Opts"

Expand Down Expand Up @@ -227,14 +242,22 @@ type FSharpCompilerServiceChecker(hasAnalyzers) =
member __.ScriptTypecheckRequirementsChanged =
scriptTypecheckRequirementsChanged.Publish

/// This function is called when the entire environment is known to have changed for reasons not encoded in the ProjectOptions of any project/compilation.
member _.ClearCaches() =
let oldlastCheckResults = lastCheckResults
lastCheckResults <- memoryCache ()
oldlastCheckResults.Dispose()
checker.InvalidateAll()
checker.ClearLanguageServiceRootCachesAndCollectAndFinalizeAllTransients()

member __.ParseFile(fn: string<LocalPath>, source, fpo) =
checkerLogger.info (Log.setMessage "ParseFile - {file}" >> Log.addContextDestructured "file" fn)

let path = UMX.untag fn
checker.ParseFile(path, source, fpo)

member __.ParseAndCheckFileInProject(filePath: string<LocalPath>, version, source: ISourceText, options) =
async {
asyncResult {
let opName = sprintf "ParseAndCheckFileInProject - %A" filePath

checkerLogger.info (Log.setMessage "{opName}" >> Log.addContextDestructured "opName" opName)
Expand All @@ -255,32 +278,56 @@ type FSharpCompilerServiceChecker(hasAnalyzers) =
>> Log.addContextDestructured "errors" (List.ofArray p.Diagnostics)
)

return ResultOrString.Error(sprintf "Check aborted (%A). Errors: %A" c parseErrors)
return! ResultOrString.Error(sprintf "Check aborted (%A). Errors: %A" c parseErrors)
| FSharpCheckFileAnswer.Succeeded (c) ->
checkerLogger.info (
Log.setMessage "{opName} completed successfully"
>> Log.addContextDestructured "opName" opName
)

return Ok(ParseAndCheckResults(p, c, entityCache))
let r = ParseAndCheckResults(p, c, entityCache)

let ops =
MemoryCacheEntryOptions()
.SetSize(1)
.SetSlidingExpiration(TimeSpan.FromMinutes(5.))

return lastCheckResults.Set(filePath, r, ops)
with ex ->
return ResultOrString.Error(ex.ToString())
return! ResultOrString.Error(ex.ToString())
}

member _.TryGetLastCheckResultForFile(file: string<LocalPath>) =
let opName = sprintf "TryGetLastCheckResultForFile - %A" file

checkerLogger.info (Log.setMessage "{opName}" >> Log.addContextDestructured "opName" opName)

match lastCheckResults.TryGetValue<ParseAndCheckResults>(file) with
| (true, v) -> Some v
| _ -> None

member __.TryGetRecentCheckResultsForFile(file: string<LocalPath>, options, source: ISourceText) =
let opName = sprintf "TryGetRecentCheckResultsForFile - %A" file

checkerLogger.info (
Log.setMessage "{opName} - {hash}"
>> Log.addContextDestructured "opName" opName
>> Log.addContextDestructured "hash" (source.GetHashCode() |> int)

)

let options = clearProjectReferences options

let result =
checker.TryGetRecentCheckResultsForFile(UMX.untag file, options, sourceText = source, userOpName = opName)
|> Option.map (fun (pr, cr, _) -> ParseAndCheckResults(pr, cr, entityCache))
|> Option.map (fun (pr, cr, version) ->
checkerLogger.info (
Log.setMessage "{opName} - got results - {version}"
>> Log.addContextDestructured "opName" opName
>> Log.addContextDestructured "version" version
)

ParseAndCheckResults(pr, cr, entityCache))

checkerLogger.info (
Log.setMessage "{opName} - {hash} - cacheHit {cacheHit}"
Expand Down
Loading

0 comments on commit 63e5bbd

Please sign in to comment.