Skip to content

Commit

Permalink
Merge pull request #191 from akash-akya/dev
Browse files Browse the repository at this point in the history
Do resource cleanup on a dirty scheduler
  • Loading branch information
akash-akya authored Jan 7, 2025
2 parents 16774f1 + eb0991b commit 516a929
Show file tree
Hide file tree
Showing 9 changed files with 221 additions and 7 deletions.
50 changes: 47 additions & 3 deletions c_src/g_object/g_boxed.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

ErlNifResourceType *G_BOXED_RT;

static ERL_NIF_TERM ATOM_UNREF_GBOXED;

bool erl_term_to_g_boxed(ErlNifEnv *env, ERL_NIF_TERM term, gpointer *ptr) {
GBoxedResource *boxed_r = NULL;

Expand All @@ -29,6 +31,26 @@ bool erl_term_boxed_type(ErlNifEnv *env, ERL_NIF_TERM term, GType *type) {
return false;
}

ERL_NIF_TERM nif_g_boxed_unref(ErlNifEnv *env, int argc,
const ERL_NIF_TERM argv[]) {
ASSERT_ARGC(argc, 1);

GBoxedResource *gboxed_r = NULL;

if (!enif_get_resource(env, argv[0], G_BOXED_RT, (void **)&gboxed_r)) {
// This should never happen, since g_boxed_unref is an internal call
return ATOM_ERROR;
}

g_boxed_free(gboxed_r->boxed_type, gboxed_r->boxed_ptr);

gboxed_r->boxed_ptr = NULL;

debug("GBoxed unref");

return ATOM_OK;
}

ERL_NIF_TERM boxed_to_erl_term(ErlNifEnv *env, gpointer ptr, GType type) {
ERL_NIF_TERM term;
GBoxedResource *boxed_r;
Expand All @@ -47,9 +69,29 @@ ERL_NIF_TERM boxed_to_erl_term(ErlNifEnv *env, gpointer ptr, GType type) {
}

static void g_boxed_dtor(ErlNifEnv *env, void *obj) {
GBoxedResource *boxed_r = (GBoxedResource *)obj;
g_boxed_free(boxed_r->boxed_type, boxed_r->boxed_ptr);
debug("GBoxedResource dtor");
GBoxedResource *orig_boxed_r = (GBoxedResource *)obj;

/*
* Safely unref objects using the janitor process.
* See g_object_dtor() for details
*/
if (orig_boxed_r->boxed_ptr != NULL) {
GBoxedResource *temp_gboxed_r = NULL;
ERL_NIF_TERM temp_term;

temp_gboxed_r = enif_alloc_resource(G_BOXED_RT, sizeof(GBoxedResource));
temp_gboxed_r->boxed_ptr = orig_boxed_r->boxed_ptr;
temp_gboxed_r->boxed_type = orig_boxed_r->boxed_type;

temp_term = enif_make_resource(env, temp_gboxed_r);
enif_release_resource(temp_gboxed_r);

send_to_janitor(env, ATOM_UNREF_GBOXED, temp_term);

debug("GBoxedResource is sent to janitor process");
} else {
debug("GBoxedResource is already unset");
}
}

int nif_g_boxed_init(ErlNifEnv *env) {
Expand All @@ -62,5 +104,7 @@ int nif_g_boxed_init(ErlNifEnv *env) {
return 1;
}

ATOM_UNREF_GBOXED = make_atom(env, "unref_gboxed");

return 0;
}
3 changes: 3 additions & 0 deletions c_src/g_object/g_boxed.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ bool erl_term_to_g_boxed(ErlNifEnv *env, ERL_NIF_TERM term, gpointer *ptr);

bool erl_term_boxed_type(ErlNifEnv *env, ERL_NIF_TERM term, GType *type);

ERL_NIF_TERM nif_g_boxed_unref(ErlNifEnv *env, int argc,
const ERL_NIF_TERM argv[]);

ERL_NIF_TERM boxed_to_erl_term(ErlNifEnv *env, gpointer ptr, GType type);

int nif_g_boxed_init(ErlNifEnv *env);
Expand Down
75 changes: 71 additions & 4 deletions c_src/g_object/g_object.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@

ErlNifResourceType *G_OBJECT_RT;

// Ownership is traferred to beam resource, `obj` must *not* be freed
static ERL_NIF_TERM ATOM_UNREF_GOBJECT;

// Ownership is transferred to beam, `obj` must *not* be freed
// by the caller
ERL_NIF_TERM g_object_to_erl_term(ErlNifEnv *env, GObject *obj) {
ERL_NIF_TERM term;
Expand Down Expand Up @@ -36,6 +38,25 @@ ERL_NIF_TERM nif_g_object_type_name(ErlNifEnv *env, int argc,
return make_binary(env, G_OBJECT_TYPE_NAME(obj));
}

ERL_NIF_TERM nif_g_object_unref(ErlNifEnv *env, int argc,
const ERL_NIF_TERM argv[]) {
ASSERT_ARGC(argc, 1);

GObjectResource *gobject_r = NULL;

if (!enif_get_resource(env, argv[0], G_OBJECT_RT, (void **)&gobject_r)) {
// This should never happen, since g_object_unref is an internal call
return ATOM_ERROR;
}

g_object_unref(gobject_r->obj);
gobject_r->obj = NULL;

debug("GObject unref");

return ATOM_OK;
}

bool erl_term_to_g_object(ErlNifEnv *env, ERL_NIF_TERM term, GObject **obj) {
GObjectResource *gobject_r = NULL;
if (enif_get_resource(env, term, G_OBJECT_RT, (void **)&gobject_r)) {
Expand All @@ -46,9 +67,53 @@ bool erl_term_to_g_object(ErlNifEnv *env, ERL_NIF_TERM term, GObject **obj) {
}

static void g_object_dtor(ErlNifEnv *env, void *ptr) {
GObjectResource *gobject_r = (GObjectResource *)ptr;
g_object_unref(gobject_r->obj);
debug("GObjectResource dtor");
GObjectResource *orig_gobject_r = (GObjectResource *)ptr;

/**
* The resource destructor is executed inside a normal scheduler instead of a
* dirty scheduler, which can cause issues if the code is time-consuming.
* See: https://erlangforums.com/t/4290
*
* To address this, we avoid performing time-consuming work in the destructor
* and offload it to a janitor process. The Janitor process then calls the
* time-consuming cleanup NIF code on a dirty scheduler. Since Beam
* deallocates the resource at the end of the `dtor` call, we must create a
* new resource term to pass the object to the janitor process.
*
* Resources can be of two types:
*
* 1. Normal Resource: Constructed during normal operations; the pointer to
* the object is never NULL in this case.
*
* 2. Internal Resource: Constructed within the `dtor` of a normal resource
* solely for cleanup purposes and not for image processing operations. The
* pointer to the object will be NULL after cleanup.
*
* Currently, we use this length approach for all `g_object` and
* `g_boxed` objects, including smaller types like `VipsArray` of
* integers or doubles. For these smaller objects, it might be more
* efficient to skip certain steps. However, we are deferring the
* implementation of such special cases to keep the code simple for
* now.
*
*/
if (orig_gobject_r->obj != NULL) {
GObjectResource *temp_gobject_r = NULL;
ERL_NIF_TERM temp_term;

/* Create temporary internal resource for the cleanup */
temp_gobject_r = enif_alloc_resource(G_OBJECT_RT, sizeof(GObjectResource));
temp_gobject_r->obj = orig_gobject_r->obj;

temp_term = enif_make_resource(env, temp_gobject_r);
enif_release_resource(temp_gobject_r);
send_to_janitor(env, ATOM_UNREF_GOBJECT, temp_term);
debug("GObjectResource is sent to janitor process");
} else {
debug("GObjectResource is already unset");
}

return;
}

int nif_g_object_init(ErlNifEnv *env) {
Expand All @@ -61,5 +126,7 @@ int nif_g_object_init(ErlNifEnv *env) {
return 1;
}

ATOM_UNREF_GOBJECT = make_atom(env, "unref_gobject");

return 0;
}
3 changes: 3 additions & 0 deletions c_src/g_object/g_object.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ ERL_NIF_TERM g_object_to_erl_term(ErlNifEnv *env, GObject *obj);
ERL_NIF_TERM nif_g_object_type_name(ErlNifEnv *env, int argc,
const ERL_NIF_TERM argv[]);

ERL_NIF_TERM nif_g_object_unref(ErlNifEnv *env, int argc,
const ERL_NIF_TERM argv[]);

bool erl_term_to_g_object(ErlNifEnv *env, ERL_NIF_TERM term, GObject **obj);

int nif_g_object_init(ErlNifEnv *env);
Expand Down
33 changes: 33 additions & 0 deletions c_src/utils.c
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ ERL_NIF_TERM ATOM_NULL_VALUE;
ERL_NIF_TERM ATOM_UNDEFINED;
ERL_NIF_TERM ATOM_EAGAIN;

/**
* Name of the process responsible for cleanup, we send resources
* requiring cleanup to this process.
*/
static ERL_NIF_TERM VIX_JANITOR_PROCESS_NAME;

const guint VIX_LOG_LEVEL_NONE = 0;
const guint VIX_LOG_LEVEL_WARNING = 1;
const guint VIX_LOG_LEVEL_ERROR = 2;
Expand Down Expand Up @@ -102,6 +108,26 @@ VixResult vix_result(ERL_NIF_TERM term) {
return (VixResult){.is_success = true, .result = term};
}

void send_to_janitor(ErlNifEnv *env, ERL_NIF_TERM label,
ERL_NIF_TERM resource_term) {
ErlNifPid pid;

/* Currently there is no way to raise error when any of the
condition fail. Realistically this should never fail */
if (!enif_whereis_pid(env, VIX_JANITOR_PROCESS_NAME, &pid)) {
error("Failed to get pid for vix janitor process");
return;
}

if (!enif_send(env, &pid, NULL,
enif_make_tuple2(env, label, resource_term))) {
error("Failed to send unref msg to vix janitor");
return;
}

return;
}

static void vix_binary_dtor(ErlNifEnv *env, void *ptr) {
VixBinaryResource *vix_bin_r = (VixBinaryResource *)ptr;
g_free(vix_bin_r->data);
Expand All @@ -118,6 +144,8 @@ int utils_init(ErlNifEnv *env, const char *log_level) {
ATOM_UNDEFINED = make_atom(env, "undefined");
ATOM_EAGAIN = make_atom(env, "eagain");

VIX_JANITOR_PROCESS_NAME = make_atom(env, "Elixir.Vix.Nif.Janitor");

VIX_BINARY_RT = enif_open_resource_type(
env, NULL, "vix_binary_resource", (ErlNifResourceDtor *)vix_binary_dtor,
ERL_NIF_RT_CREATE | ERL_NIF_RT_TAKEOVER, NULL);
Expand All @@ -127,7 +155,12 @@ int utils_init(ErlNifEnv *env, const char *log_level) {
} else if (strcmp(log_level, "error") == 0) {
VIX_LOG_LEVEL = VIX_LOG_LEVEL_ERROR;
} else {
#ifdef DEBUG
// default to ERROR if we are running in debug mode
VIX_LOG_LEVEL = VIX_LOG_LEVEL_ERROR;
#else
VIX_LOG_LEVEL = VIX_LOG_LEVEL_NONE;
#endif
}

if (VIX_LOG_LEVEL == VIX_LOG_LEVEL_WARNING ||
Expand Down
3 changes: 3 additions & 0 deletions c_src/utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -124,4 +124,7 @@ void notify_consumed_timeslice(ErlNifEnv *env, ErlNifTime start,

ERL_NIF_TERM to_binary_term(ErlNifEnv *env, void *data, size_t size);

void send_to_janitor(ErlNifEnv *env, ERL_NIF_TERM label,
ERL_NIF_TERM resource_term);

#endif
2 changes: 2 additions & 0 deletions c_src/vix.c
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ static int on_load(ErlNifEnv *env, void **priv, ERL_NIF_TERM load_info) {
static ErlNifFunc nif_funcs[] = {
/* GObject */
{"nif_g_object_type_name", 1, nif_g_object_type_name, 0},
{"nif_g_object_unref", 1, nif_g_object_unref, ERL_NIF_DIRTY_JOB_CPU_BOUND},

/* GType */
{"nif_g_type_from_instance", 1, nif_g_type_from_instance, 0},
Expand Down Expand Up @@ -151,6 +152,7 @@ static ErlNifFunc nif_funcs[] = {
{"nif_vips_blob_to_erl_binary", 1, nif_vips_blob_to_erl_binary, 0},
{"nif_vips_ref_string_to_erl_binary", 1, nif_vips_ref_string_to_erl_binary,
0},
{"nif_g_boxed_unref", 1, nif_g_boxed_unref, ERL_NIF_DIRTY_JOB_CPU_BOUND},

/* VipsForeign */
{"nif_foreign_find_load", 1, nif_foreign_find_load, 0},
Expand Down
58 changes: 58 additions & 0 deletions lib/vix/nif.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,59 @@ defmodule Vix.Nif do
@moduledoc false
@on_load :load_nifs

defmodule Janitor do
@moduledoc false
use GenServer

# Singleton process to safely cleanup native resources
alias __MODULE__

require Logger

def start do
GenServer.start(Janitor, nil, name: Janitor)
end

## Callbacks

@impl true
def init(nil) do
{:ok, nil}
end

@allowd_types [:unref_gobject, :unref_gboxed]

@impl true
def handle_info({type, term}, nil) when type in @allowd_types do
# Use a dedicated process to prevent blocking the Janitor
# singleton process. The task process is not linked to the
# parent to ensure that errors do not cause the Janitor to
# crash.
_ = Task.start(Janitor, :unref, [type, term])
{:noreply, nil}
end

@doc false
@spec unref(atom, reference()) :: :ok
def unref(type, term) do
case type do
:unref_gboxed ->
:ok = Vix.Nif.nif_g_boxed_unref(term)

:unref_gobject ->
:ok = Vix.Nif.nif_g_object_unref(term)
end
end
end

def load_nifs do
# must be started outside the supervision tree since the process
# that calls `load_nifs` will exit eventually.
case Vix.Nif.Janitor.start() do
{:ok, _pid} -> :ok
{:error, {:already_started, _pid}} -> :ok
end

nif_path = :filename.join(:code.priv_dir(:vix), "vix")
:erlang.load_nif(nif_path, load_config())
end
Expand All @@ -11,6 +63,9 @@ defmodule Vix.Nif do
def nif_g_object_type_name(_obj),
do: :erlang.nif_error(:nif_library_not_loaded)

def nif_g_object_unref(_obj),
do: :erlang.nif_error(:nif_library_not_loaded)

# GType
def nif_g_type_from_instance(_instance),
do: :erlang.nif_error(:nif_library_not_loaded)
Expand Down Expand Up @@ -173,6 +228,9 @@ defmodule Vix.Nif do
def nif_vips_ref_string_to_erl_binary(_vips_blob),
do: :erlang.nif_error(:nif_library_not_loaded)

def nif_g_boxed_unref(_obj),
do: :erlang.nif_error(:nif_library_not_loaded)

# VipsForeign
def nif_foreign_find_load_buffer(_binary),
do: :erlang.nif_error(:nif_library_not_loaded)
Expand Down
1 change: 1 addition & 0 deletions test/test_helper.exs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
Logger.configure(level: :warning)
ExUnit.start()

0 comments on commit 516a929

Please sign in to comment.