The registry compacted whenever the table was three quarters full, and a compaction reclaims dead entries and nothing else. A program holding more than three quarters of the table in live blocks therefore compacted on every allocation for the rest of its life, reclaiming nothing each time and holding the table-wide epoch odd while it did. A listing racing that loop lost all eight of its attempts and answered with zero rows -- "nothing is held", about a program holding three thousand blocks, from the verb that exists to find a leak. Measured at 199 wrong answers in 200. The trigger now also asks whether there is an eighth of a table's worth of dead to reclaim, which is a count four places maintain: a death, an arena's free-all, a note written over a dead slot, and the sweep itself. That bounds the cost from the other side too, since a sweep that runs reclaims at least 512 slots and so cannot run twice in 512 allocations. Separately, flan_dev_reg_by_type answered a walk it could not take with zero rows, which is the same number a program that had freed everything gets, and stepped past slots flan_reg_snap could not copy while still calling the walk whole. It now counts those slots and returns -1 with the count, the agent refuses in a sentence the daemon already renders, and the snap contract says which caller keeps it and why reg_at is allowed not to. A note that finds no slot is still dropped -- dying because a diagnostic ran out of room would be the diagnostic shooting the patient -- and now says so on stderr once, quoting how many entries were dead rather than claiming the table is all live. test/dev_limits.c gains three modes, driven from test_reload: 3100 live blocks read under a writer thread (1 right in 200 before, 200 after), 3000 live with 600 churned on top of them to prove the sweep still runs, and a genuinely full table that must say so exactly once.
304 lines
12 KiB
C
304 lines
12 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;
|
|
}
|
|
|
|
/* And a table that really is full, which is the case the trigger above must
|
|
* not paper over: 4096 live blocks and nothing dead anywhere, then one more.
|
|
* The note is dropped — that is the standing decision, and dying because a
|
|
* diagnostic ran out of room would be worse — and what is checked here is that
|
|
* the drop is *said*, once, rather than only being discoverable by asking the
|
|
* flag. A second and third dropped note must add nothing. */
|
|
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;
|
|
}
|
|
|
|
int main(int argc, char **argv) {
|
|
flan_rt_init(argc, argv);
|
|
if (argc < 2) {
|
|
fprintf(stderr, "usage: %s cap|names|regfull|regchurn|regoverflow\n",
|
|
argv[0]);
|
|
return 2;
|
|
}
|
|
if (strcmp(argv[1], "cap") == 0) return cap();
|
|
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], "regoverflow") == 0) return regoverflow();
|
|
fprintf(stderr, "unknown mode %s\n", argv[1]);
|
|
return 2;
|
|
}
|