Skip to content

Commit

Permalink
react: Update only dependents of the just-set cell (#131)
Browse files Browse the repository at this point in the history
* react: Update only dependents of the just-set cell

The initial version of react (see #126) let the reactor store all cells
in a linked list, then blindly updated all cells whenever any cell was
set. This was correct but wasteful.

This commit rearchitects the example solution so that:

* reactors only keep track of the input cells
* all other cells keep track of their dependent compute cells
* only dependents get updated when a cell is set

Space cost of:

* one extra `struct child` allocated per compute2 cell created.
* one extra integer field per cell.

* react: Add NULL checks on all allocations
  • Loading branch information
petertseng authored and ryanplusplus committed Apr 7, 2017
1 parent 6b67dc5 commit a583448
Showing 1 changed file with 106 additions and 34 deletions.
140 changes: 106 additions & 34 deletions exercises/react/src/example.c
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,20 @@ struct cb {
};

struct reactor {
struct child *first;
struct child *last;
struct child *input;
};

struct cell {
struct reactor *reactor;
int value;
enum cell_kind kind;
struct child *child;

struct cell *input1;
struct cell *input2;
compute1 compute1;
compute2 compute2;

int last_cb_value;
struct cb *cb;
int callbacks_issued;
};
Expand All @@ -41,42 +43,74 @@ struct reactor *create_reactor()
return calloc(1, sizeof(struct reactor));
}

void destroy_reactor(struct reactor *r)
static void destroy_cell(struct cell *c)
{
struct child *child = r->first;
while (child) {
struct cb *cb = child->cell->cb;
while (cb) {
struct cb *next_cb = cb->next;
free(cb);
cb = next_cb;
}
free(child->cell);
struct cb *cb = c->cb;
while (cb) {
struct cb *next_cb = cb->next;
free(cb);
cb = next_cb;
}

struct child *child = c->child;
while (child) {
struct child *next = child->next;
if (c == child->cell->input1) {
// Don't double-free for a compute2 cell.
destroy_cell(child->cell);
}
free(child);
child = next;
}

free(c);
}

void destroy_reactor(struct reactor *r)
{
struct child *input = r->input;
while (input) {
struct child *next = input->next;
destroy_cell(input->cell);
free(input);
input = next;
}

free(r);
}

static void add_child(struct reactor *r, struct cell *cell)
#define SUCCESS 1
#define FAIL 0

static int add_child(struct child **list, struct cell *cell)
{
struct child *child = calloc(1, sizeof(struct child));
child->cell = cell;
if (!r->first) {
r->first = child;
} else {
r->last->next = child;
if (!child) {
return FAIL;
}
r->last = child;
child->cell = cell;
child->next = *list;
*list = child;
return SUCCESS;
}

static void remove_child(struct child **list)
{
struct child *to_remove = *list;
*list = to_remove->next;
free(to_remove);
}

struct cell *create_input_cell(struct reactor *r, int initial_value)
{
struct cell *c = calloc(1, sizeof(struct cell));
add_child(r, c);
c->reactor = r;
if (!c) {
return NULL;
}
if (add_child(&r->input, c) != SUCCESS) {
free(c);
return NULL;
}
c->kind = kind_input;
c->value = initial_value;
return c;
Expand All @@ -85,30 +119,51 @@ struct cell *create_input_cell(struct reactor *r, int initial_value)
struct cell *create_compute1_cell(struct reactor *r, struct cell *input,
compute1 compute)
{
(void)r;
struct cell *c = calloc(1, sizeof(struct cell));
add_child(r, c);
c->reactor = r;
if (!c) {
return NULL;
}
if (add_child(&input->child, c) != SUCCESS) {
free(c);
return NULL;
}
c->kind = kind_compute1;
c->input1 = input;
c->compute1 = compute;
c->value = compute(get_cell_value(input));
c->last_cb_value = c->value;
return c;
}

struct cell *create_compute2_cell(struct reactor *r, struct cell *input1,
struct cell *input2, compute2 compute)
{
(void)r;
struct cell *c = calloc(1, sizeof(struct cell));
add_child(r, c);
c->reactor = r;
if (!c) {
return NULL;
}
if (add_child(&input1->child, c) != SUCCESS) {
free(c);
return NULL;
}
if (add_child(&input2->child, c) != SUCCESS) {
remove_child(&input1->child);
free(c);
return NULL;
}
c->kind = kind_compute2;
c->input1 = input1;
c->input2 = input2;
c->compute2 = compute;
c->value = compute(get_cell_value(input1), get_cell_value(input2));
c->last_cb_value = c->value;
return c;
}

#define each_child(c) struct child *child = (c)->child; child; child = child->next

int get_cell_value(struct cell *c)
{
return c->value;
Expand All @@ -131,29 +186,46 @@ static void propagate(struct cell *c)

if (new_value != c->value) {
c->value = new_value;
for (struct cb * cb = c->cb; cb; cb = cb->next) {
cb->f(cb->obj, new_value);
for (each_child(c)) {
propagate(child->cell);
}
}
}

static void fire_callbacks(struct cell *c)
{
if (c->value == c->last_cb_value) {
return;
}
c->last_cb_value = c->value;
for (struct cb * cb = c->cb; cb; cb = cb->next) {
cb->f(cb->obj, c->value);
}
for (each_child(c)) {
fire_callbacks(child->cell);
}
}

void set_cell_value(struct cell *c, int new_value)
{
c->value = new_value;
struct reactor *r = c->reactor;

// We take the very naive route of updating all cells.
// Traversing the tree and only updating the cells dependent on the just-changed cell is possible,
// but requires much more memory management to make each cell aware of its dependents.
for (struct child * child = r->first; child; child = child->next) {
for (each_child(c)) {
propagate(child->cell);
}
for (each_child(c)) {
// Why can't we put propagate and fire_callbacks in same for loop?
// Because then a compute2 cell might fire its callbacks too early
// (before it's seen updates from both of its inputs)!
fire_callbacks(child->cell);
}
}

callback_id add_callback(struct cell *c, void *obj, callback f)
{
struct cb *cb = calloc(1, sizeof(struct cb));
if (!cb) {
return -1;
}
cb->id = c->callbacks_issued++;
cb->next = c->cb;
cb->obj = obj;
Expand Down

0 comments on commit a583448

Please sign in to comment.