locked
Iterating Through CodeElements occasionally very laggy, any ideas? RRS feed

  • Question

  • Hello, I've thrown in the towel on this one!  I am writing an application that connects to an instance of the VS IDE - that works.  I get a reference to a ProjectItem that is a folder which contains several .cs files.  I iterate through those files and give my user a list (MyClass01.cs, YourClass01.cs, HisClass02.cs, etc.)

    When the user clicks on a give code file, I iterate through the .CodeElements of that file, looking for a couple other child structs and/or classes.  That all works fine, except every few clicks, the search for the child classes/structs will hang for long periods.  These code files are relatively small (300-1000 lines on average).  They have at most 6-7 public structs that I'm looking for, and a file that works fine might intermittantly lag out.  For example, looking at a single CodeElement may take 300 milliseconds, or more. 

    I've tried peppering DoEvents all over the place and tried using CodeElement2.  No luck.  Anyone have any ideas?  This basically makes my entire effort unusable.

            /// <summary>

            /// Recursively looks through a CodeElements collection

            /// for a given class name and returns that object

            /// </summary>

            /// <param name="pobjFileCodeModel"></param>

            /// <param name="pstrClassName"></param>

            public CodeElement GetClassCodeElement(CodeElements pobjCodeElements, string pstrClassName)

            {

                Application.DoEvents();  //not sure this is 100% necessary, but the COM stuff gets a little weird...

                foreach (CodeElement objCodeElement in pobjCodeElements)

                {

                    if (objCodeElement.Kind == vsCMElement.vsCMElementClass

                        || objCodeElement.Kind == vsCMElement.vsCMElementStruct)

                    {

                        if(objCodeElement.Name == pstrClassName)

                        {

                            //This is the terminating condition of the recursion

                            return objCodeElement;

                        }

                    }

                    else if (objCodeElement.Children != null)

                    {

                        CodeElement objReturn =

                            GetClassCodeElement(objCodeElement.Children, pstrClassName);

                        if(objReturn != null)

                        {

                            //Bubble up the class we found

                            return objReturn;

                        }

                    }

                }

                return null;

            }

     

     

     

     

     

     

     

    Friday, January 14, 2011 10:35 PM

Answers

  • I meant to reply earlier... but been working :)

    For anyone else that might find this article, at the end of the day, doing it as an out-of-process call was the core problem.  I (erroneously) didn't think there would be any real difference between doing a stand-alone and an add-in other than minor performance issues.

    I bit the bullet and converted the code to be an Add-In.  It flies and has absolutely no problems.  Debugging is still a little clunky due to the fact that I have to close the entire debugged IDE and re-open any time I make a significant code change (this is a pain because the particular project I'm coding against is very large).  However, it works for the most part to just "pause" the code, make my edits, and continue.

    At some point, I would like to figure out how to do "non-destructive debugging", but I just have too much real work to get done to fiddle with that, and the documentation is pretty sparse.

    Ryan, thanks for spending the time and trying to help.  You did offer some nice insights and I appreciate it.  Best wishes.


    I program in C#... but I can't bring myself to retire the 10 year old vbSlinger moniker...
    Wednesday, January 19, 2011 2:35 PM

All replies

  • You say 'connects to an instance of the VS IDE' so I presume this code is running out of proc and using DTE to communicate with VS?

    Ryan

    Saturday, January 15, 2011 12:25 AM
  • You are correct - sorry if that was vague.

    In short, I cycle through all the open IDEs, find the one with the desired project open, and get a reference to it's Solution object. 

    I do some varition of what is displayed here (using the moniker, etc).
    http://msdn.microsoft.com/en-us/library/ms228755.aspx

    I also implement an OLE filter to avoid retry/pending errors (basically the code in this link)
    http://msdn.microsoft.com/en-us/library/ms228772(v=vs.80).aspx

    That all is fairly seamless (and I actually already use that logic in another application for automatically attaching the debugger to multiple applications, which no issues).

    I should note that when attaching a debugger in my other efforts, I do catch a few "retry" errors, but in this use, I have never received one.  I do get the occasional MessagePending call, but those are rare - except when it's lagging.  I will see a few MessagePendings when the lagging I describe happens (although it's usually 10-20, not hundreds or thousands).

    Once I have the DTE2, I then iterate through the Projects to find the one I want... then I cycle through its files, sticking them in a list box (there are a couple hundred). 

    When the user clicks on the specific file, I get an instance of that ProjectItem and pass its CodeElements to the function above.  It usually works great, every call to a property comes back in less than 10 milliseconds.  But sometimes, just checking the objCodeElement.Kind property will lag out for 200-300 milliseconds (or even a full 1-2 seconds).  Of course, this is being called several times, so it virtually cripples the app. 

    It is intermittent, but not enough where I can say "just wait it out" to the developers I'm trying to share this with.

    Sorry if my first post didn't have enough detail... it was Friday afternoon and I was defeated :)  And double sorry if this one's too long, it's Saturday and I'm learning WPF ;)

     


    I program in C#... but I can't bring myself to retire the 10 year old vbSlinger moniker...
    Saturday, January 15, 2011 10:17 PM
  • NP, so I am not an expert on the code model stuff but I think it is not terribly performant, the main problem is they don't cache much if anything because maintaining the cache in the face of frequent code edits (which is the normal scenario inside the IDE) was probably deemed to be worse than simply recalculating the information everytime it was asked for (and not having to worry about some cache being out of date / in need of an update).

    That said the DoEvents won't help any, that code is running in your process not VS so pumping at that point would only help if it was your app that was hung up, if your app is simply waiting for a remote call to complete it won't help.  One thing I noticed, though probably shouldn't account for intermittent slowdowns, is that every single property access/method call on ANY DTE object you have will be a cross process call, so you really should make sure you access the value of a property one time and store it locally (like the Kind property you access possibly twice in your if loop, each access will be a cross process RPC call).

    I have heard people having much better luck using the interfaces that back DTE and not DTE itself, DTE was never intended to be speedy or even terribly performant (speed or memorywise), it was intended to expose VS automation to scripting, it often does things in a very subpar manner if one was hoping for speed or light memory footprint. I can look later today or tomorrow when I connect to corp net at what interfaces DTE is using and see if they are publically exposed, the less 'hidden' stuff going on the better when you are trying to understand where the slowdown is happening.

    The fact that it only happens occasionally could be a luck of the draw type situation, it could be that when you attempt to make the RPC call the VS instance is doing some idle time task (like parsing the code file, or doing command updates, or doing auto-save, etc..), if that is the case your RPC call won't be handled until that completes, which may explain the lag and the intermittent nature of it.  There isn't much you can do to avoid that, it is a risk of making cross process RPC calls, you are at the mercy of the other process not being busy and handling the call in a timely manner, which can never be guaranteed.


    Ryan

    Sunday, January 16, 2011 12:03 AM
  • Thanks for your feedback.  Thanks for confirming that the DoEvents is pointless in my case.

    And you're right, I shouldn't be looking at the property twice, which actually was part of my question - is there any way to get a chunk of objects at a time?  For example, can I get a code element and all its properties marshalled across in one shot?  I'm guessing not based on you mentioning that there is little caching. 

    Your note about the IDE doing something in idle time is what I fear.  The solution I'm hitting is very large (10 projects or so and a few thousand files).  Of course... this is why I'm looking to do some automated clean ups :)  I played with the SuspendUI property because of that very idea, and it did not seem to have any effect. 

    If there are any other interfaces or suggestions, I would appreciate it.  I'm going to hack at it a little more this weekend.


    I program in C#... but I can't bring myself to retire the 10 year old vbSlinger moniker...
    Sunday, January 16, 2011 2:35 AM
  • One other note - I did experiment some with the FileCodeModel2 interface.  It actually has "GetElementByID" method.  However, anytime I try to use it or reference the CodeElement's ElementID, I get a NotImplemented exception.

    I also replaced the two checks to .Kind, copying it to a local enum and then checking.

    Additionally, I tried using the FileCodeModel2.BeginBatch and .EndBatch.  They did not help, and interestingly, every time I called .EndBatch, it prompted the GUI to checkout the file, even though no changes were made.

    And finally, I even tried doing FileCodeModel2.Synchronize all over the place.  It too had no effect. 

    I am assuming that doing this as an Add-In wouldn't produce any better results since it too is across processes and using the same model.  I actually started down that path, but debugging was a complete bear.

    Any suggestions continue to be welcomed!


    I program in C#... but I can't bring myself to retire the 10 year old vbSlinger moniker...
    Sunday, January 16, 2011 4:02 PM
  • >is there any way to get a chunk of objects at a time?  For example, can I get a code element and all its properties marshalled across in one shot?  I'm guessing not based on you mentioning that there is little caching. 


    Nope, not unless the object has some GetAllPropertiesXX type call.  It has little to do with caching here and more to do with the design of the interfaces and how COM works across process. You don't actually have an object in the normal sense in your process, you have a COM interface stub. This stub is auto-generated and thus not written by a human and thus not terribly clever, specifically it will never ever under any circumstances cache anything (and even if one were to write one that did it would be a bear keeping it up to date with every change in the IDE). The object you have basically looks like this

    class SomeProxy : ISomeInterface
    {
        public int DoSomething()
        { 
            //Code here to make a cross process call into the process this proxy is associated with and execute DoSomething on the REAL
            //object, and marshal the results back here and return it from this method. Literally this is auto-generated code to make an RPC call.
        }

        public bool IsSomething
        {
            get
            {
                 //This code is identical to the code in DoSomething except it is accessing a property (more likely a method called get_IsSomething)
                 //and returning its value after the RPC call + marshalling, so again nothing clever and no local state exists.
            }
        }
    }

    So every method you call / property you access is an RPC call, there is no way around that, that is why good interface design for efficient use out of process dictates that instead of lots of granular little properties/methods, you generally have larger aggregated methods like GetProperties(SomePropertyId[] properties, out object[] values) so that you can fetch multiple values in a single RPC call.

    >I played with the SuspendUI property because of that very idea, and it did not seem to have any effect. 

    I am not familar with that property but I highly doubt it would do what you want, it likely does something like surpressing or lessening UI update, what I am talking about are idle time tasks, they would not be suspended and while they are occuring the message pump is not being serviced thus your RPC call is sitting there waiting to be handled. That will occur as soon as the running idle task completes, but if it takes a long time (like parsing code files or writing out in-memory data to disk to perform an auto-save) you will be waiting. There is no way around that.


    >Additionally, I tried using the FileCodeModel2.BeginBatch and .EndBatch. 

    According to MSDN that simply 'Suspends the occurrence of edit events until all changes to the code model have been applied.', that won't help you any since you aren't modifying the code model and what you are dealing with isn't edit events.

    >And finally, I even tried doing FileCodeModel2.Synchronize all over the place.  It too had no effect. 

    Same as above, this certainly would have no effect on making idle time tasks complete any faster.

    >I am assuming that doing this as an Add-In wouldn't produce any better results since it too is across processes and using the same model.  I actually started down that path, but debugging was a complete bear.

    What do you mean as an AddIn?  You mean inside the instance of VS that has the file you want to access?  That would be better in that it wouldn't be cross process, AddIns (and packages) are run inside the VS instance.  That said if what you are seeing is truly hold up due to idle time activities then being in process will make no difference since idle time activities are non-interruptible (by and large) regardless of where you might be located.

    I am by no means a CodeModel expert (I have actually never used it), but it may well be that there is nothing that can be done. If the hold up is due to idle time activity (and I strongly suspect it is as it is intermittent and seemingly unrelated to say a specific code file or function) then there is absolutely no way around it. As a mitigation I would suggest your app NOT make any of these RPC calls from the UI thread and further have some kind of little indicator that it is working (so the user doesn't think it is hung).  300 milliseconds really isn't terribly long (3/10ths of a second) if it only happens occasionally, though if it locks up your apps UI then users will think your app is hung. Then again one should never be making unbound calls on the UI thread if you want to keep your app responsive since even without these intermittant hold ups there is no way to guarantee RPC calls will complete in a timely manner.

    Ryan

    Sunday, January 16, 2011 6:42 PM
  • I meant to reply earlier... but been working :)

    For anyone else that might find this article, at the end of the day, doing it as an out-of-process call was the core problem.  I (erroneously) didn't think there would be any real difference between doing a stand-alone and an add-in other than minor performance issues.

    I bit the bullet and converted the code to be an Add-In.  It flies and has absolutely no problems.  Debugging is still a little clunky due to the fact that I have to close the entire debugged IDE and re-open any time I make a significant code change (this is a pain because the particular project I'm coding against is very large).  However, it works for the most part to just "pause" the code, make my edits, and continue.

    At some point, I would like to figure out how to do "non-destructive debugging", but I just have too much real work to get done to fiddle with that, and the documentation is pretty sparse.

    Ryan, thanks for spending the time and trying to help.  You did offer some nice insights and I appreciate it.  Best wishes.


    I program in C#... but I can't bring myself to retire the 10 year old vbSlinger moniker...
    Wednesday, January 19, 2011 2:35 PM
  • One thing to add, if you are doing a normal AddIn project you should be able to launch a new instance of VS via F5 iirc.  So you could set your AddIn to not auto-load (or else it will load in your VS instance that you are using to code in, which is what would necessitate the shutdown + restart cycle) and just make code changes, F5, load AddIn (manually) in newly launched instance, test out changes, close newly launched instance, continue coding changes in original instance.

    Ryan

    Wednesday, January 19, 2011 3:41 PM