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
1 change: 1 addition & 0 deletions gn/core.gni
Original file line number Diff line number Diff line change
Expand Up @@ -858,6 +858,7 @@ skia_core_sources += [
"$_src/c/sk_imagefilter.cpp",
"$_src/c/sk_maskfilter.cpp",
"$_src/c/sk_matrix.cpp",
"$_src/c/sk_memory.cpp",
"$_src/c/sk_paint.cpp",
"$_src/c/sk_path.cpp",
"$_src/c/sk_pathbuilder.cpp",
Expand Down
38 changes: 38 additions & 0 deletions include/c/sk_memory.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2026 Microsoft Corporation. All rights reserved.
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/

#ifndef sk_memory_DEFINED
#define sk_memory_DEFINED

#include "include/c/sk_types.h"

SK_C_PLUS_PLUS_BEGIN_GUARD

// Total number of bytes currently held by Skia's allocator
// (sk_malloc/sk_calloc/sk_realloc allocations, measured by
// malloc_usable_size / _msize / malloc_size). Updated atomically on every
// allocation and free.
SK_C_API uint64_t sk_memory_get_native_allocated(void);

// Threshold-crossing notification. When installed, `callback` is invoked
// on the allocator's thread whenever the total native allocation delta
// since the last notification crosses +/- threshold_bytes.
//
// The callback fires from within the allocator hot path and may run on any
// thread; it MUST be reentrancy-safe and MUST NOT call back into Skia or
// take any lock that could be held by the caller. Typical implementation:
// flag a managed work item and return immediately.
//
// Pass callback=NULL or threshold_bytes=0 to disable notifications.
typedef void (*sk_memory_threshold_proc)(void);
SK_C_API void sk_memory_set_threshold_callback(
sk_memory_threshold_proc callback,
uint64_t threshold_bytes);

SK_C_PLUS_PLUS_END_GUARD

#endif
120 changes: 120 additions & 0 deletions src/c/sk_memory.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* Copyright 2026 Microsoft Corporation. All rights reserved.
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*
* Native memory accounting for SkiaSharp. The upstream allocator
* (src/ports/SkMemory_malloc.cpp) is instrumented with a handful of
* extern "C" hook calls into the helpers in this file; this file holds
* all of the bookkeeping state and exposes the public C-API.
*
* Why a separate file: the only code that lives in the upstream allocator
* is the hook call sites (a few lines, easy to maintain across Skia
* milestone bumps). Everything else -- the atomic counter, the threshold
* watermark, the CAS-claimed crossing logic, malloc_usable_size
* dispatching, the C-API -- lives here in the SkiaSharp C shim where
* customizations belong.
*/

#include "include/c/sk_memory.h"
#include "include/private/base/SkFeatures.h"

#include <atomic>
#include <cstdint>
#include <cstdlib>

#if defined(SK_BUILD_FOR_MAC) || defined(SK_BUILD_FOR_IOS)
#include <malloc/malloc.h>
#elif defined(SK_BUILD_FOR_ANDROID) || defined(SK_BUILD_FOR_UNIX)
#include <malloc.h>
#elif defined(SK_BUILD_FOR_WIN)
#include <malloc.h>
#endif

namespace {

std::atomic<int64_t> g_allocated{0};
std::atomic<sk_memory_threshold_proc> g_threshold_cb{nullptr};
std::atomic<int64_t> g_threshold{0};
std::atomic<int64_t> g_last_notified{0};

inline void notify_if_threshold_crossed(int64_t current) {
// Fast-path bail when nobody is listening.
auto cb = g_threshold_cb.load(std::memory_order_acquire);
if (cb == nullptr) {
return;
}
int64_t threshold = g_threshold.load(std::memory_order_relaxed);
if (threshold <= 0) {
return;
}
int64_t last = g_last_notified.load(std::memory_order_relaxed);
int64_t diff = current - last;
if (diff < 0) {
diff = -diff;
}
if (diff < threshold) {
return;
}
// Claim this crossing: only one thread succeeds in advancing
// last_notified, the rest bail and let the winner fire the callback.
// The managed side re-reads the current counter on its own, so a
// missed inter-thread delta is reconciled on the next crossing.
if (g_last_notified.compare_exchange_strong(
last, current, std::memory_order_relaxed)) {
cb();
}
}

} // namespace

// ----------------------------------------------------------------------
// Internal hooks called from src/ports/SkMemory_malloc.cpp
// (declared there as `extern "C"` forward decls).
// ----------------------------------------------------------------------

extern "C" size_t sk_memory_internal_size_of(void* p) {
if (p == nullptr) {
return 0;
}
#if defined(SK_BUILD_FOR_MAC) || defined(SK_BUILD_FOR_IOS)
return malloc_size(p);
#elif defined(SK_BUILD_FOR_ANDROID) || defined(SK_BUILD_FOR_UNIX)
return malloc_usable_size(p);
#elif defined(SK_BUILD_FOR_WIN)
return _msize(p);
#else
return 0;
#endif
}

extern "C" void sk_memory_internal_account_delta(int64_t bytes) {
if (bytes == 0) {
return;
}
int64_t prev = g_allocated.fetch_add(bytes, std::memory_order_relaxed);
notify_if_threshold_crossed(prev + bytes);
}

// ----------------------------------------------------------------------
// Public C-API.
// ----------------------------------------------------------------------

uint64_t sk_memory_get_native_allocated(void) {
int64_t v = g_allocated.load(std::memory_order_relaxed);
return v < 0 ? 0 : static_cast<uint64_t>(v);
}

void sk_memory_set_threshold_callback(
sk_memory_threshold_proc callback, uint64_t threshold_bytes) {
int64_t clamped = threshold_bytes > static_cast<uint64_t>(INT64_MAX)
? INT64_MAX
: static_cast<int64_t>(threshold_bytes);
g_threshold.store(clamped, std::memory_order_relaxed);
// Reset the watermark on (re)installation so the first crossing fires
// relative to the counter value at the time the callback was attached.
g_last_notified.store(g_allocated.load(std::memory_order_relaxed),
std::memory_order_relaxed);
g_threshold_cb.store(callback, std::memory_order_release);
}
22 changes: 21 additions & 1 deletion src/ports/SkMemory_malloc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include "include/private/base/SkMalloc.h"

#include <algorithm>
#include <cstdint>
#include <cstdlib>

#if defined(SK_BUILD_FOR_MAC) || defined(SK_BUILD_FOR_IOS)
Expand All @@ -35,6 +36,12 @@
#define SK_DEBUGFAILF(fmt, ...) SkASSERT((SkDebugf(fmt"\n", __VA_ARGS__), false))
#endif

// SkiaSharp native memory accounting hooks. Implementation in
// src/c/sk_memory.cpp. Updates a process-wide atomic counter and fires a
// threshold callback for the managed pressure monitor.
extern "C" size_t sk_memory_internal_size_of(void* p);
extern "C" void sk_memory_internal_account_delta(int64_t bytes);

static inline void sk_out_of_memory(size_t size) {
SK_DEBUGFAILF("sk_out_of_memory (asked for %zu bytes)",
size);
Expand Down Expand Up @@ -77,13 +84,22 @@ void* sk_realloc_throw(void* addr, size_t size) {
sk_free(addr);
return nullptr;
}
return throw_on_failure(size, realloc(addr, size));
size_t old_size = sk_memory_internal_size_of(addr); // SkiaSharp accounting
void* new_addr = realloc(addr, size);
if (new_addr != nullptr) {
sk_memory_internal_account_delta(
static_cast<int64_t>(sk_memory_internal_size_of(new_addr)) -
static_cast<int64_t>(old_size));
}
return throw_on_failure(size, new_addr);
}

void sk_free(void* p) {
// The guard here produces a performance improvement across many tests, and many platforms.
// Removing the check was tried in skia cl 588037.
if (p != nullptr) {
sk_memory_internal_account_delta(
-static_cast<int64_t>(sk_memory_internal_size_of(p))); // SkiaSharp accounting
free(p);
}
}
Expand All @@ -109,6 +125,10 @@ void* sk_malloc_flags(size_t size, unsigned flags) {
(void)mallopt(M_THREAD_DISABLE_MEM_INIT, 0);
#endif
}
if (p != nullptr) {
sk_memory_internal_account_delta( // SkiaSharp accounting
static_cast<int64_t>(sk_memory_internal_size_of(p)));
}
if (flags & SK_MALLOC_THROW) {
return throw_on_failure(size, p);
} else {
Expand Down