package unit; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; /** * hbase-0001: DefaultStoreFileManager.getUnneededFiles() — ArrayList.contains() O(C) * called inside a filter over all store files. Total: O(F * C). * * Root cause: HStore.filesCompacting = Lists.newArrayList() (ArrayList). * Fix: build a local HashSet at start of getUnneededFiles() for O(1) lookup. * * Standalone unit test — no JUnit required. * Compile: javac -d . HBaseStoreFileManagerTest.java * Run: java -ea unit.HBaseStoreFileManagerTest */ public class HBaseStoreFileManagerTest { static long slowOps; static long fastOps; /** * Simulates getUnneededFiles() with ArrayList filesCompacting. * For each of F store files, ArrayList.contains() scans C compacting files. */ static int slowGetUnneededFiles(int numStoreFiles, List filesCompacting, long maxTs) { slowOps = 0; int unneededCount = 0; // storeFiles.all — F files, first (F-1) are candidates (skip the last) for (int i = 0; i < numStoreFiles - 1; i++) { slowOps++; // stream entry int fileId = i; long fileTs = (long) fileId; // fileTs = id, so fileTs < maxTs when id < maxTs if (fileTs < maxTs) { for (Integer compacting : filesCompacting) { // ArrayList.contains() scan slowOps++; if (compacting.equals(fileId)) break; } if (!filesCompacting.contains(fileId)) { unneededCount++; } } } return unneededCount; } /** * Patched: build HashSet once at method entry — O(C) one time, then O(1) per lookup. */ static int fastGetUnneededFiles(int numStoreFiles, List filesCompacting, long maxTs) { fastOps = 0; Set compactingSet = new HashSet<>(filesCompacting); // O(C) once fastOps += filesCompacting.size(); int unneededCount = 0; for (int i = 0; i < numStoreFiles - 1; i++) { fastOps++; // stream entry int fileId = i; long fileTs = (long) fileId; if (fileTs < maxTs) { fastOps++; // O(1) HashSet.contains() if (!compactingSet.contains(fileId)) { unneededCount++; } } } return unneededCount; } static void run(int F, int C, long maxTs, int expectedRatio) { // Build filesCompacting: C files evenly distributed across F List filesCompacting = new ArrayList<>(); for (int i = 0; i < C; i++) { filesCompacting.add(i * (F / Math.max(C, 1))); } int slowResult = slowGetUnneededFiles(F, filesCompacting, maxTs); int fastResult = fastGetUnneededFiles(F, filesCompacting, maxTs); boolean resultsMatch = (slowResult == fastResult); boolean quadraticWorse = slowOps > fastOps * expectedRatio; boolean pass = resultsMatch && quadraticWorse; System.out.printf("F=%-4d C=%-4d maxTs=%-6d slow=%8d fast=%5d ratio=%6.1fx match=%b PASS=%b%n", F, C, maxTs, slowOps, fastOps, (double) slowOps / fastOps, resultsMatch, pass); if (!pass) { throw new AssertionError( "FAIL F=" + F + " C=" + C + " resultsMatch=" + resultsMatch + " slowOps=" + slowOps + " fastOps=" + fastOps + " needed ratio>" + expectedRatio); } } public static void main(String[] args) { System.out.println("=== hbase-0001: DefaultStoreFileManager.getUnneededFiles() O(F*C) vs O(F+C) ==="); run(100, 10, 80, 3); run(500, 50, 400, 20); run(1000, 100, 900, 40); System.out.println("3/3 PASS"); } }