aboutsummaryrefslogtreecommitdiffstats
path: root/prototypes/connections
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-26 22:56:32 +0100
committereunhhu <[email protected]>2026-09-26 22:56:32 +0100
commit42021a500d19dbe091075338d1a621498a81f167 (patch)
treeee3e2df5f0d1ba20ef5d681bf67a148d8c896b03 /prototypes/connections
parentd2adae5deacc1545cd5dbdfcc5b80697c91dac09 (diff)
downloadpure-42021a500d19dbe091075338d1a621498a81f167.tar.gz
pure-42021a500d19dbe091075338d1a621498a81f167.zip
Snapshot an app's saved states for readers on other threads
Right after a save, pure_app_snapshot makes a read-only snapshot of the saved states: numbers are copied, text states are frozen drafts that share the store mapping (pure_draft_freeze, no copy), and lists are copies of their item pointers that share the items. An unchanged list shares the last snapshot's copy. pure_app_read binds a reader app on another thread to a snapshot so the existing view functions read it. A shared item never changes again: a list stamps each item with its share count, copies a shared item it changes, and retires shared items it lets go instead of freeing them. Snapshots form a chain that goes oldest first, and items retired while a snapshot was the newest go with it. References are atomic and may be released on any thread. test-module checks that each snapshot equals the app, that kept snapshots stay as made through changes, removals, a module switch, the app stopping and the store closing, that a failed save's snapshot equals the one before, and that two threads read the newest snapshots while the app goes on (also under ASan, UBSan and LeakSanitizer).
Diffstat (limited to 'prototypes/connections')
-rw-r--r--prototypes/connections/Makefile2
-rw-r--r--prototypes/connections/module-list.c133
-rw-r--r--prototypes/connections/module-list.h14
-rw-r--r--prototypes/connections/module.c82
-rw-r--r--prototypes/connections/module.h22
-rw-r--r--prototypes/connections/state.h9
-rw-r--r--prototypes/connections/store-tree.c13
-rw-r--r--prototypes/connections/test-module.c228
8 files changed, 483 insertions, 20 deletions
diff --git a/prototypes/connections/Makefile b/prototypes/connections/Makefile
index ec3394d..6775e5a 100644
--- a/prototypes/connections/Makefile
+++ b/prototypes/connections/Makefile
@@ -126,7 +126,7 @@ $(BUILD)/test-splice: test-splice.c program.c $(CORE) $(HOST) $(HEADERS) | $(BUI
# Text states are runtime-owned drafts, so the executor links the tree engine.
$(BUILD)/test-module: test-module.c module-store.c $(MODULE) module-store.h $(MODULE_HEADERS) | $(BUILD)
- $(CC) $(CPPFLAGS) $(CFLAGS) -DPURE_STORE_TEST test-module.c module-store.c $(MODULE) $(LDFLAGS) -lm $(LDLIBS) -o $@
+ $(CC) $(CPPFLAGS) $(CFLAGS) -DPURE_STORE_TEST test-module.c module-store.c $(MODULE) $(LDFLAGS) -pthread -lm $(LDLIBS) -o $@
$(BUILD)/pure-run: pure-run.c module-store.c $(MODULE) module-store.h $(MODULE_HEADERS) | $(BUILD)
$(CC) $(CPPFLAGS) $(CFLAGS) pure-run.c module-store.c $(MODULE) $(LDFLAGS) -lm $(LDLIBS) -o $@
diff --git a/prototypes/connections/module-list.c b/prototypes/connections/module-list.c
index 148dc61..2cd2b1c 100644
--- a/prototypes/connections/module-list.c
+++ b/prototypes/connections/module-list.c
@@ -9,25 +9,75 @@ struct PureList {
uint8_t text_fields[16];
PureCell **items;
size_t count, capacity;
- PureDraft *draft; /* the stored copy, or NULL */
- size_t header; /* draft bytes before the first item */
+ PureDraft *draft; /* the stored copy, or NULL */
+ size_t header; /* draft bytes before the first item */
+ uint64_t id, version; /* which list, and a number every change moves on */
+ uint64_t shares; /* copies made so far: an item made before the last one is shared */
+ PureRetired *retired; /* where shared items go when the list lets them go */
+ unsigned refs; /* a shared copy's references */
+ bool copy; /* a shared copy: it owns its item array, not its items */
};
-typedef enum { INSERTED, REMOVED, CHANGED } Change;
+typedef enum { INSERTED, REMOVED, CHANGED, REPLACED } Change;
typedef struct {
Change change;
PureList *list;
size_t at;
unsigned field;
- PureCell *item; /* REMOVED: the removed item */
+ PureCell *item; /* REMOVED: the removed item; REPLACED: the shared item a copy replaced */
PureCell old; /* CHANGED: the replaced cell */
} Entry;
+typedef struct { PureCell *item; uint16_t text; uint8_t fields; } Retired;
+
+static uint64_t lists_made;
+
+/* An item's cells follow the list's share count when the item was made. One
+ * made before the last share may be read by a shared copy, so it never
+ * changes again (a change copies it) and is retired instead of freed. */
+static PureCell *new_item(const PureList *list) {
+ uint64_t *made = calloc(1, sizeof(*made) + (list->fields ? list->fields : 1) * sizeof(PureCell));
+ if (!made) return NULL;
+ *made = list->shares;
+ return (PureCell *)(made + 1);
+}
+
+static bool shared(const PureList *list, const PureCell *item) { return ((const uint64_t *)item)[-1] < list->shares; }
+
+static void free_cells(PureCell *item, unsigned fields, unsigned text) {
+ for (unsigned f = 0; f < fields; ++f)
+ if (text >> f & 1) free(item[f].text);
+ free((uint64_t *)item - 1);
+}
+
+static unsigned text_mask(const PureList *list) {
+ unsigned text = 0;
+ for (unsigned f = 0; f < list->fields; ++f) text |= (unsigned)(list->text_fields[f] != 0) << f;
+ return text;
+}
static void free_item(const PureList *list, PureCell *item) {
- if (!item) return;
- for (unsigned f = 0; f < list->fields; ++f)
- if (list->text_fields[f]) free(item[f].text);
- free(item);
+ if (item) free_cells(item, list->fields, text_mask(list));
+}
+
+/* The list no longer has item: free it, or retire it while a copy may read it. */
+static void let_go(PureList *list, PureCell *item) {
+ if (!shared(list, item)) { free_item(list, item); return; }
+ PureRetired *retired = list->retired;
+ if (retired->count == retired->capacity) {
+ size_t capacity = retired->capacity ? 2 * retired->capacity : 16;
+ Retired *grown = realloc(retired->entries, capacity * sizeof(*grown));
+ if (!grown) return; /* kept for good rather than freed while read */
+ retired->entries = grown;
+ retired->capacity = capacity;
+ }
+ ((Retired *)retired->entries)[retired->count++] = (Retired){item, (uint16_t)text_mask(list), (uint8_t)list->fields};
+}
+
+void pure_retired_free(PureRetired *retired) {
+ Retired *entries = retired->entries;
+ for (size_t i = 0; i < retired->count; ++i) free_cells(entries[i].item, entries[i].fields, entries[i].text);
+ free(entries);
+ *retired = (PureRetired){0};
}
PureList *pure_list_new(unsigned fields, const uint8_t *text_fields) {
@@ -36,18 +86,45 @@ PureList *pure_list_new(unsigned fields, const uint8_t *text_fields) {
if (!list) return NULL;
list->fields = fields;
memcpy(list->text_fields, text_fields, fields);
+ list->id = __atomic_add_fetch(&lists_made, 1, __ATOMIC_RELAXED);
return list;
}
void pure_list_free(PureList *list) {
if (!list) return;
- for (size_t i = 0; i < list->count; ++i) free_item(list, list->items[i]);
+ if (list->copy) {
+ if (__atomic_sub_fetch(&list->refs, 1, __ATOMIC_ACQ_REL)) return;
+ } else
+ for (size_t i = 0; i < list->count; ++i) let_go(list, list->items[i]);
free(list->items);
free(list);
}
size_t pure_list_count(const PureList *list) { return list->count; }
+PureList *pure_list_share(PureList *list, PureRetired *retired) {
+ PureList *copy = malloc(sizeof(*copy));
+ PureCell **items = list->count ? malloc(list->count * sizeof(*items)) : NULL;
+ if (!copy || (list->count && !items)) {
+ free(copy);
+ free(items);
+ return NULL;
+ }
+ if (list->count) memcpy(items, list->items, list->count * sizeof(*items));
+ *copy = (PureList){.fields = list->fields, .items = items, .count = list->count, .capacity = list->count,
+ .id = list->id, .version = list->version, .refs = 1, .copy = true};
+ memcpy(copy->text_fields, list->text_fields, sizeof(copy->text_fields));
+ list->retired = retired;
+ ++list->shares;
+ return copy;
+}
+
+void pure_list_hold(PureList *copy) { __atomic_add_fetch(&copy->refs, 1, __ATOMIC_RELAXED); }
+
+bool pure_list_current(const PureList *copy, const PureList *list) {
+ return copy->id == list->id && copy->version == list->version;
+}
+
static bool log_change(PureUndo *undo, Entry entry) {
if (undo->count == undo->capacity) {
size_t capacity = undo->capacity ? 2 * undo->capacity : 16;
@@ -154,12 +231,14 @@ static void place(PureList *list, size_t at, PureCell *item) {
memmove(list->items + at + 1, list->items + at, (list->count - at) * sizeof(*list->items));
list->items[at] = item;
++list->count;
+ ++list->version;
}
static PureCell *take(PureList *list, size_t at) {
PureCell *item = list->items[at];
memmove(list->items + at, list->items + at + 1, (list->count - at - 1) * sizeof(*list->items));
--list->count;
+ ++list->version;
return item;
}
@@ -172,7 +251,7 @@ PureResult pure_list_insert(PureList *list, size_t at, const PureCell *cells, Pu
list->items = items;
list->capacity = capacity;
}
- PureCell *item = calloc(list->fields ? list->fields : 1, sizeof(*item));
+ PureCell *item = new_item(list);
if (!item) return PURE_ALLOCATION;
for (unsigned f = 0; f < list->fields; ++f)
if (!copy_cell(list, f, cells[f], &item[f])) { free_item(list, item); return PURE_ALLOCATION; }
@@ -206,19 +285,33 @@ PureResult pure_list_remove(PureList *list, size_t at, PureUndo *undo) {
return PURE_OK;
}
+/* A shared item's copy with every field but one (left empty). */
+static PureCell *copy_item(const PureList *list, const PureCell *item, unsigned except) {
+ PureCell *fresh = new_item(list);
+ for (unsigned f = 0; fresh && f < list->fields; ++f)
+ if (f != except && !copy_cell(list, f, item[f], &fresh[f])) {
+ free_item(list, fresh);
+ fresh = NULL;
+ }
+ return fresh;
+}
+
PureResult pure_list_set(PureList *list, size_t at, unsigned field, PureCell value, PureUndo *undo) {
if (at >= list->count) return PURE_NOT_FOUND;
PureCell copy, *item = list->items[at];
if (!copy_cell(list, field, value, &copy)) return PURE_ALLOCATION;
+ PureCell *fresh = shared(list, item) ? copy_item(list, item, field) : item;
size_t size = list->draft ? cell_bytes(list, field, copy) : 0;
unsigned char *bytes = size ? malloc(size) : NULL;
- if (size && !bytes) { free(copy.text); return PURE_ALLOCATION; }
- if (bytes) encode_cell(list, field, copy, bytes);
- if (!log_change(undo, (Entry){.change = CHANGED, .list = list, .at = at, .field = field, .old = item[field]})) {
+ Entry entry = fresh == item ? (Entry){.change = CHANGED, .list = list, .at = at, .field = field, .old = item[field]} :
+ (Entry){.change = REPLACED, .list = list, .at = at, .item = item};
+ if (!fresh || (size && !bytes) || !log_change(undo, entry)) {
free(bytes);
free(copy.text);
+ if (fresh != item) free_item(list, fresh);
return PURE_ALLOCATION;
}
+ if (bytes) encode_cell(list, field, copy, bytes);
size_t within = 0;
for (unsigned f = 0; f < field; ++f) within += cell_bytes(list, f, item[f]);
PureResult result = mirror(list, at, within, cell_bytes(list, field, item[field]), bytes, size);
@@ -226,9 +319,12 @@ PureResult pure_list_set(PureList *list, size_t at, unsigned field, PureCell val
if (result != PURE_OK) {
--undo->count;
free(copy.text);
+ if (fresh != item) free_item(list, fresh);
return result;
}
- item[field] = copy;
+ fresh[field] = copy;
+ list->items[at] = fresh;
+ ++list->version;
return PURE_OK;
}
@@ -303,9 +399,14 @@ void pure_undo_rollback(PureUndo *undo, size_t mark) {
Entry *e = &entries[i];
if (e->change == INSERTED) free_item(e->list, take(e->list, e->at));
else if (e->change == REMOVED) place(e->list, e->at, e->item); /* capacity is still there */
- else {
+ else if (e->change == REPLACED) {
+ free_item(e->list, e->list->items[e->at]);
+ e->list->items[e->at] = e->item;
+ ++e->list->version;
+ } else {
if (e->list->text_fields[e->field]) free(e->list->items[e->at][e->field].text);
e->list->items[e->at][e->field] = e->old;
+ ++e->list->version;
}
}
undo->count = mark;
@@ -315,7 +416,7 @@ void pure_undo_commit(PureUndo *undo) {
Entry *entries = undo->entries;
for (size_t i = 0; i < undo->count; ++i) {
Entry *e = &entries[i];
- if (e->change == REMOVED) free_item(e->list, e->item);
+ if (e->change == REMOVED || e->change == REPLACED) let_go(e->list, e->item);
else if (e->change == CHANGED && e->list->text_fields[e->field]) free(e->old.text);
}
undo->count = 0;
diff --git a/prototypes/connections/module-list.h b/prototypes/connections/module-list.h
index 9052679..a721ff0 100644
--- a/prototypes/connections/module-list.h
+++ b/prototypes/connections/module-list.h
@@ -22,12 +22,26 @@
typedef struct { double num; unsigned char *text; size_t size; } PureCell;
typedef struct PureList PureList;
typedef struct { void *entries; size_t count, capacity; } PureUndo;
+/* Items a list let go while a shared copy may still read them. */
+typedef struct { void *entries; size_t count, capacity; } PureRetired;
enum { PURE_LIST_ITEMS = 65536 };
PureList *pure_list_new(unsigned fields, const uint8_t *text_fields);
void pure_list_free(PureList *);
size_t pure_list_count(const PureList *);
+/* Share the items as they are with a read-only copy (for readers on other
+ * threads), without copying them: from then on the list copies an item it
+ * changes, and items it lets go go to retired instead of being 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);
+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);
+/* Free items retired while no copy that shared them remains. */
+void pure_retired_free(PureRetired *);
/* Insert a copy of cells (text bytes are copied) before position at. A NULL
* log means the change is not undone. */
PureResult pure_list_insert(PureList *, size_t at, const PureCell *cells, PureUndo *);
diff --git a/prototypes/connections/module.c b/prototypes/connections/module.c
index e4c7309..c1a71d5 100644
--- a/prototypes/connections/module.c
+++ b/prototypes/connections/module.c
@@ -1259,14 +1259,50 @@ 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. */
+struct PureSnapshot {
+ unsigned refs;
+ const PureModule *module;
+ uint64_t changes;
+ double values[PURE_MODULE_STATES];
+ PureDraft *texts[PURE_MODULE_STATES];
+ PureList *lists[PURE_MODULE_STATES];
+ PureRetired retired;
+ struct PureSnapshot *newer;
+};
+
+void pure_snapshot_hold(PureSnapshot *snapshot) { __atomic_add_fetch(&snapshot->refs, 1, __ATOMIC_RELAXED); }
+
+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]);
+ }
+ pure_retired_free(&snapshot->retired);
+ free(snapshot);
+ snapshot = newer;
+ }
+}
+
void pure_app_stop(PureApp *app) {
pure_app_finish(app);
close_burst(app, true); /* unsaved changes are dropped with the app */
- for (uint32_t i = 0; i < PURE_MODULE_STATES; ++i) {
+ for (uint32_t i = 0; i < PURE_MODULE_STATES && !app->reading; ++i) {
pure_draft_close(app->texts[i]);
pure_list_free(app->lists[i]);
pure_draft_close(app->list_drafts[i]);
}
+ /* Items the newest snapshot may still read go when it goes. */
+ if (app->snapshot) {
+ app->snapshot->retired = app->retired;
+ pure_snapshot_release(app->snapshot);
+ } else
+ pure_retired_free(&app->retired);
pure_draft_close(app->stored_values);
free(app->pending);
pure_undo_free(&app->undo);
@@ -1580,6 +1616,50 @@ PureResult pure_app_cell_text(PureApp *app, uint32_t column, size_t row, bool (*
return result == PURE_OK ? visit_value(app, &app->view_arena, slot.bytes, visit, context) : result;
}
+PureSnapshot *pure_app_snapshot(PureApp *app) {
+ 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;
+ made->refs = 1;
+ made->module = m;
+ made->changes = app->changes;
+ memcpy(made->values, app->values, sizeof(made->values));
+ bool ok = true;
+ for (uint32_t i = 0; i < m->states && ok; ++i) {
+ if (app->texts[i]) ok = (made->texts[i] = pure_draft_freeze(app->texts[i])) != NULL;
+ if (!ok || !app->lists[i]) continue;
+ 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];
+ } else
+ ok = (made->lists[i] = pure_list_share(app->lists[i], &app->retired)) != NULL;
+ }
+ if (!ok) {
+ 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;
+ last->newer = made;
+ pure_snapshot_release(last);
+ } else
+ pure_retired_free(&app->retired); /* shared only by a snapshot never made */
+ app->retired = (PureRetired){0};
+ app->snapshot = made;
+ return made;
+}
+
+void pure_app_read(PureApp *reader, const PureSnapshot *snapshot) {
+ reader->module = snapshot->module;
+ reader->reading = true;
+ reader->changes = snapshot->changes; /* what the row cache is fresh for */
+ memcpy(reader->values, snapshot->values, sizeof(reader->values));
+ memcpy(reader->texts, snapshot->texts, sizeof(reader->texts));
+ memcpy(reader->lists, snapshot->lists, sizeof(reader->lists));
+}
+
/* A list with record's fields: each takes the field of `from` with the same
* name and kind, or its default. */
PURE_COLD PureResult pure_list_migrate(PureList **out, const PureList *from, const char *const *names,
diff --git a/prototypes/connections/module.h b/prototypes/connections/module.h
index 313c6e1..3a75fc1 100644
--- a/prototypes/connections/module.h
+++ b/prototypes/connections/module.h
@@ -228,6 +228,9 @@ typedef struct {
unsigned char *copied;
int64_t revision;
PureArena arena, view_arena;
+ struct PureSnapshot *snapshot; /* the newest snapshot made, or NULL */
+ PureRetired retired; /* list items let go since it was made */
+ bool reading; /* a reader: its states are a snapshot's */
} PureApp;
PureResult pure_app_start(PureApp *, const PureModule *, PureData data);
@@ -274,4 +277,23 @@ PureResult pure_app_cell_text(PureApp *, uint32_t column, size_t row, bool (*vis
* states keep their content, cursors their position in that text); new states
* start at their initial value. The module must outlive the app. */
PureResult pure_app_replace(PureApp *, const PureModule *);
+
+/* Readers on other threads. Right after a save an app can make a snapshot of
+ * its saved states, for evaluating views anywhere while it keeps applying
+ * events: numbers are copied; text states share their stored versions and
+ * lists their items (a list unchanged since the last snapshot shares that
+ * snapshot's copy), so the cost does not grow with the text. NULL when a
+ * change waits for a save, when a text state is not all stored (for example
+ * without a data host that keeps states), or when memory runs out. The caller
+ * gets a reference; the app keeps its own until the next snapshot. */
+typedef struct PureSnapshot PureSnapshot;
+PureSnapshot *pure_app_snapshot(PureApp *);
+void pure_snapshot_hold(PureSnapshot *);
+/* On any thread; the last reference frees it. */
+void pure_snapshot_release(PureSnapshot *);
+/* Show a snapshot's states in reader, a zeroed app or a reader before: its
+ * views read them while the snapshot is held, with the reader's own arena and
+ * row cache (one thread at a time). It applies no events. pure_app_stop frees
+ * the reader and leaves the snapshot alone. */
+void pure_app_read(PureApp *reader, const PureSnapshot *);
#endif
diff --git a/prototypes/connections/state.h b/prototypes/connections/state.h
index 43489c1..2bf78b0 100644
--- a/prototypes/connections/state.h
+++ b/prototypes/connections/state.h
@@ -35,7 +35,8 @@ PureResult pure_store_state(PureStore *, const char *name, PureDraft **out);
* nodes from (NULL for other drafts). Spans that mapping holds stay readable
* until the reference is released, after the draft closes and after the
* store moves on. Spans of a draft's unsaved edits are not in it. Take it on
- * the handle's owner thread; the reference may be released on any thread. */
+ * the handle's owner thread (a frozen draft's on any); the reference may be
+ * released on any thread. */
typedef struct PureMapping PureMapping;
PureMapping *pure_draft_mapping(const PureDraft *);
bool pure_mapping_holds(const PureMapping *, PureSpan);
@@ -60,6 +61,12 @@ typedef struct { uint64_t ref, bytes; } PureVersion;
PureVersion pure_draft_version(const PureDraft *);
PureResult pure_draft_read_version(const PureDraft *, PureVersion, size_t offset, size_t length,
bool (*visit)(void *, PureSpan), void *context);
+/* A read-only copy of a draft opened through a store handle, at its current
+ * version, when that version is all stored (no edits since its last commit,
+ * as right after one): it shares the draft's mapping, and since reading
+ * changes nothing it may be read, lend its mapping and close on any thread.
+ * NULL otherwise, or when memory runs out. */
+PureDraft *pure_draft_freeze(const PureDraft *);
/* While held, edits keep every earlier version's nodes (one transaction). */
void pure_draft_hold(PureDraft *, bool held);
/* Abandon edits made since version was taken; the draft was held since then. */
diff --git a/prototypes/connections/store-tree.c b/prototypes/connections/store-tree.c
index 45d470e..322a163 100644
--- a/prototypes/connections/store-tree.c
+++ b/prototypes/connections/store-tree.c
@@ -1288,6 +1288,19 @@ PureDraft *pure_draft_new(void) {
return calloc(1, sizeof(PureDraft)); /* no path: nothing mapped, nothing to commit */
}
+PureDraft *pure_draft_freeze(const PureDraft *draft) {
+ if (!draft->tree.shared || (draft->root.ref & MEMORY_BIT)) return NULL;
+ PureDraft *frozen = calloc(1, sizeof(*frozen));
+ if (!frozen) return NULL;
+ Mapping *mapping = draft->tree.shared;
+ __atomic_add_fetch(&mapping->refs, 1, __ATOMIC_RELAXED);
+ *frozen = (PureDraft){.tree = {.map = mapping->map, .end = mapping->end, .shared = mapping},
+ .device = draft->device, .inode = draft->inode, .root = draft->root,
+ .revision = draft->revision};
+ memcpy(frozen->key, draft->key, sizeof(frozen->key));
+ return frozen;
+}
+
void pure_draft_close(PureDraft *draft) {
if (!draft) return;
release_tree(&draft->tree);
diff --git a/prototypes/connections/test-module.c b/prototypes/connections/test-module.c
index 257c632..d885c92 100644
--- a/prototypes/connections/test-module.c
+++ b/prototypes/connections/test-module.c
@@ -5,6 +5,7 @@
#include "store.h"
#include <dirent.h>
#include <math.h>
+#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -2195,6 +2196,230 @@ static void keyed_mutations(void) {
printf("{\"keyed_module_mutations\":20000,\"accepted_after_damage\":%u}\n", accepted);
}
+/* ---- Snapshots. Everything an app's states and views show, as text (safe
+ * on any thread): a reader of a snapshot must show what the app showed. */
+typedef struct { char *text; size_t used, size; } Shown;
+
+static bool show_span(void *context, PureSpan span) {
+ Shown *s = context;
+ CHECK(s->used + span.size < s->size);
+ if (span.size) memcpy(s->text + s->used, span.data, span.size);
+ s->used += span.size;
+ return true;
+}
+
+static void show(Shown *s, const char *format, double value) {
+ int n = snprintf(s->text + s->used, s->size - s->used, format, value);
+ CHECK(n >= 0 && (size_t)n < s->size - s->used);
+ s->used += (size_t)n;
+}
+
+static void describe(PureApp *app, Shown *s) {
+ const PureModule *m = app->module;
+ s->used = 0;
+ for (uint32_t i = 0; i < m->states; ++i) {
+ if (m->state_type[i] == PURE_STATE_TEXT)
+ CHECK(pure_draft_read(app->texts[i], 0, pure_draft_size(app->texts[i]), show_span, s) == PURE_OK);
+ else if (m->state_type[i] == PURE_STATE_LIST) {
+ const PureRecord *r = &m->record[m->state_ref[i]];
+ for (size_t at = 0; at < pure_list_count(app->lists[i]); ++at)
+ for (unsigned f = 0; f < r->count; ++f) {
+ PureCell cell = pure_list_get(app->lists[i], at, f);
+ if (m->record_field_text[r->first + f]) show_span(s, (PureSpan){cell.text, cell.size});
+ else show(s, "%g", cell.num);
+ show(s, f + 1 < r->count ? "," : ";", 0);
+ }
+ } else show(s, "%g", app->values[i]);
+ show(s, "|", 0);
+ }
+ for (uint32_t v = 0; v < m->views; ++v) {
+ double value;
+ if (m->view_table[v] || m->view_type[v] == PURE_VIEW_TABLE) continue;
+ if (m->view_type[v] == PURE_VIEW_TEXT) {
+ if (pure_app_view_text(app, v, show_span, s) != PURE_OK) show(s, "!", 0);
+ } else if (pure_app_view(app, v, &value)) show(s, "%g", value);
+ else show(s, "!", 0);
+ show(s, "|", 0);
+ }
+ s->text[s->used] = 0;
+}
+
+/* One random event of the keyed app (see keyed), saved as it returns. */
+static void keyed_event(PureApp *app, uint32_t *seed) {
+ static const char *const names[] = {"a", "b", "ab", "", "\xea\xb0\x80", "zz", "bob", "m", "q", "c"};
+ uint32_t input = next_random(seed) % app->module->inputs;
+ if (app->module->input[input].kind == PURE_KIND_TEXT) type_text(app, input, names[next_random(seed) % LEN(names)]);
+ else pure_app_input(app, input, PURE_EDGE_DOWN);
+}
+
+/* A snapshot shows the states of the save before it, as they were, however
+ * the app goes on (items it changes or drops, a module it switches to, even
+ * after it stops and its store closes); a change not saved yet refuses one,
+ * and a save that fails is never shown. */
+static size_t snapshot_retired, snapshot_reads;
+
+static void snapshots_kept(Spec spec, const Spec *other, void (*event)(PureApp *, uint32_t *), unsigned steps) {
+ enum { KEPT = 8, SIZE = 1 << 16 };
+ static char texts[KEPT + 2][SIZE];
+ char *path = new_store();
+ PureStore *store;
+ CHECK(load(spec, &module_a) == PURE_OK && pure_store_open(path, &store) == PURE_OK);
+ CHECK(!other || load(*other, &module_b) == PURE_OK);
+ PureApp app, reader = {0};
+ CHECK(pure_app_start(&app, &module_a, pure_store_data(store)) == PURE_OK);
+ PureSnapshot *kept[KEPT] = {NULL};
+ Shown seen[KEPT], now = {texts[KEPT], 0, SIZE}, shown = {texts[KEPT + 1], 0, SIZE};
+ for (unsigned k = 0; k < KEPT; ++k) seen[k] = (Shown){texts[k], 0, SIZE};
+ uint32_t seed = 0x51a95u;
+ for (unsigned step = 0; step < steps; ++step) {
+ /* Late on, another module (the keyed app unkeyed), and back if it can. */
+ if (other && (step == steps - 400 || step == steps - 300))
+ CHECK(pure_app_replace(&app, app.module == &module_a ? &module_b : &module_a) != PURE_INVALID);
+ event(&app, &seed);
+ snapshot_retired += app.retired.count;
+ PureSnapshot *made = pure_app_snapshot(&app);
+ CHECK(made);
+ describe(&app, &now);
+ pure_app_read(&reader, made);
+ describe(&reader, &shown);
+ CHECK(!strcmp(now.text, shown.text));
+ unsigned slot = next_random(&seed) % KEPT;
+ if (kept[slot]) {
+ pure_app_read(&reader, kept[slot]);
+ describe(&reader, &shown);
+ CHECK(!strcmp(seen[slot].text, shown.text));
+ pure_snapshot_release(kept[slot]);
+ }
+ kept[slot] = made;
+ memcpy(seen[slot].text, now.text, now.used + 1);
+ }
+ /* Deferred: an unsaved change refuses a snapshot, and a failed save
+ * leaves the one before it true. */
+ CHECK(pure_app_defer(&app, true));
+ describe(&app, &now);
+ PureResult saved = PURE_OK;
+ for (unsigned tries = 0; tries < 64 && saved == PURE_OK; ++tries) {
+ event(&app, &seed);
+ if (!app.open) continue;
+ CHECK(!pure_app_snapshot(&app));
+ fail_stage = PURE_AFTER_HEADER;
+ saved = pure_app_save(&app); /* OK when the event changed nothing */
+ fail_stage = -1;
+ }
+ CHECK(saved == PURE_STORE_ERROR);
+ PureSnapshot *made = pure_app_snapshot(&app);
+ CHECK(made);
+ pure_app_read(&reader, made);
+ describe(&reader, &shown);
+ CHECK(!strcmp(now.text, shown.text));
+ pure_snapshot_release(made);
+ /* The kept ones outlive the app and its store. */
+ pure_app_stop(&app);
+ pure_store_close(store);
+ for (unsigned k = 0; k < KEPT; ++k) {
+ pure_app_read(&reader, kept[k]);
+ describe(&reader, &shown);
+ CHECK(!strcmp(seen[k].text, shown.text));
+ pure_snapshot_release(kept[k]);
+ }
+ pure_app_stop(&reader);
+ remove_store(path);
+}
+
+/* Readers on two threads take the newest snapshot while the app applies
+ * events and makes one after each save: each shows what the app showed. */
+typedef struct {
+ pthread_mutex_t lock;
+ PureSnapshot *newest;
+ char *seen;
+ bool done;
+ unsigned reads;
+} Published;
+
+static void *read_newest(void *context) {
+ Published *p = context;
+ PureApp reader = {0};
+ static _Thread_local char a[1 << 16], b[1 << 16];
+ Shown expected = {a, 0, sizeof(a)}, shown = {b, 0, sizeof(b)};
+ for (bool done = false; !done;) {
+ pthread_mutex_lock(&p->lock);
+ PureSnapshot *s = p->newest;
+ if (s) {
+ pure_snapshot_hold(s);
+ strcpy(expected.text, p->seen);
+ }
+ done = p->done;
+ pthread_mutex_unlock(&p->lock);
+ if (!s) continue;
+ pure_app_read(&reader, s);
+ describe(&reader, &shown);
+ CHECK(!strcmp(expected.text, shown.text));
+ pure_snapshot_release(s);
+ __atomic_add_fetch(&p->reads, 1, __ATOMIC_RELAXED);
+ }
+ pure_app_stop(&reader);
+ return NULL;
+}
+
+static void snapshots_read(Spec spec, void (*event)(PureApp *, uint32_t *), unsigned steps) {
+ static char seen[1 << 16], now[1 << 16];
+ char *path = new_store();
+ PureStore *store;
+ CHECK(load(spec, &module_a) == PURE_OK && pure_store_open(path, &store) == PURE_OK);
+ PureApp app;
+ CHECK(pure_app_start(&app, &module_a, pure_store_data(store)) == PURE_OK);
+ Published p = {.lock = PTHREAD_MUTEX_INITIALIZER, .seen = seen};
+ pthread_t readers[2];
+ for (unsigned r = 0; r < 2; ++r) CHECK(pthread_create(&readers[r], NULL, read_newest, &p) == 0);
+ Shown shown = {now, 0, sizeof(now)};
+ uint32_t seed = 0x7eadu;
+ for (unsigned step = 0; step < steps; ++step) {
+ event(&app, &seed);
+ PureSnapshot *made = pure_app_snapshot(&app);
+ CHECK(made);
+ describe(&app, &shown);
+ pthread_mutex_lock(&p.lock);
+ PureSnapshot *old = p.newest;
+ p.newest = made;
+ memcpy(seen, shown.text, shown.used + 1);
+ pthread_mutex_unlock(&p.lock);
+ pure_snapshot_release(old);
+ }
+ pthread_mutex_lock(&p.lock);
+ p.done = true;
+ pthread_mutex_unlock(&p.lock);
+ for (unsigned r = 0; r < 2; ++r) CHECK(pthread_join(readers[r], NULL) == 0);
+ CHECK(p.reads > steps / 4);
+ snapshot_reads += p.reads;
+ pure_snapshot_release(p.newest);
+ 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);
+}
+
+static void snapshots(void) {
+ Spec keyed = keyed_spec(keyed_states, keyed_code, LEN(keyed_code), keyed_handlers, LEN(keyed_handlers));
+ Spec crashing = {crash_nums, 1, crash_inputs, 1, NULL, 0, crash_handlers, 1, crash_code, LEN(crash_code), NULL, 0,
+ crash_states, 4, crash_constants, 2, NULL, 0, crash_records, 1, NULL, 0};
+ Spec unkeyed = keyed_spec(unkeyed_states, unkeyed_code, LEN(unkeyed_code), unkeyed_handlers, 1);
+ snapshots_kept(keyed, &unkeyed, keyed_event, 3000);
+ snapshots_kept(crashing, NULL, crash_event, 300);
+ snapshots_read(keyed, keyed_event, 3000);
+ snapshots_read(crashing, crash_event, 300);
+ /* Without stored states (texts only in memory) there is no snapshot. */
+ CHECK(load(crashing, &module_a) == PURE_OK);
+ PureApp app;
+ 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);
+}
+
/* A message of a text, a number, bytes and a u32, in that order: the prefix
* holds the text's length, the number and the u32; the text's bytes come
* first after it and the bytes (the last variable field) take the rest. A
@@ -2331,6 +2556,7 @@ int main(void) {
filters();
keyed();
keyed_mutations();
+ snapshots();
fold_mutations();
pointing();
list_mutations();
@@ -2339,6 +2565,6 @@ int main(void) {
stored_states();
deferred();
printf("{\"sizeof_module\":%zu,\"sizeof_app\":%zu}\n", sizeof(PureModule), sizeof(PureApp));
- puts("{\"module_tests\":\"verified-load-atomic-events-faults-signed-zero-if-else-replace-by-name-rejections-mutations-messages-documents-typed-verification-text-cursors-utf8-views-rollback-replace-lists-indices-undo-migration-joins-tables-columns-pick-slide-stored-equivalence-crashes-frames-messages-migration-deferred-bursts-pointing-marks-folds-filters-keyed-lists-text-order-message-fields-timers-now\"}");
+ puts("{\"module_tests\":\"verified-load-atomic-events-faults-signed-zero-if-else-replace-by-name-rejections-mutations-messages-documents-typed-verification-text-cursors-utf8-views-rollback-replace-lists-indices-undo-migration-joins-tables-columns-pick-slide-stored-equivalence-crashes-frames-messages-migration-deferred-bursts-pointing-marks-folds-filters-keyed-lists-text-order-message-fields-timers-now-snapshots\"}");
return 0;
}