From 91893a1418abaccfd33007127e03d2401fa2320d Mon Sep 17 00:00:00 2001 From: vpstackhub Date: Fri, 5 Dec 2025 12:55:49 -0800 Subject: [PATCH 1/7] =?UTF-8?q?#95=20=E2=80=93=20Implement=20cache=20purge?= =?UTF-8?q?=20fallback=20(admin=20+=20non-admin)=20+=20timing=20logs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nbproject/project.properties | 3 ++ src/jdiskmark/App.java | 68 +++++++++++++++++++++++------- src/jdiskmark/Benchmark.java | 45 ++++++++++++++++++-- src/jdiskmark/BenchmarkWorker.java | 63 +++++++++++++++++++++++++-- src/jdiskmark/Gui.java | 6 +++ src/jdiskmark/RunDetailsPanel.java | 63 +++++++++++++++++++++++++++ src/jdiskmark/Util.java | 23 ++++++++++ src/jdiskmark/UtilOs.java | 54 ++++++++++++++++++++++++ 8 files changed, 302 insertions(+), 23 deletions(-) create mode 100644 src/jdiskmark/RunDetailsPanel.java diff --git a/nbproject/project.properties b/nbproject/project.properties index 9681f1d..6923ac7 100644 --- a/nbproject/project.properties +++ b/nbproject/project.properties @@ -7,6 +7,7 @@ annotation.processing.source.output=${build.generated.sources.dir}/ap-source-out application.homepage=https://www.jdiskmark.net/ application.title=JDiskMark application.vendor=JDiskMark Team +project.dir=C:/Users/valer/OneDrive/Documenti/NetBeansProjects/jdm-java build.classes.dir=${build.dir}/classes build.classes.excludes=**/*.java,**/*.form # This directory is removed when the project is cleaned: @@ -117,3 +118,5 @@ run.test.modulepath=\ source.encoding=UTF-8 src.dir=src test.src.dir=test +run.working.dir=${project.dir} +work.dir=${project.dir} diff --git a/src/jdiskmark/App.java b/src/jdiskmark/App.java index 0412753..fd350f8 100644 --- a/src/jdiskmark/App.java +++ b/src/jdiskmark/App.java @@ -91,6 +91,18 @@ public enum Mode { CLI, GUI } public static HashMap benchmarks = new HashMap<>(); public static HashMap operations = new HashMap<>(); + 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 */ @@ -155,29 +167,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(); @@ -188,25 +230,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 73209a8..f8c0805 100644 --- a/src/jdiskmark/Benchmark.java +++ b/src/jdiskmark/Benchmark.java @@ -1,4 +1,3 @@ - package jdiskmark; import com.fasterxml.jackson.annotation.JsonIgnore; @@ -96,6 +95,21 @@ public enum BenchmarkType { @Column BenchmarkType benchmarkType; + // --------------------------------------------------- + // Cache purge metadata (for read-after-write benchmarks) + // --------------------------------------------------- + @Column + boolean cachePurgePerformed; + + @Column + long cachePurgeSizeBytes; + + @Column + long cachePurgeDurationMs; + + @Column + String cachePurgeMethod; + // timestamps @Convert(converter = LocalDateTimeAttributeConverter.class) @Column(name = "startTime", columnDefinition = "TIMESTAMP") @@ -165,11 +179,19 @@ public String toResultString() { public Benchmark() { startTime = LocalDateTime.now(); + cachePurgePerformed = false; + cachePurgeSizeBytes = 0L; + cachePurgeDurationMs = 0L; + cachePurgeMethod = "none"; } Benchmark(BenchmarkType type) { startTime = LocalDateTime.now(); benchmarkType = type; + cachePurgePerformed = false; + cachePurgeSizeBytes = 0L; + cachePurgeDurationMs = 0L; + cachePurgeMethod = "none"; } // basic getters and setters @@ -201,6 +223,23 @@ 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 String getCachePurgeMethod() { + return cachePurgeMethod; + } // utility methods for collection @@ -235,7 +274,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) @@ -249,4 +288,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 83208df..32e5d2d 100644 --- a/src/jdiskmark/BenchmarkWorker.java +++ b/src/jdiskmark/BenchmarkWorker.java @@ -63,6 +63,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 @@ -236,13 +237,67 @@ 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 ? "drop-cache" : "single-pass-read"; + + // 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(); - // TODO: review renaming all files to clear catch - if (App.isReadEnabled() && App.isWriteEnabled() && !isCancelled()) { - // TODO: review refactor to App.dropCache() & Gui.dropCache() - Gui.dropCache(); + System.out.println(">>> BenchmarkWorker: dropOsCache() returned = " + ok); + + if (!ok) { + System.out.println(">>> BenchmarkWorker: dropOsCache FAILED — fallback to read purge"); + Gui.msg("⚠ drop-cache failed — falling back to read purge."); + Util.readPurge(estimatedBytes); + } + } 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); + } + + } else { + // ========================== + // USER MODE: Soft purge + // ========================== + Util.readPurge(estimatedBytes); + } + + 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); diff --git a/src/jdiskmark/Gui.java b/src/jdiskmark/Gui.java index 87c12ee..2ac083d 100644 --- a/src/jdiskmark/Gui.java +++ b/src/jdiskmark/Gui.java @@ -48,6 +48,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 */ @@ -297,6 +302,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/RunDetailsPanel.java b/src/jdiskmark/RunDetailsPanel.java new file mode 100644 index 0000000..ac8e9eb --- /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); + 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..105b427 100644 --- a/src/jdiskmark/Util.java +++ b/src/jdiskmark/Util.java @@ -12,6 +12,7 @@ import java.util.List; import java.util.Random; import javax.swing.filechooser.FileSystemView; +import java.io.RandomAccessFile; /** * Utility methods for JDiskMark @@ -41,6 +42,28 @@ static public boolean deleteDirectory(File path) { return (path.delete()); } + public static void readPurge(long estimatedBytes) { + try { + int block = 1024 * 1024; // 1MB chunks + long toRead = Math.min(estimatedBytes, 512L * 1024 * 1024); // cap at 512MB for safety + 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; + } + } + } + } catch (Exception ignored) { + // Soft purge — failure is not fatal + } + } + /** * Returns a pseudo-random number between min and max, inclusive. * The difference between min and max can be at most diff --git a/src/jdiskmark/UtilOs.java b/src/jdiskmark/UtilOs.java index 0d8d26c..30fdc3c 100644 --- a/src/jdiskmark/UtilOs.java +++ b/src/jdiskmark/UtilOs.java @@ -40,6 +40,60 @@ 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 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); + } catch (Exception e) { + e.printStackTrace(); + App.msg("Failed to restart as Admin."); + } + } + + 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. From 47d2df6c2cdb6c3b4029672c495790d2ccb2ac2d Mon Sep 17 00:00:00 2001 From: vpstackhub Date: Mon, 8 Dec 2025 11:56:00 -0800 Subject: [PATCH 2/7] Remove hard-coded project.dir from project.properties (#95) --- nbproject/project.properties | 1 - 1 file changed, 1 deletion(-) diff --git a/nbproject/project.properties b/nbproject/project.properties index 6923ac7..e9fd763 100644 --- a/nbproject/project.properties +++ b/nbproject/project.properties @@ -7,7 +7,6 @@ annotation.processing.source.output=${build.generated.sources.dir}/ap-source-out application.homepage=https://www.jdiskmark.net/ application.title=JDiskMark application.vendor=JDiskMark Team -project.dir=C:/Users/valer/OneDrive/Documenti/NetBeansProjects/jdm-java build.classes.dir=${build.dir}/classes build.classes.excludes=**/*.java,**/*.form # This directory is removed when the project is cleaned: From ce99e1e2b81655f75c20e8a7aa5d75acc43b6e98 Mon Sep 17 00:00:00 2001 From: vpstackhub Date: Mon, 8 Dec 2025 12:12:46 -0800 Subject: [PATCH 3/7] Improve soft purge exception logging (Issue #95) --- src/jdiskmark/Util.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/jdiskmark/Util.java b/src/jdiskmark/Util.java index 105b427..7ecf718 100644 --- a/src/jdiskmark/Util.java +++ b/src/jdiskmark/Util.java @@ -13,6 +13,8 @@ import java.util.Random; import javax.swing.filechooser.FileSystemView; import java.io.RandomAccessFile; +import java.util.logging.Level; +import java.util.logging.Logger; /** * Utility methods for JDiskMark @@ -59,8 +61,12 @@ public static void readPurge(long estimatedBytes) { } } } - } catch (Exception ignored) { - // Soft purge — failure is not fatal + } 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); } } From ebd641ddf4b3b0d8b5d9e5ae4ec8164dd40a6b69 Mon Sep 17 00:00:00 2001 From: vpstackhub Date: Mon, 8 Dec 2025 12:56:39 -0800 Subject: [PATCH 4/7] Add logging for soft purge attempts (Issue #95) --- src/jdiskmark/BenchmarkWorker.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/jdiskmark/BenchmarkWorker.java b/src/jdiskmark/BenchmarkWorker.java index 32e5d2d..a19b332 100644 --- a/src/jdiskmark/BenchmarkWorker.java +++ b/src/jdiskmark/BenchmarkWorker.java @@ -271,9 +271,12 @@ protected Benchmark doInBackground() throws Exception { System.out.println(">>> BenchmarkWorker: dropOsCache() returned = " + ok); if (!ok) { - System.out.println(">>> BenchmarkWorker: dropOsCache FAILED — fallback to read purge"); - Gui.msg("⚠ drop-cache failed — falling back to read purge."); + 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); + System.out.println(">>> BenchmarkWorker: Soft purge completed."); } } catch (Exception ex) { System.out.println(">>> BenchmarkWorker: EXCEPTION in dropOsCache — fallback to read purge"); @@ -286,7 +289,12 @@ protected Benchmark doInBackground() throws Exception { // ========================== // USER MODE: Soft purge // ========================== + System.out.println(">>> BenchmarkWorker: Starting SOFT purge (readPurge)"); + Gui.msg("Performing soft cache purge (read-through)…"); + Util.readPurge(estimatedBytes); + + System.out.println(">>> BenchmarkWorker: Soft purge completed."); } long purgeEndNs = System.nanoTime(); From e679860d304a884308152704864e6912868c5677 Mon Sep 17 00:00:00 2001 From: vpstackhub Date: Mon, 8 Dec 2025 13:57:12 -0800 Subject: [PATCH 5/7] #95 - Replace purge method strings with enum, fix UI panel, update CLI --- src/jdiskmark/App.java | 2 + src/jdiskmark/Benchmark.java | 28 ++++++++++--- src/jdiskmark/BenchmarkWorker.java | 29 +++++++++++--- src/jdiskmark/RunBenchmarkCommand.java | 3 ++ src/jdiskmark/RunDetailsPanel.java | 2 +- src/jdiskmark/Util.java | 36 +++++++++++++++-- src/jdiskmark/UtilOs.java | 55 ++++++++++++++++++++++---- 7 files changed, 134 insertions(+), 21 deletions(-) diff --git a/src/jdiskmark/App.java b/src/jdiskmark/App.java index fd350f8..882eee9 100644 --- a/src/jdiskmark/App.java +++ b/src/jdiskmark/App.java @@ -90,6 +90,8 @@ 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"); diff --git a/src/jdiskmark/Benchmark.java b/src/jdiskmark/Benchmark.java index f8c0805..a7ee5ba 100644 --- a/src/jdiskmark/Benchmark.java +++ b/src/jdiskmark/Benchmark.java @@ -20,6 +20,8 @@ import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.List; +import jakarta.persistence.Enumerated; +import jakarta.persistence.EnumType; /** * A read or write benchmark @@ -94,6 +96,21 @@ public enum BenchmarkType { // benchmark parameters @Column BenchmarkType 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) @@ -108,7 +125,8 @@ public enum BenchmarkType { long cachePurgeDurationMs; @Column - String cachePurgeMethod; + @Enumerated(EnumType.STRING) + CachePurgeMethod cachePurgeMethod; // timestamps @Convert(converter = LocalDateTimeAttributeConverter.class) @@ -182,7 +200,7 @@ public Benchmark() { cachePurgePerformed = false; cachePurgeSizeBytes = 0L; cachePurgeDurationMs = 0L; - cachePurgeMethod = "none"; + cachePurgeMethod = CachePurgeMethod.NONE; } Benchmark(BenchmarkType type) { @@ -191,7 +209,7 @@ public Benchmark() { cachePurgePerformed = false; cachePurgeSizeBytes = 0L; cachePurgeDurationMs = 0L; - cachePurgeMethod = "none"; + cachePurgeMethod = CachePurgeMethod.NONE; } // basic getters and setters @@ -237,10 +255,10 @@ public long getCachePurgeDurationMs() { return cachePurgeDurationMs; } - public String getCachePurgeMethod() { + public CachePurgeMethod getCachePurgeMethod() { return cachePurgeMethod; } - + // utility methods for collection @JsonIgnore diff --git a/src/jdiskmark/BenchmarkWorker.java b/src/jdiskmark/BenchmarkWorker.java index a19b332..a3a612b 100644 --- a/src/jdiskmark/BenchmarkWorker.java +++ b/src/jdiskmark/BenchmarkWorker.java @@ -247,7 +247,9 @@ protected Benchmark doInBackground() throws Exception { long purgeStartNs = System.nanoTime(); benchmark.cachePurgePerformed = true; - benchmark.cachePurgeMethod = App.isAdmin ? "drop-cache" : "single-pass-read"; + 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 * @@ -275,14 +277,14 @@ protected Benchmark doInBackground() throws Exception { Gui.msg("⚠ drop-cache failed — switching to soft purge."); System.out.println(">>> BenchmarkWorker: Starting SOFT purge (fallback)"); - Util.readPurge(estimatedBytes); + 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); + Util.readPurge(estimatedBytes, pct -> setProgress(Math.min(99, pct))); } } else { @@ -292,7 +294,7 @@ protected Benchmark doInBackground() throws Exception { System.out.println(">>> BenchmarkWorker: Starting SOFT purge (readPurge)"); Gui.msg("Performing soft cache purge (read-through)…"); - Util.readPurge(estimatedBytes); + Util.readPurge(estimatedBytes, pct -> setProgress(Math.min(99, pct))); System.out.println(">>> BenchmarkWorker: Soft purge completed."); } @@ -430,7 +432,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 @@ -441,4 +447,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/RunBenchmarkCommand.java b/src/jdiskmark/RunBenchmarkCommand.java index ffa1bfc..ab5ea0c 100644 --- a/src/jdiskmark/RunBenchmarkCommand.java +++ b/src/jdiskmark/RunBenchmarkCommand.java @@ -74,6 +74,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 index ac8e9eb..2351e97 100644 --- a/src/jdiskmark/RunDetailsPanel.java +++ b/src/jdiskmark/RunDetailsPanel.java @@ -56,7 +56,7 @@ public void load(BenchmarkOperation op) { minMaxLabel.setText(op.getBwMinMaxDisplay()); accLabel.setText(op.getAccTimeDisplay()); - purgeMethodLabel.setText(b.cachePurgeMethod); + 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 7ecf718..786a923 100644 --- a/src/jdiskmark/Util.java +++ b/src/jdiskmark/Util.java @@ -15,6 +15,8 @@ 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 @@ -44,23 +46,33 @@ static public boolean deleteDirectory(File path) { return (path.delete()); } - public static void readPurge(long estimatedBytes) { + public static void readPurge(long estimatedBytes, java.util.function.IntConsumer progressCallback) { try { int block = 1024 * 1024; // 1MB chunks - long toRead = Math.min(estimatedBytes, 512L * 1024 * 1024); // cap at 512MB for safety + 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); @@ -69,7 +81,7 @@ public static void readPurge(long estimatedBytes) { ex.printStackTrace(System.out); } } - + /** * Returns a pseudo-random number between min and max, inclusive. * The difference between min and max can be at most @@ -92,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 30fdc3c..b72c40e 100644 --- a/src/jdiskmark/UtilOs.java +++ b/src/jdiskmark/UtilOs.java @@ -53,20 +53,61 @@ public static boolean isProcessElevated() { public static void restartAsAdmin() { try { - String javaBin = System.getProperty("java.home") + "\\bin\\java.exe"; - String jarPath = new File(App.class.getProtectionDomain() - .getCodeSource().getLocation().toURI()).getPath(); + String os = System.getProperty("os.name").toLowerCase(); - String cmd = "powershell -Command \"Start-Process '" + javaBin + "' -ArgumentList '-jar \"" + jarPath + "\"' -Verb RunAs\""; + // ------------------------------------------------------------------- + // 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); - Runtime.getRuntime().exec(cmd); - System.exit(0); } 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 { From c02aca93155618b37a5a1696e124f0d819b58261 Mon Sep 17 00:00:00 2001 From: James Mark Chan Date: Sun, 28 Dec 2025 16:40:40 -0800 Subject: [PATCH 6/7] #95 minor house keeping --- build.xml | 5 +---- src/jdiskmark/Benchmark.java | 4 +--- 2 files changed, 2 insertions(+), 7 deletions(-) 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/src/jdiskmark/Benchmark.java b/src/jdiskmark/Benchmark.java index db4d7f2..d1d606b 100644 --- a/src/jdiskmark/Benchmark.java +++ b/src/jdiskmark/Benchmark.java @@ -226,7 +226,7 @@ public String toString() { @OneToMany(mappedBy = "benchmark", cascade = CascadeType.ALL, orphanRemoval = true) - List operations; + List operations = new ArrayList<>(); public List getOperations() { return operations; @@ -284,7 +284,6 @@ public String toResultString() { } public Benchmark() { - operations = new ArrayList<>(); startTime = LocalDateTime.now(); cachePurgePerformed = false; cachePurgeSizeBytes = 0L; @@ -295,7 +294,6 @@ public Benchmark() { } Benchmark(BenchmarkType type) { - operations = new ArrayList<>(); startTime = LocalDateTime.now(); benchmarkType = type; cachePurgePerformed = false; From a4c3e32abc90878d6e7f90c4113ce722c2adc9e1 Mon Sep 17 00:00:00 2001 From: James Mark Chan Date: Sun, 28 Dec 2025 16:41:31 -0800 Subject: [PATCH 7/7] #95 allow project to build --- nbproject/project.properties | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/nbproject/project.properties b/nbproject/project.properties index e9fd763..2aa0443 100644 --- a/nbproject/project.properties +++ b/nbproject/project.properties @@ -116,6 +116,4 @@ run.test.modulepath=\ ${javac.test.modulepath} source.encoding=UTF-8 src.dir=src -test.src.dir=test -run.working.dir=${project.dir} -work.dir=${project.dir} +test.src.dir=test \ No newline at end of file