package unit; import java.util.*; /** * pulsar-0001: GetTopicsResult.getTopics() — ArrayList.contains() in dedup for loop. * * grouped is ArrayList; .contains(partitionedTopic) is O(N) per call. * For N topics, this is O(N^2). Fix: use LinkedHashSet for O(N) dedup. */ public class PulsarGetTopicsResultTest { // Slow path: exact replica of GetTopicsResult.getTopics() logic // Returns (dedupedList, opCount) static Object[] slowGetTopics(List nonPartitionedOrPartitionTopics) { List grouped = new ArrayList<>(); long ops = 0; for (String topic : nonPartitionedOrPartitionTopics) { // Simulate getPartitionedTopicName: strip "-partition-N" suffix String partitionedTopic = stripPartitionSuffix(topic); // grouped.contains: O(N) scan of ArrayList boolean found = false; for (String g : grouped) { ops++; if (g.equals(partitionedTopic)) { found = true; break; } } if (!found) { grouped.add(partitionedTopic); } } return new Object[]{grouped, ops}; } // Fast path: LinkedHashSet for O(1) dedup, preserving insertion order static Object[] fastGetTopics(List nonPartitionedOrPartitionTopics) { LinkedHashSet grouped = new LinkedHashSet<>(); long ops = 0; for (String topic : nonPartitionedOrPartitionTopics) { String partitionedTopic = stripPartitionSuffix(topic); ops++; // O(1) HashSet.add (dedup is implicit) grouped.add(partitionedTopic); } return new Object[]{new ArrayList<>(grouped), ops}; } static String stripPartitionSuffix(String topic) { int idx = topic.lastIndexOf("-partition-"); if (idx >= 0) return topic.substring(0, idx); return topic; } // Generate a list of partition topics for a given base topic static List makePartitions(String baseTopic, int numPartitions) { List result = new ArrayList<>(); for (int i = 0; i < numPartitions; i++) { result.add(baseTopic + "-partition-" + i); } return result; } public static void main(String[] args) { int passed = 0; int total = 0; // Test 1: correctness — single topic with partitions { total++; List inputs = makePartitions("persistent://tenant/ns/my-topic", 5); @SuppressWarnings("unchecked") List slowResult = (List) slowGetTopics(inputs)[0]; @SuppressWarnings("unchecked") List fastResult = (List) fastGetTopics(inputs)[0]; assert slowResult.equals(fastResult) : "Single-topic: slow and fast must agree. slow=" + slowResult + " fast=" + fastResult; assert slowResult.size() == 1 : "Should deduplicate to 1 topic, got " + slowResult.size(); assert slowResult.get(0).equals("persistent://tenant/ns/my-topic") : "Wrong base topic: " + slowResult.get(0); System.out.println(" Test 1 PASS: single topic deduplicated to " + slowResult); passed++; } // Test 2: correctness — multiple topics, mixed partitioned and non-partitioned { total++; List inputs = new ArrayList<>(); inputs.addAll(makePartitions("topic-A", 3)); inputs.add("topic-B"); // non-partitioned inputs.addAll(makePartitions("topic-C", 2)); @SuppressWarnings("unchecked") List slowResult = (List) slowGetTopics(inputs)[0]; @SuppressWarnings("unchecked") List fastResult = (List) fastGetTopics(inputs)[0]; assert slowResult.equals(fastResult) : "Multi-topic: slow and fast must agree. slow=" + slowResult + " fast=" + fastResult; assert slowResult.size() == 3 : "Should have 3 unique topics, got " + slowResult.size() + ": " + slowResult; assert slowResult.contains("topic-A") : "Must contain topic-A"; assert slowResult.contains("topic-B") : "Must contain topic-B"; assert slowResult.contains("topic-C") : "Must contain topic-C"; System.out.println(" Test 2 PASS: multi-topic case, result=" + slowResult); passed++; } // Test 3: O(N^2) vs O(N) — op count scaling { total++; int topicsCount = 50; int partitionsPerTopic = 20; // 50 * 20 = 1000 inputs List inputs = new ArrayList<>(); for (int t = 0; t < topicsCount; t++) { inputs.addAll(makePartitions("topic-" + t, partitionsPerTopic)); } Object[] slowOut = slowGetTopics(inputs); Object[] fastOut = fastGetTopics(inputs); @SuppressWarnings("unchecked") List slowResult = (List) slowOut[0]; @SuppressWarnings("unchecked") List fastResult = (List) fastOut[0]; long slowOps = (Long) slowOut[1]; long fastOps = (Long) fastOut[1]; assert slowResult.equals(fastResult) : "Scaling test: slow and fast must agree on results"; assert slowOps > fastOps : "Slow must do more ops: slowOps=" + slowOps + " fastOps=" + fastOps; assert slowResult.size() == topicsCount : "Must deduplicate to " + topicsCount + " topics, got " + slowResult.size(); long speedup = slowOps / Math.max(fastOps, 1); System.out.println(" Test 3 PASS: N=" + inputs.size() + " inputs → slowOps=" + slowOps + " fastOps=" + fastOps + " speedup=" + speedup + "x"); passed++; } // Test 4: worst-case O(N^2) — all unique topics (no dedup possible) { total++; int N = 100; // Each input is unique (already a base topic name, no partitions) List inputs = new ArrayList<>(); for (int i = 0; i < N; i++) inputs.add("unique-topic-" + i); long expectedSlowOps = 0; // After adding k elements, contains() scans k elements // Total: 0 + 1 + 2 + ... + (N-1) = N*(N-1)/2 for (int k = 0; k < N; k++) expectedSlowOps += k; Object[] slowOut = slowGetTopics(inputs); long actualSlowOps = (Long) slowOut[1]; assert actualSlowOps == expectedSlowOps : "Expected " + expectedSlowOps + " slow ops for unique N=" + N + " inputs, got " + actualSlowOps; long fastOps = N; long speedup = actualSlowOps / Math.max(fastOps, 1); System.out.println(" Test 4 PASS: O(N*(N-1)/2)=" + actualSlowOps + " vs O(N)=" + fastOps + " speedup=" + speedup + "x (N=" + N + ")"); passed++; } System.out.println(passed + "/" + total + " PASS"); if (passed != total) System.exit(1); } }