Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions jdm-core/src/main/java/jdiskmark/BenchmarkDriveInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -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() {}
}
6 changes: 4 additions & 2 deletions jdm-core/src/main/java/jdiskmark/BenchmarkRunner.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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;
Expand All @@ -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;
}

/**
Expand Down
41 changes: 35 additions & 6 deletions jdm-core/src/main/java/jdiskmark/Gui.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "<html><body style='font-family:sans-serif;font-size:11px'>"
+ "<b>" + App.APP_NAME + " " + App.VERSION + "</b><br>"
+ "JVM: " + App.jdk + "<br>"
+ "OS:&nbsp; " + App.osLabel + "<br><br>"
+ "OS:&nbsp; " + App.osLabel + "<br>"
+ "System ID: "
+ (hasSystemId
? "<a href='#select-system-id' style='text-decoration:none'><code>" + systemIdDisplay + "</code></a>"
+ " <a href='#copy-system-id' style='text-decoration:none'>\u29C9</a>"
: "<code>" + systemIdDisplay + "</code>")
+ "<br><br>"
+ "<span style='color:gray;font-size:10px'>"
+ "FlatLaf " + App.buildProp("lib.flatlaf")
+ " &middot; JFreeChart " + App.buildProp("lib.jfreechart")
Expand All @@ -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());
}
}
}
});
Expand Down
48 changes: 48 additions & 0 deletions jdm-core/src/main/java/jdiskmark/Util.java
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,54 @@ 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<String> deviceNames = UtilOs.getDeviceNamesFromPartitionLinux(partition);
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);
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):
*
Expand Down
80 changes: 79 additions & 1 deletion jdm-core/src/main/java/jdiskmark/UtilOs.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>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")
* @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}.
*
* <p>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<String, String> 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()) {
Expand Down Expand Up @@ -1315,4 +1393,4 @@ static String getSectorSizeLinux(Path path) {
return null;
}
}


Expand Down
51 changes: 51 additions & 0 deletions jdm-core/src/main/java/org/metricus/jdm/os/UtilsMacOs.java
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,57 @@ 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}.
*
* <p>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<String, String> env = pb.environment();
env.put("LC_ALL", "C");
pb.redirectErrorStream(true);
Process process = pb.start();

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;
}
}
}

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);
}
return null;
Comment thread
Copilot marked this conversation as resolved.
}

// -----------------------------------------------------------------------
// Cache flush / drop
// -----------------------------------------------------------------------
Expand Down
Loading