Compare commits

...

5 Commits

Author SHA1 Message Date
oguzhankoral 9a4017d42e fix format
.NET Build and Publish / build-connectors (push) Has been cancelled
.NET Build and Publish / deploy-installers (push) Has been cancelled
2026-01-16 10:02:54 +03:00
Oğuzhan Koral 5902cbc05e Merge branch 'dev' into oguzhan/cnx-2869-revit-crashes-performance-issues-caused-by-speckle-connector 2026-01-16 08:38:20 +03:00
oguzhankoral 51588bef8d Add tasks back 2026-01-16 08:03:55 +03:00
oguzhankoral 31492d88cf Bring old but GOLD idle manager 2026-01-16 07:50:42 +03:00
oguzhankoral a74c0f9a57 Subscribe doc change events only if we have sender model card 2026-01-16 06:44:50 +03:00
8 changed files with 144 additions and 41 deletions
@@ -24,7 +24,7 @@ namespace Speckle.Connectors.Revit.Bindings;
internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
{
private readonly IAppIdleManager _idleManager;
private readonly RevitIdleManager _revitIdleManager;
private readonly RevitContext _revitContext;
private readonly DocumentModelStore _store;
private readonly ICancellationManager _cancellationManager;
@@ -38,6 +38,8 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
private readonly LinkedModelHandler _linkedModelHandler;
private readonly IThreadContext _threadContext;
private readonly ISendOperationManagerFactory _sendOperationManagerFactory;
private bool _isDocChangedSubscribed;
private EventHandler<Autodesk.Revit.DB.Events.DocumentChangedEventArgs>? _documentChangedHandler;
/// <summary>
/// Used internally to aggregate the changed objects' id. Note we're using a concurrent dictionary here as the expiry check method is not thread safe, and this was causing problems. See:
@@ -48,7 +50,7 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
private ConcurrentHashSet<ElementId> ChangedObjectIds { get; set; } = new();
public RevitSendBinding(
IAppIdleManager idleManager,
RevitIdleManager revitIdleManager,
RevitContext revitContext,
DocumentModelStore store,
ICancellationManager cancellationManager,
@@ -66,7 +68,7 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
)
: base("sendBinding", bridge)
{
_idleManager = idleManager;
_revitIdleManager = revitIdleManager;
_revitContext = revitContext;
_store = store;
_cancellationManager = cancellationManager;
@@ -86,12 +88,54 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
revitTask.Run(() =>
{
revitContext.UIApplication.NotNull().Application.DocumentChanged += (_, e) =>
_topLevelExceptionHandler.CatchUnhandled(() => DocChangeHandler(e));
// revitContext.UIApplication.NotNull().Application.DocumentChanged += (_, e) =>
// _topLevelExceptionHandler.CatchUnhandled(() => DocChangeHandler(e));
_documentChangedHandler = (_, e) => _topLevelExceptionHandler.CatchUnhandled(() => DocChangeHandler(e));
_store.ModelCardsChanged += (_, e) => OnModelCardsChanged(e);
_store.DocumentChanged += (_, _) => topLevelExceptionHandler.FireAndForget(async () => await OnDocumentChanged());
});
}
private void OnModelCardsChanged(ModelCardsChangedEventArgs e)
{
if (e.ModelCards.Count > 0 && e.ModelCards.Any(m => m.TypeDiscriminator == nameof(SenderModelCard)))
{
SubscribeDocChanged();
}
else
{
UnsubscribeDocChanged();
}
}
private void SubscribeDocChanged()
{
if (_documentChangedHandler == null || _isDocChangedSubscribed)
{
return;
}
_threadContext.RunOnMain(() =>
{
_revitContext.UIApplication.NotNull().Application.DocumentChanged += _documentChangedHandler;
});
_isDocChangedSubscribed = true;
}
private void UnsubscribeDocChanged()
{
if (_documentChangedHandler == null || !_isDocChangedSubscribed)
{
return;
}
_threadContext.RunOnMain(() =>
{
_revitContext.UIApplication.NotNull().Application.DocumentChanged -= _documentChangedHandler;
});
_isDocChangedSubscribed = false;
}
public List<ISendFilter> GetSendFilters() =>
[
new RevitSelectionFilter { IsDefault = true },
@@ -276,7 +320,7 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
if (addedElementIds.Count > 0)
{
_idleManager.SubscribeToIdle(nameof(PostSetObjectIds), PostSetObjectIds);
_revitIdleManager.SubscribeToIdle(nameof(PostSetObjectIds), PostSetObjectIds);
}
if (HaveUnitsChanged(doc))
@@ -296,8 +340,8 @@ internal sealed class RevitSendBinding : RevitBaseBinding, ISendBinding
_sendConversionCache.EvictObjects(unpackedObjectIds);
}
_idleManager.SubscribeToIdle(nameof(CheckFilterExpiration), CheckFilterExpiration);
_idleManager.SubscribeToIdle(nameof(RunExpirationChecks), RunExpirationChecks);
_revitIdleManager.SubscribeToIdle(nameof(CheckFilterExpiration), CheckFilterExpiration);
_revitIdleManager.SubscribeToIdle(nameof(RunExpirationChecks), RunExpirationChecks);
}
// Keeps track of doc and current units
@@ -17,7 +17,7 @@ internal sealed class SelectionBinding : RevitBaseBinding, ISelectionBinding, ID
public SelectionBinding(
RevitContext revitContext,
IBrowserBridge parent,
IAppIdleManager idleManager,
RevitIdleManager idleManager,
ITopLevelExceptionHandler topLevelExceptionHandler,
IRevitTask revitTask
)
@@ -48,11 +48,12 @@ public static class ServiceRegistration
serviceCollection.AddSingleton<IBinding, SelectionBinding>();
serviceCollection.AddSingleton<IBinding, RevitSendBinding>();
serviceCollection.AddSingleton<IBinding, RevitReceiveBinding>();
serviceCollection.AddSingleton<RevitIdleManager>();
serviceCollection.AddSingleton<IBinding>(sp => sp.GetRequiredService<IBasicConnectorBinding>());
serviceCollection.AddSingleton<IBasicConnectorBinding, BasicConnectorBindingRevit>();
serviceCollection.AddSingleton<IAppIdleManager, RevitIdleManager>();
// serviceCollection.AddSingleton<IAppIdleManager, RevitIdleManager>();
// send operation and dependencies
serviceCollection.AddScoped<SendOperation<DocumentToConvert>>();
@@ -17,13 +17,16 @@ namespace Speckle.Connectors.Revit.HostApp;
internal sealed class RevitDocumentStore : DocumentModelStore
{
private readonly ILogger<RevitDocumentStore> _logger;
private readonly IAppIdleManager _idleManager;
//private readonly IAppIdleManager _idleManager;
private readonly RevitIdleManager _idleManager;
private readonly RevitContext _revitContext;
private readonly ITopLevelExceptionHandler _topLevelExceptionHandler;
private readonly ISqLiteJsonCacheManager _jsonCacheManager;
public RevitDocumentStore(
IAppIdleManager idleManager,
//IAppIdleManager idleManager,
RevitIdleManager idleManager,
RevitContext revitContext,
IJsonSerializer jsonSerializer,
ITopLevelExceptionHandler topLevelExceptionHandler,
@@ -34,6 +37,7 @@ internal sealed class RevitDocumentStore : DocumentModelStore
: base(logger, jsonSerializer)
{
_jsonCacheManager = jsonCacheManagerFactory.CreateForUser("ConnectorsFileData");
//_idleManager = idleManager;
_idleManager = idleManager;
_revitContext = revitContext;
_topLevelExceptionHandler = topLevelExceptionHandler;
@@ -1,43 +1,75 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using Autodesk.Revit.UI;
using Autodesk.Revit.UI.Events;
using Speckle.Connectors.DUI.Bridge;
using Speckle.Converters.RevitShared.Helpers;
using Speckle.Sdk.Common;
namespace Speckle.Connectors.Revit.Plugin;
public sealed class RevitIdleManager : AppIdleManager
/// <summary>
/// OK.
/// Please do not try to generalize this class with other IdleManagers for whatever reason.
/// This class is simple, targeted to host app and singleton.
/// </summary>
public class RevitIdleManager(RevitContext revitContext)
{
private readonly UIApplication _uiApplication;
private readonly IIdleCallManager _idleCallManager;
private readonly ITopLevelExceptionHandler _topLevelExceptionHandler;
private readonly UIApplication _uiApplication = revitContext.UIApplication.NotNull();
private event EventHandler<IdlingEventArgs>? OnIdle;
private readonly ConcurrentDictionary<string, Func<Task>> _calls = new();
private volatile bool _hasSubscribed;
public RevitIdleManager(
RevitContext revitContext,
IIdleCallManager idleCallManager,
ITopLevelExceptionHandler topLevelExceptionHandler,
IRevitTask revitTask
)
: base(idleCallManager)
/// <summary>
/// Subscribe deferred action to Idling event to run it whenever Revit becomes idle.
/// </summary>
/// <param name="action"> Action to call whenever Revit becomes Idle.</param>
/// some events in host app are trigerred many times, we might get 10x per object
/// Making this more like a deferred action, so we don't update the UI many times
public void SubscribeToIdle(string name, Action action)
{
_topLevelExceptionHandler = topLevelExceptionHandler;
_uiApplication = revitContext.UIApplication.NotNull();
_idleCallManager = idleCallManager;
revitTask.Run(
() => _uiApplication.Idling += (s, e) => OnIdle?.Invoke(s, e) // will be called on the main thread always and fixing the Revit exceptions on subscribing/unsubscribing Idle events
);
}
protected override void AddEvent()
{
_topLevelExceptionHandler.CatchUnhandled(() =>
// I want to be called back ONCE when the host app has become idle once more
_calls[name] = () =>
{
OnIdle += RevitAppOnIdle;
});
action();
return Task.CompletedTask;
};
if (_hasSubscribed)
{
return;
}
_hasSubscribed = true;
_uiApplication.Idling += RevitAppOnIdle;
}
private void RevitAppOnIdle(object? sender, IdlingEventArgs e) =>
_idleCallManager.AppOnIdle(() => OnIdle -= RevitAppOnIdle);
/// <summary>
/// Run once on the next Revit idle tick (deduped by name).
/// </summary>
public void SubscribeToIdle(string name, Func<Task> action)
{
_calls[name] = action;
if (_hasSubscribed)
{
return;
}
_hasSubscribed = true;
_uiApplication.Idling += RevitAppOnIdle;
}
private void RevitAppOnIdle(object? sender, IdlingEventArgs e)
{
foreach (KeyValuePair<string, Func<Task>> kvp in _calls)
{
Debug.WriteLine($"{kvp.Key}");
kvp.Value();
}
_calls.Clear();
_uiApplication.Idling -= RevitAppOnIdle;
// setting last will delay entering re-subscritption
_hasSubscribed = false;
}
}
@@ -54,9 +54,9 @@
<Compile Include="$(MSBuildThisFileDirectory)Operations\Send\Settings\DetailLevelSetting.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Plugin\IRevitPlugin.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Plugin\RevitCommand.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Plugin\RevitIdleManager.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Plugin\RevitTask.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Plugin\RevitExternalApplication.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Plugin\RevitIdleManager.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Plugin\RevitThreadContext.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Plugin\RevitCefPlugin.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Plugin\SpeckleRevitTaskException.cs" />
@@ -14,6 +14,7 @@ namespace Speckle.Connectors.DUI.Models;
public abstract class DocumentModelStore(ILogger<DocumentModelStore> logger, IJsonSerializer serializer)
: IDocumentModelStore
{
public event EventHandler<ModelCardsChangedEventArgs>? ModelCardsChanged;
private readonly List<ModelCard> _models = new();
/// <summary>
@@ -36,6 +37,17 @@ public abstract class DocumentModelStore(ILogger<DocumentModelStore> logger, IJs
protected void OnDocumentChanged() => DocumentChanged?.Invoke(this, EventArgs.Empty);
protected void OnModelCardsChanged()
{
IReadOnlyList<ModelCard> snapshot;
lock (_models)
{
snapshot = _models.ToList().AsReadOnly();
}
ModelCardsChanged?.Invoke(this, new ModelCardsChangedEventArgs(snapshot));
}
public virtual Task OnDocumentStoreInitialized() => Task.CompletedTask;
public virtual bool IsDocumentInit { get; set; }
@@ -153,6 +165,8 @@ public abstract class DocumentModelStore(ILogger<DocumentModelStore> logger, IJs
var state = Serialize();
HostAppSaveState(state);
}
OnModelCardsChanged();
}
/// <summary>
@@ -0,0 +1,8 @@
using Speckle.Connectors.DUI.Models.Card;
namespace Speckle.Connectors.DUI.Models;
public sealed class ModelCardsChangedEventArgs(IReadOnlyList<ModelCard> modelCards) : EventArgs
{
public IReadOnlyList<ModelCard> ModelCards { get; } = modelCards;
}