From 580c5bbeeaf7d2221ff9792cf1353ecba4e9326f Mon Sep 17 00:00:00 2001 From: James Mark Chan Date: Sun, 23 Aug 2026 04:22:53 -0700 Subject: [PATCH 1/5] #218 add system id to about dialog box --- jdm-core/src/main/java/jdiskmark/Gui.java | 41 +++++++++++++++++++---- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/jdm-core/src/main/java/jdiskmark/Gui.java b/jdm-core/src/main/java/jdiskmark/Gui.java index 4c70c3c..f80e9e0 100644 --- a/jdm-core/src/main/java/jdiskmark/Gui.java +++ b/jdm-core/src/main/java/jdiskmark/Gui.java @@ -566,10 +566,18 @@ public static void showAboutDialog() { // Build an HTML panel so the website URL is a clickable hyperlink. String url = "https://www.jdiskmark.net"; + boolean hasSystemId = App.systemId != null && !App.systemId.isBlank(); + String systemIdDisplay = hasSystemId ? App.systemId : "(unavailable)"; String html = "" + "" + App.APP_NAME + " " + App.VERSION + "
" + "JVM: " + App.jdk + "
" - + "OS:  " + App.osLabel + "

" + + "OS:  " + App.osLabel + "
" + + "System ID: " + + (hasSystemId + ? "" + systemIdDisplay + "" + + " \u29C9" + : "" + systemIdDisplay + "") + + "

" + "" + "FlatLaf " + App.buildProp("lib.flatlaf") + " · JFreeChart " + App.buildProp("lib.jfreechart") @@ -585,11 +593,32 @@ public static void showAboutDialog() { msgPane.setEditable(false); msgPane.setOpaque(false); msgPane.addHyperlinkListener(e -> { - if (e.getEventType() == javax.swing.event.HyperlinkEvent.EventType.ACTIVATED) { - try { - java.awt.Desktop.getDesktop().browse(new java.net.URI(url)); - } catch (IOException | URISyntaxException | RuntimeException ex) { - App.msg("Could not open browser: " + ex.getMessage()); + // Tooltip on hover (Swing's HTML ignores the title attribute) + if (e.getEventType() == javax.swing.event.HyperlinkEvent.EventType.ENTERED) { + if ("#copy-system-id".equals(e.getDescription())) { + msgPane.setToolTipText("Copy System ID to clipboard"); + } else if ("#select-system-id".equals(e.getDescription())) { + msgPane.setToolTipText("Click to select System ID"); + } + } else if (e.getEventType() == javax.swing.event.HyperlinkEvent.EventType.EXITED) { + msgPane.setToolTipText(null); + } else if (e.getEventType() == javax.swing.event.HyperlinkEvent.EventType.ACTIVATED) { + if ("#copy-system-id".equals(e.getDescription())) { + var clipboard = java.awt.Toolkit.getDefaultToolkit().getSystemClipboard(); + clipboard.setContents(new java.awt.datatransfer.StringSelection(App.systemId), null); + App.msg("System ID copied to clipboard"); + } else if ("#select-system-id".equals(e.getDescription())) { + // Select the systemId text so the user can see it highlighted + javax.swing.text.Element el = e.getSourceElement(); + if (el != null) { + msgPane.select(el.getStartOffset(), el.getEndOffset()); + } + } else { + try { + java.awt.Desktop.getDesktop().browse(new java.net.URI(url)); + } catch (IOException | URISyntaxException | RuntimeException ex) { + App.msg("Could not open browser: " + ex.getMessage()); + } } } }); From 98d8434b443ed794764193d5b0e7fac3d46e40ce Mon Sep 17 00:00:00 2001 From: James Mark Chan Date: Sun, 23 Aug 2026 16:26:04 -0700 Subject: [PATCH 2/5] #213 Add cross-platform drive interface detection and persist it in benchmark metadata (Windows/Linux/macOS), wiring the detected interface into BenchmarkDriveInfo via BenchmarkRunner. --- .../java/jdiskmark/BenchmarkDriveInfo.java | 3 + .../main/java/jdiskmark/BenchmarkRunner.java | 6 +- jdm-core/src/main/java/jdiskmark/Util.java | 36 +++++++++ jdm-core/src/main/java/jdiskmark/UtilOs.java | 78 +++++++++++++++++++ .../java/org/metricus/jdm/os/UtilsMacOs.java | 39 ++++++++++ 5 files changed, 160 insertions(+), 2 deletions(-) diff --git a/jdm-core/src/main/java/jdiskmark/BenchmarkDriveInfo.java b/jdm-core/src/main/java/jdiskmark/BenchmarkDriveInfo.java index 2dd0b76..7ad1b77 100644 --- a/jdm-core/src/main/java/jdiskmark/BenchmarkDriveInfo.java +++ b/jdm-core/src/main/java/jdiskmark/BenchmarkDriveInfo.java @@ -23,6 +23,9 @@ public class BenchmarkDriveInfo { double totalGb; @JsonSerialize(using = RoundingSerializer.class) public double getTotalGb() { return totalGb; } + @Column + String driveInterface = null; + public String getDriveInterface() { return driveInterface; } public BenchmarkDriveInfo() {} } diff --git a/jdm-core/src/main/java/jdiskmark/BenchmarkRunner.java b/jdm-core/src/main/java/jdiskmark/BenchmarkRunner.java index 1043555..09055b4 100644 --- a/jdm-core/src/main/java/jdiskmark/BenchmarkRunner.java +++ b/jdm-core/src/main/java/jdiskmark/BenchmarkRunner.java @@ -120,11 +120,12 @@ public Benchmark execute() throws Exception { String driveModel = Util.getDriveModel(configDir); String partitionId = Util.getPartitionId(configDir.toPath()); DiskUsageInfo usageInfo = Util.getDiskUsage(configDir.getAbsolutePath()); + String driveInterface = Util.getDriveInterface(configDir); // Initialize Benchmark Benchmark benchmark = new Benchmark(config); - mapEnvironment(benchmark, driveModel, partitionId, usageInfo); + mapEnvironment(benchmark, driveModel, partitionId, usageInfo, driveInterface); // capture the render mode chosen at the time this run starts benchmark.setRenderMode(App.rmOption); @@ -380,7 +381,7 @@ private BenchmarkOperation createOp(Benchmark b, IOMode mode) { return op; } - private void mapEnvironment(Benchmark b, String model, String partId, DiskUsageInfo u) { + private void mapEnvironment(Benchmark b, String model, String partId, DiskUsageInfo u, String driveIface) { b.systemId = (App.systemId != null) ? App.systemId : ""; b.systemInfo.processorName = App.processorName; @@ -395,6 +396,7 @@ private void mapEnvironment(Benchmark b, String model, String partId, DiskUsageI b.driveInfo.percentUsed = u.percentUsed; b.driveInfo.usedGb = u.usedGb; b.driveInfo.totalGb = u.totalGb; + b.driveInfo.driveInterface = driveIface; } /** diff --git a/jdm-core/src/main/java/jdiskmark/Util.java b/jdm-core/src/main/java/jdiskmark/Util.java index dd4f55f..cadaa8c 100644 --- a/jdm-core/src/main/java/jdiskmark/Util.java +++ b/jdm-core/src/main/java/jdiskmark/Util.java @@ -181,6 +181,42 @@ public static String getDriveModel(File dataDir) { return "OS not supported"; } + /** + * Get the storage bus interface (NVMe, SATA, USB, etc.) for the drive + * the path is mapped to. Returns null if the interface cannot be + * determined. + * + * @param dataDir the data directory being used in the run. + * @return interface string (e.g. "NVMe", "SATA", "USB") or null + */ + public static String getDriveInterface(File dataDir) { + Path dataDirPath = Paths.get(dataDir.getAbsolutePath()); + String iface = null; + try { + if (App.isLinux()) { + String partition = UtilOs.getPartitionFromFilePathLinux(dataDirPath); + if (partition != null) { + List deviceNames = UtilOs.getDeviceNamesFromPartitionLinux(partition); + String devicePath = deviceNames.isEmpty() + ? partition + : "/dev/" + deviceNames.getFirst(); + iface = UtilOs.getDriveInterfaceLinux(devicePath); + } + } else if (App.isMacOs()) { + String devicePath = UtilsMacOs.getDeviceFromPath(dataDirPath); + iface = UtilsMacOs.getDriveInterfaceMacOs(devicePath); + } else if (App.isWindows()) { + String driveLetter = dataDirPath.getRoot().toFile().toString().split(":")[0]; + if (driveLetter.length() == 1 && Character.isLetter(driveLetter.charAt(0))) { + iface = UtilOs.getDriveInterfaceWindows(driveLetter); + } + } + } catch (Exception e) { + System.out.println("Could not detect drive interface: " + e.getMessage()); + } + return iface; + } + /* * Example input win11 (english): * diff --git a/jdm-core/src/main/java/jdiskmark/UtilOs.java b/jdm-core/src/main/java/jdiskmark/UtilOs.java index 3f025c7..a8d1950 100644 --- a/jdm-core/src/main/java/jdiskmark/UtilOs.java +++ b/jdm-core/src/main/java/jdiskmark/UtilOs.java @@ -205,6 +205,84 @@ public static String getDriveModelWindows(String driveLetter) { return null; } + /** + * Get the storage bus interface for a Windows drive letter using + * PowerShell's Get-Partition / Get-Disk pipeline. + * + *

Returns the BusType string from {@code Get-PhysicalDisk}, e.g. + * {@code NVMe}, {@code SATA}, {@code USB}, {@code RAID}, {@code SAS}. + * + * @param driveLetter single drive letter (e.g. "C") + * @return the bus type string, or null if unavailable + */ + public static String getDriveInterfaceWindows(String driveLetter) { + driveLetter = driveLetter.toUpperCase(); + try { + // Get-Partition maps a drive letter to a DiskNumber, + // then Get-Disk gives us the BusType. + String script = String.format( + "Get-Partition -DriveLetter %s | Get-Disk | Select-Object -ExpandProperty BusType", + driveLetter); + ProcessBuilder pb = new ProcessBuilder("powershell", "-NoProfile", + "-ExecutionPolicy", "Bypass", "-Command", script); + pb.redirectErrorStream(true); + Process process = pb.start(); + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(process.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (!line.isEmpty()) { + return line; + } + } + } + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Could not detect drive interface on Windows", e); + } + return null; + } + + /** + * Get the storage transport for a Linux device using {@code lsblk --output TRAN}. + * + *

Returns values like {@code sata}, {@code nvme}, {@code usb}, or null. + * + * @param devicePath the device path (e.g. "/dev/sda", "/dev/nvme0n1") + * @return the transport string, or null if unavailable + */ + public static String getDriveInterfaceLinux(String devicePath) { + try { + ProcessBuilder pb = new ProcessBuilder( + "lsblk", devicePath, "--nodeps", "--noheadings", "--output", "TRAN"); + Map env = pb.environment(); + env.put("LC_ALL", "C"); + pb.redirectErrorStream(true); + Process process = pb.start(); + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(process.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (!line.isEmpty()) { + // Normalise common values + switch (line.toLowerCase()) { + case "nvme": return "NVMe"; + case "sata": return "SATA"; + case "usb": return "USB"; + case "sas": return "SAS"; + case "spi": return "SPI"; + default: return line; + } + } + } + } + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Could not detect drive interface on Linux", e); + } + return null; + } + public static DiskUsageInfo getCapacityWindows(String driveLetter) { File capacityPsFile = new File(CAPACITY_PS_FILENAME); if (!capacityPsFile.exists()) { diff --git a/jdm-core/src/main/java/org/metricus/jdm/os/UtilsMacOs.java b/jdm-core/src/main/java/org/metricus/jdm/os/UtilsMacOs.java index b28d4d4..23f26cb 100644 --- a/jdm-core/src/main/java/org/metricus/jdm/os/UtilsMacOs.java +++ b/jdm-core/src/main/java/org/metricus/jdm/os/UtilsMacOs.java @@ -134,6 +134,45 @@ public static String getDeviceModel(String devicePath) { return "Model unavailable for " + deviceId; } + /** + * Returns the storage bus interface for the given macOS device path + * by parsing the {@code Protocol} field from {@code diskutil info}. + * + *

Common values: {@code PCI-Express} (NVMe), {@code SATA}, + * {@code USB}, {@code Apple Fabric}. + * + * @param devicePath the device path (e.g. {@code /dev/disk4s1}) + * @return the protocol string, or null if unavailable + */ + public static String getDriveInterfaceMacOs(String devicePath) { + if (devicePath == null || devicePath.isEmpty()) { + return null; + } + try { + ProcessBuilder pb = new ProcessBuilder("diskutil", "info", devicePath); + Map env = pb.environment(); + env.put("LC_ALL", "C"); + pb.redirectErrorStream(true); + Process process = pb.start(); + + BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); + String line; + while ((line = reader.readLine()) != null) { + if (line.contains("Protocol:")) { + String protocol = line.split("Protocol:")[1].trim(); + // Normalise common macOS protocol values + if (protocol.contains("PCI-Express") || protocol.contains("PCI")) { + return "NVMe"; + } + return protocol; + } + } + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Could not detect drive interface on macOS", e); + } + return null; + } + // ----------------------------------------------------------------------- // Cache flush / drop // ----------------------------------------------------------------------- From 25b5c818a295502c21e8f61f29159eeaeed482e8 Mon Sep 17 00:00:00 2001 From: James Mark Chan Date: Sun, 23 Aug 2026 20:27:41 -0700 Subject: [PATCH 3/5] sync doc w code Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- jdm-core/src/main/java/jdiskmark/UtilOs.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jdm-core/src/main/java/jdiskmark/UtilOs.java b/jdm-core/src/main/java/jdiskmark/UtilOs.java index a8d1950..794309c 100644 --- a/jdm-core/src/main/java/jdiskmark/UtilOs.java +++ b/jdm-core/src/main/java/jdiskmark/UtilOs.java @@ -209,7 +209,7 @@ public static String getDriveModelWindows(String driveLetter) { * Get the storage bus interface for a Windows drive letter using * PowerShell's Get-Partition / Get-Disk pipeline. * - *

Returns the BusType string from {@code Get-PhysicalDisk}, e.g. + *

Returns the BusType string from {@code Get-Disk}, e.g. * {@code NVMe}, {@code SATA}, {@code USB}, {@code RAID}, {@code SAS}. * * @param driveLetter single drive letter (e.g. "C") @@ -1393,4 +1393,4 @@ static String getSectorSizeLinux(Path path) { return null; } } - \ No newline at end of file + From fadbed031fb1bddab1b13d9d0570abbd8f0220e4 Mon Sep 17 00:00:00 2001 From: James Mark Chan Date: Sun, 23 Aug 2026 20:32:42 -0700 Subject: [PATCH 4/5] copilot fix to opus code Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../java/org/metricus/jdm/os/UtilsMacOs.java | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/jdm-core/src/main/java/org/metricus/jdm/os/UtilsMacOs.java b/jdm-core/src/main/java/org/metricus/jdm/os/UtilsMacOs.java index 23f26cb..27f79e2 100644 --- a/jdm-core/src/main/java/org/metricus/jdm/os/UtilsMacOs.java +++ b/jdm-core/src/main/java/org/metricus/jdm/os/UtilsMacOs.java @@ -155,18 +155,30 @@ public static String getDriveInterfaceMacOs(String devicePath) { pb.redirectErrorStream(true); Process process = pb.start(); - BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); - String line; - while ((line = reader.readLine()) != null) { - if (line.contains("Protocol:")) { - String protocol = line.split("Protocol:")[1].trim(); - // Normalise common macOS protocol values - if (protocol.contains("PCI-Express") || protocol.contains("PCI")) { - return "NVMe"; + String protocol = null; + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + if (line.contains("Protocol:")) { + protocol = line.split("Protocol:")[1].trim(); + break; } - return protocol; } } + + try { + process.waitFor(); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + } + + if (protocol != null) { + // Normalise common macOS protocol values + if (protocol.contains("PCI-Express") || protocol.contains("PCI")) { + return "NVMe"; + } + return protocol; + } } catch (IOException e) { LOGGER.log(Level.WARNING, "Could not detect drive interface on macOS", e); } From b79ecab346808302bba073ae01cbc9057f4480be Mon Sep 17 00:00:00 2001 From: James Mark Chan Date: Sun, 23 Aug 2026 20:35:12 -0700 Subject: [PATCH 5/5] consistent handling of interfaces for list of devices Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- jdm-core/src/main/java/jdiskmark/Util.java | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/jdm-core/src/main/java/jdiskmark/Util.java b/jdm-core/src/main/java/jdiskmark/Util.java index cadaa8c..ac31b4a 100644 --- a/jdm-core/src/main/java/jdiskmark/Util.java +++ b/jdm-core/src/main/java/jdiskmark/Util.java @@ -197,10 +197,22 @@ public static String getDriveInterface(File dataDir) { String partition = UtilOs.getPartitionFromFilePathLinux(dataDirPath); if (partition != null) { List deviceNames = UtilOs.getDeviceNamesFromPartitionLinux(partition); - String devicePath = deviceNames.isEmpty() - ? partition - : "/dev/" + deviceNames.getFirst(); - iface = UtilOs.getDriveInterfaceLinux(devicePath); + if (deviceNames.size() > 1) { + StringBuilder sb = new StringBuilder(); + for (String dName : deviceNames) { + String dIface = UtilOs.getDriveInterfaceLinux("/dev/" + dName); + if (dIface != null) { + if (sb.length() > 0) sb.append(":"); + sb.append(dIface); + } + } + iface = (sb.length() > 0) ? "Multiple drives: " + sb : null; + } else { + String devicePath = deviceNames.isEmpty() + ? partition + : "/dev/" + deviceNames.getFirst(); + iface = UtilOs.getDriveInterfaceLinux(devicePath); + } } } else if (App.isMacOs()) { String devicePath = UtilsMacOs.getDeviceFromPath(dataDirPath);