Very few programs run as fast as we would like them to. The fact is that it takes processing time to do the required work and in many cases there’s not much we can do to shorten that time. When your program asks Inventor to perform a compute intensive task, i.e. create a complex feature, it’s going to take a certain amount of time for Inventor to compute and create the feature. However, there are cases where you can significantly speed up performance by using some general programming principles and taking advantage of some often neglected capabilities in the API.
Timing Your Program
The first thing to consider when looking at your program’s performance is measuring the current performance. You want to be able to determine possible areas of improvement and if your changes help the performance. If you’re using .Net you can use the StopWatch class from System.Diagnostics. Below is an example of it in use.
' Use the .Net StopWatch class to time an action.
Dim stopwatch As New System.Diagnostics.Stopwatch
stopwatch.Start()
' Do some things.
MessageBox.Show("Elapsed time: " & _
stopwatch.ElapsedMilliseconds / 1000 & " seconds")
For timing in VBA I used to use some built-in VBA functions but I noticed some problems with the results and later read about some accuracy issues. I now use the class below. To use this, copy and paste the code below into a new class module. I name the class module “clsTimer”.
' clsTimer Definition.
Private Declare PtrSafe Function QueryPerformanceFrequency _
Lib "kernel32" (lpFrequency As Currency) As LongPtr
Private Declare PtrSafe Function QueryPerformanceCounter _
Lib "kernel32" (lpPerformanceCount As Currency) As LongPtr
Private ConversionFactor As Currency
Private CurrentStartTime As Currency
Public Sub Start()
Dim iReturn As LongPtr
iReturn = QueryPerformanceCounter(CurrentStartTime)
End Sub
Public Function GetTime() As Double
Dim NewTime As Currency
Dim iReturn As LongPtr
iReturn = QueryPerformanceCounter(NewTime)
Dim TotalTime As Currency
TotalTime = NewTime - CurrentStartTime
GetTime = TotalTime / ConversionFactor
End Function
Private Sub Class_Initialize()
Dim iReturn As LongPtr
iReturn = QueryPerformanceFrequency(ConversionFactor)
End Sub
You can then add the code below into your program to time sections of your program.
' Use the timer class to time an action.
Dim timer As New clsTimer
timer.Start
' Do some things.
MsgBox "Elapsed time: " & Format(timer.GetTime, "0.00000") & " seconds"
Running In-Process vs. Out-of-Process
One thing that significantly impacts performance is if your program is running inside Inventor or not. When your program is running within Inventor’s process it’s referred to as in-process. When your program is running in another process it referred to as out-of-process. Add-ins and VBA macros, (as of Inventor 2014), run in-process. Exe’s and programs that run from within other products (i.e. Excel VBA macros) all run in a process outside of Inventor. When running out-of-process Windows has to package the API calls for inter-process communication which adds additional overhead to every API call.
Below is a test program that makes a simple API call. This call was chosen because it doesn’t take any processing on Inventor’s side to respond since it’s simply returning a known value. It makes 100,000 calls of this property and without any optimizations takes 74.8 seconds to run out-of-process. The same program running in-process takes 0.24 seconds or 312 times faster.
Public Sub CallCost()
Dim timer As New clsTimer
timer.Start
Dim i As Long
For i = 1 To 100000
Dim code As Long
code = ThisApplication.Locale
Next
Debug.Print "Total time: " & Format(timer.GetTime, "0.00000")
End Sub
Don’t expect your programs to run 312 times faster by changing them to run in-process. This test was designed to test the overhead of making out-of-process calls and doesn’t represent real use of the API. Remember that it’s making 100,000 calls so even with the slow time in the out-of- process test it is still making over 5400 calls per second. The reason you won’t see big time improvements is that typical programs are making calls that require Inventor to do some work. The time that it takes for Inventor to do the work is the same regardless of in-process or out-of-process. The difference is in the overhead of packaging to make the call land return the results. However, in most cases there will be a noticeable difference between running in-process and out-of-process, so ideally you want to run in-process.
You don’t always have the option to run in-process though. Sometimes your program might be some type of batch processor that needs to start Inventor and then perform some action. Or it might be an Excel VBA macro that uses values from Excel to perform some action in Inventor. When you’re forced to run out-of-process there are also some things you can do to speed up the processing.
Besides the overhead of packaging the API calls for inter-process communication, another reason for the performance difference between in-process and out-of-process is that Inventor is servicing the out-of-process calls while it’s also servicing the user-interface. It gives priority to the user-interface and then out-of-process API calls slip in when they can. For in-process programs, Inventor dedicates all of its processing to the API calls. For out-of-process programs, you can get this dedicated processing by using the UserInteractionDisabled property available on the UserInterfaceManager object. Setting the property to True will lock the user-interface. This is typically what you want anyway because you don’t want the user doing things in Inventor while your program is running. It also has the side effect of allowing Inventor to focus on handling API calls instead of watching the user-interface. The code below shows the test program with this optimization and drops the execution time from 74.8 seconds to 61.32 seconds. I’ve seen much bigger differences in other programs.
Public Sub CallCost()
Dim timer As New clsTimer
timer.Start
app.UserInterfaceManager.UserInteractionDisabled = True
Dim i As Long
For i = 1 To 100000
Dim code As Long
code = ThisApplication.Locale
Next
app.UserInterfaceManager.UserInteractionDisabled = False
Debug.Print "Total time: " & Format(timer.GetTime, "0.00000")
End Sub
When using this property you want make sure to add error handling to your program so that if an error does occur and your program stops, you’ll handle the error so you can turn interaction back on. Otherwise Inventor will remain frozen and you won’t be able to work with it.
Optimizing Inventor VBA Macros
Here’s an optimization that’s limited to Inventor’s VBA. In Inventor’s VBA you access the Inventor Application object using the global ThisApplication property. There is some cost to using this property. The code below calls the Locale property of the Application object 100,000 times and takes 74.8 seconds when running out-of-process.
Public Sub CallCost()
Dim timer As New clsTimer
timer.Start
Dim i As Long
For i = 1 To 100000
Dim code As Long
code = ThisApplication.Locale
Next
Debug.Print "Total time: " & Format(timer.GetTime, "0.00000")
End Sub
Making the small change highlighted below reduced the run time to 18.8 seconds, which is about 4 times faster. You can see the change gets the Application object, assigns it to a variable, and then uses that reference for the rest of the program. It’s a big improvement for a small change.
Public Sub CallCost()
Dim timer As New clsTimer
timer.Start
Dim app As Inventor.Application
Set app = ThisApplication
Dim i As Long
For i = 1 To 100000
Dim code As Long
code = app.Locale
Next
Debug.Print "Total time: " & Format(timer.GetTime, "0.00000")
End Sub
Use References Instead of Inline Calls
An optimization similar to the previous one is to get and save any objects that you’re going to use multiple times. The program below Iterates through all of the occurrences in an assembly and gets the name and filename of each one. Calling this function 10 times takes 22.5 seconds in an assembly of 500 occurrences.
Public Sub GetOccurrences1()
Dim asmDoc As AssemblyDocument
Set asmDoc = ThisApplication.ActiveDocument
Dim i As Integer
For i = 1 To asmDoc.ComponentDefinition.Occurrences.Count
Dim Name As String
Name = asmDoc.ComponentDefinition.Occurrences.Item(i).Name
Dim filename As String
filename = asmDoc.ComponentDefinition.Occurrences.Item(i). _
ReferencedDocumentDescriptor. _
ReferencedFileDescriptor.FullFileName
Next
End Sub
Here’s a slightly different version of the previous program with the differences highlighted. Calling this program 10 times takes less than half the time at 9.7 seconds. The difference is that this gets the ComponentOccurrences object and the ComponentOccurrence objects and assigns them to variables that it uses later. This significantly reduces the number of API calls. A single line of code like
results in several API calls and each call takes some time. If you can make those calls once and save and re-use the result you can improve the performance of your programs.
Public Sub GetOccurrences2()
Dim asmDoc As AssemblyDocument
Set asmDoc = ThisApplication.ActiveDocument
Dim occs As ComponentOccurrences
Set occs = asmDoc.ComponentDefinition.Occurrences
Dim i As Integer
For i = 1 To occs.Count
Dim Occ As ComponentOccurrence
Set Occ = occs.Item(i)
Dim Name As String
Name = Occ.Name
Dim filename As String
filename = Occ.ReferencedDocumentDescriptor. _
ReferencedFileDescriptor.FullFileName
Next
End Sub
Use For Each instead of Count and Item
Another optimization is to use the For Each statement when iterating over the contents of a collection. Below is a version of the same test program that uses For Each. Besides being more efficient it also makes the code easier to read. The time for this function to run 10 times was 9.4 seconds. Not a big difference from the previous sample, but the time saved will vary depending on which collection is being iterated through.
Public Sub GetOccurrences3()
Dim asmDoc As AssemblyDocument
Set asmDoc = ThisApplication.ActiveDocument
Dim occs As ComponentOccurrences
Set occs = asmDoc.ComponentDefinition.Occurrences
Dim Occ As ComponentOccurrence
For Each Occ In occs
Dim Name As String
Name = Occ.Name
Dim filename As String
filename = Occ.ReferencedDocumentDescriptor. _
ReferencedFileDescriptor.FullFileName
Next
End Sub
The reason using a For Each statement is faster is because Inventor knows you’ll be iterating over the collection so it can do some internal optimization to improve the performance. When using the Item property Inventor doesn’t know if you just want a single item or will be iterating over multiple items. Some collections are better optimized to return single items, like the ComponentOccurrences collection so you won’t see big improvements, but others will be significantly faster using a For Each statement.
Get Items By Name
Another optimization associated with collections is to use the capability of many of the collections to get an object in the collection by name instead of iterating through the collection looking for an object with a specific name. The Item property of many of the collections will take either an index or the name of the object within the collection you want. In a few cases there is an additional Item property that is specifically for the name. In either case using these is much more efficient that iterating over the entire collection looking for an object with a specific name.
Below is a code example straight from the Inventor Customization newsgroup that is looking for the DXF translator add-in.
Dim dxfAddIn As TranslatorAddIn
For i = 1 To addIns.Count
If addIns(i).AddInType = kTranslationApplicationAddIn Then
Dim desc As String
desc = addIns(i).Description
If desc = "Autodesk Internal DXF Translator" Then
Set dxfAddIn = addIns.Item(i)
Exit For
End If
End If
Next
Here’s the equivalent code using the ItemById property of the ApplicationAddIns object. As you can see the code is much simpler but it also much more efficient because Inventor is doing the look-up instead and is able to it in a much more optimized way than looking through the objects one-by-one.
Dim dxfAddIn As TranslatorAddIn
Set dxfAddIn = addIns. _
ItemById("{C24E3AC4-122E-11D5-8E91-0010B541CD80}")
Don’t Use Constraints and Defer Updates When Possible
When working with assemblies, Inventor solves the constraints in order to determine the occurrences positions and orientations. This constraint solving takes time and the more constraints there are the longer it will take to solve. Often, when constructing an assembly with the API, you position the occurrences exactly so you don’t need constraints to position the occurrences.
The first optimization you can do when working with assemblies is to not use constraints at all. If your program positions everything precisely and the assembly won’t be edited by the user, you can place the occurrences and leave out the constraints. You can optionally ground the occurrences so the user doesn’t accidentally reposition them. A program that runs in-process and places 500 occurrences of the same part without any constraints takes 5.32 seconds to run. Compare that to the same final assembly with constraints that takes 699 seconds (11 m 39 s). There are additional API calls to create the constraints but the time Inventor takes to solve the assembly with each new constraint is the big difference.
There is something you can do to so you can still use constraints but get reasonable performance. You can defer assembly updates. You’re essentially turning off the compute until you either turn it back on or explicitly update the assembly. This setting is available in the user-interface as an application option, as shown below.
This setting is also available through the API as the Application.AssemblyOptions.DeferUpdate property. After setting this property to True and placing the 500 occurrences with constraints the total processing time drops down to 54 seconds. It was trimmed down to 49 seconds by also using the Application.ScreenUpdating property. This freezes the screen so Inventor doesn’t try to update the graphics while the program is running.
Use Defer When Creating Sketches
Sketches also solve as you add additional sketch entities and constraints. When working with sketches you can’t choose to not use constraints because it’s constraints that connect the entities. If the sketch won’t be edited by the user you can minimize the number of constraints used but you can never eliminate them.
A sample that that runs in-process and creates 500 lines with the minimum amount of constraints takes 18.5 seconds to complete. Sketches support the ability to defer compute through the Sketch.DeferUpdates property. Setting this to True for the program that creates the 500 lines reduced the run time to 1.6 seconds, which is a huge difference.
Minimize API Calls
An obvious way to improve performance is to minimize the number of API calls made. Usually this isn’t possible because there are a fixed number of calls you need to make to accomplish a specific objective. However, there are a few cases where you might be using the API as a general programming library, rather than creating or querying things in Inventor.
An example of this is the Facet Enabler add-in I wrote a few years ago. It allows you to read in STL files and supports sectioning of the data. Initially I was using the Inventor API Point object for all of the coordinates and the Matrix object to do transformations. There often are hundreds of thousands of points in an STL file so there were a lot of API calls being made and the program was quite slow. I changed this to use my own data and functions to do the point manipulation and the program sped up several times. This is a fringe case but in cases where you are making many thousands of calls you should look at whether there are alternatives to do some of the processing outside of Inventor to avoid the API call overhead.
Global Transactions
An overhead in Inventor any time you create or edit anything is transactions. A transaction is how Inventor wraps actions so they can be undone. Anything you do that is undoable is wrapped within a transaction. It takes some time for Inventor to do the work to support transactions. You can’t perform work in Inventor without it being wrapped in a transaction, but it is possible to minimize the number of transactions that occur. Trying to minimize the transaction overhead will only help in cases where you are doing a lot of creation or edit. Query operations aren’t impacted at all because they don’t participate in undo and a few transactions aren’t going to have enough impact to matter.
The Inventor API supports the ability for you to wrap multiple actions into a single transaction, which means it will result in a single undo. This makes is nice for the user if they want to undo the action of your program. The simple way to do this is shown below. You start a transaction, do whatever your command does, and then end the transaction. Now the user will be able to undo everything the command did in a single undo.
Public Sub SampleCommand()
Dim partDoc As PartDocument
Set partDoc = ThisApplication.ActiveDocument
' Start a transaction.
Dim trans As Transaction
Set trans = ThisApplication.TransactionManager. _
StartTransaction( partDoc, "My Command")
' ** Do stuff that creates or edits the model.
' End the transaction.
trans.End
End Sub
The above is convenient for the user but it doesn’t improve performance because there are just as many transactions. This creates a custom transaction that wraps all of the transactions that are created between the time it was started and ends. There is another type of transaction that will block the individual transactions from being created and the commands will rely on the outer custom transaction. This capability is accessed by using the hidden StartGlobalTransaction method. This is used in exactly the same as the standard StartTransaction and is shown below. The downside of using this, and the reason it’s hidden, is that you need to be very careful to make sure you always end a global transaction or you can leave Inventor in a bad state that will most likely result in a crash as you or the user continues to do work. You’ll want to wrap the code that’s doing the work in an error handler so that if an error occurs you can either end, or more typically, abort the transaction.
' Start a transaction.
Dim trans As Transaction
Set trans = ThisApplication.TransactionManager. _
StartGlobalTransaction(partDoc, "My Command")
' ** Do stuff that creates or edits the model.
' End the transaction.
trans.End
The assembly test described previously that creates 500 occurrences with constraints was taking about 50 seconds. By wrapping that code in a global transaction it’s down to about 40 seconds. The other example that creates 500 sketch lines goes from 1.6 seconds to 1 second.
-Brian