Skip to content

Commit 664808f

Browse files
authored
Merge pull request #1017 from shorepine/perf/sequencer-active-list
sequencer: walk the slots that are scheduled, not 0..highest_tag
2 parents 187735a + 57db108 commit 664808f

5 files changed

Lines changed: 456 additions & 10 deletions

File tree

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ amy-message: $(OBJECTS) src/amy-message.o
104104

105105
# Plain C tests for things the audio-rendering suite can't reach -- e.g. clock
106106
# rollovers 50 days out, which you can only hit by fast-forwarding the counters.
107-
CTESTS = tests/test_clock_wrap
107+
CTESTS = tests/test_clock_wrap tests/test_sequencer_active tests/test_sequencer_bounds
108108

109109
# Static pattern rules, so these win over the generic %.o: %.c above (which
110110
# would compile without -Isrc and fail to find amy.h).

src/patches.c

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,28 @@ void snprintfloat3dp(char *s, size_t max_len, float val) {
222222
} \
223223
} \
224224
}
225+
// As _EPRINT_I_SEQ but unsigned. ticks needs this: its values are uint32_t,
226+
// and printing one past INT32_MAX as a negative number makes the unsigned
227+
// list parser on the other end stop at the '-' -- for a 3-value ticks that
228+
// silently turned an (invalid, should-be-rejected) tag into a 2-value
229+
// anonymous entry.
230+
#define _EPRINT_U_SEQ(FIELD, NAME, LEN, WIRECODE) { \
231+
int last_set = -1; \
232+
for (int i = 0; i < LEN; ++i) { \
233+
if (AMY_IS_SET(e->FIELD[i])) last_set = i; \
234+
} \
235+
if (last_set >= 0) { \
236+
snprintf(s, len - (size_t)(s - s_entry), "%s", wirecode ? WIRECODE : " " NAME ": "); \
237+
s += strlen(s); \
238+
for (int i = 0; i <= last_set; ++i) { \
239+
if (i > 0) { snprintf(s, len - (size_t)(s - s_entry), ","); s += strlen(s); } \
240+
if (AMY_IS_SET(e->FIELD[i])) { \
241+
snprintf(s, len - (size_t)(s - s_entry), "%" PRIu32, (uint32_t)e->FIELD[i]); \
242+
s += strlen(s); \
243+
} \
244+
} \
245+
} \
246+
}
225247
#define _EPRINT_F_SEQ(FIELD, NAME, LEN, WIRECODE) { \
226248
int last_set = -1; \
227249
for (int i = 0; i < LEN; ++i) { \
@@ -296,12 +318,12 @@ int sprint_event(amy_event *e, char *s, size_t len, bool wirecode) {
296318
snprintf(s, len - (size_t)(s - s_entry), "amy_event(time=%" PRIu32 ", osc=%u, addr_osc=%d adr_syn=%d adr_bus=%d): ", e->time, (unsigned)e->osc,
297319
event_addresses_oscs(e), event_addresses_synth(e), event_addresses_bus(e));
298320
s += strlen(s);
299-
_EPRINT_I_SEQ(ticks, "ticks", 3, "H"); // tick, period, tag
321+
_EPRINT_U_SEQ(ticks, "ticks", 3, "H"); // tick, period, tag
300322
} else {
301323
// e->time has no wire representation anymore (there's no 't' command);
302324
// it's only ever meaningful as this event's own near-term playback time.
303325
// ticks ("H") must always be the first entry in wire code if used.
304-
_EPRINT_I_SEQ(ticks, "ticks", 3, "H"); // tick, period, tag
326+
_EPRINT_U_SEQ(ticks, "ticks", 3, "H"); // tick, period, tag
305327
_EPRINT_I(osc, "osc", "v");
306328
}
307329
_EPRINT_I(wave, "wave", "w");

src/sequencer.c

Lines changed: 85 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,24 @@ typedef struct sequence_info_t {
1515
//uint32_t tag; // tag is implicit, it's its index in the table
1616
uint32_t tick; // 0 means not used
1717
uint32_t period; // 0 means not used
18+
// Next OCCUPIED slot, or -1 for the end. Only meaningful while this
19+
// entry has a wire -- `wire != NULL` is what "in the list" means, so
20+
// there is one source of truth and not two to keep in step.
21+
int32_t next_active;
1822
} sequence_info_t;
1923

2024
struct sequence_info_t *sequences = NULL; // An array indexed by tag.
2125
int32_t max_sequences = 0; // Number of user-addressable tags.
22-
int32_t highest_tag = -1;
26+
// Head of the ascending list of occupied slots (user tags and anonymous
27+
// entries alike); -1 when nothing is scheduled. This replaces `highest_tag`,
28+
// which was a HIGH-WATER MARK: it only ever grew, so one event at a high tag
29+
// made every tick scan that far for the rest of the session, long after that
30+
// sequence was cleared. The anonymous pool made that the common case, not a
31+
// corner: anonymous entries are allocated round-robin at indices past
32+
// max_sequences, so a burst of ticks= one-shots pinned the mark at the very
33+
// end of the table permanently. The cost is proportional to what is
34+
// scheduled now.
35+
int32_t first_active = -1;
2336
// Anonymous (no-tag) entries live past the user-addressable tag range, at
2437
// indices [max_sequences .. max_sequences+AMY_ANON_SEQUENCE_SLOTS), so a
2538
// user-supplied tag (bounds-checked against max_sequences) can never reach
@@ -49,7 +62,9 @@ void sequencer_init(int max_sequencer_tags) {
4962
sequences[i].wire = NULL;
5063
sequences[i].tick = 0;
5164
sequences[i].period = 0;
65+
sequences[i].next_active = -1;
5266
}
67+
first_active = -1;
5368
// We are read to go.
5469
sequencer_recompute();
5570
}
@@ -64,8 +79,9 @@ void sequencer_reset() {
6479
sequences[i].tick = 0;
6580
sequences[i].period = 0;
6681
}
82+
sequences[i].next_active = -1;
6783
}
68-
highest_tag = -1;
84+
first_active = -1;
6985
}
7086

7187
void sequencer_deinit() {
@@ -78,15 +94,70 @@ void sequencer_deinit() {
7894
}
7995

8096
void sequencer_debug() {
81-
fprintf(stderr, "sequencer: max_sequences %" PRIi32" highest_tag %" PRIi32 "\n", max_sequences, highest_tag);
82-
for (int32_t tag = 0; tag <= highest_tag; ++tag) {
97+
int32_t n_active = 0;
98+
for (int32_t t = first_active; t != -1; t = sequences[t].next_active) ++n_active;
99+
fprintf(stderr, "sequencer: max_sequences %" PRIi32" active %" PRIi32 "\n", max_sequences, n_active);
100+
for (int32_t tag = first_active; tag != -1; tag = sequences[tag].next_active) {
83101
if (sequences[tag].wire) {
84102
fprintf(stderr, "sequence tag %" PRIi32"%s tick %" PRIu32 " period %"PRIu32 " wire \"%s\"\n",
85103
tag, tag >= max_sequences ? " (anon)" : "", sequences[tag].tick, sequences[tag].period, sequences[tag].wire);
86104
}
87105
}
88106
}
89107

108+
/* The occupied slots, threaded through the table as an ASCENDING list.
109+
*
110+
* Why threaded rather than a list of its own: the table has to stay
111+
* indexable, because add and clear both reach a tag directly and want O(1)
112+
* to do it. This gets the tick scan down to the number of sequences
113+
* actually scheduled without giving that up, and without allocating
114+
* anything the render thread could walk into while it is being freed.
115+
*
116+
* WHY ASCENDING, and it is not tidiness: two sequences that hit on the same
117+
* tick play in the order they are visited, so the order decides which one
118+
* wins if they touch the same parameter. That order was slot order when
119+
* this was an indexed sweep, and keeping the list sorted keeps it slot
120+
* order. An insertion-ordered list would make a pattern sound different
121+
* after an edit.
122+
*
123+
* THREAD SAFETY. Link mutations happen only under the amy lock --
124+
* sequencer_add_wire() takes it, the tick loop's delete path takes it, and
125+
* sequencer_reset() is called with it already held -- so writers are
126+
* serialized. The tick WALK, though, runs without the lock, which is safe
127+
* because the links are INDICES INTO A FIXED ARRAY, not pointers:
128+
*
129+
* - publishing a splice is one aligned 32-bit store, so a walker sees
130+
* either the old link or the new one, never half of one;
131+
* - every stored link is greater than the slot holding it, so walking
132+
* strictly increases the index. A stale link can make a walker skip a
133+
* sequence or revisit one for a single tick; it cannot form a cycle,
134+
* cannot hang, and cannot leave the array.
135+
*
136+
* So the worst a race costs is one tick's events being wrong, which is the
137+
* same class of hazard the indexed sweep already had. A list of malloc'd
138+
* nodes would be a different class entirely -- a torn next pointer walks
139+
* the render thread into freed memory.
140+
*/
141+
static void active_link(int32_t tag)
142+
{
143+
int32_t *prev = &first_active;
144+
while (*prev != -1 && *prev < tag)
145+
prev = &sequences[*prev].next_active;
146+
if (*prev == tag)
147+
return; /* already in */
148+
sequences[tag].next_active = *prev; /* point at the tail we found... */
149+
*prev = tag; /* ...then publish, in one store */
150+
}
151+
152+
static void active_unlink(int32_t tag)
153+
{
154+
int32_t *prev = &first_active;
155+
while (*prev != -1 && *prev != tag)
156+
prev = &sequences[*prev].next_active;
157+
if (*prev == tag)
158+
*prev = sequences[tag].next_active; /* one store, again */
159+
}
160+
90161
void sequencer_recompute() {
91162
// 60000000 us/min / (bpm * ticks per beat); keep it single-precision -
92163
// unsuffixed double literals pull in software double emulation on 32-bit.
@@ -129,6 +200,7 @@ uint8_t sequencer_add_wire(uint32_t tick, uint32_t period, uint32_t tag, bool ha
129200
sequences[tag].wire = NULL;
130201
sequences[tag].tick = 0;
131202
sequences[tag].period = 0;
203+
active_unlink(tag); // out of the list while it has nothing in it
132204
if ((tick == 0 && period == 0) || // Non-schedulable event: just clear the tag.
133205
(tick != 0 && period == 0 && tick <= amy_global.sequencer_tick_count)) { // don't schedule things in the past.
134206
amy_release_lock();
@@ -138,7 +210,7 @@ uint8_t sequencer_add_wire(uint32_t tick, uint32_t period, uint32_t tag, bool ha
138210
sequences[tag].tick = tick;
139211
sequences[tag].period = period;
140212
sequences[tag].wire = wire;
141-
if ((int32_t)tag > highest_tag) highest_tag = tag; // To limit scanning through tags.
213+
active_link(tag); // ...and back in, now that it has a message again
142214
amy_release_lock();
143215
return 1;
144216
}
@@ -150,8 +222,12 @@ static void sequencer_process_tick(void) {
150222
// while still processing this tick's fires; restore on the way out.
151223
bool was_firing = wire_firing;
152224
wire_firing = true;
153-
// Scan through the tag table looking for matches
154-
for (int32_t tag = 0; tag <= highest_tag; ++tag) {
225+
// Walk only the slots that have something scheduled. This used to sweep
226+
// 0..highest_tag, a mark that never came down.
227+
int32_t tag = first_active;
228+
while (tag != -1) {
229+
// Read the link BEFORE anything below can unlink this entry.
230+
int32_t next = sequences[tag].next_active;
155231
if (sequences[tag].wire != NULL) {
156232
bool hit = false;
157233
bool delete = false;
@@ -174,6 +250,7 @@ static void sequencer_process_tick(void) {
174250
sequences[tag].wire = NULL;
175251
sequences[tag].tick = 0;
176252
sequences[tag].period = 0;
253+
active_unlink(tag);
177254
} else {
178255
size_t len = strlen(sequences[tag].wire);
179256
wire = (char *)malloc_caps(len + 1, amy_global.config.ram_caps_events);
@@ -189,6 +266,7 @@ static void sequencer_process_tick(void) {
189266
}
190267
}
191268
}
269+
tag = next;
192270
}
193271
wire_firing = was_firing;
194272
if(amy_global.config.amy_external_sequencer_hook != NULL) {

0 commit comments

Comments
 (0)