flan/test/dev_limits.c

486 lines
19 KiB
C

/* dev_limits.c — the two fixed-size limits in runtime/flan_dev.c, driven
* directly.
*
* Neither is reachable from a Flan program that behaves. The result buffer
* only truncates when a renderer emits more than 4K, which no fixture does,
* and the name table only fills after 4096 distinct run-time-introduced names,
* which is more forms than the whole corpus has. So both were code nothing had
* ever run: the first thing either would do in anger is either lose the end of
* a value or scribble past the end of a static array, and the ellipsis and the
* abort are the two behaviours that say which.
*
* A C main, for the same reason reload_host.c is one: these are C entry points
* with no Flan spelling, and flan_dev.c is compiled into every build (see
* build.ml), so linking any Flan program brings them along. The .flan this is
* linked against therefore has no [main] of its own.
*
* One mode per run, chosen by argv, because both of them are one-way: the
* name table never shrinks and the overflow case aborts the process.
*/
#include <pthread.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void **flan_dev_cell(const char *name);
void flan_dev_result_begin(void);
void flan_dev_emit(const uint8_t *bytes, int64_t len);
void flan_dev_result_end(void);
uint64_t flan_dev_result_cap(void);
int flan_dev_result_read(char *dst, uint64_t cap, uint64_t *gen, uint64_t *len);
/* [flan_dev_result_get] was replaced by a real seqlock, which copies into the
* caller's buffer rather than handing back a pointer into one being written.
* The buffer is sized from [flan_dev_result_cap] rather than from CAP below,
* which is this test's own expectation of the answer: sizing it from the
* number under test would make a wrong cap copy the wrong amount and agree
* with itself. */
static char rbuf[1 << 16];
static const char *result_read(uint64_t *gen, uint64_t *len) {
if (flan_dev_result_cap() > sizeof rbuf) {
printf("result buffer too small for cap %llu\n",
(unsigned long long)flan_dev_result_cap());
exit(2);
}
if (!flan_dev_result_read(rbuf, sizeof rbuf, gen, len)) {
/* Single-threaded here: the writer is this thread, so a failed read means
* the counter was left odd, not that a write was racing. */
printf("result read did not settle\n");
exit(2);
}
return rbuf;
}
void flan_rt_init(int32_t argc, char **argv);
void flan_dev_reg_enable(void);
void flan_dev_reg_note(void *base, int64_t bytes, int64_t elem,
const char *type, int64_t typelen);
void flan_dev_reg_dead(void *base);
int flan_dev_reg_overflowed(void);
int64_t flan_dev_reg_count(int32_t live_only);
int64_t flan_dev_reg_by_type(int32_t live_only, int64_t *counts,
int64_t *bytes, const char **types,
int64_t *typelens, int64_t cap, int64_t *unread);
#define CAP 4096
/* Emit more than fits, twice the buffer's worth, and report what came back.
* What is printed is everything a wrong cap gets wrong: the length, the three
* characters that say it was truncated, a byte from the middle to show the
* content up to the cut is the content that was emitted, and the generation
* counter, which is what a reader waits on and must move exactly once. */
static int cap(void) {
static uint8_t a[3000], b[3000];
uint64_t gen0, gen1, len;
const char *r;
memset(a, 'a', sizeof a);
memset(b, 'b', sizeof b);
r = result_read(&gen0, &len);
flan_dev_result_begin();
flan_dev_emit(a, (int64_t)sizeof a);
/* A negative length is not a huge one: the cast to size_t would make it
* about 2^64 and the clamp would then be the only thing between it and a
* memcpy of everything. */
flan_dev_emit(b, -1);
flan_dev_emit(b, (int64_t)sizeof b);
flan_dev_result_end();
r = result_read(&gen1, &len);
printf("len %llu\n", (unsigned long long)len);
printf("tail %.3s\n", len >= 3 ? r + len - 3 : "");
printf("mid %c\n", len > 3500 ? r[3500] : '?');
printf("head %c\n", len > 0 ? r[0] : '?');
printf("gen %llu\n", (unsigned long long)(gen1 - gen0));
/* And a short value after a truncated one: the flag has to be cleared by
* [begin] or every later render ends in an ellipsis it did not earn. */
flan_dev_result_begin();
flan_dev_emit((const uint8_t *)"12", 2);
flan_dev_result_end();
r = result_read(&gen1, &len);
printf("again %.*s\n", (int)len, r);
return 0;
}
/* Fill the name table and then ask for one more. The table is fixed and never
* moves — a module holds the address of a cell for as long as it is loaded —
* so the only honest answer past the end is to stop. */
static int names(void) {
char buf[32];
for (int i = 0; i < CAP; i++) {
snprintf(buf, sizeof buf, "n%d", i);
if (flan_dev_cell(buf) == NULL) {
printf("null cell at %d\n", i);
return 1;
}
}
/* Distinct names, all of them: a table that deduplicated wrongly would not
* be full here and the line below would not abort. */
printf("interned %d\n", CAP);
fflush(stdout);
(void)flan_dev_cell("one-too-many");
printf("survived\n");
return 0;
}
/* ── A table full of live blocks, read while it is being written ─────
*
* The shape of the bug this pins, which is worth writing down because the
* mode looks like a stress test and is not one. A compaction reclaims dead
* entries. The trigger used to be "three quarters full" and nothing else, so a
* program holding more than three quarters of the table live compacted on
* every single allocation, for ever, reclaiming nothing each time. Each
* compaction holds the table-wide epoch odd from end to end, a listing retries
* eight times and each retry meets another one, and [flan_dev_reg_by_type]
* used to answer that with zero rows — "nothing is held", about a program
* holding three thousand blocks. It was measured at 198 wrong answers in 200.
*
* So: 3100 live blocks, which is over the old three-quarter mark and under the
* 4096 the table holds, one thread noting allocations as fast as it can, and
* this thread asking the leak question two hundred times. The count printed is
* how many of those answers were right. Nothing here is timing-dependent in
* the direction that matters — a wrong answer is a wrong answer on any
* machine, and the fixed table means "right" is a number this file knows.
*
* The blocks are fake addresses rather than real allocations. The registry is
* told about a block, it never reads one, and 3100 real mallocs would only be
* measuring the allocator. */
enum { REG_LIVE = 3100, REG_ASKS = 200 };
static volatile int reg_stop;
/* The writer: re-note blocks that are already in the table, which is what a
* game loop overwriting its own allocations looks like from in here, and is
* the path that takes the compaction branch. The addresses are the ones
* already recorded, so the live count does not move and the table never
* fills. */
static void *reg_writer(void *arg) {
int64_t n = 0;
(void)arg;
while (!reg_stop) {
for (int i = 0; i < REG_LIVE && !reg_stop; i++) {
void *p = (void *)(uintptr_t)(0x100000 + (uintptr_t)i * 64);
flan_dev_reg_note(p, 64, 8, "Enemy", 5);
n++;
}
}
return (void *)(uintptr_t)n;
}
static int regfull(void) {
enum { ROWS = 16 };
int64_t counts[ROWS], bytes[ROWS], typelens[ROWS], unread;
const char *types[ROWS];
pthread_t w;
int right = 0, refused = 0, wrong = 0, zero = 0;
flan_dev_reg_enable();
for (int i = 0; i < REG_LIVE; i++) {
void *p = (void *)(uintptr_t)(0x100000 + (uintptr_t)i * 64);
flan_dev_reg_note(p, 64, 8, "Enemy", 5);
}
printf("live %lld\n", (long long)flan_dev_reg_count(1));
if (pthread_create(&w, NULL, reg_writer, NULL) != 0) {
printf("no writer thread\n");
return 2;
}
for (int a = 0; a < REG_ASKS; a++) {
int64_t n = flan_dev_reg_by_type(1, counts, bytes, types, typelens, ROWS,
&unread);
/* One type, 3100 blocks of it. Any other answer is this table described as
something it is not — and zero rows is the specific lie, because it is
the answer a program that had freed everything would get. */
if (n < 0) refused++;
else if (n == 0) zero++;
else if (n == 1 && counts[0] == REG_LIVE) right++;
else wrong++;
}
reg_stop = 1;
pthread_join(w, NULL);
printf("right %d\n", right);
printf("zero %d\n", zero);
printf("wrong %d\n", wrong);
printf("refused %d\n", refused);
return 0;
}
/* ── The dead count the compaction trigger reads ─────────────────────
*
* [regfull] above proves a table of live blocks stops compacting. This proves
* the other half still does, which is the failure that would be silent: the
* trigger now asks how many entries are dead, four places maintain that number
* — a death, an arena's free-all, a dead slot being written over, and the sweep
* itself, which drops all of them — and a count that drifted low would simply
* stop reclaiming. Nothing would crash. The table would pin at its cap, the
* overflow flag would come on, and every listing from then on would be a floor
* quietly described as a count.
*
* So: three thousand blocks that stay live, and a pool of six hundred noted and
* freed over and over on top of them. Used passes the three-quarter mark, the
* dead pass the reclaim floor several times over the run, and the sweep runs.
* The assertion that catches a drifted count is the last one — a table that
* never reclaimed would have overflowed long before this returns. */
enum { CHURN_LIVE = 3000, CHURN_POOL = 600, CHURN_PASSES = 8 };
static void *churn_addr(int i) {
return (void *)(uintptr_t)(0x800000 + (uintptr_t)i * 64);
}
static int regchurn(void) {
enum { ROWS = 16 };
int64_t counts[ROWS], bytes[ROWS], typelens[ROWS], unread, n;
const char *types[ROWS];
flan_dev_reg_enable();
for (int i = 0; i < CHURN_LIVE; i++)
flan_dev_reg_note(churn_addr(i), 64, 8, "Enemy", 5);
/* Noted and freed on top of the live set. A note landing on the slot of a
* block that died is the decrement; the free is the increment; the sweep in
* between is the reset. */
for (int pass = 0; pass < CHURN_PASSES; pass++)
for (int i = 0; i < CHURN_POOL; i++) {
void *p = churn_addr(CHURN_LIVE + i);
flan_dev_reg_note(p, 32, 4, "Bullet", 6);
flan_dev_reg_dead(p);
}
printf("live %lld\n", (long long)flan_dev_reg_count(1));
n = flan_dev_reg_by_type(1, counts, bytes, types, typelens, ROWS, &unread);
printf("rows %lld\n", (long long)n);
printf("blocks %lld\n", (long long)(n == 1 ? counts[0] : -1));
/* The one that catches a dead count that drifted: a table that stopped
reclaiming fills, and a full table is a floor for the rest of the run. */
printf("overflowed %d\n", flan_dev_reg_overflowed());
return 0;
}
/* A live set larger than the table's first size: 4096 live blocks and
* nothing dead anywhere, then three more. The table grows, so none is
* dropped and the overflow flag stays clear. */
static int regoverflow(void) {
flan_dev_reg_enable();
for (int i = 0; i < CAP; i++)
flan_dev_reg_note((void *)(uintptr_t)(0x2000000 + (uintptr_t)i * 64), 64, 8,
"Enemy", 5);
printf("live %lld\n", (long long)flan_dev_reg_count(1));
printf("overflowed %d\n", flan_dev_reg_overflowed());
fflush(stdout);
for (int i = 0; i < 3; i++)
flan_dev_reg_note((void *)(uintptr_t)(0x9000000 + (uintptr_t)i * 64), 64, 8,
"Latecomer", 9);
printf("overflowed %d\n", flan_dev_reg_overflowed());
printf("live %lld\n", (long long)flan_dev_reg_count(1));
return 0;
}
/* ── A listing taken while the table is actually being rearranged ────
*
* [regfull] is a table of live blocks, where the fix is that no compaction
* runs at all. This is the other race: a writer churning hard enough that
* compactions do run, under a reader asking the leak question through them.
*
* What it pins is not a rate. Rates here are the machine's, and the counts
* printed are deliberately the two that cannot come out any other way on any
* machine: [zero], an answer of no rows, which is the original lie and the one
* shape a program holding three thousand blocks must never produce; and
* [low], a count that is neither right nor a refusal, which is what a torn
* read or a walk that straddled a compaction would produce. Right-versus-
* refused is the interesting number and it is printed rather than asserted,
* because a refusal is an honest answer and how often it comes up is the
* writer's business. It moves a lot: on the machine this was written on, the
* paced retry turned 8 right in 200 into 200 in 200 at one churn rate and
* changed nothing at all at another.
*
* The throttle is a spin and not a sleep because the whole interesting range
* is under nanosleep's floor of tens of microseconds. */
enum { RACE_LIVE = 3000, RACE_ASKS = 200, RACE_SPIN = 1000 };
static volatile int race_stop;
static void *race_writer(void *arg) {
long long i = 0;
(void)arg;
while (!race_stop) {
/* Noted and freed, which is what makes the dead count climb and the
* compaction actually run — the difference from [regfull]'s writer, which
* re-notes live blocks and therefore never earns a sweep. */
void *p = (void *)(uintptr_t)(0x40000000 + (uintptr_t)(i % 100000) * 64);
flan_dev_reg_note(p, 32, 4, "Bullet", 6);
flan_dev_reg_dead(p);
for (volatile long k = 0; k < RACE_SPIN; k++) { }
i++;
}
return NULL;
}
static int regrace(void) {
enum { ROWS = 16 };
int64_t counts[ROWS], bytes[ROWS], typelens[ROWS], unread;
const char *types[ROWS];
pthread_t w;
int right = 0, refused = 0, zero = 0, low = 0;
flan_dev_reg_enable();
for (int i = 0; i < RACE_LIVE; i++)
flan_dev_reg_note((void *)(uintptr_t)(0x100000 + (uintptr_t)i * 64), 64, 8,
"Enemy", 5);
if (pthread_create(&w, NULL, race_writer, NULL) != 0) {
printf("no writer thread\n");
return 2;
}
for (int a = 0; a < RACE_ASKS; a++) {
int64_t n =
flan_dev_reg_by_type(1, counts, bytes, types, typelens, ROWS, &unread);
if (n < 0) refused++;
else if (n == 0) zero++;
else {
int64_t enemies = 0;
for (int64_t j = 0; j < n && j < ROWS; j++)
if (typelens[j] == 5) enemies = counts[j];
if (enemies == RACE_LIVE) right++; else low++;
}
}
race_stop = 1;
pthread_join(w, NULL);
printf("zero %d\n", zero);
printf("low %d\n", low);
printf("answered %d refused %d\n", right, refused);
return 0;
}
/* ── The seqlock's losing side ───────────────────────────────────────
*
* [flan_dev_result_read] copies, then checks the counter has not moved. The
* single-threaded reads above only ever take the winning side. These take the
* other one, deterministically: [flan_dev_result_read_hook] runs inside the
* window between the copy and the check, and what it does there is a write.
*
* Three writers. One that publishes a value once: the read must throw its
* copy away and come back with the new value, numbered one higher — a read
* that did not check would return the old bytes under the old number, which
* is a stale answer described as current. One that publishes on every
* attempt: the read runs out of attempts and says so, with no bytes and the
* number of the last complete value. And one that opens a write and never
* closes it: every attempt sees an odd counter, so the copy never happens and
* the hook never runs. */
extern void (*flan_dev_result_read_hook)(void);
static int race_calls;
static void publish(const char *v) {
flan_dev_result_begin();
flan_dev_emit((const uint8_t *)v, (int64_t)strlen(v));
flan_dev_result_end();
}
static void race_once(void) {
race_calls++;
flan_dev_result_read_hook = NULL;
publish("second");
}
static void race_every(void) {
race_calls++;
publish("again");
}
static int race(void) {
uint64_t gen0, gen, len;
int ok;
publish("first");
(void)result_read(&gen0, &len);
race_calls = 0;
flan_dev_result_read_hook = race_once;
ok = flan_dev_result_read(rbuf, sizeof rbuf, &gen, &len);
printf("once ok %d calls %d gen +%llu value %.*s\n", ok, race_calls,
(unsigned long long)(gen - gen0), (int)len, rbuf);
race_calls = 0;
flan_dev_result_read_hook = race_every;
ok = flan_dev_result_read(rbuf, sizeof rbuf, &gen, &len);
flan_dev_result_read_hook = NULL;
{
uint64_t now, nlen;
(void)result_read(&now, &nlen);
printf("every ok %d calls %d len %llu gen %s\n", ok, race_calls,
(unsigned long long)len,
gen == now ? "last complete" : "not the last complete");
}
race_calls = 0;
(void)result_read(&gen0, &len);
flan_dev_result_read_hook = race_every;
flan_dev_result_begin();
ok = flan_dev_result_read(rbuf, sizeof rbuf, &gen, &len);
flan_dev_result_read_hook = NULL;
printf("open ok %d calls %d len %llu gen +%llu\n", ok, race_calls,
(unsigned long long)len, (unsigned long long)(gen - gen0));
flan_dev_result_end();
(void)result_read(&gen, &len);
printf("closed gen +%llu\n", (unsigned long long)(gen - gen0));
return 0;
}
/* A String's character walk over bytes that are not valid UTF-8, which no
* Flan program can make: a lead byte promising three bytes where the length
* leaves one. The header is flan_rt.c's flan_vec, restated; a null allocator
* skips the epoch check. The bytes past the length are valid continuation
* bytes, so a walk that read past the end would count and decode them. */
typedef struct {
void *ptr;
int64_t len, cap;
void *alloc;
int64_t epoch;
} limits_vec;
int64_t flan_string_index(void *v, int32_t i, int32_t past_end,
const uint8_t *loc, int64_t loclen, void *xfer);
int32_t flan_string_remove(void *v, int64_t off, const uint8_t *loc,
int64_t loclen);
static int string_walk(void) {
static uint8_t lead_last[4] = { 0x61, 0xe6, 0x80, 0x80 };
static uint8_t lead_first[4] = { 0xe6, 0x61, 0x80, 0x80 };
limits_vec a = { lead_last, 2, 4, NULL, 0 };
limits_vec b = { lead_first, 2, 4, NULL, 0 };
const uint8_t *loc = (const uint8_t *)"dev_limits.c";
printf("index %lld\n", (long long)flan_string_index(&b, 1, 0, loc, 12, NULL));
printf("end %lld\n", (long long)flan_string_index(&a, 2, 1, loc, 12, NULL));
printf("removed %d len %lld\n", flan_string_remove(&a, 1, loc, 12),
(long long)a.len);
return 0;
}
int main(int argc, char **argv) {
flan_rt_init(argc, argv);
if (argc < 2) {
fprintf(stderr,
"usage: %s cap|race|string|names|regfull|regchurn|regrace|regoverflow\n",
argv[0]);
return 2;
}
if (strcmp(argv[1], "string") == 0) return string_walk();
if (strcmp(argv[1], "cap") == 0) return cap();
if (strcmp(argv[1], "race") == 0) return race();
if (strcmp(argv[1], "names") == 0) return names();
if (strcmp(argv[1], "regfull") == 0) return regfull();
if (strcmp(argv[1], "regchurn") == 0) return regchurn();
if (strcmp(argv[1], "regrace") == 0) return regrace();
if (strcmp(argv[1], "regoverflow") == 0) return regoverflow();
fprintf(stderr, "unknown mode %s\n", argv[1]);
return 2;
}