java-topology/defects/freeswitch/unit/FreeSWITCHTest.java

280 lines
12 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package unit;
import java.util.*;
/**
* CWE-407 benchmark for freeswitch defects:
* freeswitch-0001: mod_conference.c relationship list scan inside per-sample audio mixing
* O(S × M × M × R) — relationship linked-list scan per sample per member pair
*
* Simulates the audio mixing loop in C with Java equivalents:
* - S = samples per frame (160 at 8kHz/20ms)
* - M = conference members
* - R = relationships per member (singly-linked list)
*/
public class FreeSWITCHTest {
static void bench(String label, Runnable slow, Runnable fast, long sOps, long fOps) {
slow.run(); fast.run();
long t0 = System.nanoTime(); slow.run(); long sMs = (System.nanoTime() - t0) / 1_000_000;
long t1 = System.nanoTime(); fast.run(); long fMs = (System.nanoTime() - t1) / 1_000_000;
double r = fOps > 0 ? (double) sOps / fOps : 0;
System.out.printf(" %-60s slow:%4dms (%,d ops) fast:%4dms (%,d ops) speedup:%.0fx%n",
label, sMs, sOps, fMs, fOps, r);
}
// Simulated conference relationship (singly-linked list like C struct)
static class Rel {
int id; // member ID this relationship targets (0 = all)
boolean canSpeak;
boolean canHear;
Rel next;
Rel(int id, boolean speak, boolean hear) { this.id = id; canSpeak = speak; canHear = hear; }
}
static class Member {
int id;
short[] frame;
Rel relationships; // singly-linked list head
Member(int id, int samples) {
this.id = id;
frame = new short[samples];
Arrays.fill(frame, (short) 100);
}
void addRelationship(int targetId, boolean canSpeak, boolean canHear) {
Rel r = new Rel(targetId, canSpeak, canHear);
r.next = relationships;
relationships = r;
}
}
// ─────────────────────────────────────────────────────────────────────────
// freeswitch-0001: relationship scan per sample
// ─────────────────────────────────────────────────────────────────────────
/**
* Slow: O(S × M × M × R) — relationship linked-list scanned for every sample.
* Matches the original mod_conference.c code structure.
*/
static long slowConferenceMix(List<Member> members, int samples, boolean hasRelationships) {
long ops = 0;
int[] mainFrame = new int[samples];
// Build main frame: sum all members' audio
for (Member m : members) {
for (int x = 0; x < samples; x++) {
mainFrame[x] += m.frame[x];
ops++;
}
}
// Per output member: subtract self, subtract excluded members
for (Member omember : members) {
int[] writeFrame = new int[samples];
for (int x = 0; x < samples; x++) {
ops++;
int z = mainFrame[x] - omember.frame[x];
if (hasRelationships) {
// Inner member loop
for (Member imember : members) {
if (imember == omember) continue;
boolean found = false;
// Scan imember->relationships linked list — O(R)
for (Rel rel = imember.relationships; rel != null; rel = rel.next) {
ops++;
if ((rel.id == omember.id || rel.id == 0) && !rel.canSpeak) {
z -= imember.frame[x];
found = true;
break;
}
}
if (!found) {
// Scan omember->relationships — O(R)
for (Rel rel = omember.relationships; rel != null; rel = rel.next) {
ops++;
if ((rel.id == imember.id || rel.id == 0) && !rel.canHear) {
z -= imember.frame[x];
break;
}
}
}
}
}
writeFrame[x] = z;
}
}
return ops;
}
/**
* Fast: O(M² × R + S × M²) — pre-compute exclusion matrix outside sample loop.
* Relationship scan moved out of the per-sample hot path.
*/
static long fastConferenceMix(List<Member> members, int samples, boolean hasRelationships) {
long ops = 0;
int[] mainFrame = new int[samples];
// Build main frame
for (Member m : members) {
for (int x = 0; x < samples; x++) {
mainFrame[x] += m.frame[x];
ops++;
}
}
// Per output member: pre-compute exclusion bitmask, then apply per sample
for (Member omember : members) {
// Pre-compute: which imembers are excluded for this omember?
// O(M × R) — done ONCE per omember, not per sample
boolean[] excludeAudio = new boolean[members.size()];
if (hasRelationships) {
for (int ii = 0; ii < members.size(); ii++) {
Member imember = members.get(ii);
if (imember == omember) continue;
boolean found = false;
for (Rel rel = imember.relationships; rel != null; rel = rel.next) {
ops++;
if ((rel.id == omember.id || rel.id == 0) && !rel.canSpeak) {
excludeAudio[ii] = true;
found = true;
break;
}
}
if (!found) {
for (Rel rel = omember.relationships; rel != null; rel = rel.next) {
ops++;
if ((rel.id == imember.id || rel.id == 0) && !rel.canHear) {
excludeAudio[ii] = true;
break;
}
}
}
}
}
// Per-sample loop: O(S × M) with O(1) exclusion lookup
int[] writeFrame = new int[samples];
for (int x = 0; x < samples; x++) {
ops++;
int z = mainFrame[x] - omember.frame[x];
if (hasRelationships) {
for (int ii = 0; ii < members.size(); ii++) {
ops++;
if (excludeAudio[ii]) {
z -= members.get(ii).frame[x];
}
}
}
writeFrame[x] = z;
}
}
return ops;
}
// ─────────────────────────────────────────────────────────────────────────
public static void main(String[] args) {
int M = 20; // conference members
int S = 160; // samples per frame (8kHz, 20ms)
int R = 4; // relationships per member
// Build members
List<Member> members = new ArrayList<>();
for (int i = 0; i < M; i++) {
members.add(new Member(i, S));
}
// Add relationships: each member excludes 1-2 others (simulate mute/subconference)
Random rng = new Random(42);
for (int i = 0; i < M; i++) {
for (int r = 0; r < R / 2; r++) {
int target = rng.nextInt(M);
if (target != i) {
members.get(i).addRelationship(target, false, true); // can't speak to target
}
}
}
System.out.println("=== freeswitch CWE-407 benchmark ===");
System.out.printf(" M=%d members, S=%d samples/frame, R=%d relationships/member%n%n", M, S, R);
long sOps = slowConferenceMix(members, S, true);
long fOps = fastConferenceMix(members, S, true);
bench("freeswitch-0001 conference mix [M=" + M + ",S=" + S + ",R=" + R + "] with rels",
() -> slowConferenceMix(members, S, true),
() -> fastConferenceMix(members, S, true),
sOps, fOps);
// Also benchmark without relationships (baseline)
long sOpsNoRel = slowConferenceMix(members, S, false);
long fOpsNoRel = fastConferenceMix(members, S, false);
bench("freeswitch-0001 conference mix [M=" + M + ",S=" + S + "] no rels (baseline)",
() -> slowConferenceMix(members, S, false),
() -> fastConferenceMix(members, S, false),
sOpsNoRel, fOpsNoRel);
// Higher M to show quadratic growth
int M2 = 40;
List<Member> members2 = new ArrayList<>();
for (int i = 0; i < M2; i++) {
Member m2 = new Member(i, S);
for (int r = 0; r < R / 2; r++) {
int target = rng.nextInt(M2);
if (target != i) m2.addRelationship(target, false, true);
}
members2.add(m2);
}
long sOps2 = slowConferenceMix(members2, S, true);
long fOps2 = fastConferenceMix(members2, S, true);
bench("freeswitch-0001 conference mix [M=" + M2 + ",S=" + S + ",R=" + R + "] with rels",
() -> slowConferenceMix(members2, S, true),
() -> fastConferenceMix(members2, S, true),
sOps2, fOps2);
System.out.println();
// Assertions
int pass = 0, total = 0;
// slow ops should scale as O(S*M*M*R), fast as O(S*M + M*M*R)
// slow / fast > R (relationship scan eliminated from inner loop)
total++;
long expectedSlowOps = (long) S * M * M; // at minimum (without R)
if (sOps > fOps * 2 && sOps >= expectedSlowOps) {
System.out.printf(" freeswitch-0001 M=%d: PASS (slow=%,d >= S*M²=%,d, fast=%,d, ratio=%.1fx)%n",
M, sOps, expectedSlowOps, fOps, (double)sOps/fOps);
pass++;
} else {
System.out.printf(" freeswitch-0001 M=%d: FAIL (slow=%,d fast=%,d)%n", M, sOps, fOps);
}
// At M2=40 slow ops should be roughly 4× M=20 (quadratic growth)
total++;
long expectedSlowOps2 = (long) S * M2 * M2;
if (sOps2 > fOps2 * 2 && sOps2 >= expectedSlowOps2) {
System.out.printf(" freeswitch-0001 M=%d: PASS (slow=%,d >= S*M²=%,d, fast=%,d, ratio=%.1fx)%n",
M2, sOps2, expectedSlowOps2, fOps2, (double)sOps2/fOps2);
pass++;
} else {
System.out.printf(" freeswitch-0001 M=%d: FAIL (slow=%,d fast=%,d)%n", M2, sOps2, fOps2);
}
// Quadratic growth check: sOps2 / sOps should be roughly (M2/M)² = 4
total++;
double growthRatio = (double) sOps2 / sOps;
double expectedGrowth = (double)(M2 * M2) / (M * M);
if (growthRatio >= expectedGrowth * 0.5) {
System.out.printf(" freeswitch-0001 quadratic growth: PASS (ratio=%.1fx, expected~%.1fx)%n",
growthRatio, expectedGrowth);
pass++;
} else {
System.out.printf(" freeswitch-0001 quadratic growth: FAIL (ratio=%.1fx, expected~%.1fx)%n",
growthRatio, expectedGrowth);
}
System.out.println();
System.out.println(pass + "/" + total + (pass == total ? " PASS" : " FAIL"));
if (pass != total) System.exit(1);
}
}