Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified bin/netcore/engines/IPY2712PR/pyRevitAssemblyBuilder.dll
Binary file not shown.
Binary file modified bin/netcore/engines/IPY2712PR/pyRevitExtensionParser.dll
Binary file not shown.
Binary file modified bin/netcore/engines/IPY2712PR/pyRevitLoader.dll
Binary file not shown.
Binary file modified bin/netcore/engines/IPY2712PR/pyRevitRunner.dll
Binary file not shown.
Binary file modified bin/netcore/engines/IPY342/pyRevitAssemblyBuilder.dll
Binary file not shown.
Binary file modified bin/netcore/engines/IPY342/pyRevitExtensionParser.dll
Binary file not shown.
Binary file modified bin/netcore/engines/IPY342/pyRevitLoader.dll
Binary file not shown.
Binary file modified bin/netcore/engines/IPY342/pyRevitRunner.dll
Binary file not shown.
Binary file modified bin/netfx/engines/IPY2712PR/pyRevitAssemblyBuilder.dll
Binary file not shown.
Binary file modified bin/netfx/engines/IPY2712PR/pyRevitExtensionParser.dll
Binary file not shown.
Binary file modified bin/netfx/engines/IPY2712PR/pyRevitLoader.dll
Binary file not shown.
Binary file modified bin/netfx/engines/IPY2712PR/pyRevitRunner.dll
Binary file not shown.
Binary file modified bin/netfx/engines/IPY342/pyRevitAssemblyBuilder.dll
Binary file not shown.
Binary file modified bin/netfx/engines/IPY342/pyRevitExtensionParser.dll
Binary file not shown.
Binary file modified bin/netfx/engines/IPY342/pyRevitLoader.dll
Binary file not shown.
Binary file modified bin/netfx/engines/IPY342/pyRevitRunner.dll
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -133,38 +133,60 @@ public ExtensionAssemblyInfo BuildExtensionAssembly(ParsedExtension extension, I
}

/// <summary>
/// Checks if any assembly for this extension is already loaded in the AppDomain.
/// This is used to detect if we are reloading an extension.
/// Cached list of loaded pyRevit assembly names, built once per session on first access.
/// </summary>
/// <param name="extension">The extension to check.</param>
/// <returns>True if an assembly for this extension is already loaded.</returns>
private bool IsAnyExtensionAssemblyLoaded(ParsedExtension extension)
/// <remarks>
/// Perf fix for #3268 issue #4: The original code called
/// <c>AppDomain.CurrentDomain.GetAssemblies()</c> and iterated every loaded assembly
/// (hundreds in a typical Revit session) for <em>each</em> extension. This field stores
/// only the pyRevit-prefixed names (~10-20) so per-extension checks are O(N) over a
/// tiny set instead of O(M) over the full AppDomain.
/// </remarks>
private List<string> _loadedPyRevitAssemblyNames;

private void EnsureLoadedAssemblyNamesCached()
{
if (_loadedPyRevitAssemblyNames != null)
return;

const string PYREVIT_PREFIX = "pyRevit_";

_loadedPyRevitAssemblyNames = new List<string>();

foreach (var loadedAsm in AppDomain.CurrentDomain.GetAssemblies())
{
try
{
var asmName = loadedAsm.GetName().Name;
if (asmName == null)
continue;

// Check if this is a pyRevit extension assembly for this extension
// Assembly names follow the pattern: pyRevit_{revitVersion}_{hash}_{extensionName}
if (asmName.StartsWith(PYREVIT_PREFIX) && asmName.EndsWith(extension.Name))
{
_logger.Debug($"Found loaded extension assembly: {asmName}");
return true;
}
if (asmName != null && asmName.StartsWith(PYREVIT_PREFIX))
_loadedPyRevitAssemblyNames.Add(asmName);
}
catch (Exception ex)
catch
{
// Some assemblies may throw when getting their name, skip them
_logger.Debug($"Error checking assembly: {ex.Message}");
// Some dynamic/collectible assemblies throw — skip silently
}
}

}

/// <summary>
/// Checks if any assembly for this extension is already loaded in the AppDomain.
/// This is used to detect if we are reloading an extension.
/// </summary>
/// <param name="extension">The extension to check.</param>
/// <returns>True if an assembly for this extension is already loaded.</returns>
private bool IsAnyExtensionAssemblyLoaded(ParsedExtension extension)
{
EnsureLoadedAssemblyNamesCached();

// Assembly names follow: pyRevit_{revitVersion}_{hash}_{extensionName}
foreach (var name in _loadedPyRevitAssemblyNames)
{
if (name.EndsWith(extension.Name, StringComparison.OrdinalIgnoreCase))
{
_logger.Debug($"Found loaded extension assembly: {name}");
return true;
}
}

return false;
}

Expand Down Expand Up @@ -298,23 +320,49 @@ private void BuildWithRoslyn(ParsedExtension extension, string outputPath, IEnum
/// Resolves and returns the metadata references required for Roslyn compilation.
/// </summary>
/// <remarks>
/// <para>
/// Includes references to core .NET assemblies, Revit API assemblies, and pyRevit runtime.
/// </para>
/// <para>
/// Perf fix for #3268 issue #3: <c>MetadataReference.CreateFromFile()</c> reads assembly
/// metadata from disk on every call. These references never change during a Revit session,
/// so they are built once and cached statically. A version guard ensures correctness if
/// the static field somehow survives across different Revit version contexts (it won't in
/// practice, but defensive coding costs nothing here).
/// </para>
/// </remarks>
/// <returns>A list of metadata references for the Roslyn compiler.</returns>
private static List<MetadataReference> _cachedRoslynRefs;
private static string _cachedRoslynRefsVersion;
private static readonly object _roslynRefsLock = new object();

private List<MetadataReference> ResolveRoslynReferences()
{
string baseDir = _baseDir;
var refs = new List<MetadataReference>
// Fast path — already cached for this Revit version
if (_cachedRoslynRefs != null && _cachedRoslynRefsVersion == _revitVersion)
return _cachedRoslynRefs;

lock (_roslynRefsLock)
{
MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
MetadataReference.CreateFromFile(typeof(Console).Assembly.Location),
MetadataReference.CreateFromFile(Path.Combine(AppContext.BaseDirectory, "RevitAPI.dll")),
MetadataReference.CreateFromFile(Path.Combine(AppContext.BaseDirectory, "RevitAPIUI.dll")),
MetadataReference.CreateFromFile(Path.Combine(baseDir, $"PyRevitLabs.PyRevit.Runtime.{_revitVersion}.dll"))
};
string sys = Path.Combine(Path.GetDirectoryName(typeof(object).Assembly.Location), "System.Runtime.dll");
if (File.Exists(sys)) refs.Add(MetadataReference.CreateFromFile(sys));
return refs;
if (_cachedRoslynRefs != null && _cachedRoslynRefsVersion == _revitVersion)
return _cachedRoslynRefs;

string baseDir = _baseDir;
var refs = new List<MetadataReference>
{
MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
MetadataReference.CreateFromFile(typeof(Console).Assembly.Location),
MetadataReference.CreateFromFile(Path.Combine(AppContext.BaseDirectory, "RevitAPI.dll")),
MetadataReference.CreateFromFile(Path.Combine(AppContext.BaseDirectory, "RevitAPIUI.dll")),
MetadataReference.CreateFromFile(Path.Combine(baseDir, $"PyRevitLabs.PyRevit.Runtime.{_revitVersion}.dll"))
};
string sys = Path.Combine(Path.GetDirectoryName(typeof(object).Assembly.Location), "System.Runtime.dll");
if (File.Exists(sys)) refs.Add(MetadataReference.CreateFromFile(sys));

_cachedRoslynRefs = refs;
_cachedRoslynRefsVersion = _revitVersion;
return _cachedRoslynRefs;
}
}

/// <summary>
Expand All @@ -341,11 +389,18 @@ private static string GetStableHash(string input)
return BitConverter.ToString(hash).Replace("-", string.Empty).ToLowerInvariant();
}

private static string GetAssemblyBuildFingerprint()
// Perf fix for #3268 issue #7: The executing assembly never changes during a
// Revit session — cache the fingerprint once as a static readonly instead of
// re-reading file metadata and assembly version on every extension build.
private static readonly string _assemblyBuildFingerprint = ComputeAssemblyBuildFingerprint();

private static string GetAssemblyBuildFingerprint() => _assemblyBuildFingerprint;

private static string ComputeAssemblyBuildFingerprint()
{
try
{
var asmPath = Assembly.GetExecutingAssembly().Location;
var asmPath = _executingAssemblyLocation;
var writeTime = File.Exists(asmPath)
? File.GetLastWriteTimeUtc(asmPath).Ticks.ToString()
: "0";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ public string GenerateExtensionCode(ParsedExtension extension, string revitVersi
// typemaker.make_bundle_types(); this HashSet provides equivalent safety.
var emittedClassNames = new HashSet<string>(StringComparer.Ordinal);

// Perf fix for #3268 issue #9: Materialize library extension directories
// ONCE before the command loop. Previously .ToList() was called per command,
// creating hundreds of unnecessary list copies for the default install.
var libExtDirectories = libraryExtensions?
.Select(le => le.Directory)
.Where(d => !string.IsNullOrEmpty(d))
.ToList();

foreach (var cmd in extension.CollectCommandComponents())
{
string safeClassName = SanitizeClassName(cmd.UniqueId);
Expand Down Expand Up @@ -103,18 +111,14 @@ public string GenerateExtensionCode(ParsedExtension extension, string revitVersi

// Add binary paths from component hierarchy for module DLLs
searchPathsList.AddRange(extension.CollectBinaryPaths(cmd));

// Add all library extension directories
if (libraryExtensions != null)
// Add all library extension directories (pre-materialized above)
if (libExtDirectories != null)
{
var libExtList = libraryExtensions.ToList();
foreach (var libExt in libExtList)
{
if (!string.IsNullOrEmpty(libExt.Directory))
searchPathsList.Add(libExt.Directory);
}
searchPathsList.AddRange(libExtDirectories);
}

// Add pyrevitlib/ and site-packages/ paths if pyRevitRoot is valid
if (!string.IsNullOrEmpty(_pyRevitRoot))
{
Expand Down
Loading