返回 Skill 列表
extension
分类: 开发与工程无需 API Key

xaf-memory-leaks

XAF内存泄漏预防 - 事件处理程序对称性(OnActivated/OnDeactivated/Dispose),使用using语句进行ObjectSpace范围的释放,批量处理大数据集,控制器中使用IDisposable模式并带有List<IDisposable>跟踪器,WeakEventSubscription,静态引用反模式,CollectionSource释放,Session/HttpContext/Application反模式(WebForms),ObjectSpacePool,控制器生命周期跟踪,NavigationMonitor,警告信号,诊断工具(dotMemory, PerfView, XAF跟踪)。在诊断DevExpress XAF应用程序中的内存泄漏、审核控制器释放、审查ObjectSpace生命周期或审查Session使用时使用。

person作者: jakexiaohubgithub

XAF: Memory Leak Prevention

Root Causes

| Cause | Symptom | |---|---| | Event handler not unsubscribed | Memory grows with navigation; handler fires multiple times | | ObjectSpace not disposed | Memory grows proportional to data accessed | | Static reference to instance | Objects never GC'd; growing memory profile | | CollectionSource not disposed | Cached data retained after view closes | | ObjectSpace/objects in Session | Memory scales with active user count; session timeout failures | | Controller holds undisposed resources | Finalizer queue pressure; slow GC |


Event Handler Pattern — Most Common Leak

Every += in OnActivated needs a matching -= in both OnDeactivated and Dispose.

public class SafeController : ViewController {
    private bool eventsSubscribed;

    protected override void OnActivated() {
        base.OnActivated();
        if (!eventsSubscribed) {
            View.SelectionChanged += View_SelectionChanged;
            View.CurrentObjectChanged += View_CurrentObjectChanged;
            View.ObjectSpace.ObjectChanged += ObjectSpace_ObjectChanged;
            eventsSubscribed = true;
        }
    }

    protected override void OnDeactivated() {
        UnsubscribeEvents();
        base.OnDeactivated();
    }

    protected override void Dispose(bool disposing) {
        if (disposing) UnsubscribeEvents(); // safety net
        base.Dispose(disposing);
    }

    private void UnsubscribeEvents() {
        if (!eventsSubscribed) return;
        if (View != null) {
            View.SelectionChanged -= View_SelectionChanged;
            View.CurrentObjectChanged -= View_CurrentObjectChanged;
        }
        if (View?.ObjectSpace != null)
            View.ObjectSpace.ObjectChanged -= ObjectSpace_ObjectChanged;
        eventsSubscribed = false;
    }
}

Comprehensive Disposal Pattern — ResourceTrackingController

Use List<IDisposable> + List<Action> to track all resources safely:

public class ResourceTrackingController : ViewController {
    private readonly List<IDisposable> _disposables = new();
    private readonly List<Action> _cleanups = new();
    private bool _disposed;

    protected T Track<T>(T resource) where T : IDisposable {
        _disposables.Add(resource);
        return resource;
    }

    protected void AddCleanup(Action cleanup) => _cleanups.Add(cleanup);

    protected override void OnActivated() {
        base.OnActivated();
        // Subscribe and auto-track cleanup
        View.SelectionChanged += OnSelectionChanged;
        AddCleanup(() => { if (View != null) View.SelectionChanged -= OnSelectionChanged; });

        // Track a CollectionSource
        var cs = Track(new CollectionSource(ObjectSpace, typeof(MyObject)));
    }

    protected override void OnDeactivated() {
        RunCleanups();
        base.OnDeactivated();
    }

    protected override void Dispose(bool disposing) {
        if (!_disposed && disposing) {
            RunCleanups();
            foreach (var d in _disposables)
                try { d?.Dispose(); } catch { }
            _disposables.Clear();
            _disposed = true;
        }
        base.Dispose(disposing);
    }

    private void RunCleanups() {
        foreach (var action in _cleanups)
            try { action(); } catch { }
        _cleanups.Clear();
    }
}

Weak Event Subscription

For long-lived controllers that subscribe to events on short-lived objects:

public class WeakEventSubscription : IDisposable {
    private WeakReference _sourceRef;
    private readonly string _eventName;
    private readonly EventHandler _handler;

    public WeakEventSubscription(object source, string eventName, EventHandler handler) {
        _sourceRef = new WeakReference(source);
        _eventName = eventName;
        _handler = handler;
        source.GetType().GetEvent(eventName)?.AddEventHandler(source, handler);
    }

    public void Dispose() {
        var source = _sourceRef?.Target;
        if (source != null)
            source.GetType().GetEvent(_eventName)?.RemoveEventHandler(source, _handler);
        _sourceRef = null;
    }
}

// Usage in controller:
private readonly List<WeakEventSubscription> _weakSubs = new();

protected override void OnActivated() {
    base.OnActivated();
    _weakSubs.Add(new WeakEventSubscription(
        View, nameof(View.SelectionChanged), OnSelectionChanged));
}

protected override void Dispose(bool disposing) {
    if (disposing) {
        foreach (var sub in _weakSubs) sub.Dispose();
        _weakSubs.Clear();
    }
    base.Dispose(disposing);
}

ObjectSpace — Scoped Disposal

Never store IObjectSpace in a static field, singleton, or Session.

// ✅ Short-lived operation
public void ProcessData() {
    using var os = Application.CreateObjectSpace(typeof(MyObject));
    var obj = os.FindObject<MyObject>(CriteriaOperator.Parse("Name = ?", "Test"));
    obj.Status = Status.Processed;
    os.CommitChanges();
} // disposed here — tracked objects released

// ❌ Anti-patterns
private static IObjectSpace _globalOs;           // never
Session["XafObjectSpace"] = Application.CreateObjectSpace(); // never in Session

ObjectSpace — Batch Processing Large Datasets

public void ProcessAllRecords() {
    const int batchSize = 500;
    int skip = 0;

    while (true) {
        using var os = Application.CreateObjectSpace(typeof(MyObject));
        var batch = os.GetObjects<MyObject>()
            .Skip(skip).Take(batchSize).ToList();

        if (batch.Count == 0) break;

        foreach (var obj in batch)
            Process(obj);

        os.CommitChanges();
        skip += batch.Count;

        // Force GC between large batches
        if (skip % 5000 == 0) {
            GC.Collect();
            GC.WaitForPendingFinalizers();
        }
    }
}

ObjectSpace Pool (Advanced — High-Throughput Scenarios)

public class ObjectSpacePool : IDisposable {
    private readonly ConcurrentQueue<IObjectSpace> _pool = new();
    private readonly XafApplication _app;
    private readonly int _maxSize;
    private int _currentSize;

    public ObjectSpacePool(XafApplication app, int maxSize = 10) {
        _app = app; _maxSize = maxSize;
    }

    public IObjectSpace Rent() =>
        _pool.TryDequeue(out var os)
            ? (Interlocked.Decrement(ref _currentSize), os).os
            : _app.CreateObjectSpace();

    public void Return(IObjectSpace os) {
        if (os == null) return;
        if (os.IsModified) os.ReloadChangedObjects();
        if (_currentSize < _maxSize) {
            _pool.Enqueue(os);
            Interlocked.Increment(ref _currentSize);
        } else os.Dispose();
    }

    public void Dispose() {
        while (_pool.TryDequeue(out var os)) os.Dispose();
    }
}

CollectionSource Disposal

public class ListController : ViewController<ListView> {
    private CollectionSource _cs;

    protected override void OnActivated() {
        base.OnActivated();
        _cs = new CollectionSource(ObjectSpace, typeof(MyObject));
        _cs.Criteria["Date"] = CriteriaOperator.Parse(
            "CreatedOn >= ?", DateTime.Today.AddDays(-30));
        View.CollectionSource = _cs;
    }

    protected override void OnDeactivated() {
        _cs?.Dispose(); _cs = null;
        base.OnDeactivated();
    }

    protected override void Dispose(bool disposing) {
        if (disposing) { _cs?.Dispose(); _cs = null; }
        base.Dispose(disposing);
    }
}

Session / HttpContext (WebForms Only)

Anti-Patterns

// ❌ ObjectSpace in Session — lives until session expires (30 min+ of retained objects)
Session["XafObjectSpace"] = Application.CreateObjectSpace();

// ❌ Large collection in Session
Session["AllCustomers"] = objectSpace.GetObjects<Customer>();

// ❌ No cleanup on session end
protected void Application_Session_End(object sender, EventArgs e) {
    // Missing: dispose XAF objects
}

Correct Patterns

// ✅ Request-scoped ObjectSpace (disposed at end of request)
public static IObjectSpace GetRequestObjectSpace(XafApplication app) {
    var ctx = HttpContext.Current;
    if (ctx == null) return app.CreateObjectSpace();

    var os = ctx.Items["XafOs"] as IObjectSpace;
    if (os == null) {
        os = app.CreateObjectSpace();
        ctx.Items["XafOs"] = os;
        ctx.DisposeOnPipelineCompleted(os); // auto-dispose at request end
    }
    return os;
}

// ✅ Session_End cleanup — dispose all IDisposable objects stored in session
public class SessionCleanupModule : IHttpModule {
    public void Init(HttpApplication context) {
        context.Session_End += (s, e) => {
            var session = HttpContext.Current?.Session;
            if (session == null) return;
            foreach (string key in session.Keys.Cast<string>().ToList()) {
                if (session[key] is IDisposable d) {
                    try { d.Dispose(); session.Remove(key); }
                    catch (Exception ex) { Tracing.Tracer.LogError(ex.ToString()); }
                }
            }
        };
    }
    public void Dispose() { }
}

Controller Lifecycle Tracker (Diagnostic)

Detect duplicate activations or missing deactivation in development:

public class ControllerLifecycleTracker {
    private static readonly Dictionary<string, int> _active = new();

    public static void TrackActivation(Controller controller) {
        var key = controller.GetType().Name;
        _active[key] = _active.GetValueOrDefault(key, 0) + 1;
        if (_active[key] > 1)
            Tracing.Tracer.LogWarning(
                $"Multiple active instances of {key}: {_active[key]}");
    }

    public static void TrackDeactivation(Controller controller) {
        var key = controller.GetType().Name;
        if (_active.ContainsKey(key))
            if (--_active[key] <= 0) _active.Remove(key);
    }
}

// In your controller (development only):
protected override void OnActivated() {
    base.OnActivated();
    ControllerLifecycleTracker.TrackActivation(this);
}
protected override void OnDeactivated() {
    ControllerLifecycleTracker.TrackDeactivation(this);
    base.OnDeactivated();
}

Navigation Memory Monitor (Diagnostic)

public class NavigationMonitorController : WindowController {
    private static int _navCount;
    private static readonly Dictionary<string, int> _viewCounts = new();

    protected override void OnFrameAssigned() {
        base.OnFrameAssigned();
        if (Frame != null) Frame.ViewChanged += Frame_ViewChanged;
    }

    private void Frame_ViewChanged(object sender, ViewChangedEventArgs e) {
        _navCount++;
        if (e.View != null) {
            var t = e.View.GetType().Name;
            _viewCounts[t] = _viewCounts.GetValueOrDefault(t, 0) + 1;
        }

        if (_navCount % 10 == 0) {
            GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
            Tracing.Tracer.LogValue("Navigations", _navCount);
            Tracing.Tracer.LogValue("Memory after nav",
                GC.GetTotalMemory(true));
        }
    }

    protected override void Dispose(bool disposing) {
        if (disposing && Frame != null)
            Frame.ViewChanged -= Frame_ViewChanged;
        base.Dispose(disposing);
    }
}

Static Reference Anti-Patterns

// ❌ Never store controller/view/ObjectSpace in static fields
private static List<Controller> _allControllers = new();  // leak
private static IObjectSpace _sessionOs;                    // leak
private static MyController _instance;                     // leak

// ✅ Use instance fields; clean up in Dispose
private readonly List<IDisposable> _ownedResources = new();

Warning Signs

| Sign | Likely Cause | |---|---| | Memory grows with each navigation | Event handlers not unsubscribed in OnDeactivated | | Same handler fires multiple times | Controller activated multiple times without deactivation | | Memory scales with user count | ObjectSpace or objects stored in Session | | OutOfMemoryException under normal load | ObjectSpace retained / large unconstrained query | | GC pauses increase over time | Static collections or long-lived ObjectSpaces | | IIS app pool recycling | Session memory pressure | | Session timeout failures | Large objects (ObjectSpace) in session |


Diagnostic Tools

// Enable XAF tracing to monitor ObjectSpace create/dispose
Tracing.Tracer.Initialize(TraceLevel.Verbose,
    Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + "\\xaf_trace.log");

// Memory snapshot (development only)
var before = GC.GetTotalMemory(false);
GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
Tracing.Tracer.LogValue("Retained after GC", GC.GetTotalMemory(true));

// ObjectSpace internal tracking (debug reflection)
public static void LogObjectSpaceStats(IObjectSpace os) {
    if (os is not BaseObjectSpace baseOs) return;
    var modified = typeof(BaseObjectSpace)
        .GetField("modifiedObjects", BindingFlags.NonPublic | BindingFlags.Instance)
        ?.GetValue(baseOs) as IDictionary;
    Tracing.Tracer.LogValue("Modified objects in OS", modified?.Count ?? 0);
}

Profilers:

  • dotMemory (JetBrains) — best for XAF object tracking
  • PerfView (Microsoft, free) — .NET GC and heap analysis
  • Application Insights — production memory monitoring

Code Review Checklist

✅ Every View.Event += in OnActivated has -= in OnDeactivated AND Dispose
✅ No static fields storing Controller, View, or IObjectSpace references
✅ All IObjectSpace created outside the view use `using` or explicit Dispose
✅ CollectionSource disposed in OnDeactivated and Dispose
✅ Large dataset operations use batching (new ObjectSpace per batch)
✅ No ObjectSpace stored in Session, Application, or singleton services
✅ Session_End handler disposes all IDisposable objects in Session (WebForms)
✅ Dispose(bool) calls base.Dispose(disposing) as the last statement
✅ Double-disposal safe (guard with bool _disposed)
✅ Disposal methods wrap each operation in try/catch to ensure full cleanup

Source Links

  • Controller lifecycle: https://docs.devexpress.com/eXpressAppFramework/112621/ui-construction/controllers-and-actions
  • IObjectSpace.Dispose: https://docs.devexpress.com/eXpressAppFramework/DevExpress.ExpressApp.IObjectSpace
  • XAF Tracing: https://docs.devexpress.com/eXpressAppFramework/112649/debugging-testing-and-error-handling/tracing
  • XafMemoryLeakDiagnosticMcp (source): https://github.com/egarim/XafMemoryLeakDiagnosticMcp
  • dotMemory: https://www.jetbrains.com/dotmemory/
  • PerfView: https://github.com/microsoft/perfview