aboutsummaryrefslogtreecommitdiffstats
path: root/core
diff options
context:
space:
mode:
Diffstat (limited to 'core')
-rw-r--r--core/module-list.c40
-rw-r--r--core/module-list.h14
-rw-r--r--core/module.c100
-rw-r--r--core/test-module.c57
4 files changed, 174 insertions, 37 deletions
diff --git a/core/module-list.c b/core/module-list.c
index f32fd05..861798e 100644
--- a/core/module-list.c
+++ b/core/module-list.c
@@ -67,7 +67,7 @@ struct PureList {
unsigned char *journal;
size_t journal_size, journal_capacity;
uint64_t id, version; /* which list, and a number every change moves on */
- uint64_t shares; /* copies made so far: a node or text made before the last one is shared */
+ uint64_t shares; /* the last copy's epoch: a node or text made before it is shared */
PureRetired *retired; /* where shared nodes and texts go when the list lets them go */
Node *spare[2]; /* inner nodes and leaves let go that no copy read, kept for the next ones (linked by first) */
unsigned spares[2];
@@ -195,6 +195,36 @@ void pure_retired_free(PureRetired *retired) {
*retired = (PureRetired){0};
}
+static uint64_t made_at(const Retired *r) {
+ return r->text ? ((const uint64_t *)r->pointer)[-1] : ((const Node *)r->pointer)->made;
+}
+
+/* Room for more entries in retired; false when memory runs out. */
+static bool retired_room(PureRetired *retired, size_t more) {
+ if (retired->count + more <= retired->capacity) return true;
+ size_t capacity = 2 * retired->capacity > retired->count + more ? 2 * retired->capacity : retired->count + more;
+ Retired *grown = realloc(retired->entries, capacity * sizeof(*grown));
+ if (!grown) return false;
+ retired->entries = grown;
+ retired->capacity = capacity;
+ return true;
+}
+
+void pure_retired_pass(PureRetired *from, uint64_t epoch, PureRetired *kept, PureRetired *gone) {
+ Retired *entries = from->entries;
+ size_t keeping = 0;
+ for (size_t i = 0; kept && i < from->count; ++i) keeping += made_at(&entries[i]) < epoch;
+ bool room = (!keeping || retired_room(kept, keeping)) &&
+ (!gone || keeping == from->count || retired_room(gone, from->count - keeping));
+ for (size_t i = 0; i < from->count; ++i) {
+ PureRetired *to = kept && made_at(&entries[i]) < epoch ? kept : gone;
+ if (room && to) ((Retired *)to->entries)[to->count++] = entries[i];
+ /* else kept for good rather than freed while read (memory ran out) */
+ }
+ free(entries);
+ *from = (PureRetired){0};
+}
+
static Node *new_node(PureList *list, bool leaf) {
Node *node = list->spare[leaf];
if (node) {
@@ -570,16 +600,20 @@ void pure_list_clear(PureList *list) {
size_t pure_list_count(const PureList *list) { return list->count; }
-PureList *pure_list_share(PureList *list, PureRetired *retired) {
+PureList *pure_list_share(PureList *list, PureRetired *retired, uint64_t epoch) {
PureList *copy = malloc(sizeof(*copy));
if (!copy) return NULL;
*copy = (PureList){.layout = list->layout, .root = list->root, .count = list->count, .bytes = list->bytes,
.id = list->id, .version = list->version, .refs = 1, .copy = true};
list->retired = retired;
- ++list->shares;
+ list->shares = epoch > list->shares ? epoch : list->shares + 1;
return copy;
}
+void pure_list_mark(PureList *list, uint64_t epoch) {
+ if (list->retired && epoch > list->shares) list->shares = epoch;
+}
+
void pure_list_hold(PureList *copy) { __atomic_add_fetch(&copy->refs, 1, __ATOMIC_RELAXED); }
bool pure_list_current(const PureList *copy, const PureList *list) {
diff --git a/core/module-list.h b/core/module-list.h
index a4e760a..abe19cb 100644
--- a/core/module-list.h
+++ b/core/module-list.h
@@ -59,8 +59,18 @@ size_t pure_list_count(const PureList *);
* freed, since the copy may still read them. The copy is freed with
* pure_list_free, on any thread, when its last reference goes
* (pure_list_hold takes one more). Only between events (no change waits to
- * be committed or undone). */
-PureList *pure_list_share(PureList *, PureRetired *retired);
+ * be committed or undone). epoch numbers the copy (as a snapshot of an app
+ * numbers its lists' copies, one number for all of them): a node or text
+ * the list makes after it has the epoch, and a copy of epoch e reads only
+ * what was made before e. */
+PureList *pure_list_share(PureList *, PureRetired *retired, uint64_t epoch);
+/* A copy made before and still current (the list unchanged since) is read
+ * as of epoch too: what the list lets go from now on is retired. */
+void pure_list_mark(PureList *, uint64_t epoch);
+/* Of what was retired, what a copy of epoch may still read (made before
+ * it) goes to kept (none when kept is NULL), and the rest to gone, for
+ * pure_retired_free (later, and where it pleases): from is left empty. */
+void pure_retired_pass(PureRetired *from, uint64_t epoch, PureRetired *kept, PureRetired *gone);
void pure_list_hold(PureList *copy);
/* Whether copy shares list's items as they are now. */
bool pure_list_current(const PureList *copy, const PureList *list);
diff --git a/core/module.c b/core/module.c
index c65ef80..79d3c5b 100644
--- a/core/module.c
+++ b/core/module.c
@@ -2544,22 +2544,40 @@ bool pure_app_defer(PureApp *app, bool deferred) {
return true;
}
-/* ---- Snapshots. Snapshots of one app form a chain from oldest to newest,
- * each holding the next, so they go oldest first. List items the app lets go
- * while a snapshot is its newest may be read by that one and older ones, so
- * they go with it. */
+/* ---- Snapshots. An app's snapshots are numbered as they are made (their
+ * epoch), and the ones still held are linked, oldest to newest, under their
+ * register (which each of them holds, so it outlives the app). A list's node
+ * or text the app lets go while a snapshot is its newest may be read by that
+ * snapshot and by older ones made after it was: it goes with that snapshot,
+ * and when that one goes, to the newest older one still held that may read
+ * it (made after it was), or back to the system. So a snapshot held for long
+ * keeps what it shares, the states as they were then, and nothing made
+ * since. The links and retired lists move under the register's lock, a spin
+ * (none holds it long; what is freed is freed outside it). */
+typedef struct {
+ unsigned refs; /* the snapshots not yet freed */
+ bool locked;
+ uint64_t epoch; /* snapshots made */
+} Register;
+
struct PureSnapshot {
unsigned refs;
const PureModule *module;
- uint64_t changes;
+ uint64_t changes, epoch;
double values[PURE_MODULE_STATES];
PureDraft *texts[PURE_MODULE_STATES];
PureList *lists[PURE_MODULE_STATES];
PureList *runtime[PURE_LISTS_STORED - PURE_LIST_HEARD]; /* only in a checkpoint's of one */
PureRetired retired;
- struct PureSnapshot *newer;
+ struct PureSnapshot *older, *newer; /* the held ones beside it */
+ Register *all;
};
+static void lock_register(Register *r) {
+ while (__atomic_test_and_set(&r->locked, __ATOMIC_ACQUIRE)) { /* another thread moves links: a moment */ }
+}
+static void unlock_register(Register *r) { __atomic_clear(&r->locked, __ATOMIC_RELEASE); }
+
void pure_snapshot_hold(PureSnapshot *snapshot) { __atomic_add_fetch(&snapshot->refs, 1, __ATOMIC_RELAXED); }
const PureList *pure_snapshot_list(const PureSnapshot *snapshot, uint32_t state) {
@@ -2567,17 +2585,26 @@ const PureList *pure_snapshot_list(const PureSnapshot *snapshot, uint32_t state)
}
void pure_snapshot_release(PureSnapshot *snapshot) {
- while (snapshot && !__atomic_sub_fetch(&snapshot->refs, 1, __ATOMIC_ACQ_REL)) {
- PureSnapshot *newer = snapshot->newer;
- for (uint32_t i = 0; i < PURE_MODULE_STATES; ++i) {
- pure_draft_close(snapshot->texts[i]);
- pure_list_free(snapshot->lists[i]);
- }
- for (unsigned r = 0; r < PURE_LISTS_STORED - PURE_LIST_HEARD; ++r) pure_list_free(snapshot->runtime[r]);
- pure_retired_free(&snapshot->retired);
- free(snapshot);
- snapshot = newer;
+ if (!snapshot || __atomic_sub_fetch(&snapshot->refs, 1, __ATOMIC_ACQ_REL)) return;
+ Register *all = snapshot->all;
+ PureRetired gone = {0};
+ if (all) { /* out of the links: what the one before may read goes to it (still linked, so still there) */
+ lock_register(all);
+ PureSnapshot *older = snapshot->older;
+ if (older) older->newer = snapshot->newer;
+ if (snapshot->newer) snapshot->newer->older = older;
+ pure_retired_pass(&snapshot->retired, older ? older->epoch : 0, older ? &older->retired : NULL, &gone);
+ unlock_register(all);
+ } else
+ gone = snapshot->retired;
+ pure_retired_free(&gone);
+ for (uint32_t i = 0; i < PURE_MODULE_STATES; ++i) {
+ pure_draft_close(snapshot->texts[i]);
+ pure_list_free(snapshot->lists[i]);
}
+ for (unsigned r = 0; r < PURE_LISTS_STORED - PURE_LIST_HEARD; ++r) pure_list_free(snapshot->runtime[r]);
+ free(snapshot);
+ if (all && !__atomic_sub_fetch(&all->refs, 1, __ATOMIC_ACQ_REL)) free(all);
}
void pure_app_stop(PureApp *app) {
@@ -2588,9 +2615,11 @@ void pure_app_stop(PureApp *app) {
pure_list_free(app->lists[i]);
pure_draft_close(app->list_drafts[i]);
}
- /* Items the newest snapshot may still read go when it goes. */
+ /* Items the newest snapshot may still read go with it. */
if (app->snapshot) {
- app->snapshot->retired = app->retired;
+ lock_register(app->snapshot->all);
+ pure_retired_pass(&app->retired, UINT64_MAX, &app->snapshot->retired, NULL);
+ unlock_register(app->snapshot->all);
pure_snapshot_release(app->snapshot);
} else
pure_retired_free(&app->retired);
@@ -3149,10 +3178,16 @@ PureSnapshot *pure_save_snapshot(PureApp *app, uint32_t runtime) {
if (app->open || app->reading) return NULL;
const PureModule *m = app->module;
PureSnapshot *made = calloc(1, sizeof(*made)), *last = app->snapshot;
- if (!made) return NULL;
+ Register *all = last ? last->all : calloc(1, sizeof(*all));
+ if (!made || !all) {
+ free(made);
+ if (!last) free(all);
+ return NULL;
+ }
made->refs = 1;
made->module = m;
made->changes = app->changes;
+ made->epoch = all->epoch + 1; /* the app's thread alone makes them */
memcpy(made->values, app->values, sizeof(made->values));
bool ok = true;
for (uint32_t i = 0; i < m->states && ok; ++i) {
@@ -3161,25 +3196,32 @@ PureSnapshot *pure_save_snapshot(PureApp *app, uint32_t runtime) {
if (last && last->lists[i] && pure_list_current(last->lists[i], app->lists[i])) {
pure_list_hold(last->lists[i]);
made->lists[i] = last->lists[i];
+ pure_list_mark(app->lists[i], made->epoch);
} else
- ok = (made->lists[i] = pure_list_share(app->lists[i], &app->retired)) != NULL;
+ ok = (made->lists[i] = pure_list_share(app->lists[i], &app->retired, made->epoch)) != NULL;
}
if (ok && runtime >= PURE_LIST_HEARD && runtime < PURE_LISTS_STORED) {
PureList *list = runtime == PURE_LIST_HEARD ? app->heard : app->outbox;
- ok = (made->runtime[runtime - PURE_LIST_HEARD] = pure_list_share(list, &app->retired)) != NULL;
+ ok = (made->runtime[runtime - PURE_LIST_HEARD] = pure_list_share(list, &app->retired, made->epoch)) != NULL;
}
- if (!ok) {
+ if (!ok) { /* never linked: it goes alone */
+ if (!last) free(all);
pure_snapshot_release(made);
return NULL;
}
- made->refs = last ? 3 : 2; /* the app's, the caller's and the last one's */
- if (last) {
- last->retired = app->retired;
+ made->refs = 2; /* the app's and the caller's */
+ made->all = all;
+ lock_register(all);
+ all->epoch = made->epoch;
+ ++all->refs;
+ made->older = last;
+ if (last) { /* what the app let go while last was its newest goes with it */
last->newer = made;
- pure_snapshot_release(last);
- } else
- pure_retired_free(&app->retired); /* shared only by a snapshot never made */
- app->retired = (PureRetired){0};
+ pure_retired_pass(&app->retired, UINT64_MAX, &last->retired, NULL);
+ }
+ unlock_register(all);
+ if (last) pure_snapshot_release(last);
+ else pure_retired_free(&app->retired); /* shared only by a snapshot never made */
app->snapshot = made;
return made;
}
diff --git a/core/test-module.c b/core/test-module.c
index 788bfc6..1446e73 100644
--- a/core/test-module.c
+++ b/core/test-module.c
@@ -5,6 +5,9 @@
#include "store.h"
#include <dirent.h>
#include <fcntl.h>
+#ifdef __GLIBC__
+#include <malloc.h>
+#endif
#include <math.h>
#include <pthread.h>
#include <stdio.h>
@@ -1056,6 +1059,7 @@ static void list_trees(void) {
Model shared[SHARES] = {{0}};
PureRetired retired[SHARES + 1] = {{0}};
unsigned first = 0, held = 0, splits_seen = 0;
+ uint64_t epoch = 0; /* the copies', one after another */
size_t largest = 0, events = 0, undone = 0;
for (unsigned event = 0; event < 6000; ++event) {
size_t mark = undo.count, journal = pure_list_changes(list).size;
@@ -1137,7 +1141,7 @@ static void list_trees(void) {
--held;
}
unsigned slot = (first + held) % SHARES;
- CHECK((copies[slot] = pure_list_share(list, &retired[slot])));
+ CHECK((copies[slot] = pure_list_share(list, &retired[slot], ++epoch)));
model_copy(&shared[slot], &model);
++held;
}
@@ -1201,7 +1205,7 @@ static void list_trees(void) {
CHECK(keyed);
for (unsigned round = 0; round < 40000; ++round) {
if (round == 20000) {
- PureList *copy = pure_list_share(keyed, &keyed_retired);
+ PureList *copy = pure_list_share(keyed, &keyed_retired, 1);
CHECK(copy && pure_list_keyed(keyed, 0) == PURE_OK && same_items(copy, &keys) && same_items(keyed, &keys));
pure_list_free(copy);
}
@@ -3675,6 +3679,51 @@ static void snapshots_read(Spec spec, void (*event)(PureApp *, uint32_t *), unsi
remove_store(path);
}
+/* A snapshot held while the app goes on for long keeps the states as they
+ * were then, and nothing the app makes and lets go after it: holding one
+ * costs the states of its time, however many events and snapshots follow
+ * (it held them all before, each snapshot holding the next). */
+static size_t snapshot_held_growth;
+
+static void snapshot_held(Spec spec, void (*event)(PureApp *, uint32_t *)) {
+ static char was[1 << 16], shown_text[1 << 16];
+ char *path = new_store();
+ PureStore *store;
+ CHECK(load(spec, &module_a) == PURE_OK && pure_store_open(path, &store) == PURE_OK);
+ PureApp app, reader = {0};
+ CHECK(pure_app_start(&app, &module_a, pure_store_data(store)) == PURE_OK);
+ uint32_t seed = 0x401du;
+ for (unsigned step = 0; step < 200; ++step) event(&app, &seed);
+ PureSnapshot *held = pure_app_snapshot(&app);
+ CHECK(held);
+ Shown before = {was, 0, sizeof(was)}, shown = {shown_text, 0, sizeof(shown_text)};
+ describe(&app, &before);
+ size_t used[3] = {0, 0, 0};
+ for (unsigned half = 0; half < 2; ++half) {
+#ifdef __GLIBC__
+ used[half] = mallinfo2().uordblks;
+#endif
+ for (unsigned step = 0; step < 20000; ++step) {
+ event(&app, &seed);
+ pure_snapshot_release(pure_app_snapshot(&app));
+ }
+ }
+#ifdef __GLIBC__
+ used[2] = mallinfo2().uordblks;
+ /* The first half may make the app's own copies of what the held one shares; the second makes none to keep. */
+ snapshot_held_growth = used[2] > used[1] ? used[2] - used[1] : 0;
+ CHECK(snapshot_held_growth < 256 * 1024);
+#endif
+ pure_app_read(&reader, held);
+ describe(&reader, &shown);
+ CHECK(!strcmp(before.text, shown.text));
+ pure_snapshot_release(held);
+ pure_app_stop(&reader);
+ pure_app_stop(&app);
+ pure_store_close(store);
+ remove_store(path);
+}
+
static void crash_event(PureApp *app, uint32_t *seed) {
(void)seed;
CHECK(pure_app_input(app, 0, PURE_EDGE_DOWN) == PURE_OK);
@@ -3688,6 +3737,7 @@ static void snapshots(void) {
snapshots_kept(keyed, &unkeyed, keyed_event, 3000);
snapshots_kept(crashing, NULL, crash_event, 300);
snapshots_read(keyed, keyed_event, 3000);
+ snapshot_held(keyed, keyed_event);
snapshots_read(crashing, crash_event, 300);
/* Without stored states (texts only in memory) there is no snapshot. */
CHECK(load(crashing, &module_a) == PURE_OK);
@@ -3695,7 +3745,8 @@ static void snapshots(void) {
CHECK(pure_app_start(&app, &module_a, (PureData){0}) == PURE_OK && !pure_app_snapshot(&app));
pure_app_stop(&app);
CHECK(snapshot_retired > 100);
- printf("{\"snapshots\":6600,\"items_retired\":%zu,\"reads_on_threads\":%zu}\n", snapshot_retired, snapshot_reads);
+ printf("{\"snapshots\":6600,\"items_retired\":%zu,\"reads_on_threads\":%zu,\"held_growth_bytes\":%zu}\n",
+ snapshot_retired, snapshot_reads, snapshot_held_growth);
}
/* A message of a text, a number, bytes and a u32, in that order: the prefix