Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
7 changes: 7 additions & 0 deletions binding/SkiaSharp/DelegateProxies.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,5 +104,12 @@ private static partial void SKGlyphPathProxyImplementation (IntPtr pathOrNull, S
var path = SKPath.GetObject (pathOrNull, false);
del.Invoke (path, *matrix);
}

private static partial void SKMemoryThresholdProxyImplementation ()
{
// Fires on the native allocator's thread. Must return immediately
// without taking managed locks or calling back into Skia.
SKNativeMemoryPressureMonitor.OnNativeThresholdCrossed ();
}
}
}
19 changes: 19 additions & 0 deletions binding/SkiaSharp/SKGraphics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,24 @@ public static long SetResourceCacheSingleAllocationByteLimit (long bytes) =>

public static void DumpMemoryStatistics (SKTraceMemoryDump dump) =>
SkiaApi.sk_graphics_dump_memory_statistics (dump?.Handle ?? throw new ArgumentNullException (nameof (dump)));

// native memory accounting

/// <summary>
/// Returns the total number of bytes currently held by SkiaSharp's
/// native allocator (the sum of all outstanding
/// <c>sk_malloc</c>/<c>sk_realloc</c> allocations, measured via the
/// platform's <c>malloc_usable_size</c>/<c>_msize</c>/<c>malloc_size</c>).
/// </summary>
/// <remarks>
/// This counter is updated atomically by the allocator on every
/// alloc/free. It does not include allocations made by third-party
/// libraries linked into the SkiaSharp native binary (libpng,
/// freetype, harfbuzz, etc.) that call <c>malloc</c> directly
/// without going through Skia's allocator.
/// </remarks>
/// <seealso cref="SKNativeMemoryPressureMonitor"/>
public static long GetNativeMemoryAllocated () =>
(long)SkiaApi.sk_memory_get_native_allocated ();
}
}
189 changes: 189 additions & 0 deletions binding/SkiaSharp/SKNativeMemoryPressureMonitor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
#nullable disable

using System;
using System.Threading;

namespace SkiaSharp
{
/// <summary>
/// Reports SkiaSharp's outstanding native memory usage to the managed
/// garbage collector via
/// <see cref="GC.AddMemoryPressure(long)"/> and
/// <see cref="GC.RemoveMemoryPressure(long)"/>.
/// </summary>
/// <remarks>
/// <para>
/// The GC has no native visibility into Skia's pixel buffers, paths,
/// glyph caches, etc. Without this monitor, a process can hold gigabytes
/// of native memory through SkiaSharp wrappers without the GC ever
/// being prompted to reclaim eligible managed wrappers.
/// </para>
/// <para>
/// Implementation: SkiaSharp's native allocator tracks outstanding
/// bytes in an atomic counter. When the counter drifts past +/- a
/// configurable threshold since the last notification, the allocator
/// fires a callback (on the thread that performed the allocation) which
/// queues a single <see cref="ThreadPool"/> work item that reconciles
/// the delta with the GC. No background timer, no managed polling.
/// </para>
/// <para>
/// The monitor is opt-in. Enable it by calling <see cref="Start()"/>
/// (typically at application startup), and disable it with
/// <see cref="Stop"/>.
/// </para>
/// </remarks>
public static unsafe class SKNativeMemoryPressureMonitor
{
/// <summary>
/// The default threshold (1 MB) used by <see cref="Start()"/>.
/// </summary>
public const long DefaultThresholdBytes = 1024 * 1024;

private static readonly object Sync = new object ();

private static bool s_running;
private static long s_reportedPressure;
private static int s_pendingReconcile;

/// <summary>
/// Gets a value indicating whether the monitor is currently
/// installed and reporting pressure to the GC.
/// </summary>
public static bool IsRunning {
get {
lock (Sync) {
return s_running;
}
}
}

/// <summary>
/// The number of bytes currently reported to the GC via
/// <see cref="GC.AddMemoryPressure(long)"/>. Returns to zero after
/// <see cref="Stop"/>. Provided for diagnostics.
/// </summary>
public static long ReportedPressure {
get {
lock (Sync) {
return s_reportedPressure;
}
}
}

/// <summary>
/// Installs the monitor with the default 1 MB threshold.
/// </summary>
/// <remarks>
/// Idempotent: calling <see cref="Start()"/> while already running
/// is a no-op.
/// </remarks>
public static void Start () =>
Start (DefaultThresholdBytes);

/// <summary>
/// Installs the monitor with a custom delta threshold. The native
/// allocator notifies the managed layer each time the outstanding
/// allocation has changed by at least <paramref name="thresholdBytes"/>
/// since the last notification.
/// </summary>
/// <param name="thresholdBytes">
/// Minimum signed delta (in bytes) that triggers a notification.
/// Smaller values report pressure more accurately but fire the
/// callback more often. Must be positive.
/// </param>
/// <exception cref="ArgumentOutOfRangeException">
/// <paramref name="thresholdBytes"/> is zero or negative.
/// </exception>
public static void Start (long thresholdBytes)
{
if (thresholdBytes <= 0)
throw new ArgumentOutOfRangeException (nameof (thresholdBytes), "Threshold must be positive.");

lock (Sync) {
if (s_running)
return;
SkiaApi.sk_memory_set_threshold_callback (
DelegateProxies.SKMemoryThresholdProxy, (ulong)thresholdBytes);
s_running = true;
// Anchor s_reportedPressure to the current counter so the
// first callback's delta isn't inflated by allocations that
// pre-date Start().
Reconcile ();
}
}

/// <summary>
/// Detaches the monitor and releases any pressure previously
/// reported to the GC. Safe to call when the monitor is not running.
/// </summary>
public static void Stop ()
{
lock (Sync) {
if (!s_running)
return;
// Detach the native callback first so no new fires arrive.
SkiaApi.sk_memory_set_threshold_callback (null, 0);
s_running = false;

// Release any pressure we previously reported. In-flight
// ThreadPool reconciles that run after Stop() will see
// s_running == false and refuse to add new pressure.
if (s_reportedPressure > 0) {
GC.RemoveMemoryPressure (s_reportedPressure);
s_reportedPressure = 0;
}
}
}

// Invoked (indirectly, via DelegateProxies) from the native
// allocator when the threshold is crossed. Runs on whatever thread
// did the allocation -- must not take any lock that could be held
// by the caller, must not call back into Skia, must return
// immediately.
internal static void OnNativeThresholdCrossed ()
{
// Coalesce: if a reconcile is already pending, drop this fire.
if (Interlocked.Exchange (ref s_pendingReconcile, 1) == 1)
return;

ThreadPool.UnsafeQueueUserWorkItem (static _ => {
// Clear the pending flag BEFORE reconciling so a fire that
// arrives mid-reconcile schedules a follow-up tick rather
// than getting dropped.
Interlocked.Exchange (ref s_pendingReconcile, 0);
try {
lock (Sync) {
Reconcile ();
}
} catch {
// Never propagate from a ThreadPool work item.
}
}, null);
}

// Must be called under Sync.
private static void Reconcile ()
{
long current = (long)SkiaApi.sk_memory_get_native_allocated ();
if (current < 0)
current = 0;

long delta = current - s_reportedPressure;
if (delta == 0)
return;

// After Stop() has run, suppress new additions to avoid leaking
// pressure that nobody will ever release. Removals stay allowed
// (harmless).
if (delta > 0 && !s_running)
return;

s_reportedPressure = current;

if (delta > 0)
GC.AddMemoryPressure (delta);
else
GC.RemoveMemoryPressure (-delta);
}
}
}
56 changes: 56 additions & 0 deletions binding/SkiaSharp/SkiaApi.generated.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8455,6 +8455,48 @@ internal static bool sk_matrix_try_invert (SKMatrix* matrix, SKMatrix* result) =

#endregion

#region sk_memory.h

// uint64_t sk_memory_get_native_allocated()
#if !USE_DELEGATES
#if USE_LIBRARY_IMPORT
[LibraryImport (SKIA)]
internal static partial UInt64 sk_memory_get_native_allocated ();
#else // !USE_LIBRARY_IMPORT
[DllImport (SKIA, CallingConvention = CallingConvention.Cdecl)]
internal static extern UInt64 sk_memory_get_native_allocated ();
#endif
#else
private partial class Delegates {
[UnmanagedFunctionPointer (CallingConvention.Cdecl)]
internal delegate UInt64 sk_memory_get_native_allocated ();
}
private static Delegates.sk_memory_get_native_allocated sk_memory_get_native_allocated_delegate;
internal static UInt64 sk_memory_get_native_allocated () =>
(sk_memory_get_native_allocated_delegate ??= GetSymbol<Delegates.sk_memory_get_native_allocated> ("sk_memory_get_native_allocated")).Invoke ();
#endif

// void sk_memory_set_threshold_callback(sk_memory_threshold_proc callback, uint64_t threshold_bytes)
#if !USE_DELEGATES
#if USE_LIBRARY_IMPORT
[LibraryImport (SKIA)]
internal static partial void sk_memory_set_threshold_callback (void* callback, UInt64 threshold_bytes);
#else // !USE_LIBRARY_IMPORT
[DllImport (SKIA, CallingConvention = CallingConvention.Cdecl)]
internal static extern void sk_memory_set_threshold_callback (SKMemoryThresholdProxyDelegate callback, UInt64 threshold_bytes);
#endif
#else
private partial class Delegates {
[UnmanagedFunctionPointer (CallingConvention.Cdecl)]
internal delegate void sk_memory_set_threshold_callback (SKMemoryThresholdProxyDelegate callback, UInt64 threshold_bytes);
}
private static Delegates.sk_memory_set_threshold_callback sk_memory_set_threshold_callback_delegate;
internal static void sk_memory_set_threshold_callback (SKMemoryThresholdProxyDelegate callback, UInt64 threshold_bytes) =>
(sk_memory_set_threshold_callback_delegate ??= GetSymbol<Delegates.sk_memory_set_threshold_callback> ("sk_memory_set_threshold_callback")).Invoke (callback, threshold_bytes);
#endif

#endregion

#region sk_paint.h

// sk_paint_t* sk_paint_clone(sk_paint_t*)
Expand Down Expand Up @@ -17722,6 +17764,10 @@ namespace SkiaSharp {
[return: MarshalAs (UnmanagedType.I1)]
internal unsafe delegate bool SKManagedWStreamWriteProxyDelegate(sk_wstream_managedstream_t s, void* context, void* buffer, /* size_t */ IntPtr size);

// typedef void (*)()* sk_memory_threshold_proc
[UnmanagedFunctionPointer (CallingConvention.Cdecl)]
internal unsafe delegate void SKMemoryThresholdProxyDelegate();

// typedef void (*)(void* addr, void* context)* sk_surface_raster_release_proc
[UnmanagedFunctionPointer (CallingConvention.Cdecl)]
internal unsafe delegate void SKSurfaceRasterReleaseProxyDelegate(void* addr, void* context);
Expand Down Expand Up @@ -21924,6 +21970,16 @@ internal static unsafe partial class DelegateProxies {
[return: MarshalAs (UnmanagedType.I1)]
private static partial bool SKManagedWStreamWriteProxyImplementation(sk_wstream_managedstream_t s,void* context,void* buffer,/* size_t */ IntPtr size);

/// Proxy for sk_memory_threshold_proc native function.
#if USE_LIBRARY_IMPORT
public static readonly delegate* unmanaged[Cdecl] <void> SKMemoryThresholdProxy = &SKMemoryThresholdProxyImplementation;
[UnmanagedCallersOnly(CallConvs = new [] {typeof(CallConvCdecl)})]
#else
public static readonly SKMemoryThresholdProxyDelegate SKMemoryThresholdProxy = SKMemoryThresholdProxyImplementation;
[MonoPInvokeCallback (typeof (SKMemoryThresholdProxyDelegate))]
#endif
private static partial void SKMemoryThresholdProxyImplementation();

/// Proxy for sk_surface_raster_release_proc native function.
#if USE_LIBRARY_IMPORT
public static readonly delegate* unmanaged[Cdecl] <void*, void*, void> SKSurfaceRasterReleaseProxy = &SKSurfaceRasterReleaseProxyImplementation;
Expand Down
1 change: 1 addition & 0 deletions documentation/dev/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ C# Wrapper (binding/SkiaSharp/) → P/Invoke → C API (externals/skia/src/c
|----------|-------------|
| [architecture.md](architecture.md) | Three layers, type mappings, call flow, threading |
| [memory-management.md](memory-management.md) | Pointer types, ownership, lifecycle |
| [native-memory-pressure.md](native-memory-pressure.md) | Opt-in `GC.AddMemoryPressure` reporting for Skia native allocations |
| [error-handling.md](error-handling.md) | Error patterns across layers |

### Contributing
Expand Down
Loading
Loading