Files
freeswitch/tests/unit/switch_event.c
T
Calvin EllisonandCalvin Ellison 4fa2630a5b [Core] Avoid O(n^2) header dedup when cloning EF_UNIQ_HEADERS events (#3061)
* events: avoid O(n^2) header dedup when cloning EF_UNIQ_HEADERS events

switch_event_dup() copies a source event header by header via
switch_event_add_header_string(). When the source carries
EF_UNIQ_HEADERS (SWITCH_EVENT_CHANNEL_DATA / REQUEST_PARAMS / MESSAGE --
e.g. a channel's variable list), the destination inherits the flag, so
switch_event_base_add_header() runs a full switch_event_del_header()
linear scan of the partially-built list on every add to enforce
uniqueness. Cloning an N-header event is therefore O(n^2).

That scan is redundant while cloning: the source already enforced
uniqueness on insert, so copying it cannot introduce duplicates
regardless of the per-add scan. Clear EF_UNIQ_HEADERS on the
destination for the duration of the copy loop, then restore it.

This is a hot path. switch_channel_execute_on() -- invoked on ring,
answer, media, transfer, park, record and playback -- calls
switch_core_get_variables() and switch_channel_get_variables(), each of
which dup()s an EF_UNIQ_HEADERS event. On a production voicemail server
carrying ~191 channel variables per call, switch_event_del_header_val()
accounted for 47-69% of all FreeSWITCH CPU time in on-box perf profiles.

Microbenchmark (tests/unit/switch_event.c, switch_event_dup of an
N-header CHANNEL_DATA event, 4000 iterations, time per dup):

    N      before     after
    50     4.80 us    3.19 us
    100   11.34 us    5.88 us
    191   28.29 us   10.99 us   (2.6x)
    400  107.05 us   23.27 us   (4.6x)

"before" scales ~O(n^2); "after" is linear. Behaviour is unchanged: the
clone has identical headers, values and order, keeps EF_UNIQ_HEADERS,
and still enforces uniqueness on subsequent adds (covered by the test).

Signed-off-by: Calvin Ellison <cellison@youmail.com>

* tests: cover switch_event_dup uniqueness and add a dup benchmark

Adds a dup_uniq_bench case to the switch_event unit test. For an
EF_UNIQ_HEADERS (CHANNEL_DATA) source event it verifies the clone
preserves header count, values and the EF_UNIQ_HEADERS flag, and that
re-setting an existing key does not produce a duplicate in the clone.
It also prints a switch_event_dup timing sweep across header counts,
used to validate the O(n^2) -> O(n) change in switch_event_dup().

Signed-off-by: Calvin Ellison <cellison@youmail.com>

* tests: add dup_faithful_copy regression for switch_event_dup

Covers the EF_UNIQ_HEADERS edge case for the O(n^2)->O(n) dup change: a well-formed EF_UNIQ source stays unique through dup, and a malformed source already holding duplicate names (only possible if headers were added before the flag was set) is copied faithfully rather than silently collapsed to the last value.

Signed-off-by: Calvin Ellison <cellison@youmail.com>

* tests: gate dup_uniq_bench behind #ifdef BENCHMARK

The dup_uniq_bench timing sweep is a benchmark rather than a pass/fail correctness test. Guard the whole test with #ifdef BENCHMARK (matching the existing benchmark in this file) so it stays out of normal test runs; dup_faithful_copy continues to cover correctness of the switch_event_dup change.

Signed-off-by: Calvin Ellison <cellison@youmail.com>

* switch_event: clarify EF_UNIQ_HEADERS dup comment, brace one-line test loops

Address review feedback on the switch_event_dup change:

- src/switch_event.c: the fast-path comment claimed todup's headers are "already unique" unconditionally. That only holds when EF_UNIQ_HEADERS is set (names are deduped on insert); reword to scope the claim to that case and to the verbatim-copy intent.

- tests/unit/switch_event.c: expand one-line loops to braced form per the SignalWire coding guidelines.

Signed-off-by: Calvin Ellison <cellison@youmail.com>

---------

Signed-off-by: Calvin Ellison <cellison@youmail.com>
Co-authored-by: Calvin Ellison <cellison@youmail.com>
2026-09-04 19:43:23 +03:00

324 lines
11 KiB
C

#include <switch.h>
#include <test/switch_test.h>
// #define BENCHMARK 1
FST_MINCORE_BEGIN("./conf")
FST_SUITE_BEGIN(switch_event)
FST_SETUP_BEGIN()
{
}
FST_SETUP_END()
FST_TEARDOWN_BEGIN()
{
}
FST_TEARDOWN_END()
FST_TEST_BEGIN(benchmark)
{
switch_event_t *event = NULL;
switch_time_t start_ts, end_ts;
int loops = 10, x = 0;
switch_status_t status = SWITCH_STATUS_SUCCESS;
char **index = NULL;
uint64_t micro_total = 0;
double micro_per = 0;
double rate_per_sec = 0;
#ifdef BENCHMARK
switch_time_t small_start_ts, small_end_ts;
#endif
index = calloc(loops, sizeof(char *));
for ( x = 0; x < loops; x++) {
index[x] = switch_mprintf("%d", x);
}
/* START LOOPS */
start_ts = switch_time_now();
status = switch_event_create(&event, SWITCH_EVENT_MESSAGE);
fst_xcheck(status == SWITCH_STATUS_SUCCESS, "Failed to create event");
#ifndef BENCHMARK
for ( x = 0; x < loops; x++) {
status = switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, index[x], index[x]);
fst_xcheck(status == SWITCH_STATUS_SUCCESS, "Failed to add header to event");
}
#else
small_start_ts = switch_time_now();
for ( x = 0; x < loops; x++) {
if ( switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, index[x], index[x]) != SWITCH_STATUS_SUCCESS) {
fst_fail("Failed to add header to event");
}
}
small_end_ts = switch_time_now();
micro_total = small_end_ts - small_start_ts;
micro_per = micro_total / (double) loops;
rate_per_sec = 1000000 / micro_per;
printf("switch_event add_header: Total %" SWITCH_UINT64_T_FMT "us / %d loops, %.2f us per loop, %.0f loops per second\n",
micro_total, loops, micro_per, rate_per_sec);
#endif
#ifndef BENCHMARK
for ( x = 0; x < loops; x++) {
fst_check_string_equals(switch_event_get_header(event, index[x]), index[x]);
}
#else
small_start_ts = switch_time_now();
for ( x = 0; x < loops; x++) {
if ( !switch_event_get_header(event, index[x])) {
fst_fail("Failed to lookup event header value");
}
}
small_end_ts = switch_time_now();
micro_total = small_end_ts - small_start_ts;
micro_per = micro_total / (double) loops;
rate_per_sec = 1000000 / micro_per;
printf("switch_event get_header: Total %" SWITCH_UINT64_T_FMT "us / %d loops, %.2f us per loop, %.0f loops per second\n",
micro_total, loops, micro_per, rate_per_sec);
#endif
switch_event_destroy(&event);
/* END LOOPS */
end_ts = switch_time_now();
for ( x = 0; x < loops; x++) {
free(index[x]);
}
free(index);
micro_total = end_ts - start_ts;
micro_per = micro_total / (double) loops;
rate_per_sec = 1000000 / micro_per;
printf("switch_event Total %" SWITCH_UINT64_T_FMT "us / %d loops, %.2f us per loop, %.0f loops per second\n",
micro_total, loops, micro_per, rate_per_sec);
}
FST_TEST_END()
FST_TEST_BEGIN(expand_headers_offset)
{
/* Covers the ${var:offset[:length]} slicing branches in
* switch_event_expand_headers_check(). */
static const struct {
const char *expr;
const char *expected;
const char *note;
} cases[] = {
/* offset == 0: no clone (outer offset||ooffset guard is false). */
{ "${foo:0}", "ABC", "offset=0 identity, no clone" },
{ "${empty:0}", "", "offset=0 on empty value" },
/* ooffset alone forces the clone, front offset is a no-op. */
{ "${foo:0:2}", "AB", "offset=0 with ooffset" },
/* Positive offset within range. */
{ "${foo:1}", "BC", "positive in-range" },
{ "${foo:3}", "", "offset == strlen boundary" },
{ "${foo:99}", "", "offset > strlen yields empty" },
{ "${empty:5}", "", "offset > strlen on empty value" },
/* Combined offset:length form. */
{ "${foo:1:1}", "B", "offset:length in range" },
{ "${foo:0:3}", "ABC", "ooffset == strlen, strict < gate skips trim" },
{ "${foo:99:2}", "", "offset > strlen with ooffset, gate no-ops" },
/* Negative offsets. */
{ "${foo:-1}", "C", "negative offset in range" },
{ "${foo:-3}", "ABC", "abs(offset) == strlen, <= gate boundary" },
{ "${foo:-99}", "ABC", "abs(offset) > strlen, gate falls through" },
};
switch_event_t *event = NULL;
switch_status_t status;
size_t i;
status = switch_event_create(&event, SWITCH_EVENT_MESSAGE);
fst_xcheck(status == SWITCH_STATUS_SUCCESS, "Failed to create event");
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "foo", "ABC");
switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, "empty", "");
for (i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
char msg[160];
char *out = switch_event_expand_headers(event, cases[i].expr);
switch_snprintf(msg, sizeof(msg), "%s: %s -> expected '%s', got '%s'",
cases[i].note, cases[i].expr, cases[i].expected,
out ? out : "(null)");
fst_xcheck(out && !strcmp(out, cases[i].expected), msg);
if (out != cases[i].expr) {
switch_safe_free(out);
}
}
switch_event_destroy(&event);
}
FST_TEST_END()
#ifdef BENCHMARK
FST_TEST_BEGIN(dup_uniq_bench)
{
switch_event_t *src = NULL, *dup = NULL;
switch_status_t status;
const int loops = 4000;
const int Ns[4] = {50, 100, 191, 400};
int i, j, count, k, S, Nv;
char name[32], val[64];
switch_event_header_t *hp;
switch_time_t start_ts, end_ts;
double per_us;
/* A channel's variables are a SWITCH_EVENT_CHANNEL_DATA event => EF_UNIQ_HEADERS.
* switch_event_create() also adds the standard event headers, so the source
* count is (191 added + standard); assert relative to the actual source count. */
status = switch_event_create(&src, SWITCH_EVENT_CHANNEL_DATA);
fst_xcheck(status == SWITCH_STATUS_SUCCESS, "create CHANNEL_DATA event");
fst_xcheck(switch_test_flag(src, EF_UNIQ_HEADERS) != 0, "CHANNEL_DATA event is EF_UNIQ_HEADERS");
for (i = 0; i < 191; i++) {
switch_snprintf(name, sizeof(name), "var_%d", i);
switch_snprintf(val, sizeof(val), "value_%d_some_padding_payload", i);
switch_event_add_header_string(src, SWITCH_STACK_BOTTOM, name, val);
}
S = 0;
for (hp = src->headers; hp; hp = hp->next) {
S++;
}
fst_xcheck(S >= 191, "source has at least the 191 added headers");
/* correctness: dup preserves header count, the uniq flag, and values */
status = switch_event_dup(&dup, src);
fst_xcheck(status == SWITCH_STATUS_SUCCESS, "dup ok");
count = 0;
for (hp = dup->headers; hp; hp = hp->next) {
count++;
}
fst_xcheck(count == S, "dup header count equals source");
fst_xcheck(switch_test_flag(dup, EF_UNIQ_HEADERS) != 0, "dup keeps EF_UNIQ_HEADERS");
fst_check_string_equals(switch_event_get_header(dup, "var_0"), "value_0_some_padding_payload");
fst_check_string_equals(switch_event_get_header(dup, "var_190"), "value_190_some_padding_payload");
switch_event_destroy(&dup);
/* correctness: uniqueness preserved (re-setting a key must not duplicate it in the dup) */
switch_event_add_header_string(src, SWITCH_STACK_BOTTOM, "var_0", "REPLACED");
status = switch_event_dup(&dup, src);
fst_xcheck(status == SWITCH_STATUS_SUCCESS, "dup ok (2)");
k = 0;
for (hp = dup->headers; hp; hp = hp->next) {
if (!strcmp(hp->name, "var_0")) {
k++;
}
}
fst_xcheck(k == 1, "var_0 present exactly once after re-set + dup");
fst_check_string_equals(switch_event_get_header(dup, "var_0"), "REPLACED");
switch_event_destroy(&dup);
switch_event_destroy(&src);
/* timing sweep: dup cost vs header count (baseline ~O(n^2), patched ~O(n)) */
for (j = 0; j < 4; j++) {
Nv = Ns[j];
switch_event_create(&src, SWITCH_EVENT_CHANNEL_DATA);
for (i = 0; i < Nv; i++) {
switch_snprintf(name, sizeof(name), "var_%d", i);
switch_snprintf(val, sizeof(val), "value_%d_some_padding_payload", i);
switch_event_add_header_string(src, SWITCH_STACK_BOTTOM, name, val);
}
start_ts = switch_time_now();
for (i = 0; i < loops; i++) {
switch_event_dup(&dup, src);
switch_event_destroy(&dup);
}
end_ts = switch_time_now();
per_us = (double)(end_ts - start_ts) / (double)loops;
printf("DUP_BENCH N=%d loops=%d total=%" SWITCH_UINT64_T_FMT "us per_dup=%.3f us\n",
Nv, loops, (uint64_t)(end_ts - start_ts), per_us);
switch_event_destroy(&src);
}
}
FST_TEST_END()
#endif /* BENCHMARK */
FST_TEST_BEGIN(dup_faithful_copy)
{
/* Regression for the switch_event_dup O(n^2)->O(n) change (suppressing the
* per-add EF_UNIQ_HEADERS dedup scan while cloning). dup must reproduce the
* source faithfully:
* (1) a well-formed EF_UNIQ source stays unique through dup (the real case);
* (2) a malformed source that already holds duplicate names (only possible
* if headers were added before EF_UNIQ_HEADERS was set) is copied
* faithfully -- dup PRESERVES the duplicates rather than silently
* collapsing to the last value. A dup-bearing EF_UNIQ event is already
* a bug upstream of here; a copy must not silently drop data. */
switch_event_t *src = NULL, *dup = NULL;
switch_event_header_t *hp;
switch_status_t status;
int n;
/* (1) well-formed EF_UNIQ source -> unique dup */
status = switch_event_create(&src, SWITCH_EVENT_CHANNEL_DATA);
fst_xcheck(status == SWITCH_STATUS_SUCCESS, "create CHANNEL_DATA");
fst_xcheck(switch_test_flag(src, EF_UNIQ_HEADERS) != 0, "CHANNEL_DATA is EF_UNIQ");
switch_event_add_header_string(src, SWITCH_STACK_BOTTOM, "k", "v1");
switch_event_add_header_string(src, SWITCH_STACK_BOTTOM, "k", "v2");
n = 0;
for (hp = src->headers; hp; hp = hp->next) {
if (!strcmp(hp->name, "k")) {
n++;
}
}
fst_xcheck(n == 1, "EF_UNIQ source keeps 'k' unique (collapsed to last)");
status = switch_event_dup(&dup, src);
fst_xcheck(status == SWITCH_STATUS_SUCCESS, "dup ok (unique)");
n = 0;
for (hp = dup->headers; hp; hp = hp->next) {
if (!strcmp(hp->name, "k")) {
n++;
}
}
fst_xcheck(n == 1, "dup of unique source stays unique");
fst_check_string_equals(switch_event_get_header(dup, "k"), "v2");
switch_event_destroy(&dup);
switch_event_destroy(&src);
/* (2) malformed source (duplicate names under EF_UNIQ) -> faithful copy */
status = switch_event_create(&src, SWITCH_EVENT_GENERAL);
fst_xcheck(status == SWITCH_STATUS_SUCCESS, "create GENERAL");
fst_xcheck(switch_test_flag(src, EF_UNIQ_HEADERS) == 0, "GENERAL is not EF_UNIQ");
switch_event_add_header_string(src, SWITCH_STACK_BOTTOM, "dupkey", "first");
switch_event_add_header_string(src, SWITCH_STACK_BOTTOM, "dupkey", "second");
switch_set_flag(src, EF_UNIQ_HEADERS);
n = 0;
for (hp = src->headers; hp; hp = hp->next) {
if (!strcmp(hp->name, "dupkey")) {
n++;
}
}
fst_xcheck(n == 2, "malformed source holds 2 'dupkey' headers");
status = switch_event_dup(&dup, src);
fst_xcheck(status == SWITCH_STATUS_SUCCESS, "dup ok (malformed)");
fst_xcheck(switch_test_flag(dup, EF_UNIQ_HEADERS) != 0, "dup keeps EF_UNIQ flag");
n = 0;
for (hp = dup->headers; hp; hp = hp->next) {
if (!strcmp(hp->name, "dupkey")) {
n++;
}
}
fst_xcheck(n == 2, "dup faithfully preserves both 'dupkey' headers (no silent collapse)");
switch_event_destroy(&dup);
switch_event_destroy(&src);
}
FST_TEST_END()
FST_SUITE_END()
FST_MINCORE_END()