diff --git a/build.xml b/build.xml index cdf9631..b26bb88 100644 --- a/build.xml +++ b/build.xml @@ -83,7 +83,7 @@ - + @@ -114,11 +114,8 @@ - - - diff --git a/nbproject/project.properties b/nbproject/project.properties index 9681f1d..2aa0443 100644 --- a/nbproject/project.properties +++ b/nbproject/project.properties @@ -116,4 +116,4 @@ run.test.modulepath=\ ${javac.test.modulepath} source.encoding=UTF-8 src.dir=src -test.src.dir=test +test.src.dir=test \ No newline at end of file diff --git a/src/jdiskmark/App.java b/src/jdiskmark/App.java index 5176eea..24edbf3 100644 --- a/src/jdiskmark/App.java +++ b/src/jdiskmark/App.java @@ -95,7 +95,21 @@ public enum Mode { CLI, GUI } // benchmarks and operations public static HashMap benchmarks = new HashMap<>(); public static HashMap operations = new HashMap<>(); + //Cli Mode + public static boolean isCliMode = false; + public static boolean isWindows() { + return os != null && os.toLowerCase().contains("win"); + } + + public static boolean isLinux() { + return os != null && os.toLowerCase().contains("linux"); + } + + public static boolean isMac() { + return os != null && os.toLowerCase().contains("mac"); + } + /** * @param args the command line arguments */ @@ -160,29 +174,59 @@ public static String getVersion() { /** * Initialize the GUI Application. */ + /** + * Initialize the GUI Application. + */ public static void init() { - + + // ========= SAFE ELEVATION CHECK (NETBEANS + EXE/JAR) ============ + if (!UtilOs.isProcessElevated()) { + + // Detect if running inside NetBeans (dev mode) + String classPath = System.getProperty("java.class.path"); + boolean runningInNetBeans = + classPath != null && classPath.toLowerCase().contains("netbeans"); + + if (!runningInNetBeans) { + // Only elevate when running as packaged JAR/EXE + System.out.println("Restarting JDiskMark with Administrator privileges..."); + UtilOs.restartAsAdmin(); + return; + } else { + System.out.println("Running inside NetBeans — skipping elevation."); + } + } + // ================================================================ + + os = System.getProperty("os.name"); arch = System.getProperty("os.arch"); - processorName = Util.getProcessorName(); - jdk = Util.getJvmInfo(); - + + // Processor name based on OS + if (os.startsWith("Windows")) { + processorName = UtilOs.getProcessorNameWindows(); + } else if (os.startsWith("Mac OS")) { + processorName = UtilOs.getProcessorNameMacOS(); + } else if (os.contains("Linux")) { + processorName = UtilOs.getProcessorNameLinux(); + } + checkPermission(); if (!APP_CACHE_DIR.exists()) { APP_CACHE_DIR.mkdirs(); } - + if (mode == Mode.GUI) { loadConfig(); } - + // initialize data dir if necessary if (locationDir == null) { locationDir = new File(System.getProperty("user.home")); dataDir = new File(locationDir.getAbsolutePath() + File.separator + DATADIRNAME); } - + if (mode == Mode.GUI) { Gui.configureLaf(); Gui.mainFrame = new MainFrame(); @@ -193,25 +237,19 @@ public static void init() { Gui.mainFrame.setLocationRelativeTo(null); Gui.progressBar = Gui.mainFrame.getProgressBar(); } - + if (App.autoSave) { - // configure the embedded DB in .jdm System.setProperty("derby.system.home", APP_CACHE_DIR_NAME); loadBenchmarks(); } if (mode == Mode.GUI) { - // load current drive Gui.updateDiskInfo(); Gui.mainFrame.setVisible(true); - // save configuration on exit... - Runtime.getRuntime().addShutdownHook(new Thread() { - @Override - public void run() { App.saveConfig(); } - }); + Runtime.getRuntime().addShutdownHook(new Thread(App::saveConfig)); } } - + public static void checkPermission() { String osName = System.getProperty("os.name"); if (osName.contains("Linux")) { diff --git a/src/jdiskmark/Benchmark.java b/src/jdiskmark/Benchmark.java index 86f9c21..d1d606b 100644 --- a/src/jdiskmark/Benchmark.java +++ b/src/jdiskmark/Benchmark.java @@ -1,4 +1,3 @@ - package jdiskmark; import com.fasterxml.jackson.annotation.JsonIgnore; @@ -27,6 +26,8 @@ import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.List; +import jakarta.persistence.Enumerated; +import jakarta.persistence.EnumType; import java.util.UUID; /** @@ -184,6 +185,37 @@ public void serialize(UUID value, JsonGenerator gen, SerializerProvider serializ BenchmarkType benchmarkType; public BenchmarkType getBenchmarkType() { return benchmarkType; } + public enum CachePurgeMethod { + NONE, + DROP_CACHE, + SOFT_PURGE; + + @Override + public String toString() { + return switch (this) { + case DROP_CACHE -> "Drop Cache (OS Flush)"; + case SOFT_PURGE -> "Soft Purge (Read-Through)"; + case NONE -> "None"; + }; + } + } + + // --------------------------------------------------- + // Cache purge metadata (for read-after-write benchmarks) + // --------------------------------------------------- + @Column + boolean cachePurgePerformed; + + @Column + long cachePurgeSizeBytes; + + @Column + long cachePurgeDurationMs; + + @Column + @Enumerated(EnumType.STRING) + CachePurgeMethod cachePurgeMethod; + // timestamps @Convert(converter = LocalDateTimeAttributeConverter.class) @Column(name = "startTime", columnDefinition = "TIMESTAMP") @@ -194,7 +226,7 @@ public void serialize(UUID value, JsonGenerator gen, SerializerProvider serializ @OneToMany(mappedBy = "benchmark", cascade = CascadeType.ALL, orphanRemoval = true) - List operations; + List operations = new ArrayList<>(); public List getOperations() { return operations; @@ -252,16 +284,22 @@ public String toResultString() { } public Benchmark() { - operations = new ArrayList<>(); startTime = LocalDateTime.now(); + cachePurgePerformed = false; + cachePurgeSizeBytes = 0L; + cachePurgeDurationMs = 0L; + cachePurgeMethod = CachePurgeMethod.NONE; appVersion = App.VERSION; profileName = App.activeProfile.getName(); } Benchmark(BenchmarkType type) { - operations = new ArrayList<>(); startTime = LocalDateTime.now(); benchmarkType = type; + cachePurgePerformed = false; + cachePurgeSizeBytes = 0L; + cachePurgeDurationMs = 0L; + cachePurgeMethod = CachePurgeMethod.NONE; appVersion = App.VERSION; profileName = App.activeProfile.getName(); } @@ -295,7 +333,24 @@ public String getDuration() { long diffMs = Duration.between(startTime, endTime).toMillis(); return String.valueOf(diffMs); } - + + // cache purge getters (for UI / JSON) + public boolean isCachePurgePerformed() { + return cachePurgePerformed; + } + + public long getCachePurgeSizeBytes() { + return cachePurgeSizeBytes; + } + + public long getCachePurgeDurationMs() { + return cachePurgeDurationMs; + } + + public CachePurgeMethod getCachePurgeMethod() { + return cachePurgeMethod; + } + // utility methods for collection @JsonIgnore @@ -333,7 +388,7 @@ static int delete(List benchmarkIds) { .setParameter("benchmarkIds", benchmarkIds) .executeUpdate(); - // delete the parent BenchmarkOperation records + // delete the parent Benchmark records String deleteBenchmarksJpql = "DELETE FROM Benchmark b WHERE b.id IN :benchmarkIds"; int deletedBenchmarksCount = em.createQuery(deleteBenchmarksJpql) .setParameter("benchmarkIds", benchmarkIds) @@ -347,4 +402,4 @@ static int delete(List benchmarkIds) { em.getTransaction().commit(); return deletedBenchmarksCount; } -} \ No newline at end of file +} diff --git a/src/jdiskmark/BenchmarkWorker.java b/src/jdiskmark/BenchmarkWorker.java index 5404030..5039f6c 100644 --- a/src/jdiskmark/BenchmarkWorker.java +++ b/src/jdiskmark/BenchmarkWorker.java @@ -64,6 +64,7 @@ public static int[][] divideIntoRanges(int startIndex, int endIndex, int numThre protected Benchmark doInBackground() throws Exception { if (App.verbose) { + msg(">>> USING CUSTOM PURGE BUILD (V1)"); msg("*** starting new worker thread"); msg("Running readTest " + App.isReadEnabled() + " writeTest " + App.isWriteEnabled()); msg("num samples: " + App.numOfSamples + ", num blks: " + App.numOfBlocks @@ -238,13 +239,77 @@ protected Benchmark doInBackground() throws Exception { App.wIops = wOperation.iops; Gui.mainFrame.refreshWriteMetrics(); } + + System.out.println(">>> BenchmarkWorker: ENTERING CACHE PURGE SECTION"); + + // ------------------------------------------------------------ + // Cache Purge Step (only for Read-after-Write scenario) + // ------------------------------------------------------------ + if (App.isWriteEnabled() && App.isReadEnabled() && !isCancelled()) { + + long purgeStartNs = System.nanoTime(); + benchmark.cachePurgePerformed = true; + benchmark.cachePurgeMethod = App.isAdmin + ? Benchmark.CachePurgeMethod.DROP_CACHE + : Benchmark.CachePurgeMethod.SOFT_PURGE; + + // Detect purge size roughly equal to the full test data footprint + long estimatedBytes = (long) App.numOfSamples * (long) App.numOfBlocks * + ((long) App.blockSizeKb * 1024L); + benchmark.cachePurgeSizeBytes = estimatedBytes; + + Gui.msg("⚡ Starting cache purge (" + benchmark.cachePurgeMethod + + ", ~" + (estimatedBytes / (1024*1024)) + " MB)"); + + if (App.isAdmin) { + // ========================== + // ADMIN MODE: Use drop-cache + // ========================== + + System.out.println(">>> BenchmarkWorker: ADMIN purge path selected"); + System.out.println(">>> BenchmarkWorker: Calling UtilOs.dropOsCache() ..."); + + try { + boolean ok = UtilOs.dropOsCache(); + + System.out.println(">>> BenchmarkWorker: dropOsCache() returned = " + ok); + + if (!ok) { + System.out.println(">>> BenchmarkWorker: dropOsCache FAILED — fallback to SOFT purge"); + Gui.msg("⚠ drop-cache failed — switching to soft purge."); + + System.out.println(">>> BenchmarkWorker: Starting SOFT purge (fallback)"); + Util.readPurge(estimatedBytes, pct -> setProgress(Math.min(99, pct))); + System.out.println(">>> BenchmarkWorker: Soft purge completed."); + } + } catch (Exception ex) { + System.out.println(">>> BenchmarkWorker: EXCEPTION in dropOsCache — fallback to read purge"); + ex.printStackTrace(); + Gui.msg("⚠ Exception during drop-cache, fallback to read purge."); + Util.readPurge(estimatedBytes, pct -> setProgress(Math.min(99, pct))); + } - // TODO: review renaming all files to clear catch - if (App.isReadEnabled() && App.isWriteEnabled() && !isCancelled()) { - // TODO: review refactor to App.dropCache() & Gui.dropCache() - Gui.dropCache(); + } else { + // ========================== + // USER MODE: Soft purge + // ========================== + System.out.println(">>> BenchmarkWorker: Starting SOFT purge (readPurge)"); + Gui.msg("Performing soft cache purge (read-through)…"); + + Util.readPurge(estimatedBytes, pct -> setProgress(Math.min(99, pct))); + + System.out.println(">>> BenchmarkWorker: Soft purge completed."); + } + + long purgeEndNs = System.nanoTime(); + long purgeDurationMs = (purgeEndNs - purgeStartNs) / 1_000_000L; + + benchmark.cachePurgeDurationMs = purgeDurationMs; + + Gui.msg("✔ Purge finished in " + purgeDurationMs + " ms"); } + if (App.isReadEnabled()) { BenchmarkOperation rOperation = new BenchmarkOperation(); rOperation.setBenchmark(benchmark); @@ -369,7 +434,11 @@ protected void process(List sampleList) { case Sample.Type.WRITE -> Gui.addWriteSample(s); case Sample.Type.READ -> Gui.addReadSample(s); } - }); + }); + if (App.isCliMode) { + int pct = this.getProgress(); + printCliProgress(pct); + } } @Override @@ -380,4 +449,17 @@ protected void done() { App.state = App.State.IDLE_STATE; Gui.mainFrame.adjustSensitivity(); } + + private void printCliProgress(int pct) { + int bars = pct / 5; // 20-bar progress + int spaces = 20 - bars; + + String bar = "[" + "#".repeat(bars) + "-".repeat(spaces) + "] " + pct + "%"; + + System.out.print("\r" + bar); + + if (pct >= 100) { + System.out.println(); + } + } } diff --git a/src/jdiskmark/Gui.java b/src/jdiskmark/Gui.java index 89c18f7..972a8aa 100644 --- a/src/jdiskmark/Gui.java +++ b/src/jdiskmark/Gui.java @@ -49,6 +49,11 @@ public static enum Palette { CLASSIC, BLUE_GREEN, BARD_COOL, BARD_WARM }; public static XYLineAndShapeRenderer bwRenderer; public static XYLineAndShapeRenderer msRenderer; + public static void msg(String s) { + App.msg(s); + } + + /** * Setup the look and feel */ @@ -298,6 +303,7 @@ static public void updateDiskInfo() { * GH-2 need solution for dropping catch */ static public void dropCache() { + App.msg(">>> Dropping OS cache..."); String osName = System.getProperty("os.name"); if (osName.contains("Linux")) { if (App.isRoot) { diff --git a/src/jdiskmark/RunBenchmarkCommand.java b/src/jdiskmark/RunBenchmarkCommand.java index 227fa24..0d09303 100644 --- a/src/jdiskmark/RunBenchmarkCommand.java +++ b/src/jdiskmark/RunBenchmarkCommand.java @@ -76,6 +76,9 @@ public Integer call() { return 0; // Return 0 (Success) immediately after help is printed } try { + + App.isCliMode = true; + // 1. Apply CLI parameters to the global App state App.setLocationDir(locationDir); App.benchmarkType = benchmarkType; diff --git a/src/jdiskmark/RunDetailsPanel.java b/src/jdiskmark/RunDetailsPanel.java new file mode 100644 index 0000000..2351e97 --- /dev/null +++ b/src/jdiskmark/RunDetailsPanel.java @@ -0,0 +1,63 @@ +package jdiskmark; + +import java.awt.GridLayout; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.border.TitledBorder; + +public class RunDetailsPanel extends JPanel { + + private final JLabel modeLabel = new JLabel("-"); + private final JLabel samplesLabel = new JLabel("-"); + private final JLabel blocksLabel = new JLabel("-"); + private final JLabel threadsLabel = new JLabel("-"); + private final JLabel avgLabel = new JLabel("-"); + private final JLabel minMaxLabel = new JLabel("-"); + private final JLabel accLabel = new JLabel("-"); + + // --- NEW purge metadata labels --- + private final JLabel purgeMethodLabel = new JLabel("-"); + private final JLabel purgeSizeLabel = new JLabel("-"); + private final JLabel purgeDurationLabel = new JLabel("-"); + + public RunDetailsPanel() { + setLayout(new GridLayout(0, 2)); + setBorder(new TitledBorder("Run Details")); + + add(new JLabel("Mode:")); add(modeLabel); + add(new JLabel("Samples:")); add(samplesLabel); + add(new JLabel("Blocks:")); add(blocksLabel); + add(new JLabel("Threads:")); add(threadsLabel); + add(new JLabel("Avg Speed (MB/s):")); add(avgLabel); + add(new JLabel("Min/Max (MB/s):")); add(minMaxLabel); + add(new JLabel("Access Time (ms):")); add(accLabel); + + // --- PURGE METADATA --- + add(new JLabel("Purge Method:")); add(purgeMethodLabel); + add(new JLabel("Purge Size (MB):")); add(purgeSizeLabel); + add(new JLabel("Purge Duration (ms):")); add(purgeDurationLabel); + } + + /** + * Populate details for a selected benchmark operation. + * + * @param op the BenchmarkOperation to load into the panel + */ + public void load(BenchmarkOperation op) { + if (op == null) return; + + Benchmark b = op.getBenchmark(); + + modeLabel.setText(op.getModeDisplay()); + samplesLabel.setText(String.valueOf(op.numSamples)); + blocksLabel.setText(op.getBlocksDisplay()); + threadsLabel.setText(String.valueOf(op.numThreads)); + avgLabel.setText(String.valueOf(op.bwAvg)); + minMaxLabel.setText(op.getBwMinMaxDisplay()); + accLabel.setText(op.getAccTimeDisplay()); + + purgeMethodLabel.setText(b.cachePurgeMethod.toString()); + purgeSizeLabel.setText(String.valueOf(b.cachePurgeSizeBytes / (1024 * 1024))); + purgeDurationLabel.setText(String.valueOf(b.cachePurgeDurationMs)); + } +} diff --git a/src/jdiskmark/Util.java b/src/jdiskmark/Util.java index abfa4b8..786a923 100644 --- a/src/jdiskmark/Util.java +++ b/src/jdiskmark/Util.java @@ -12,6 +12,11 @@ import java.util.List; import java.util.Random; import javax.swing.filechooser.FileSystemView; +import java.io.RandomAccessFile; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.lang.management.ManagementFactory; +import com.sun.management.OperatingSystemMXBean; /** * Utility methods for JDiskMark @@ -41,6 +46,42 @@ static public boolean deleteDirectory(File path) { return (path.delete()); } + public static void readPurge(long estimatedBytes, java.util.function.IntConsumer progressCallback) { + try { + int block = 1024 * 1024; // 1MB chunks + long toRead = Util.getRecommendedPurgeSize(estimatedBytes); + + System.out.println(">>> Soft purge dynamic size = " + (toRead / (1024*1024)) + " MB"); + + byte[] buf = new byte[block]; + long read = 0; + + File f = App.testFile; + + if (f != null && f.exists()) { + try (RandomAccessFile raf = new RandomAccessFile(f, "r")) { + while (read < toRead) { + int r = raf.read(buf, 0, block); + if (r == -1) break; + + read += r; + + // ✔ NEW: progress callback + int pct = (int)((read * 100) / toRead); + progressCallback.accept(pct); + } + } + } + + } catch (Exception ex) { + Logger.getLogger(Util.class.getName()) + .log(Level.WARNING, "Soft cache purge failed: " + ex.getMessage(), ex); + + System.out.println(">>> Util.readPurge: EXCEPTION during soft purge"); + ex.printStackTrace(System.out); + } + } + /** * Returns a pseudo-random number between min and max, inclusive. * The difference between min and max can be at most @@ -63,6 +104,24 @@ public static int randInt(int min, int max) { return randomNum; } + public static long getRecommendedPurgeSize(long estimatedBytes) { + try { + OperatingSystemMXBean osBean = + (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean(); + + long totalRam = osBean.getTotalPhysicalMemorySize(); // system RAM in bytes + long twentyPercent = (long)(totalRam * 0.20); // use 20% of RAM + long minSafe = 128L * 1024 * 1024; // never below 128MB + + long dynamicCap = Math.max(minSafe, twentyPercent); + + return Math.min(estimatedBytes, dynamicCap); + } catch (Throwable t) { + // If anything fails, fall back to old 512MB cap + return Math.min(estimatedBytes, 512L * 1024 * 1024); + } + } + /* * Not used kept here for reference. */ diff --git a/src/jdiskmark/UtilOs.java b/src/jdiskmark/UtilOs.java index 0d8d26c..b72c40e 100644 --- a/src/jdiskmark/UtilOs.java +++ b/src/jdiskmark/UtilOs.java @@ -40,6 +40,101 @@ static public void readPhysicalDriveWindows() throws FileNotFoundException, IOEx System.out.println("content " + Arrays.toString(content)); } + public static boolean isProcessElevated() { + try { + String groups = System.getProperty("user.name"); + Process p = Runtime.getRuntime().exec("net session"); + p.waitFor(); + return p.exitValue() == 0; + } catch (Exception ex) { + return false; + } + } + + public static void restartAsAdmin() { + try { + String os = System.getProperty("os.name").toLowerCase(); + + // ------------------------------------------------------------------- + // WINDOWS + // ------------------------------------------------------------------- + if (os.contains("win")) { + + String javaBin = System.getProperty("java.home") + "\\bin\\java.exe"; + String jarPath = new File(App.class.getProtectionDomain() + .getCodeSource().getLocation().toURI()).getPath(); + + String cmd = "powershell -Command \"Start-Process '" + javaBin + + "' -ArgumentList '-jar \"" + jarPath + + "\"' -Verb RunAs\""; + + Runtime.getRuntime().exec(cmd); + System.exit(0); + return; + } + + // ------------------------------------------------------------------- + // MAC / LINUX + // ------------------------------------------------------------------- + App.msg("⚠ Restart-as-admin is only supported on Windows."); + System.out.println(">>> restartAsAdmin(): unsupported OS = " + os); + + } catch (Exception e) { + e.printStackTrace(); + App.msg("Failed to restart as Admin."); + } + } + + /** + * Attempts to purge the OS filesystem cache. + * + * OS Behavior Summary: + * --------------------- + * WINDOWS: + * - Admin user: uses EmptyStandbyList.exe to drop standby lists. + * - Non-admin: cannot drop OS cache; BenchmarkWorker falls back + * to soft purge (single-pass read). + * + * LINUX: + * - drop-cache requires root ("sync; echo 3 > /proc/sys/vm/drop_caches"). + * - JDiskMark does NOT attempt privileged Linux cache purge. + * - Always falls back to soft purge. + * + * MACOS: + * - No supported user-mode drop-cache command. + * - Always falls back to soft purge. + * + * The caller (BenchmarkWorker) must detect user privilege and select + * either admin purge (Windows only) or soft purge. + */ + + public static boolean dropOsCache() { + System.out.println(">>> DROP CACHE CALLED inside UtilOs"); // DEBUG + try { + if (App.isWindows()) { + // Windows requires EmptyStandbyList.exe + File exe = new File(App.ESBL_EXE); + if (!exe.exists()) { + return false; + } + Process p = new ProcessBuilder(exe.getAbsolutePath(), "standbylist").start(); + p.waitFor(); + return true; + } else if (App.isLinux() || App.isMac()) { + // Attempt typical Linux drop cache + Process p = new ProcessBuilder("sync").start(); + p.waitFor(); + p = new ProcessBuilder("sudo", "sh", "-c", "echo 3 > /proc/sys/vm/drop_caches").start(); + p.waitFor(); + return true; + } else { + return false; + } + } catch (Exception ex) { + return false; + } + } + /** * This method became obsolete with an updated version of windows 10. * A newer version of the method is used.