Memory Optimization in XAF
Memory Usage
Original (Memory Leak)
Optimized (Proper GC)
Chart shows simulated results for demonstration purposes only
History
We started writing an XAF application back in 2019 as a WinForms application to manage work orders for in-house designed and fabricated manufacturing equipment. As the size and complexity of the system grew, the number of active users also grew to the point where supporting WinForms was more difficult than a web-based solution. When DevExpress released Blazor XAF capabilities, the decision was made to move that direction.
As more and more users connected to the system, we started to see a significant increase in memory consumption that was not being released. This was accompanied by low performance of the system as a whole, especially at login. At one point, the system was using 72GB+ of memory on a server before crashing. This occurred daily, sometimes multiple times a day.
The following details describe some of the significant findings that were addressed and resulted in a final outcome of approximately 75-150MB of memory per user on average.
Blazor Circuit Lifecycle Management
Problem: Disconnected Blazor circuits were being retained too long, holding onto memory and event subscriptions.
Solution:
- Reduced disconnected circuit retention from default 100 circuits/3 minutes to 10 circuits/1 minute
- Implemented
CircuitHandlerProxywith proper cleanup of circuit-specific event subscriptions
Key Code (Startup.cs):
services.AddServerSideBlazor(o => {
o.DisconnectedCircuitMaxRetained = 10;
o.DisconnectedCircuitRetentionPeriod = TimeSpan.FromMinutes(1);
});
Server Mode & Virtual Scrolling
WARNING
Do this step AFTER you try reducing memory with views in Client mode. Enabling this too early will make issues more difficult to find.
NOTE
Server mode has limitations and complexities. If you are currently using client-side filtered collections in a property, these do not work and require a custom solution. Contact Us for more details.
Problem: List views were loading entire collections into memory, causing excessive memory usage on large datasets.
Solution:
- Created a custom ViewsGeneratorUpdater to automatically enable virtual scrolling on standard list views (
*_ListView) - Created a custom ViewsGeneratorUpdater to set
DataAccessMode = Serverfor all persistent relevant ListView types (Some restrictions apply)
XPCollection Caching
Problem: Properties returning XPCollection objects were creating new collections on every access, causing repeated database queries and object allocations.
NOTE
This is only an issue on collections where we manually create XPCollections. The default GetCollection<T>() works as expected and does not need optimization.
Solution:
- Cached XPCollection
- created once per object, invalidated on property change
private List<WorkCenter> _availableWorkCenters;
[Browsable(false)]
public List<WorkCenter> AvailableWorkCenters
{
get
{
if (_availableWorkCenters == null)
{
_availableWorkCenters = new XPCollection<WorkCenter>(Session,
CriteriaOperator.Parse("...Your Criteria...")).ToList();
}
return _availableWorkCenters;
}
}
private void OperationCenterChanged()
{
_availableWorkCenters = null; // Invalidate cache when OperationCenter changes
}
Incorrect Service Patterns
Problem: Singleton services that hold IObjectSpaceProvider, IObjectSpace, and other scoped references that are never released
Solution:
- Converted the service to a singleton service, removing dependency on IObjectSpaceProvider instance and using calls where the IServiceProvider, IObjectSpace, or Session (XPO) is provided to work with so the service doesn't hold the reference directly.
- Alternatively, convert to a scoped service if possible
services.AddSingleton<MyService>();
public class MyService
{
//Pass the IServiceProvider in when needed so you can access the IObjectSpaceProvider and other services
public void TrackDetails(IServiceProvider serviceProvider)
{
// Do what you need here with access to other services through the IServiceProvider
}
}
Permissions Caching
Problem: Permission checks were hitting the database repeatedly during user sessions.
TIP
This solution isn't recommended for everyone. If you expect frequent changes to security rules that need to apply immediately without the user logging back in or refreshing the browser, don't use caching.
Solution:
- Enabled
PermissionsReloadMode.CacheOnFirstAccessin security strategy - Enabled
UseXpoPermissionsCaching()
Key Code (Startup.cs):
builder.Security
.UseIntegratedMode(options =>
{
options.UseXpoPermissionsCaching();
options.Events.OnSecurityStrategyCreated = securityStrategy => {
((SecurityStrategy)securityStrategy).PermissionsReloadMode =
PermissionsReloadMode.CacheOnFirstAccess;
};
})
Delayed Loading
Problem:
- Large binary thumbnails are being loaded immediately during loading.
- Large binary thumbnail data was being loaded unnecessarily when only checking for image existence (even after implementing delayed loading).
Solution:
- Move byte array properties to delayed loading patterns DevExpress Delayed Loading
- Added HasThumbnail boolean property to check image existence without loading the full byte array
[Appearance("PartInfo-MissingThumbnail", "[HasThumbnail] = False", BackColor = "Red", TargetItems = "*")]
public class PartInfo(Session session) : CustomBaseObject(session)
{
[Delayed(true)]
[ImageEditor(ListViewImageEditorCustomHeight = 32)]
public byte[] PartThumbnail
{
get { return GetDelayedPropertyValue<byte[]>(nameof(PartThumbnail)); }
set { SetDelayedPropertyValue<byte[]>(nameof(PartThumbnail), value); }
}
private bool _HasThumbnail;
public bool HasThumbnail
{
get { return _HasThumbnail; }
set { SetPropertyValue<bool>(nameof(HasThumbnail), ref _HasThumbnail, value); }
}
protected override void OnSaving()
{
base.OnSaving();
HasThumbnail = PartThumbnail != null;
}
}
Server-Side Calculations
Problem: Business objects contained properties with complex getters that included client-side loading of data.
Solution:
- Convert to PersistentAlias when possible
- Make the calculated values persistent and update them only when necessary
NOTE
We implemented our own calculation service that forces business objects to recalculate persisted values after the Session / IObjectSpace is committed to the database. This requires careful consideration but can be an excellent way to ensure calculations are accurate and consistent.
Simplified Example
[PersistentAlias("[Children][[IsActive] = True].Count()")]
public int ChildCount => Convert.ToInt32(EvaluateAlias(nameof(ChildCount)));
Dashboard Module Memory Cleanup
Problem: Dashboard data sources were being cached, holding references to large datasets.
NOTE
Caching dashboard data in our case was unnecessary because they are only accessed once before the user navigates away. If your application relies on dashboards being accessed more frequently, the cache may offer better performance and changing cache-specific settings may be better.
Solution:
- Disabled dashboard data source caching (
DataSourceCacheEnabled = false) - Use BlazorDashboardViewDataSourceFillService for lightweight data loading (Faster, Less Memory)
Key Code (Startup.cs):
.AddDashboards(options =>
{
options.SetupDashboardConfigurator = (c, s) =>
{
c.DataSourceCacheEnabled = false;
c.SetObjectDataSourceCustomFillService(new BlazorDashboardViewDataSourceFillService(s));
};
})
Model Files
Problem: Logging in seemed to hold up the entire application, especially when other users were trying to log in.
Solution:
- Remove any code that modifies the model at runtime (Example: We were modifying navigation items in the model on login)
- The model mechanism works differently in Blazor than it does in Win and model locking can become a significant issue if multiple users attempt to modify at the same time.