diff options
Diffstat (limited to 'prototypes')
| -rw-r--r-- | prototypes/connections/Makefile | 2 | ||||
| -rw-r--r-- | prototypes/connections/module-list.c | 133 | ||||
| -rw-r--r-- | prototypes/connections/module-list.h | 14 | ||||
| -rw-r--r-- | prototypes/connections/module.c | 82 | ||||
| -rw-r--r-- | prototypes/connections/module.h | 22 | ||||
| -rw-r--r-- | prototypes/connections/state.h | 9 | ||||
| -rw-r--r-- | prototypes/connections/store-tree.c | 13 | ||||
| -rw-r--r-- | prototypes/connections/test-module.c | 228 |
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(©->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, ©)) 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; } |
