Skip to content

Conversation

@vasily-kirichenko
Copy link
Contributor

Before

image

After

image

(it's the code fix recalculation for 1100 lines file from FSharp.Editing.Tests project from VFPT solution)

It looks like the diagnostics is called twice by VS, I think we should cache the last result in a Dictionary<DocumentId, TextVersion hash * ImmutableArray<Diagnostic>>

@saul
Copy link
Contributor

saul commented Jan 14, 2017

These speed improvements look great - but over 400 lines of code added with no tests? :(

let getProjectInfoManager (document: Document) = document.Project.Solution.Workspace.Services.GetService<FSharpCheckerWorkspaceService>().ProjectInfoManager
let getChecker (document: Document) = document.Project.Solution.Workspace.Services.GetService<FSharpCheckerWorkspaceService>().Checker
let getPlidLength (plid: string list) = (plid |> List.sumBy String.length) + plid.Length
static let cache = ConditionalWeakTable<DocumentId, TextVersionHash * ImmutableArray<Diagnostic>>()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious if the cache entry deleted when the document is closed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@smoothdeveloper read what this ConditionalWeakTable is :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

automatically removes the key/value entry as soon as no other references to a key exist outside the table

That's nice, didn't know about that class, thanks for the pointer. So I guess the answer is it doesn't get deleted when document is closed but when it is garbage collected.

@vasily-kirichenko
Copy link
Contributor Author

Added the cache:

image

@vasily-kirichenko
Copy link
Contributor Author

These speed improvements look great - but over 400 lines of code added with no tests? :(

It's all copied with adjustments, and the code is not used anywhere except this analyzer.

@vasily-kirichenko
Copy link
Contributor Author

image

@forki
Copy link
Contributor

forki commented Jan 14, 2017

so how much faster is it now?

@vasily-kirichenko
Copy link
Contributor Author

@forki 29 seconds => 800 ms ~= 36 times faster.

@forki
Copy link
Contributor

forki commented Jan 14, 2017 via email

@vasily-kirichenko
Copy link
Contributor Author

I've not optimized the compiler, I copied functions and make them fast for my particular case. In short, the existing functions return all items resolvable in a scope, my functions just check that an already resolved item can be resolved being prefixed with a given long ident. It's faster because 1. I pull the available items lazily, until the needed item is found 2. I know the kind of item (ctor, prop, ns or module, etc.) and search only for items of same kind. So these optimizations cannot be used in compiler.

@forki
Copy link
Contributor

forki commented Jan 14, 2017 via email

@smoothdeveloper
Copy link
Contributor

@vasily-kirichenko is it wroth to put some comments on the copied functions, to mention the place they are copied from?

I'm thinking that if down the road the original functions change, having that mention would help figure out if catch-up is necessary in those customized functions.

@vasily-kirichenko
Copy link
Contributor Author

More optimization.

before last commit

image

after

image

(TypeChecker.fs in FCS solution).

@vasily-kirichenko vasily-kirichenko force-pushed the optimize-simplify-name-analyzer branch from a80e607 to c8edcc3 Compare January 14, 2017 20:53
@KevinRansom
Copy link
Contributor

A few IDE tests failes

Test Run Summary
    Overall result: Failed
   Tests run: 2936, Passed: 2901, Errors: 9, Failures: 26, Inconclusive: 0
     Not run: 99, Invalid: 0, Ignored: 99, Explicit: 0, Skipped: 0
  Start time: 2017-01-14 21:41:17Z
    End time: 2017-01-14 21:57:58Z
    Duration: 1000.828 seconds

let ctorInfos = GetIntrinsicConstructorInfosOfType ncenv.InfoReader m typ
if isInterfaceTy g typ && isNil ctorInfos then
let ctorInfos = GetIntrinsicConstructorInfosOfType ncenv.InfoReader m typ
if isNil ctorInfos && isInterfaceTy g typ then
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice one.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, we should read the whole compiler code to fix such stupid perf penalties.

@vasily-kirichenko
Copy link
Contributor Author

@dsyme when memory pressure is here, even tooltips can appear with a very long delay, even though code has not changed at all, i.e.:

  1. We occupy 2.5GB
  2. Hover over a symbol - tooltip appears immediately
  3. Hover over another symbol - high CPU load, very long delay and finally we got our tooltip

I think that's because either generating the second tooltip triggered blocking GC or a weak cache data for current file have become available for GC between the first and second tooltips, so the file was rechecked.

@dsyme
Copy link
Contributor

dsyme commented Jan 16, 2017

@dsyme when memory pressure is here...

My impression is that we must avoid memory pressure > 2GB at all costs, even if some features simply don't work in reasonable time. Once you go above 2GB, nothing works anymore until you restart.

@smoothdeveloper
Copy link
Contributor

In my case (with VS2015 + VFPT), I've seen that things may stall or not disregarding the memory occupied by devenv.exe (looking at Working Set (Memory) in task manager).

I've seen process with 2.7gb still doing fine and process with 2gb being unworkable, and I can't really identify why.

@vasily-kirichenko
Copy link
Contributor Author

My impression is that we must avoid memory pressure > 2GB at all costs, even if some features simply don't work in reasonable time.

It's not about features, it's about FCS caches. Features themselves do not add anything to gen 2.

I was thinking about an external process. Maybe it's better to move some FCS internal caches there, not FCS itself, keeping the current VS <-> FCS API untouched.

@dsyme
Copy link
Contributor

dsyme commented Jan 16, 2017

It's not about features, it's about FCS caches. Features themselves do not add anything to gen 2.

But features do cause information to be required more often. This adds requests to FCS (potentially lengthening the request queue, especially if cancellation is not performed), and may add to compute/allocate/populate/collect churn. The use of a feature can trigger a Gen2 collection in the worst case, while turning off a feature can only improve things.

Also for large files, I believe we reguarly get Gen2 collections even within the typecheck phase of that file itself (even if no caching occurs). So any feature requiring CheckFile results may trigger Gen2.

I'm just concerned we're going to see Gen2 collections or blocking UI pauses triggered by features we can't turn off. When using VFPT I regularly turned off all but a couple of the most important features. Advanced F# programmers don't need to have their code constantly re-analyzed every few seconds for removing "new" or the like.... :) But they do need to have the tools work over 100,000s lines of code :)

@Pilchie
Copy link
Member

Pilchie commented Jan 17, 2017

Just as an FYI - a 32-bit process on a 64-bit OS will get a full 4GB of address space, so yes, it's possible to use 4GB of Virtual Memory. Note that things like task manager don't actually report the total VM though - they leave out some categories of memory. To get an accurate picture of VM, use something like vmmap.

For people running 32-bit processes on 32-bit machines, they get either 2 or 3 GB of VM, depending on OS Version and whether the app declares itself large address aware (VS does). The OS always keeps at least 1GB for kernel address space though.

Note however that even with 4GB of address space, you can run into issues with fragmentation on large allocations - the OS (or the CLR) may not have enough contiguous address space to perform an allocation.

Note also that everything I said above was about virtual memory. The amount of physical memory the machine has has no bearing on it - if the machine doesn't have enough physical memory for the virtual memory, some will be swapped to disk. If the machine has more physical memory, it will not be accessible to the process (except through transparent OS optimizations like file-system caching). Address space/VM really come down to pointer-size. A 32-bit process uses 32-bit pointers, which means they can only address 4GB of memory, no matter how much is physically present.

@vasily-kirichenko
Copy link
Contributor Author

We'd better merge it to RTM (because it speeds up the analyzer at about 80 times) or disable it there.

@KevinRansom
Copy link
Contributor

@dotnet-bot test this please

Copy link
Contributor

@KevinRansom KevinRansom left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good

@KevinRansom
Copy link
Contributor

@vasily-kirichenko can you resolve the conflicts pls.

Kevin

…ame-analyzer

# Conflicts:
#	src/fsharp/TypeChecker.fs
#	src/fsharp/vs/service.fs
#	src/fsharp/vs/service.fsi
This reverts commit a2f791d.

# Conflicts:
#	src/fsharp/TypeChecker.fs
…copes) by line number"

This reverts commit c8edcc3.

# Conflicts:
#	src/fsharp/vs/service.fs
#	src/fsharp/vs/service.fsi
@vasily-kirichenko
Copy link
Contributor Author

vasily-kirichenko commented Feb 11, 2017

@KevinRansom Could you please merge it? The analyzer is slow and it's still disabled in this PR (as in master) until we have a proper settings dialog to turn it on and off. Keep it up to date with master is cumbersome.

@KevinRansom KevinRansom merged commit 6efa3b3 into dotnet:master Feb 13, 2017
// PERFORMANCE: consider making this a struct.
[<System.Diagnostics.DebuggerDisplay("{idText}")>]
[<Sealed>]
[<Struct>]
Copy link
Contributor

@dsyme dsyme Feb 17, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm only now reviewing these changes. Please make sure I'm listed as a reviewer for all changes to the core compiler data structures, just to double check.

Do we know for sure this as a good idea? range is already a largish struct, and we had previously held off making Ident a struct too until we had some data

member x.SetIsStructRecordOrUnion b = let x = x.Data in let flags = x.entity_flags in x.entity_flags <- EntityFlags(flags.IsPrefixDisplay, flags.IsModuleOrNamespace, flags.PreEstablishedHasDefaultConstructor, flags.HasSelfReferentialConstructor, b)

and [<RequireQualifiedAccess>] MaybeLazy<'T> =
| Strict of 'T
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good change, thanks. The name is a bit confusing since it might be confused with the Haskell terminology of maybe for option. Also in future please make sure we have /// comments for all new types, methods and properties for these core types.

nlr: NonLocalValOrMemberRef }
member x.IsLocalRef = match box x.nlr with null -> true | _ -> false
member x.IsResolved = match box x.binding with null -> false | _ -> true
member x.IsLocalRef = obj.ReferenceEquals(x.nlr, null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please double check this change for performance or at least the generated IL

match box x with null -> ... | ... should, AFAIK, generate the best code, and should also allow optimization to brfalse conditiona sitching in the IL. There's no need to remove it from the compiler - if it's not generating good code we should fix our codegen.

member vr.Deref =
match box vr.binding with
| null ->
if obj.ReferenceEquals(vr.binding, null) then
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again there's no need for this change AFAIK. It's not a big problem either way, it's just that we should only make changes like this if they're needed


// Can cpath2 be accessed given a right to access cpath1. That is, is cpath2 a nested type or namespace of cpath1. Note order of arguments.
let canAccessCompPathFrom (CompPath(scoref1,cpath1)) (CompPath(scoref2,cpath2)) =
let inline canAccessCompPathFrom (CompPath(scoref1,cpath1)) (CompPath(scoref2,cpath2)) =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't add inline to the compiler unless there's a very specific known and measured perf reason to do it, thanks

let GetBestEnvForPos cursorPos =

let bestSoFar = ref None
let mutable bestSoFar = None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good change

let getChecker (document: Document) = document.Project.Solution.Workspace.Services.GetService<FSharpCheckerWorkspaceService>().Checker
let getPlidLength (plid: string list) = (plid |> List.sumBy String.length) + plid.Length
static let cache = ConditionalWeakTable<DocumentId, TextVersionHash * ImmutableArray<Diagnostic>>()
static let guard = new SemaphoreSlim(1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's this lock for? Please add a big comment on this to explain why we need a lock here, thanks

@dsyme
Copy link
Contributor

dsyme commented Feb 17, 2017

@vasily-kirichenko Lots of good changes here - I'm only just reviewing them - I hadn't realized there were core changes in the compiler. I added some comments about the changes to the core compiler code

@KevinRansom Probably best you make sure I review changes to the core compiler code. Just for a double check.

The cool thing about making Ident a struct is that there were a lot of such
objects (~1M in the above scenario) and all of them lived in Gen 2, so the GC had to check
them all in memory pressure case.

@vasily-kirichenko Yes, you're likely correct that Ident should be made a struct. However it can be hard to tell for objects that contain range - which is already a struct - and quite a large one for 32-bit systems. Also in many cases Ident objects can I presume be shared. Making them structs can potentially lose that sharing.

BTW I've found it quite hard to measure the pros/cons of these perf choices - the compiler perf scripts aren't yet accurate enough, though the memory tests are fairly accurate. Focusing on reducing memory by using structs can sometimes lead to reduced throughput through lots of data copying.

nosami pushed a commit to xamarin/visualfsharp that referenced this pull request Jan 26, 2022
* remove unnecessary filtering and deduplication

* write lazy early return functions for SimplifyName analyzer

* make the rest of nr lazy

add cache

* fix cache locking

* do not try to resolve ctors for non-ctor Item

* more lazyness to NameResolution

* optimize GetBestEnvForPos by indexing nameres environments (scopes) by line number

* look for best resolution env on previous line as well

* Reformat ResolveObjectConstructorPrim

* remove double hash table lookup in NotifyNameResolution

* make CurrentSink non-optional

* eliminate a list creation

* renaming

* optimize ValRef

* small optimizations

* make Ident a struct

* add MaybeLazy

* SimplifyNameDiagnosticAnalyzer should do semantic analysis, not syntax one

* remove dead code

* fix after merge

* Revert "make CurrentSink non-optional"

This reverts commit a2f791d.

# Conflicts:
#	src/fsharp/TypeChecker.fs

* Revert "optimize GetBestEnvForPos by indexing nameres environments (scopes) by line number"

This reverts commit c8edcc3.

# Conflicts:
#	src/fsharp/vs/service.fs
#	src/fsharp/vs/service.fsi

* turn off SimplifyNameDiagnosticAnalyzer for now
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants