Forge Rooms, Effective Filtered Element Collectors

I successfully made it from Switzerland to Paris by train yesterday, in spite of the strikes here.

Now I am happily occupied with the Forge accelerator in the Autodesk office.

My only worry is how to get back again tomorrow. This time again, the train I had originally booked has been cancelled.

We'll see how it goes.

Meanwhile, let's take a look at:

DA4R Room Support and New Samples

Some recent Forge Design Automation for Revit or DA4R news and samples:

Most importantly, a new RVT to SVF model derivative parameter generates additional content, including rooms and spaces.

Lots of other exciting new samples are available, including:

Here is a link to view all samples.

Effective Filtered Element Collection

Back to the desktop Revit API, one issue that almost every Revit add-in developer faces is the efficiency of filtered element collectors.

Several interesting aspects are pointed out (and repeated) in the thread on an efficient way to check if an element exists in a view:

Question: I'm creating a list of views that contain .dwg ImportInstance(s). For each view in the document, I'm using a FilteredElementCollector to get a list of elements meeting the criteria; if this list is not empty, the view is added to the list:

  foreachElement e in viewElements )
  {
    View view = (View) e;

    var stopwatch = new Stopwatch();
    stopwatch.Start();

    List<Element> elementsInView
      = new FilteredElementCollector( doc, view.Id )
        .OfClass( typeofImportInstance ) )
        .Where( e => e.Category.Name.EndsWith( ".dwg" ) )
        .OfType<Element>()
        .ToList();

    stopwatch.Stop();

    Debug.WriteLine( view.Name + ": "
      + stopwatch.ElapsedMilliseconds + "ms" );

    // if the current view contains at least 1 DWG
    // ImportInstance, add the view to the list

    if( elementsInView.Count > 0 )
    {
      viewsWithCAD.Add( view );
      continue;
    }
  }

The FilteredElementCollector can understandably take more than 4000 ms to collect elements from a view containing many elements.

My goal is only to see if a single element exists in a view – not to collect all of the elements meeting the criteria; if I could make the FilteredElementCollector stop immediately after finding an element meeting the criteria, that would be helpful.

I would appreciate any advice on how to achieve this more efficiently.

Thank you.

Answer by Fair59, Frank Aarssen: Stopping the collector at the first element:

  Element e1
    = new FilteredElementCollector( doc, view.Id )
      .OfClass( typeofImportInstance ) )
      .FirstElement();

Possible further speed improvement:

  IEnumerable<ImportInstance> instances
    = new FilteredElementCollector( doc )
      .OfClass( typeofImportInstance ) )
      .Cast<ImportInstance>();

  List<ElementId> toExclude = new List<ElementId>();
  foreachImportInstance instance in instances )
  {
    if( !instance.Category.Name.EndsWith( ".dwg" ) )
    {
      toExclude.Add( instance.Id );
      continue;
    }
    if( instance.ViewSpecific ) // dwg only exists in ownerview
    {
      View ownerview = doc.GetElement( instance.OwnerViewId ) as View;
      viewsWithCAD.Add( ownerview );
      if( viewElements.Contains( ownerview ) )
        viewElements.Remove( ownerview );
    }
  }

  foreachElement e in viewElements )
  {
    View view = (View) e;
    var stopwatch = new Stopwatch();
    stopwatch.Start();
    Element e1 = null;
    if( toExclude.Count > 0 )
    {
      e1 = new FilteredElementCollector( doc, view.Id )
        .Excluding( toExclude )
        .OfClass( typeofImportInstance ) )
        .FirstElement();
    }
    else
    {
      e1 = new FilteredElementCollector( doc, view.Id )
        .OfClass( typeofImportInstance ) )
        .FirstElement();
    }
    stopwatch.Stop();
    Debug.WriteLine( view.Name + ": "
      + stopwatch.ElapsedMilliseconds + "ms" );

    // if the current view contains at least 1 DWG
    // ImportInstance, add the view to the list

    if( e1 != null )
    {
      viewsWithCAD.Add( view );
    }
  }

Many thanks for the interesting question, and many thanks to Fair59 for yet another extremely knowledgeable and helpful solution!

Notes: I do keep pointing out that converting a filtered element collector to a List is an inefficient thing to do, if you can avoid it.

It forces the collector to retrieve all the data, convert it to the .NET memory space, duplicate it, costing time and space.

For the same reasons, it is much more efficient to test and apply as many filters as possible within the Revit memory space before passing any data across to .NET.

In this case, you can test the parameter values using a parameter filter instead of the LINQ post-processing that you are applying in you sample code snippet.

As Fair59 points out and we have discussed in the past, you can cancel a collector as soon as your target has been reached:

So, you can save time and space in several ways:

Both of these force the filtered element collector to retrieve and return all results.

Here is an explanation of the various types of filters versus post-processing in .NET:

Here are some discussions and a benchmark of the results of using a parameter filter versus LINQ and .NET post-processing:

We also discussed the issue of finding all views displaying an element a couple of times in the past:

Harvester

Don't Waste Time Optimising Prematurely

Mastjaso adds some important professional advice in his comment below:

Interesting discussion on filtered element collectors, though I have to say that I have personally spend far too much time worrying about the efficiencies of those collectors when it has turned out to be inconsequential.

For new developers starting out, or ones tackling a new project for the first time, I'd remember the adage that premature optimization is the death of software. I start basically all my projects with super basic OfClass filtered element collectors and then cast and convert them to the .NET memory space so that I can use LINQ which produces shorter, more readable, and easier to debug code than the filtered element collection API. It's only near the end of a project, once features are settled that I'll start swapping out LINQ for more nuanced uses of the FEC, but even then I'll only do it if I need the performance boost somewhere.

Before optimising anything at all, benchmark or profile to find out where and whether there are any serious performance issues at all.