Retrying immediately looked like eight chances and was one: a walk that bails at the epoch check costs almost nothing, so all eight fit inside the single compaction they were all losing to, and the listing refused having waited for nothing. A quarter of a millisecond between attempts -- the agent's break-loop idiom, legal here because the waiter is the listener thread and never the game loop -- bounds the whole refusal at two milliseconds. Measured with a writer noting and freeing on top of three thousand live blocks: 8 right answers in 200 without the pause, 200 in 200 with it. It is not magic, and the comment says so: a writer that spends most of its time rearranging the table still gets refused, which is the honest answer and used to be a zero-row lie. The two cases the last commit left unwired are wired now, and a third joins them: a listing taken while the table really is being compacted, which nothing covered -- the full-of-live case never compacts and the churn case is single-threaded, so the retry itself was exercised by nothing. It asserts only what a slower machine cannot change: never zero rows, never a count that is neither right nor a refusal. How the rest divides is printed, not pinned.
383 lines
15 KiB
C
383 lines
15 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;
|
|
}
|
|
|
|
|
|
/* ── 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;
|
|
}
|
|
|
|
int main(int argc, char **argv) {
|
|
flan_rt_init(argc, argv);
|
|
if (argc < 2) {
|
|
fprintf(stderr,
|
|
"usage: %s cap|names|regfull|regchurn|regrace|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], "regrace") == 0) return regrace();
|
|
if (strcmp(argv[1], "regoverflow") == 0) return regoverflow();
|
|
fprintf(stderr, "unknown mode %s\n", argv[1]);
|
|
return 2;
|
|
}
|