280 lines
12 KiB
Java
280 lines
12 KiB
Java
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);
|
||
}
|
||
}
|