486 lines
19 KiB
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;
|
|
}
|