From a3b96fb53e00d2e982d29a3e4f4b85e0dc01956a Mon Sep 17 00:00:00 2001 From: Will Smith Date: Wed, 21 Jul 2021 18:22:29 -0700 Subject: [PATCH 1/2] Basic notebook for F# performance --- VisualFSharp.sln | 2 +- .../FSharp.Compiler.Service.fsproj | 2 +- .../CompilerServiceBenchmarks/Benchmarks.fs | 381 +++++++++++++++++ ...proj => FSharp.Compiler.Benchmarks.fsproj} | 4 +- .../CompilerServiceBenchmarks/Helpers.fs | 116 ++++++ .../CompilerServiceBenchmarks/Program.fs | 389 +----------------- .../benchmarks.ipynb | 154 +++++++ .../Navigation/GoToDefinitionService.fs | 2 + 8 files changed, 661 insertions(+), 389 deletions(-) create mode 100644 tests/benchmarks/CompilerServiceBenchmarks/Benchmarks.fs rename tests/benchmarks/CompilerServiceBenchmarks/{CompilerServiceBenchmarks.fsproj => FSharp.Compiler.Benchmarks.fsproj} (91%) create mode 100644 tests/benchmarks/CompilerServiceBenchmarks/Helpers.fs create mode 100644 tests/benchmarks/CompilerServiceBenchmarks/benchmarks.ipynb diff --git a/VisualFSharp.sln b/VisualFSharp.sln index d35c1ba10b9..22658da2b31 100644 --- a/VisualFSharp.sln +++ b/VisualFSharp.sln @@ -166,7 +166,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VisualFSharpDebug", "vsinte EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Benchmarks", "Benchmarks", "{DFB6ADD7-3149-43D9-AFA0-FC4A818B472B}" EndProject -Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "CompilerServiceBenchmarks", "tests\benchmarks\CompilerServiceBenchmarks\CompilerServiceBenchmarks.fsproj", "{564E7DC5-11CB-4FCF-ABDD-23AD93AF3A61}" +Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharp.Compiler.Benchmarks", "tests\benchmarks\CompilerServiceBenchmarks\FSharp.Compiler.Benchmarks.fsproj", "{564E7DC5-11CB-4FCF-ABDD-23AD93AF3A61}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MicroPerfCSharp", "tests\benchmarks\MicroPerf\CS\MicroPerfCSharp.csproj", "{208E36EE-665C-42D2-B767-C6DB03C4FEB2}" EndProject diff --git a/src/fsharp/FSharp.Compiler.Service/FSharp.Compiler.Service.fsproj b/src/fsharp/FSharp.Compiler.Service/FSharp.Compiler.Service.fsproj index ff2d0cd0770..bb770b96f9a 100644 --- a/src/fsharp/FSharp.Compiler.Service/FSharp.Compiler.Service.fsproj +++ b/src/fsharp/FSharp.Compiler.Service/FSharp.Compiler.Service.fsproj @@ -74,7 +74,7 @@ - + diff --git a/tests/benchmarks/CompilerServiceBenchmarks/Benchmarks.fs b/tests/benchmarks/CompilerServiceBenchmarks/Benchmarks.fs new file mode 100644 index 00000000000..41437eb015e --- /dev/null +++ b/tests/benchmarks/CompilerServiceBenchmarks/Benchmarks.fs @@ -0,0 +1,381 @@ +namespace FSharp.Compiler.Benchmarks + +open System +open System.IO +open System.Text +open System.Threading.Tasks +open FSharp.Compiler.CodeAnalysis +open FSharp.Compiler.Diagnostics +open FSharp.Compiler.EditorServices +open FSharp.Compiler.Text +open FSharp.Compiler.AbstractIL.IL +open FSharp.Compiler.AbstractIL.ILBinaryReader +open BenchmarkDotNet.Attributes +open BenchmarkDotNet.Running +open FSharp.Compiler.Benchmarks + +[] +module BenchmarkHelpers = + + type Async with + static member RunImmediate (computation: Async<'T>, ?cancellationToken ) = + let cancellationToken = defaultArg cancellationToken Async.DefaultCancellationToken + let ts = TaskCompletionSource<'T>() + let task = ts.Task + Async.StartWithContinuations( + computation, + (fun k -> ts.SetResult k), + (fun exn -> ts.SetException exn), + (fun _ -> ts.SetCanceled()), + cancellationToken) + task.Result + + let createProject name referencedProjects = + let tmpPath = Path.GetTempPath() + let file = Path.Combine(tmpPath, Path.ChangeExtension(name, ".fs")) + { + ProjectFileName = Path.Combine(tmpPath, Path.ChangeExtension(name, ".dll")) + ProjectId = None + SourceFiles = [|file|] + OtherOptions = + Array.append [|"--optimize+"; "--target:library" |] (referencedProjects |> Array.ofList |> Array.map (fun x -> "-r:" + x.ProjectFileName)) + ReferencedProjects = + referencedProjects + |> List.map (fun x -> FSharpReferencedProject.CreateFSharp (x.ProjectFileName, x)) + |> Array.ofList + IsIncompleteTypeCheckEnvironment = false + UseScriptResolutionRules = false + LoadTime = DateTime() + UnresolvedReferences = None + OriginalLoadReferences = [] + Stamp = Some 0L (* set the stamp to 0L on each run so we don't evaluate the whole project again *) + } + + let generateSourceCode moduleName = + sprintf """ +module Benchmark.%s + +type %s = + + val X : int + + val Y : int + + val Z : int + +let function%s (x: %s) = + let x = 1 + let y = 2 + let z = x + y + z""" moduleName moduleName moduleName moduleName + + let decentlySizedStandAloneFile = File.ReadAllText(Path.Combine(__SOURCE_DIRECTORY__, "decentlySizedStandAloneFile.fsx")) + +[] +type TypeCheckingBenchmark1() = + let mutable checkerOpt = None + let mutable assembliesOpt = None + let mutable testFileOpt = None + + let parsingOptions = + { + SourceFiles = [|"decentlySizedStandAloneFile.fsx"|] + ConditionalCompilationDefines = [] + ErrorSeverityOptions = FSharpDiagnosticOptions.Default + IsInteractive = false + LightSyntax = None + CompilingFsLib = false + IsExe = false + } + + [] + member __.Setup() = + match checkerOpt with + | None -> checkerOpt <- Some(FSharpChecker.Create(projectCacheSize = 200)) + | _ -> () + + match assembliesOpt with + | None -> + assembliesOpt <- + System.AppDomain.CurrentDomain.GetAssemblies() + |> Array.map (fun x -> (x.Location)) + |> Some + + | _ -> () + + match testFileOpt with + | None -> + let options, _ = + checkerOpt.Value.GetProjectOptionsFromScript("decentlySizedStandAloneFile.fsx", SourceText.ofString decentlySizedStandAloneFile) + |> Async.RunImmediate + testFileOpt <- Some options + | _ -> () + + [] + member __.Run() = + match checkerOpt, testFileOpt with + | None, _ -> failwith "no checker" + | _, None -> failwith "no test file" + | Some(checker), Some(options) -> + let _, result = + checker.ParseAndCheckFileInProject("decentlySizedStandAloneFile.fsx", 0, SourceText.ofString decentlySizedStandAloneFile, options) + |> Async.RunImmediate + match result with + | FSharpCheckFileAnswer.Aborted -> failwith "checker aborted" + | FSharpCheckFileAnswer.Succeeded results -> + if results.Diagnostics.Length > 0 then failwithf "had errors: %A" results.Diagnostics + + [] + member __.Cleanup() = + match checkerOpt with + | None -> failwith "no checker" + | Some(checker) -> + checker.InvalidateAll() + checker.ClearLanguageServiceRootCachesAndCollectAndFinalizeAllTransients() + ClearAllILModuleReaderCache() + + +[] +type CompilerService() = + let mutable checkerOpt = None + let mutable sourceOpt = None + let mutable assembliesOpt = None + let mutable decentlySizedStandAloneFileCheckResultOpt = None + + let parsingOptions = + { + SourceFiles = [|"CheckExpressions.fs"|] + ConditionalCompilationDefines = [] + ErrorSeverityOptions = FSharpDiagnosticOptions.Default + IsInteractive = false + LightSyntax = None + CompilingFsLib = false + IsExe = false + } + + let readerOptions = + { + pdbDirPath = None + reduceMemoryUsage = ReduceMemoryFlag.No + metadataOnly = MetadataOnlyFlag.Yes + tryGetMetadataSnapshot = fun _ -> None + } + + [] + member __.Setup() = + match checkerOpt with + | None -> checkerOpt <- Some(FSharpChecker.Create(projectCacheSize = 200)) + | _ -> () + + match sourceOpt with + | None -> + sourceOpt <- Some <| FSharpSourceText.From(File.OpenRead("""..\..\..\..\..\..\..\..\..\src\fsharp\CheckExpressions.fs"""), Encoding.Default, FSharpSourceHashAlgorithm.Sha1, true) + | _ -> () + + match assembliesOpt with + | None -> + assembliesOpt <- + System.AppDomain.CurrentDomain.GetAssemblies() + |> Array.map (fun x -> (x.Location)) + |> Some + + | _ -> () + + match decentlySizedStandAloneFileCheckResultOpt with + | None -> + let options, _ = + checkerOpt.Value.GetProjectOptionsFromScript("decentlySizedStandAloneFile.fsx", SourceText.ofString decentlySizedStandAloneFile) + |> Async.RunImmediate + let _, checkResult = + checkerOpt.Value.ParseAndCheckFileInProject("decentlySizedStandAloneFile.fsx", 0, SourceText.ofString decentlySizedStandAloneFile, options) + |> Async.RunImmediate + decentlySizedStandAloneFileCheckResultOpt <- Some checkResult + | _ -> () + + [] + member __.ParsingTypeCheckerFs() = + match checkerOpt, sourceOpt with + | None, _ -> failwith "no checker" + | _, None -> failwith "no source" + | Some(checker), Some(source) -> + let results = checker.ParseFile("CheckExpressions.fs", source.ToFSharpSourceText(), parsingOptions) |> Async.RunImmediate + if results.ParseHadErrors then failwithf "parse had errors: %A" results.Diagnostics + + [] + member __.ParsingTypeCheckerFsSetup() = + match checkerOpt with + | None -> failwith "no checker" + | Some(checker) -> + checker.InvalidateAll() + checker.ClearLanguageServiceRootCachesAndCollectAndFinalizeAllTransients() + checker.ParseFile("dummy.fs", SourceText.ofString "dummy", parsingOptions) |> Async.RunImmediate |> ignore + ClearAllILModuleReaderCache() + + [] + member __.ILReading() = + match assembliesOpt with + | None -> failwith "no assemblies" + | Some(assemblies) -> + // We try to read most of everything in the assembly that matter, mainly types with their properties, methods, and fields. + // CustomAttrs and SecurityDecls are lazy until you call them, so we call them here for benchmarking. + assemblies + |> Array.iter (fun (fileName) -> + let reader = OpenILModuleReader fileName readerOptions + + let ilModuleDef = reader.ILModuleDef + + let ilAssemblyManifest = ilModuleDef.Manifest.Value + + ilAssemblyManifest.CustomAttrs |> ignore + ilAssemblyManifest.SecurityDecls |> ignore + ilAssemblyManifest.ExportedTypes.AsList + |> List.iter (fun x -> + x.CustomAttrs |> ignore + ) + + ilModuleDef.CustomAttrs |> ignore + ilModuleDef.TypeDefs.AsArray + |> Array.iter (fun ilTypeDef -> + ilTypeDef.CustomAttrs |> ignore + ilTypeDef.SecurityDecls |> ignore + + ilTypeDef.Methods.AsArray + |> Array.iter (fun ilMethodDef -> + ilMethodDef.CustomAttrs |> ignore + ilMethodDef.SecurityDecls |> ignore + ) + + ilTypeDef.Fields.AsList + |> List.iter (fun ilFieldDef -> + ilFieldDef.CustomAttrs |> ignore + ) + + ilTypeDef.Properties.AsList + |> List.iter (fun ilPropertyDef -> + ilPropertyDef.CustomAttrs |> ignore + ) + ) + ) + + [] + member __.ILReadingSetup() = + // With caching, performance increases an order of magnitude when re-reading an ILModuleReader. + // Clear it for benchmarking. + ClearAllILModuleReaderCache() + + member val TypeCheckFileWith100ReferencedProjectsOptions = + createProject "MainProject" + [ for i = 1 to 100 do + yield + createProject ("ReferencedProject" + string i) [] + ] + + member this.TypeCheckFileWith100ReferencedProjectsRun() = + let options = this.TypeCheckFileWith100ReferencedProjectsOptions + let file = options.SourceFiles.[0] + + match checkerOpt with + | None -> failwith "no checker" + | Some checker -> + let parseResult, checkResult = + checker.ParseAndCheckFileInProject(file, 0, SourceText.ofString (File.ReadAllText(file)), options) + |> Async.RunImmediate + + if parseResult.Diagnostics.Length > 0 then + failwithf "%A" parseResult.Diagnostics + + match checkResult with + | FSharpCheckFileAnswer.Aborted -> failwith "aborted" + | FSharpCheckFileAnswer.Succeeded checkFileResult -> + + if checkFileResult.Diagnostics.Length > 0 then + failwithf "%A" checkFileResult.Diagnostics + + [] + member this.TypeCheckFileWith100ReferencedProjectsSetup() = + this.TypeCheckFileWith100ReferencedProjectsOptions.SourceFiles + |> Seq.iter (fun file -> + File.WriteAllText(file, generateSourceCode (Path.GetFileNameWithoutExtension(file))) + ) + + this.TypeCheckFileWith100ReferencedProjectsOptions.ReferencedProjects + |> Seq.iter (function + | FSharpReferencedProject.FSharpReference(_, referencedProjectOptions) -> + referencedProjectOptions.SourceFiles + |> Seq.iter (fun file -> + File.WriteAllText(file, generateSourceCode (Path.GetFileNameWithoutExtension(file))) + ) + | _ -> () + ) + + this.TypeCheckFileWith100ReferencedProjectsRun() + + [] + member this.TypeCheckFileWith100ReferencedProjects() = + // Because the checker's projectcachesize is set to 200, this should be fast. + // If set to 3, it will be almost as slow as re-evaluating all project and it's projects references on setup; this could be a bug or not what we want. + this.TypeCheckFileWith100ReferencedProjectsRun() + + member val TypeCheckFileWithNoReferencesOptions = createProject "MainProject" [] + + [] + member this.TypeCheckFileWith100ReferencedProjectsCleanup() = + this.TypeCheckFileWith100ReferencedProjectsOptions.SourceFiles + |> Seq.iter (fun file -> + try File.Delete(file) with | _ -> () + ) + + this.TypeCheckFileWith100ReferencedProjectsOptions.ReferencedProjects + |> Seq.iter (function + | FSharpReferencedProject.FSharpReference(_, referencedProjectOptions) -> + referencedProjectOptions.SourceFiles + |> Seq.iter (fun file -> + try File.Delete(file) with | _ -> () + ) + | _ -> () + ) + + match checkerOpt with + | None -> failwith "no checker" + | Some(checker) -> + checker.InvalidateAll() + checker.ClearLanguageServiceRootCachesAndCollectAndFinalizeAllTransients() + ClearAllILModuleReaderCache() + + [] + member this.SimplifyNames() = + match decentlySizedStandAloneFileCheckResultOpt with + | Some checkResult -> + match checkResult with + | FSharpCheckFileAnswer.Aborted -> failwith "checker aborted" + | FSharpCheckFileAnswer.Succeeded results -> + let sourceLines = decentlySizedStandAloneFile.Split ([|"\r\n"; "\n"; "\r"|], StringSplitOptions.None) + let ranges = SimplifyNames.getSimplifiableNames(results, fun lineNum -> sourceLines.[Line.toZ lineNum]) |> Async.RunImmediate + ignore ranges + () + | _ -> failwith "oopsie" + + [] + member this.UnusedOpens() = + match decentlySizedStandAloneFileCheckResultOpt with + | Some checkResult -> + match checkResult with + | FSharpCheckFileAnswer.Aborted -> failwith "checker aborted" + | FSharpCheckFileAnswer.Succeeded results -> + let sourceLines = decentlySizedStandAloneFile.Split ([|"\r\n"; "\n"; "\r"|], StringSplitOptions.None) + let decls = UnusedOpens.getUnusedOpens(results, fun lineNum -> sourceLines.[Line.toZ lineNum]) |> Async.RunImmediate + ignore decls + () + | _ -> failwith "oopsie" + + [] + member this.UnusedDeclarations() = + match decentlySizedStandAloneFileCheckResultOpt with + | Some checkResult -> + match checkResult with + | FSharpCheckFileAnswer.Aborted -> failwith "checker aborted" + | FSharpCheckFileAnswer.Succeeded results -> + let decls = UnusedDeclarations.getUnusedDeclarations(results, true) |> Async.RunImmediate + ignore decls // should be 16 + () + | _ -> failwith "oopsie" \ No newline at end of file diff --git a/tests/benchmarks/CompilerServiceBenchmarks/CompilerServiceBenchmarks.fsproj b/tests/benchmarks/CompilerServiceBenchmarks/FSharp.Compiler.Benchmarks.fsproj similarity index 91% rename from tests/benchmarks/CompilerServiceBenchmarks/CompilerServiceBenchmarks.fsproj rename to tests/benchmarks/CompilerServiceBenchmarks/FSharp.Compiler.Benchmarks.fsproj index 27336d1c50e..68b175fc21a 100644 --- a/tests/benchmarks/CompilerServiceBenchmarks/CompilerServiceBenchmarks.fsproj +++ b/tests/benchmarks/CompilerServiceBenchmarks/FSharp.Compiler.Benchmarks.fsproj @@ -8,11 +8,11 @@ + + - - diff --git a/tests/benchmarks/CompilerServiceBenchmarks/Helpers.fs b/tests/benchmarks/CompilerServiceBenchmarks/Helpers.fs new file mode 100644 index 00000000000..21952786f1c --- /dev/null +++ b/tests/benchmarks/CompilerServiceBenchmarks/Helpers.fs @@ -0,0 +1,116 @@ +[] +module internal FSharp.Compiler.Benchmarks.Helpers + +open System +open System.IO +open System.Threading.Tasks +open Microsoft.CodeAnalysis.Text +open FSharp.Compiler.Text +open FSharp.Compiler.CodeAnalysis + +module private SourceText = + + open System.Runtime.CompilerServices + + let weakTable = ConditionalWeakTable() + + let create (sourceText: SourceText) = + + let sourceText = + { new ISourceText with + + member __.Item with get index = sourceText.[index] + + member __.GetLineString(lineIndex) = + sourceText.Lines.[lineIndex].ToString() + + member __.GetLineCount() = + sourceText.Lines.Count + + member __.GetLastCharacterPosition() = + if sourceText.Lines.Count > 0 then + (sourceText.Lines.Count, sourceText.Lines.[sourceText.Lines.Count - 1].Span.Length) + else + (0, 0) + + member __.GetSubTextString(start, length) = + sourceText.GetSubText(TextSpan(start, length)).ToString() + + member __.SubTextEquals(target, startIndex) = + if startIndex < 0 || startIndex >= sourceText.Length then + raise (ArgumentOutOfRangeException("startIndex")) + + if String.IsNullOrEmpty(target) then + raise (ArgumentException("Target is null or empty.", "target")) + + let lastIndex = startIndex + target.Length + if lastIndex <= startIndex || lastIndex >= sourceText.Length then + raise (ArgumentException("Target is too big.", "target")) + + let mutable finished = false + let mutable didEqual = true + let mutable i = 0 + while not finished && i < target.Length do + if target.[i] <> sourceText.[startIndex + i] then + didEqual <- false + finished <- true // bail out early + else + i <- i + 1 + + didEqual + + member __.ContentEquals(sourceText) = + match sourceText with + | :? SourceText as sourceText -> sourceText.ContentEquals(sourceText) + | _ -> false + + member __.Length = sourceText.Length + + member __.CopyTo(sourceIndex, destination, destinationIndex, count) = + sourceText.CopyTo(sourceIndex, destination, destinationIndex, count) + } + + sourceText + +type SourceText with + + member this.ToFSharpSourceText() = + SourceText.weakTable.GetValue(this, Runtime.CompilerServices.ConditionalWeakTable<_,_>.CreateValueCallback(SourceText.create)) + +type FSharpSourceText = SourceText +type FSharpSourceHashAlgorithm = SourceHashAlgorithm + +type Async with + static member RunImmediate (computation: Async<'T>, ?cancellationToken ) = + let cancellationToken = defaultArg cancellationToken Async.DefaultCancellationToken + let ts = TaskCompletionSource<'T>() + let task = ts.Task + Async.StartWithContinuations( + computation, + (fun k -> ts.SetResult k), + (fun exn -> ts.SetException exn), + (fun _ -> ts.SetCanceled()), + cancellationToken) + task.Result + +let CreateProject name referencedProjects = + let tmpPath = Path.GetTempPath() + let file = Path.Combine(tmpPath, Path.ChangeExtension(name, ".fs")) + { + ProjectFileName = Path.Combine(tmpPath, Path.ChangeExtension(name, ".dll")) + ProjectId = None + SourceFiles = [|file|] + OtherOptions = + Array.append [|"--optimize+"; "--target:library" |] (referencedProjects |> Array.ofList |> Array.map (fun x -> "-r:" + x.ProjectFileName)) + ReferencedProjects = + referencedProjects + |> List.map (fun x -> FSharpReferencedProject.CreateFSharp (x.ProjectFileName, x)) + |> Array.ofList + IsIncompleteTypeCheckEnvironment = false + UseScriptResolutionRules = false + LoadTime = DateTime() + UnresolvedReferences = None + OriginalLoadReferences = [] + Stamp = Some 0L (* set the stamp to 0L on each run so we don't evaluate the whole project again *) + } + diff --git a/tests/benchmarks/CompilerServiceBenchmarks/Program.fs b/tests/benchmarks/CompilerServiceBenchmarks/Program.fs index ced6ad6fcd9..d624def2d8c 100644 --- a/tests/benchmarks/CompilerServiceBenchmarks/Program.fs +++ b/tests/benchmarks/CompilerServiceBenchmarks/Program.fs @@ -1,389 +1,8 @@ -open System -open System.IO -open System.Text -open System.Threading.Tasks -open FSharp.Compiler.CodeAnalysis -open FSharp.Compiler.Diagnostics -open FSharp.Compiler.EditorServices -open FSharp.Compiler.Text -open FSharp.Compiler.AbstractIL.IL -open FSharp.Compiler.AbstractIL.ILBinaryReader -open Microsoft.CodeAnalysis.Text -open BenchmarkDotNet.Attributes -open BenchmarkDotNet.Running - -module private SourceText = - - open System.Runtime.CompilerServices - - let weakTable = ConditionalWeakTable() - - let create (sourceText: SourceText) = - - let sourceText = - { new ISourceText with - - member __.Item with get index = sourceText.[index] - - member __.GetLineString(lineIndex) = - sourceText.Lines.[lineIndex].ToString() - - member __.GetLineCount() = - sourceText.Lines.Count - - member __.GetLastCharacterPosition() = - if sourceText.Lines.Count > 0 then - (sourceText.Lines.Count, sourceText.Lines.[sourceText.Lines.Count - 1].Span.Length) - else - (0, 0) - - member __.GetSubTextString(start, length) = - sourceText.GetSubText(TextSpan(start, length)).ToString() - - member __.SubTextEquals(target, startIndex) = - if startIndex < 0 || startIndex >= sourceText.Length then - raise (ArgumentOutOfRangeException("startIndex")) - - if String.IsNullOrEmpty(target) then - raise (ArgumentException("Target is null or empty.", "target")) - - let lastIndex = startIndex + target.Length - if lastIndex <= startIndex || lastIndex >= sourceText.Length then - raise (ArgumentException("Target is too big.", "target")) - - let mutable finished = false - let mutable didEqual = true - let mutable i = 0 - while not finished && i < target.Length do - if target.[i] <> sourceText.[startIndex + i] then - didEqual <- false - finished <- true // bail out early - else - i <- i + 1 - - didEqual - - member __.ContentEquals(sourceText) = - match sourceText with - | :? SourceText as sourceText -> sourceText.ContentEquals(sourceText) - | _ -> false - - member __.Length = sourceText.Length - - member __.CopyTo(sourceIndex, destination, destinationIndex, count) = - sourceText.CopyTo(sourceIndex, destination, destinationIndex, count) - } - - sourceText - -type SourceText with - - member this.ToFSharpSourceText() = - SourceText.weakTable.GetValue(this, Runtime.CompilerServices.ConditionalWeakTable<_,_>.CreateValueCallback(SourceText.create)) - -[] -module Helpers = - - type Async with - static member RunImmediate (computation: Async<'T>, ?cancellationToken ) = - let cancellationToken = defaultArg cancellationToken Async.DefaultCancellationToken - let ts = TaskCompletionSource<'T>() - let task = ts.Task - Async.StartWithContinuations( - computation, - (fun k -> ts.SetResult k), - (fun exn -> ts.SetException exn), - (fun _ -> ts.SetCanceled()), - cancellationToken) - task.Result - - let createProject name referencedProjects = - let tmpPath = Path.GetTempPath() - let file = Path.Combine(tmpPath, Path.ChangeExtension(name, ".fs")) - { - ProjectFileName = Path.Combine(tmpPath, Path.ChangeExtension(name, ".dll")) - ProjectId = None - SourceFiles = [|file|] - OtherOptions = - Array.append [|"--optimize+"; "--target:library" |] (referencedProjects |> Array.ofList |> Array.map (fun x -> "-r:" + x.ProjectFileName)) - ReferencedProjects = - referencedProjects - |> List.map (fun x -> FSharpReferencedProject.CreateFSharp (x.ProjectFileName, x)) - |> Array.ofList - IsIncompleteTypeCheckEnvironment = false - UseScriptResolutionRules = false - LoadTime = DateTime() - UnresolvedReferences = None - OriginalLoadReferences = [] - Stamp = Some 0L (* set the stamp to 0L on each run so we don't evaluate the whole project again *) - } - - let generateSourceCode moduleName = - sprintf """ -module Benchmark.%s - -type %s = - - val X : int - - val Y : int - - val Z : int - -let function%s (x: %s) = - let x = 1 - let y = 2 - let z = x + y - z""" moduleName moduleName moduleName moduleName - - let decentlySizedStandAloneFile = File.ReadAllText(Path.Combine(__SOURCE_DIRECTORY__, "decentlySizedStandAloneFile.fsx")) - -[] -type CompilerService() = - let mutable checkerOpt = None - let mutable sourceOpt = None - let mutable assembliesOpt = None - let mutable decentlySizedStandAloneFileCheckResultOpt = None - - let parsingOptions = - { - SourceFiles = [|"CheckExpressions.fs"|] - ConditionalCompilationDefines = [] - ErrorSeverityOptions = FSharpDiagnosticOptions.Default - IsInteractive = false - LightSyntax = None - CompilingFsLib = false - IsExe = false - } - - let readerOptions = - { - pdbDirPath = None - reduceMemoryUsage = ReduceMemoryFlag.No - metadataOnly = MetadataOnlyFlag.Yes - tryGetMetadataSnapshot = fun _ -> None - } - - [] - member __.Setup() = - match checkerOpt with - | None -> checkerOpt <- Some(FSharpChecker.Create(projectCacheSize = 200)) - | _ -> () - - match sourceOpt with - | None -> - sourceOpt <- Some <| SourceText.From(File.OpenRead("""..\..\..\..\..\src\fsharp\CheckExpressions.fs"""), Encoding.Default, SourceHashAlgorithm.Sha1, true) - | _ -> () - - match assembliesOpt with - | None -> - assembliesOpt <- - System.AppDomain.CurrentDomain.GetAssemblies() - |> Array.map (fun x -> (x.Location)) - |> Some - - | _ -> () - - match decentlySizedStandAloneFileCheckResultOpt with - | None -> - let options, _ = - checkerOpt.Value.GetProjectOptionsFromScript("decentlySizedStandAloneFile.fsx", SourceText.ofString decentlySizedStandAloneFile) - |> Async.RunImmediate - let _, checkResult = - checkerOpt.Value.ParseAndCheckFileInProject("decentlySizedStandAloneFile.fsx", 0, SourceText.ofString decentlySizedStandAloneFile, options) - |> Async.RunImmediate - decentlySizedStandAloneFileCheckResultOpt <- Some checkResult - | _ -> () - - [] - member __.ParsingTypeCheckerFs() = - match checkerOpt, sourceOpt with - | None, _ -> failwith "no checker" - | _, None -> failwith "no source" - | Some(checker), Some(source) -> - let results = checker.ParseFile("CheckExpressions.fs", source.ToFSharpSourceText(), parsingOptions) |> Async.RunImmediate - if results.ParseHadErrors then failwithf "parse had errors: %A" results.Diagnostics - - [] - member __.ParsingTypeCheckerFsSetup() = - match checkerOpt with - | None -> failwith "no checker" - | Some(checker) -> - checker.InvalidateAll() - checker.ClearLanguageServiceRootCachesAndCollectAndFinalizeAllTransients() - checker.ParseFile("dummy.fs", SourceText.ofString "dummy", parsingOptions) |> Async.RunImmediate |> ignore - ClearAllILModuleReaderCache() - - [] - member __.ILReading() = - match assembliesOpt with - | None -> failwith "no assemblies" - | Some(assemblies) -> - // We try to read most of everything in the assembly that matter, mainly types with their properties, methods, and fields. - // CustomAttrs and SecurityDecls are lazy until you call them, so we call them here for benchmarking. - assemblies - |> Array.iter (fun (fileName) -> - let reader = OpenILModuleReader fileName readerOptions - - let ilModuleDef = reader.ILModuleDef - - let ilAssemblyManifest = ilModuleDef.Manifest.Value - - ilAssemblyManifest.CustomAttrs |> ignore - ilAssemblyManifest.SecurityDecls |> ignore - ilAssemblyManifest.ExportedTypes.AsList - |> List.iter (fun x -> - x.CustomAttrs |> ignore - ) - - ilModuleDef.CustomAttrs |> ignore - ilModuleDef.TypeDefs.AsArray - |> Array.iter (fun ilTypeDef -> - ilTypeDef.CustomAttrs |> ignore - ilTypeDef.SecurityDecls |> ignore - - ilTypeDef.Methods.AsArray - |> Array.iter (fun ilMethodDef -> - ilMethodDef.CustomAttrs |> ignore - ilMethodDef.SecurityDecls |> ignore - ) - - ilTypeDef.Fields.AsList - |> List.iter (fun ilFieldDef -> - ilFieldDef.CustomAttrs |> ignore - ) - - ilTypeDef.Properties.AsList - |> List.iter (fun ilPropertyDef -> - ilPropertyDef.CustomAttrs |> ignore - ) - ) - ) - - [] - member __.ILReadingSetup() = - // With caching, performance increases an order of magnitude when re-reading an ILModuleReader. - // Clear it for benchmarking. - ClearAllILModuleReaderCache() - - member val TypeCheckFileWith100ReferencedProjectsOptions = - createProject "MainProject" - [ for i = 1 to 100 do - yield - createProject ("ReferencedProject" + string i) [] - ] - - member this.TypeCheckFileWith100ReferencedProjectsRun() = - let options = this.TypeCheckFileWith100ReferencedProjectsOptions - let file = options.SourceFiles.[0] - - match checkerOpt with - | None -> failwith "no checker" - | Some checker -> - let parseResult, checkResult = - checker.ParseAndCheckFileInProject(file, 0, SourceText.ofString (File.ReadAllText(file)), options) - |> Async.RunImmediate - - if parseResult.Diagnostics.Length > 0 then - failwithf "%A" parseResult.Diagnostics - - match checkResult with - | FSharpCheckFileAnswer.Aborted -> failwith "aborted" - | FSharpCheckFileAnswer.Succeeded checkFileResult -> - - if checkFileResult.Diagnostics.Length > 0 then - failwithf "%A" checkFileResult.Diagnostics - - [] - member this.TypeCheckFileWith100ReferencedProjectsSetup() = - this.TypeCheckFileWith100ReferencedProjectsOptions.SourceFiles - |> Seq.iter (fun file -> - File.WriteAllText(file, generateSourceCode (Path.GetFileNameWithoutExtension(file))) - ) - - this.TypeCheckFileWith100ReferencedProjectsOptions.ReferencedProjects - |> Seq.iter (function - | FSharpReferencedProject.FSharpReference(_, referencedProjectOptions) -> - referencedProjectOptions.SourceFiles - |> Seq.iter (fun file -> - File.WriteAllText(file, generateSourceCode (Path.GetFileNameWithoutExtension(file))) - ) - | _ -> () - ) - - this.TypeCheckFileWith100ReferencedProjectsRun() - - [] - member this.TypeCheckFileWith100ReferencedProjects() = - // Because the checker's projectcachesize is set to 200, this should be fast. - // If set to 3, it will be almost as slow as re-evaluating all project and it's projects references on setup; this could be a bug or not what we want. - this.TypeCheckFileWith100ReferencedProjectsRun() - - member val TypeCheckFileWithNoReferencesOptions = createProject "MainProject" [] - - [] - member this.TypeCheckFileWith100ReferencedProjectsCleanup() = - this.TypeCheckFileWith100ReferencedProjectsOptions.SourceFiles - |> Seq.iter (fun file -> - try File.Delete(file) with | _ -> () - ) - - this.TypeCheckFileWith100ReferencedProjectsOptions.ReferencedProjects - |> Seq.iter (function - | FSharpReferencedProject.FSharpReference(_, referencedProjectOptions) -> - referencedProjectOptions.SourceFiles - |> Seq.iter (fun file -> - try File.Delete(file) with | _ -> () - ) - | _ -> () - ) - - match checkerOpt with - | None -> failwith "no checker" - | Some(checker) -> - checker.InvalidateAll() - checker.ClearLanguageServiceRootCachesAndCollectAndFinalizeAllTransients() - ClearAllILModuleReaderCache() - - [] - member this.SimplifyNames() = - match decentlySizedStandAloneFileCheckResultOpt with - | Some checkResult -> - match checkResult with - | FSharpCheckFileAnswer.Aborted -> failwith "checker aborted" - | FSharpCheckFileAnswer.Succeeded results -> - let sourceLines = decentlySizedStandAloneFile.Split ([|"\r\n"; "\n"; "\r"|], StringSplitOptions.None) - let ranges = SimplifyNames.getSimplifiableNames(results, fun lineNum -> sourceLines.[Line.toZ lineNum]) |> Async.RunImmediate - ignore ranges - () - | _ -> failwith "oopsie" - - [] - member this.UnusedOpens() = - match decentlySizedStandAloneFileCheckResultOpt with - | Some checkResult -> - match checkResult with - | FSharpCheckFileAnswer.Aborted -> failwith "checker aborted" - | FSharpCheckFileAnswer.Succeeded results -> - let sourceLines = decentlySizedStandAloneFile.Split ([|"\r\n"; "\n"; "\r"|], StringSplitOptions.None) - let decls = UnusedOpens.getUnusedOpens(results, fun lineNum -> sourceLines.[Line.toZ lineNum]) |> Async.RunImmediate - ignore decls - () - | _ -> failwith "oopsie" - - [] - member this.UnusedDeclarations() = - match decentlySizedStandAloneFileCheckResultOpt with - | Some checkResult -> - match checkResult with - | FSharpCheckFileAnswer.Aborted -> failwith "checker aborted" - | FSharpCheckFileAnswer.Succeeded results -> - let decls = UnusedDeclarations.getUnusedDeclarations(results, true) |> Async.RunImmediate - ignore decls // should be 16 - () - | _ -> failwith "oopsie" +open BenchmarkDotNet.Running +open FSharp.Compiler.Benchmarks [] let main _ = - BenchmarkRunner.Run() |> ignore + //BenchmarkRunner.Run() |> ignore + BenchmarkRunner.Run() |> ignore 0 diff --git a/tests/benchmarks/CompilerServiceBenchmarks/benchmarks.ipynb b/tests/benchmarks/CompilerServiceBenchmarks/benchmarks.ipynb new file mode 100644 index 00000000000..cbf5270689c --- /dev/null +++ b/tests/benchmarks/CompilerServiceBenchmarks/benchmarks.ipynb @@ -0,0 +1,154 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "source": [ + "#!pwsh\r\n", + "dotnet build -c release\r\n" + ], + "outputs": [], + "metadata": { + "dotnet_interactive": { + "language": "pwsh" + } + } + }, + { + "cell_type": "code", + "execution_count": null, + "source": [ + "#r \"../../../artifacts/bin/FSharp.Compiler.Benchmarks/Release/net5.0/FSharp.Compiler.Benchmarks.dll\"\r\n", + "#r \"../../../artifacts/bin/FSharp.Compiler.Benchmarks/Release/net5.0/BenchmarkDotNet.dll\"" + ], + "outputs": [], + "metadata": { + "dotnet_interactive": { + "language": "fsharp" + } + } + }, + { + "cell_type": "code", + "execution_count": null, + "source": [ + "open BenchmarkDotNet.Running\r\n", + "open FSharp.Compiler.Benchmarks\r\n", + "\r\n", + "let summary = BenchmarkRunner.Run()" + ], + "outputs": [], + "metadata": { + "dotnet_interactive": { + "language": "fsharp" + } + } + }, + { + "cell_type": "code", + "execution_count": null, + "source": [ + "// https://benchmarkdotnet.org/api/BenchmarkDotNet.Reports.BenchmarkReport.html\r\n", + "#r \"nuget: XPlot.Plotly.Interactive, 4.0.2\"\r\n", + "\r\n", + "open XPlot.Plotly\r\n", + "\r\n", + "let gcStats = summary.Reports |> Seq.map (fun x -> x.GcStats)\r\n", + "\r\n", + "let gen0Series =\r\n", + " Bar(\r\n", + " name = \"Gen 0\",\r\n", + " y = (gcStats |> Seq.map (fun x -> x.Gen0Collections))\r\n", + " )\r\n", + "\r\n", + "let gen1Series =\r\n", + " Bar(\r\n", + " name = \"Gen 1\",\r\n", + " y = (gcStats |> Seq.map (fun x -> x.Gen1Collections))\r\n", + " )\r\n", + "\r\n", + "let gen2Series =\r\n", + " Bar(\r\n", + " name = \"Gen 2\",\r\n", + " y = (gcStats |> Seq.map (fun x -> x.Gen2Collections))\r\n", + " )\r\n", + "\r\n", + "[gen0Series;gen1Series;gen2Series]\r\n", + "|> Chart.Plot\r\n", + "|> Chart.WithTitle(\"F# Type-Checking Benchmark 1 - GC Collection Counts\")" + ], + "outputs": [ + { + "data": { + "text/html": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "\r\n", + "
\r\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "metadata": { + "dotnet_interactive": { + "language": "fsharp" + } + } + } + ], + "metadata": { + "orig_nbformat": 4, + "language_info": { + "file_extension": ".cs", + "mimetype": "text/x-csharp", + "name": "C#", + "pygments_lexer": "csharp", + "version": "9.0" + }, + "kernelspec": { + "display_name": ".NET (C#)", + "language": "C#", + "name": ".net-csharp" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinitionService.fs b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinitionService.fs index 6ed309bf573..b14ced8bb69 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinitionService.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinitionService.fs @@ -12,6 +12,7 @@ open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Editor open Microsoft.VisualStudio.Shell open Microsoft.VisualStudio.Shell.Interop +open Microsoft.VisualStudio.Threading [)>] [)>] @@ -46,6 +47,7 @@ type internal FSharpGoToDefinitionService let gtdTask = gtd.FindDefinitionTask(document, position, cancellationToken) + JoinableTaskFactory. // Wrap this in a try/with as if the user clicks "Cancel" on the thread dialog, we'll be cancelled. // Task.Wait throws an exception if the task is cancelled, so be sure to catch it. try From 4138780323496a8df40825073780165b119cc268 Mon Sep 17 00:00:00 2001 From: Will Smith Date: Wed, 21 Jul 2021 18:28:11 -0700 Subject: [PATCH 2/2] Update GoToDefinitionService.fs Remove that --- .../src/FSharp.Editor/Navigation/GoToDefinitionService.fs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinitionService.fs b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinitionService.fs index b14ced8bb69..c81957c59b2 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/GoToDefinitionService.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/GoToDefinitionService.fs @@ -12,7 +12,6 @@ open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Editor open Microsoft.VisualStudio.Shell open Microsoft.VisualStudio.Shell.Interop -open Microsoft.VisualStudio.Threading [)>] [)>] @@ -47,7 +46,6 @@ type internal FSharpGoToDefinitionService let gtdTask = gtd.FindDefinitionTask(document, position, cancellationToken) - JoinableTaskFactory. // Wrap this in a try/with as if the user clicks "Cancel" on the thread dialog, we'll be cancelled. // Task.Wait throws an exception if the task is cancelled, so be sure to catch it. try @@ -72,4 +70,4 @@ type internal FSharpGoToDefinitionService // Don't show the dialog box as it's most likely that the user cancelled. // Don't make them click twice. - true \ No newline at end of file + true