java-topology/defects/dovecot/unit/DovecotTest.java

198 lines
8.8 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.*;
/**
* DovecotTest — CWE-407 benchmark for dovecot-0001
*
* Models dsync_mail_change_have_keyword() in
* src/doveadm/dsync/dsync-mailbox-import.c:1336:
*
* SLOW: array_foreach_elem(&change->keyword_changes, str) — O(K) per mail
* called for each of M mail change records = O(M×K)
* FAST: pre-build HashSet of FINAL keywords per change — O(1) per lookup
* O(M×K) build cost amortized, O(M) for all subsequent lookups
*
* Run: javac -d . DovecotTest.java && java -ea unit.DovecotTest
*/
public class DovecotTest {
// ── Keyword change model ──────────────────────────────────────────────────
static final char KEYWORD_CHANGE_ADD = '+';
static final char KEYWORD_CHANGE_REMOVE = '-';
static final char KEYWORD_CHANGE_FINAL = '='; // "final" state for keyword
static final char KEYWORD_CHANGE_ADD_FINAL = '!'; // add + final
static class MailChange {
final List<String> keywordChanges; // e.g. "=\\Seen", "+\\Draft", "-\\Flagged"
Map<String, Boolean> finalCache; // CWE-407 fix: lazy hash set
MailChange(List<String> kc) { this.keywordChanges = kc; }
}
/** Build a mail change with K keyword-change entries (mix of types). */
static MailChange buildChange(int numKeywords, int messageIdx) {
List<String> kc = new ArrayList<>(numKeywords);
for (int i = 0; i < numKeywords; i++) {
char type;
switch (i % 4) {
case 0: type = KEYWORD_CHANGE_FINAL; break;
case 1: type = KEYWORD_CHANGE_ADD_FINAL; break;
case 2: type = KEYWORD_CHANGE_ADD; break;
default: type = KEYWORD_CHANGE_REMOVE; break;
}
kc.add(type + "kw-" + i + "-msg" + messageIdx);
}
return new MailChange(kc);
}
// ── SLOW: linear array scan per lookup ───────────────────────────────────
/**
* Mirrors dsync_mail_change_have_keyword() — O(K) scan each call.
* Returns total number of strcasecmp operations across all M×calls.
*/
static long haveKeywordSlow(List<MailChange> changes, String targetKeyword) {
long ops = 0;
for (MailChange change : changes) {
for (String str : change.keywordChanges) {
ops++;
char type = str.charAt(0);
if ((type == KEYWORD_CHANGE_FINAL || type == KEYWORD_CHANGE_ADD_FINAL)
&& str.substring(1).equalsIgnoreCase(targetKeyword)) {
break; // found
}
}
}
return ops;
}
// ── FAST: lazy hash set, built once per MailChange ────────────────────────
/**
* Models the CWE-407 fix: build a HashSet of FINAL keywords on first
* access per change, then O(1) lookup. Returns total ops including the
* amortized build cost.
*/
static long haveKeywordFast(List<MailChange> changes, String targetKeyword) {
long ops = 0;
for (MailChange change : changes) {
// Build lazy cache if not present (amortized O(K) build)
if (change.finalCache == null) {
change.finalCache = new HashMap<>();
for (String str : change.keywordChanges) {
ops++; // build cost (paid once per change)
char type = str.charAt(0);
if (type == KEYWORD_CHANGE_FINAL || type == KEYWORD_CHANGE_ADD_FINAL) {
change.finalCache.put(str.substring(1).toLowerCase(), Boolean.TRUE);
}
}
}
ops++; // O(1) hash lookup
change.finalCache.containsKey(targetKeyword.toLowerCase());
}
return ops;
}
// ── bench harness ─────────────────────────────────────────────────────────
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(" %-56s slow:%4dms (%,d ops) fast:%4dms (%,d ops) speedup:%.0fx%n",
label, sMs, sOps, fMs, fOps, r);
}
// ── Multi-pass scan: test Q different keywords over same M changes ─────────
/**
* SLOW multi-pass: for each of Q sync passes (each with a different target
* keyword), scan all M changes O(K) each. Total: O(Q × M × K).
*/
static long haveKeywordSlowMulti(List<MailChange> changes, List<String> targets) {
long ops = 0;
for (String target : targets) {
ops += haveKeywordSlow(changes, target);
}
return ops;
}
/**
* FAST multi-pass: build the keyword hash set once per change on the first
* pass; all Q subsequent passes do O(1) lookup. Total: O(M×K) build + O(Q×M).
* For Q >= 2 and K > 1, fast is strictly cheaper.
*/
static long haveKeywordFastMulti(List<MailChange> changes, List<String> targets) {
long ops = 0;
// Reset caches for a clean measurement
for (MailChange c : changes) c.finalCache = null;
for (String target : targets) {
ops += haveKeywordFast(changes, target);
}
return ops;
}
// ── main ──────────────────────────────────────────────────────────────────
public static void main(String[] args) {
final int M = 10_000; // mail change records in a mailbox sync
final int K = 20; // keyword-change entries per mail change
final int Q = 10; // distinct keyword queries across the sync session
// (e.g., importer checks \Seen, \Flagged, $Junk, etc.)
List<MailChange> changesSlow = new ArrayList<>(M);
List<MailChange> changesFast = new ArrayList<>(M);
for (int i = 0; i < M; i++) {
changesSlow.add(buildChange(K, i));
changesFast.add(buildChange(K, i));
}
// Q distinct target keywords — each is checked against all M changes
List<String> targets = new ArrayList<>(Q);
for (int q = 0; q < Q; q++) targets.add("kw-" + q + "-msg0");
System.out.println("DovecotTest — CWE-407");
System.out.println();
System.out.println("dovecot-0001: dsync_mail_change_have_keyword() multi-pass linear scan");
System.out.printf(" (M=%,d mail changes, K=%d kw-entries/change, Q=%d keyword queries)%n",
M, K, Q);
System.out.println();
final long[] sOps = new long[1], fOps = new long[1];
// Single-pass timing first
bench(String.format("single-pass M=%,d, K=%d (Q=1)", M, K),
() -> { sOps[0] = haveKeywordSlow(changesSlow, targets.get(0)); },
() -> {
for (MailChange c : changesFast) c.finalCache = null;
fOps[0] = haveKeywordFast(changesFast, targets.get(0));
},
haveKeywordSlow(changesSlow, targets.get(0)),
haveKeywordFast(changesFast, targets.get(0)));
// Multi-pass timing — the cache pays off here
bench(String.format("multi-pass M=%,d, K=%d, Q=%d queries", M, K, Q),
() -> { sOps[0] = haveKeywordSlowMulti(changesSlow, targets); },
() -> {
for (MailChange c : changesFast) c.finalCache = null;
fOps[0] = haveKeywordFastMulti(changesFast, targets);
},
haveKeywordSlowMulti(changesSlow, targets),
haveKeywordFastMulti(changesFast, targets));
// Multi-pass assertion: slow does Q × M × K, fast does M×K (build) + Q×M
// Ratio ≈ Q*M*K / (M*K + Q*M) = Q*K / (K + Q)
// With Q=10, K=20: ratio ≈ 200/30 ≈ 6.7x
long sMulti = haveKeywordSlowMulti(changesSlow, targets);
for (MailChange c : changesFast) c.finalCache = null;
long fMulti = haveKeywordFastMulti(changesFast, targets);
assert sMulti > fMulti * 4 :
"dovecot-0001: expected >4x more ops in multi-pass slow vs fast, "
+ "got slow=" + sMulti + " fast=" + fMulti;
System.out.println();
System.out.println("All assertions passed.");
}
}