Skip to content

Flaky CI: GC-dependent lifecycle tests fail intermittently on .NET Framework #3609

@mattleibow

Description

@mattleibow

Problem

Several tests that verify object lifecycle and GC behavior fail intermittently on Windows .NET Framework. They all share the same pattern: call CollectGarbage(), then assert that objects have been finalized or reference counts have changed.

Failing tests (from 10 recent main builds)

Test Failures Error
SKColorSpaceTest.ColorSpaceIsNotDisposedPrematurely 2x Expected refcount 4, got 3
SKImageTest.DataOutLivesImageUntilFinalizersRun 1x Expected refcount 3, got 2
SKCodecTest.StreamLosesOwnershipAndCanBeGarbageCollected 1x Assert.True — object not collected
SKTypefaceTest.StreamLosesOwnershipAndCanBeGarbageCollected 1x Assert.True — object not collected
SKManagedWStreamTest.StreamIsNotDisposedWhenReferencedIsDisposed 1x Assert.True — stream unexpectedly disposed
SKManagedWStreamTest.StreamIsReferencedAndNotDisposedPrematurely 1x NullReferenceException (GC race)

Already skipped on Mono

Several of these tests call SkipOnMono(), acknowledging that GC behavior varies by runtime. The same non-determinism applies to .NET Framework under CI load.

Root Cause

All these tests assert specific GC/finalizer behavior:

  • CollectGarbage() + assert object is collected — GC is non-deterministic, especially on .NET Framework under heavy CI load with concurrent test execution. Finalizers may not run within the expected window.
  • Reference count assertions — Tests check native refcounts at specific points, but concurrent finalizer threads can alter refcounts between the CollectGarbage() call and the assertion.
  • The NullReferenceException in StreamIsReferencedAndNotDisposedPrematurely suggests a race where the finalizer disposes the stream between the GC call and the assertion.

Impact

Low individually (each fails ~1-2x per 10 builds), but collectively they contribute to CI noise. Combined with the x86 memory exhaustion tests (#3608), they make the Tests stage fail on nearly every build.

Recommendations

  1. Add retry/tolerance to GC assertions — Instead of a single CollectGarbage() + assert, retry the GC+assert loop a few times:

    // Instead of:
    CollectGarbage();
    Assert.True(collected);
    
    // Use:
    for (int i = 0; i < 5; i++)
    {
        CollectGarbage();
        if (collected) break;
        Thread.Sleep(100);
    }
    Assert.True(collected);
  2. Make reference count checks tolerant — For ColorSpaceIsNotDisposedPrematurely, the exact refcount may vary by ±1 depending on internal caching. Consider Assert.InRange instead of Assert.Equal.

  3. Gate on runtime — If .NET Framework's GC is too unpredictable, consider SkipOnNetFramework() for the most fragile tests, similar to the existing SkipOnMono().

  4. Improve CollectGarbage() helper — If it doesn't already, ensure it calls GC.Collect(), GC.WaitForPendingFinalizers(), GC.Collect() (double-collect pattern) and possibly with a small delay.

Metadata

Metadata

Assignees

No one assigned

    Labels

    os/Windows-ClassicIssues running on Microsoft Windows using Win32 APIs (Windows.Forms or WPF)type/bug

    Type

    No type

    Projects

    Status

    New

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions