Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ tests/stress.clap: tests/stress_plugin.c
$(CC) $(CLAP_CFLAGS) $(CFLAGS) -Werror -shared -o $@ $<

# every guard broken on purpose, one at a time: its test must go red
sabotage: $(PROG) $(FAKE) tests/lv2-probe-child tests/rt_interpose.so $(LV2_FIXTURE)
sabotage: $(PROG) $(FAKE) tests/probe-child tests/stress.clap tests/lv2-probe-child tests/rt_interpose.so $(LV2_FIXTURE)
MOD_HOST_DIR=$(MOD_HOST_DIR) FAKE_HOST=./$(FAKE) python3 tests/sabotage.py

# the jack server the real-worker tests run over: pipewire's own jack implementation (default) or JACK2's jackd
Expand Down
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -324,11 +324,19 @@ Each rate's grid point carries the cost clap-qualify timed at that rate, in nano
no input for the impulse, so its latency is measured from a note (`method: note-onset`), with `heard: false` at a rate
where the note did not sound.

clap-qualify runs under plugin-qualify's interposer, `rt_interpose.so` beside the child. A bare `--child` is found as execvp
finds it; name the child by path when PATH is unset or holds a directory of its name. The measured child gets the interposer
and nothing else: an `LD_PRELOAD` the runner was started with is dropped for the measurement, named in `run.tools.droppedPreload`
and in the `rt-safety` step, and put back in the runner's environment after the child. The first grid point carries what process() did under it, and an instrument's
note sweep carries what process() did while notes played. `run.tools.interposer` names it once the child says it counted
under it. With no interposer beside the child, or one that did not load, no count is written: rtSafety says
`interposer: false`, and the `rt-safety` and `note-sweep` steps are `probe.not-attempted` and say why.

An LV2 plugin is measured the way plugin-qualify's LV2 hosting pass measures it today, in two children under the same
limits and the same wall clock. The first child (`lv2_measure --pin`) pins the bundle's files without instantiating the
plugin. The second measures at the highest of `--rates`, in blocks of `--quantum`, with 30 instantiate and destroy cycles
and 3 swept rt-safety runs. It runs under plugin-qualify's interposer (`rt_interpose.so` beside the child) and the noise
stimulus. The document's binary is the plugin's library, with every pinned file of the bundle listed. The rate's grid
and 3 swept rt-safety runs. It runs under plugin-qualify's interposer (`rt_interpose.so` beside the child, found and with any
other preload dropped as for clap-qualify) and the noise stimulus. The document's binary is the plugin's library, with every pinned file of the bundle listed. The rate's grid
point carries what run() did under the interposer and, for a plugin with a MIDI input, its note sweep (what run() did
while notes played), and `behaviour` carries the lifecycles, the threads and the features
the plugin requires. A plugin no bundle holds exits 3 with no document. A plugin of which nothing could be pinned exits 2
Expand Down
10 changes: 9 additions & 1 deletion doc/plugin-hostd-probe.1
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,20 @@ plugin\-hostd\-probe \- run a plugin prober under limits and write what happened
.B plugin\-hostd\-probe
starts a program that loads a plugin (clap\-qualify for a CLAP, plugin\-qualify's lv2_measure for an LV2 named
.BI lv2: uri ,
the pin pass and then the measurement at the highest rate, under its interposer) in its own process group, with the portable
the pin pass and then the measurement at the highest rate; both run under plugin\-qualify's interposer, rt_interpose.so beside
the child) in its own process group, with the portable
limits applied before the exec, and writes the facts of the run as a JSON document: the steps the child reached, how
each ended and the limits that were applied. It gives no verdict. The runner never loads plugin code itself. On the
deadline, on a cancel or when the child is done it kills the whole process tree, including a process the plugin
started and orphaned.
.PP
A bare
.B \-\-child
is found as execvp finds it; name the child by path when PATH is unset or holds a directory of its name.
The measured child gets the interposer and nothing else: an LD_PRELOAD the runner was started with is dropped for the
measurement, named in run.tools.droppedPreload and in the rt\-safety step, and put back in the runner's environment after
the child.
.PP
The document follows
.IR plugin\-hostd/probe\-output.schema.json .
Nothing a plugin prints can enter it.
Expand Down
156 changes: 126 additions & 30 deletions src/probe.c
Original file line number Diff line number Diff line change
Expand Up @@ -1345,13 +1345,108 @@ static const char *child_end(const char *name, const options_t *o, const run_t *
* it; plugin-qualify is no dependency of this package (it builds against it), so the program is looked for at run time */
#define PROBE_LV2_PREFIX "lv2:"
#define PROBE_LV2_CHILD "/usr/libexec/plugin-qualify/lv2_measure"
/* the measurement runs as plugin-qualify runs it: under its allocation-and-syscall interposer, the file of this name
* beside the child (none beside it: rtSafety says interposer false), and the noise stimulus */
#define PROBE_LV2_INTERPOSER "rt_interpose.so"
/* both measurements run as plugin-qualify runs them: under its allocation-and-syscall interposer, the file of this name
* beside the child (none beside it: rtSafety says interposer false, and the rt-safety step says why); an LV2 one also
* under the noise stimulus */
#define PROBE_INTERPOSER "rt_interpose.so"
#define PROBE_LV2_STIMULUS_ENV "OMX_STIM"
#define PROBE_LV2_STIMULUS "noise"

/* the interposer preloaded into the measuring child, or empty; when empty, why none was */
static char g_interposer[PATH_MAX];
static char g_no_interposer[PATH_MAX + 128];

/* the interposer beside `child`, the child found as execvp finds it (a bare name on PATH): 1 with g_interposer its path,
* else 0 with g_no_interposer saying where none was found */
static int interposer_beside(const char *child)
{
char dir[PATH_MAX];
const char *slash = strrchr(child, '/');

g_interposer[0] = '\0';
if (slash)
snprintf(dir, sizeof(dir), "%.*s", (int)(slash - child), child);
else
{
const char *path = getenv("PATH"), *p;
int found = 0;

for (p = path ? path : ""; !found && *p;)
{
size_t n = strcspn(p, ":");
char exe[PATH_MAX];

snprintf(dir, sizeof(dir), "%.*s", (int)(n ? n : 1), n ? p : ".");
snprintf(exe, sizeof(exe), "%s/%s", dir, child);
found = access(exe, X_OK) == 0;
p += n + (p[n] == ':');
}
if (!found)
{
snprintf(g_no_interposer, sizeof(g_no_interposer), "%s is not on PATH, so no " PROBE_INTERPOSER " beside it was preloaded", child);
return 0;
}
}
snprintf(g_interposer, sizeof(g_interposer), "%s/" PROBE_INTERPOSER, dir);
if (access(g_interposer, R_OK) == 0)
return 1;
snprintf(g_no_interposer, sizeof(g_no_interposer), "no " PROBE_INTERPOSER " beside the child in %s", dir[0] ? dir : "/");
g_interposer[0] = '\0';
return 0;
}

/* The measured child gets the interposer and nothing else: a preload the runner was started with (another malloc
* interposer would hide the allocations) is dropped for the measurement, the record says so, and the runner's own
* LD_PRELOAD is put back after the child */
static int g_had_preload;
static char g_saved_preload[PATH_MAX * 4];
static char g_dropped_preload[PATH_MAX * 4];

static void preload_for_measurement(const char *child)
{
const char *was = getenv("LD_PRELOAD");

g_had_preload = was != NULL;
snprintf(g_saved_preload, sizeof(g_saved_preload), "%s", was ? was : "");
g_dropped_preload[0] = '\0';
if (interposer_beside(child))
setenv("LD_PRELOAD", g_interposer, 1);
else
unsetenv("LD_PRELOAD");
/* what the runner had and the child does not get is dropped */
if (g_saved_preload[0] && strcmp(g_saved_preload, g_interposer))
snprintf(g_dropped_preload, sizeof(g_dropped_preload), "%s", g_saved_preload);
}

static void preload_restore(void)
{
if (g_had_preload)
setenv("LD_PRELOAD", g_saved_preload, 1);
else
unsetenv("LD_PRELOAD");
}

/* an rt-safety detail, with the preload that was dropped for the measurement when there was one */
static const char *with_dropped(const char *text)
{
static char out[512];

if (!g_dropped_preload[0])
return text;
snprintf(out, sizeof(out), "%s; dropped LD_PRELOAD=%s for the measurement", text, g_dropped_preload);
return out;
}

/* why a child's line says no interposer counted: none was found beside it, or the one preloaded did not load */
static const char *no_interposer_reason(void)
{
static char text[PATH_MAX + 128];

if (!g_interposer[0])
return g_no_interposer[0] ? g_no_interposer : "no interposer was preloaded";
snprintf(text, sizeof(text), "%s was preloaded and the child says no interposer was loaded", g_interposer);
return text;
}

static jnode_t *first_line(sb_t *out)
{
Expand Down Expand Up @@ -1423,25 +1518,11 @@ static int run_lv2(const options_t *o, const char *uri, long rate, const cpu_set
argv[4] = cycles_s;
argv[5] = repeats_s;
argv[6] = NULL;
{
char dir[PATH_MAX];
const char *slash;

snprintf(dir, sizeof(dir), "%s", o->child);
slash = strrchr(dir, '/');
if (slash)
{
snprintf(g_interposer, sizeof(g_interposer), "%.*s/" PROBE_LV2_INTERPOSER, (int)(slash - dir), dir);
if (access(g_interposer, R_OK) == 0)
setenv("LD_PRELOAD", g_interposer, 1);
else
g_interposer[0] = '\0';
}
setenv(PROBE_LV2_STIMULUS_ENV, PROBE_LV2_STIMULUS, 1);
}
preload_for_measurement(o->child);
setenv(PROBE_LV2_STIMULUS_ENV, PROBE_LV2_STIMULUS, 1);
memset(r, 0, sizeof(*r));
run_child(o, argv, pinned, pin_option, r, deadline);
unsetenv("LD_PRELOAD");
preload_restore();
*measure = first_line(&r->out);
if (*measure && jbool(*measure, "instantiated", 0))
{
Expand All @@ -1452,7 +1533,7 @@ static int run_lv2(const options_t *o, const char *uri, long rate, const cpu_set
s = add_step("rt-safety", jbool(rt, "interposer", 0) ? OUT_OK : OUT_NOT_ATTEMPTED, 0);
s->rate = rate;
s->quantum = o->quantum;
set_detail(s, jbool(rt, "interposer", 0) ? "read from the child's line" : "no interposer was loaded");
set_detail(s, "%s", with_dropped(jbool(rt, "interposer", 0) ? "read from the child's line" : no_interposer_reason()));
}
else if (*measure && jbool(*measure, "found", 0))
{
Expand All @@ -1473,7 +1554,7 @@ static void put_count(sb_t *b, const jnode_t *o, const char *from, const char *t
}

/* a prober's note sweep, as a point carries it: whether it applied, the dialect it played in, and what process() or run()
* did while it played when the interposer counted */
* did while it played when the interposer counted; a sweep the interposer did not count carries no counts */
static void put_note_sweep(sb_t *b, const jnode_t *ns)
{
sb_printf(b, ", \"noteSweep\": {\"applicable\": %s", jbool(ns, "applicable", 0) ? "true" : "false");
Expand All @@ -1482,9 +1563,12 @@ static void put_note_sweep(sb_t *b, const jnode_t *ns)
sb_printf(b, ", \"dialect\": ");
sb_str(b, jstr(ns, "dialect"));
}
put_count(b, ns, "allocationsInRun", "allocations");
put_count(b, ns, "locksInRun", "locks");
put_count(b, ns, "syscallsInRun", "syscalls");
if (jbool(ns, "interposer", 0))
{
put_count(b, ns, "allocationsInRun", "allocations");
put_count(b, ns, "locksInRun", "locks");
put_count(b, ns, "syscallsInRun", "syscalls");
}
sb_printf(b, "}");
}

Expand Down Expand Up @@ -1820,7 +1904,10 @@ int main(int argc, char **argv)

add_step("hash", OUT_OK, now_ms() - t0);
clap_argv(&o, argv_child);
/* the CLAP prober counts under the interposer beside it, as the LV2 measurement does */
preload_for_measurement(o.child);
run_child(&o, argv_child, &pinned, pin_option, &r, t0 + o.wall_s * 1000);
preload_restore();
run_outcome = child_end("lifecycle", &o, &r, &complete);
}
read_idle(g_idle_after, &g_nidle, &pinned);
Expand Down Expand Up @@ -1875,21 +1962,24 @@ int main(int argc, char **argv)
{
step_t *x = add_step("rt-safety", OUT_OK, 0);

set_detail(x, "counts are totals over every rate, read from the child's line");
set_detail(x, "%s", with_dropped("counts are totals over every rate, read from the child's line"));
}
else if (rt)
{
step_t *x = add_step("rt-safety", OUT_NOT_ATTEMPTED, 0);

set_detail(x, "no interposer was loaded");
set_detail(x, "%s", with_dropped(no_interposer_reason()));
}
{
step_t *x = add_step("note-sweep", jbool(notes, "applicable", 0) ? OUT_OK : OUT_NOT_ATTEMPTED, 0);
int counted = jbool(notes, "applicable", 0) && jbool(notes, "interposer", 0);
step_t *x = add_step("note-sweep", counted ? OUT_OK : OUT_NOT_ATTEMPTED, 0);

if (!jbool(notes, "applicable", 0))
set_detail(x, "no note input");
else
else if (counted)
set_detail(x, "counts are totals over every rate, read from the child's line");
else
set_detail(x, "the notes played uncounted: %s", no_interposer_reason());
}
}
for (i = 0; i < g_nsteps; i++)
Expand All @@ -1911,11 +2001,17 @@ int main(int argc, char **argv)
PHD_PROBE_PROTOCOL_VERSION);
sb_printf(&doc, " \"tools\": {\"runner\": \"" RUNNER_NAME " %s\", \"child\": ", PROBE_VERSION);
sb_str(&doc, base_name(o.child));
if (g_interposer[0])
/* the interposer is named when it was preloaded and the child says it counted under it */
if (g_interposer[0] && jbool(lv2_uri ? jget(measure_line, "rtSafety") : rt, "interposer", 0))
{
sb_printf(&doc, ", \"interposer\": ");
sb_str(&doc, base_name(g_interposer));
}
if (g_dropped_preload[0])
{
sb_printf(&doc, ", \"droppedPreload\": ");
sb_str(&doc, g_dropped_preload);
}
sb_printf(&doc, "},\n \"via\": \"runner\"\n },\n \"plugin\": {\"format\": \"%s\", \"id\": ", lv2_uri ? "lv2" : "clap");
if (lv2_uri)
{
Expand Down
43 changes: 37 additions & 6 deletions tests/probe_child.c
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,14 @@
* seen. PROBE_CHILD_INSTRUMENT makes the plugin an instrument: its latency is taken from a note, its cost holds the
* notes --hold or --probe-notes give (no count: the rate is not timed) and it has a note sweep, counted under the
* interposer when PROBE_CHILD_NOTE_ALLOCATIONS gives the allocations it saw. PROBE_CHILD_SILENT_RATE is a rate where
* the note was not heard, PROBE_CHILD_UNMEASURED_RATE one the cost arm did not time. */
* the note was not heard, PROBE_CHILD_UNMEASURED_RATE one the cost arm did not time.
*
* As clap-qualify does, it says it ran under the interposer only when the interposer is in the process: the symbol of
* tests/rt_interpose_stub.c, which the runner preloads as the rt_interpose.so beside this program. Then its
* rtSafety carries counts, PROBE_CHILD_ALLOCATIONS allocations (0 when unset); without it rtSafety says interposer false
* and has none. Its note sweep says the same of the interposer but prints PROBE_CHILD_NOTE_ALLOCATIONS either way, so a
* runner that copies counts the interposer did not take is seen. PROBE_CHILD_NO_RECORDS writes no step records, as
* clap-qualify writes none, so the runner makes its steps from the line. */

#ifndef _GNU_SOURCE
#define _GNU_SOURCE
Expand Down Expand Up @@ -104,7 +111,11 @@ static bool no_events_push(const clap_output_events_t *list, const clap_event_he
static void record(const char *step, long rate, long quantum, long since)
{
char line[256];
int n = snprintf(line, sizeof(line), "{\"step\":\"%s\",\"outcome\":\"probe.ok\",\"elapsedMs\":%ld", step, now_ms() - since);
int n;

if (getenv("PROBE_CHILD_NO_RECORDS"))
return;
n = snprintf(line, sizeof(line), "{\"step\":\"%s\",\"outcome\":\"probe.ok\",\"elapsedMs\":%ld", step, now_ms() - since);

if (rate)
n += snprintf(line + n, sizeof(line) - (size_t)n, ",\"rate\":%ld,\"quantum\":%ld", rate, quantum);
Expand All @@ -117,6 +128,8 @@ static void record(const char *step, long rate, long quantum, long since)
static void put_rates(const char *rates, long quantum, unsigned notes)
{
const char *instrument = getenv("PROBE_CHILD_INSTRUMENT"), *note_allocs = getenv("PROBE_CHILD_NOTE_ALLOCATIONS");
const char *allocs = getenv("PROBE_CHILD_ALLOCATIONS");
int interposed = dlsym(RTLD_DEFAULT, "plugin_hostd_rt_interpose_stub") != NULL;
long silent = getenv("PROBE_CHILD_SILENT_RATE") ? atol(getenv("PROBE_CHILD_SILENT_RATE")) : 0;
long unmeasured = getenv("PROBE_CHILD_UNMEASURED_RATE") ? atol(getenv("PROBE_CHILD_UNMEASURED_RATE")) : 0;
const char *p;
Expand All @@ -127,12 +140,18 @@ static void put_rates(const char *rates, long quantum, unsigned notes)
for (p = rates; (r = strtol(p, &end, 10)) > 0; p = *end ? end + 1 : end)
printf("%s\"%ld\":{\"declaredFrames\":0,\"measuredFrames\":%d%s%s}", p == rates ? "" : ",", r, instrument ? 1 : 0,
instrument ? ",\"from\":\"note\"" : "", instrument && r == silent ? ",\"heard\":false" : "");
printf("},\"threadViolations\":0,\"processErrors\":0},\"rtSafety\":{\"interposer\":false}");
printf("},\"threadViolations\":0,\"processErrors\":0}");
if (interposed)
printf(",\"rtSafety\":{\"interposer\":true,\"swept\":false,\"sweepApplicable\":true,\"blocks\":64,\"allocationsInRun\":%ld,"
"\"locksInRun\":0,\"syscallsInRun\":0,\"events\":[]}", allocs ? atol(allocs) : 0L);
else
printf(",\"rtSafety\":{\"interposer\":false}");
if (instrument && note_allocs)
printf(",\"noteSweep\":{\"applicable\":true,\"dialect\":\"clap\",\"interposer\":true,\"allocationsInRun\":%ld,"
"\"locksInRun\":0,\"syscallsInRun\":0,\"events\":[]}", atol(note_allocs));
printf(",\"noteSweep\":{\"applicable\":true,\"dialect\":\"clap\",\"interposer\":%s,\"allocationsInRun\":%ld,"
"\"locksInRun\":0,\"syscallsInRun\":0,\"events\":[]}", interposed ? "true" : "false", atol(note_allocs));
else
printf(",\"noteSweep\":{\"applicable\":%s%s}", instrument ? "true" : "false", instrument ? ",\"dialect\":\"clap\",\"interposer\":false" : "");
printf(",\"noteSweep\":{\"applicable\":%s%s%s}", instrument ? "true" : "false", instrument ? ",\"dialect\":\"clap\",\"interposer\":" : "",
instrument ? (interposed ? "true" : "false") : "");
printf(",\"cost\":{\"stimulus\":\"%s\",\"blockFrames\":%ld,\"warmupBlocks\":64,\"percentile\":95,\"perRate\":{",
instrument ? "notes-held" : "noise", quantum);
for (p = rates; (r = strtol(p, &end, 10)) > 0; p = *end ? end + 1 : end)
Expand Down Expand Up @@ -198,6 +217,18 @@ int main(int argc, char **argv)
if (f)
fclose(f);
}
/* PROBE_CHILD_ENV names a file to write the environment the runner gave, one variable to a line */
if (getenv("PROBE_CHILD_ENV"))
{
extern char **environ;
FILE *f = fopen(getenv("PROBE_CHILD_ENV"), "w");
char **e;

for (e = environ; f && *e; e++)
fprintf(f, "%s\n", *e);
if (f)
fclose(f);
}
rate = atol(rates);
lib = dlopen(path, RTLD_NOW | RTLD_LOCAL);
entry = lib ? dlsym(lib, "clap_entry") : NULL;
Expand Down
Loading
Loading