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
9 changes: 6 additions & 3 deletions .github/actions/ci-version/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,11 @@ runs:
RAW=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)
BASE=${RAW%-SNAPSHOT}

# Read the dedicated msi.version property — always purely numeric
# Read the dedicated msi.version and rpm.version properties — these are
# maintained separately in the POM with format restrictions (numeric-only
# for MSI, no hyphens for RPM) and must not be derived from project.version.
MSI_BASE=$(mvn help:evaluate -Dexpression=msi.version -q -DforceStdout)
RPM_BASE=$(mvn help:evaluate -Dexpression=rpm.version -q -DforceStdout)

# Prefer the PR head branch; fall back to push/tag branch name
BRANCH="${GITHUB_HEAD_REF:-$GITHUB_REF_NAME}"
Expand All @@ -63,7 +66,7 @@ runs:
echo "install_version=${RAW}" >> "$GITHUB_OUTPUT"
echo "base_version=${BASE}" >> "$GITHUB_OUTPUT"
echo "msi_version=${MSI_BASE}" >> "$GITHUB_OUTPUT"
echo "rpm_version=${BASE}" >> "$GITHUB_OUTPUT"
echo "rpm_version=${RPM_BASE}" >> "$GITHUB_OUTPUT"
echo "rpm_release=1" >> "$GITHUB_OUTPUT"
echo "CI label skipped (branch=${BRANCH}, disable=${DISABLE})"
exit 0
Expand Down Expand Up @@ -96,7 +99,7 @@ runs:
echo "install_version=${INSTALL}" >> "$GITHUB_OUTPUT"
echo "base_version=${BASE}" >> "$GITHUB_OUTPUT"
echo "msi_version=${MSI_BASE}" >> "$GITHUB_OUTPUT"
echo "rpm_version=${BASE}" >> "$GITHUB_OUTPUT"
echo "rpm_version=${RPM_BASE}" >> "$GITHUB_OUTPUT"
echo "rpm_release=ci.${SLUG}.${{ github.run_number }}" >> "$GITHUB_OUTPUT"
echo "CI version: ${DISPLAY}"
echo "Install version: ${INSTALL}"
1 change: 1 addition & 0 deletions AGENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ The `jdm-core/docs/` folder contains authoritative design documents:
- **Keep design docs current.** When a code change touches an area covered by
`jdm-core/docs/`, update the relevant doc in the same session — do not leave
design documentation out of sync with the code.
- Write robust, production-ready code that follows existing project conventions and remains easy to understand and maintain.

---

Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,10 @@ IOPS: 28892857

JDiskMark is developed with [NetBeans 25](https://netbeans.apache.org/front/main/download/) and [Java 25](https://www.oracle.com/java/technologies/downloads/).

On Ubuntu Desktop, the JDK can be installed with:

`sudo apt update && sudo apt install -y openjdk-25-jdk`

## Build from Source

### Prerequisites
Expand Down
9 changes: 7 additions & 2 deletions jdm-core/src/main/java/jdiskmark/BatchConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,13 @@ public static BatchConfig of(List<File> drives, List<BenchmarkProfile> profiles,

public void applyProfileToApp(BenchmarkProfile profile, File driveLocation) {
App.loadProfile(profile);
App.locationDir = driveLocation;
App.dataDir = new File(driveLocation.getAbsolutePath() + File.separator + App.DATADIRNAME);
File resolved = DriveChecker.resolveLocationForRoot(driveLocation);
if (resolved == null) {
throw new IllegalStateException(
"No writable location found on " + driveLocation.getAbsolutePath());
}
App.locationDir = resolved;
App.dataDir = new File(resolved.getAbsolutePath() + File.separator + App.DATADIRNAME);
}

public int totalRuns() {
Expand Down
65 changes: 39 additions & 26 deletions jdm-core/src/main/java/jdiskmark/BatchPanel.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
public class BatchPanel extends JPanel {
private static final Logger LOG = Logger.getLogger(BatchPanel.class.getName());
private static final DecimalFormat DF = new DecimalFormat("###.##");
private static final DecimalFormat DF2 = new DecimalFormat("###.00");

private static final String CARD_SETUP = "setup";
private static final String CARD_RUNNING = "running";
Expand Down Expand Up @@ -79,6 +80,8 @@ public class BatchPanel extends JPanel {
private JPanel chartContainer;
private JTable summaryTable;
private JLabel durationLabel;
private JButton startBatchButton;
private JButton newBatchButton;

// State — benchmarks from the currently displayed results (for double-click loading)
private List<Benchmark> currentResultBenchmarks = new ArrayList<>();
Expand Down Expand Up @@ -176,13 +179,13 @@ private JPanel buildSetupCard() {
profileColumn.add(profileSelectPanel);

profileColumn.add(Box.createVerticalStrut(8));
JButton startBtn = new JButton("Start Batch");
startBtn.putClientProperty("FlatLaf.style", org.metricus.jdm.ui.ButtonStyles.DEFAULT_START);
startBtn.addActionListener(e -> startBatch());
startBatchButton = new JButton("Start Batch");
startBatchButton.putClientProperty("FlatLaf.style", org.metricus.jdm.ui.ButtonStyles.DEFAULT_START);
startBatchButton.addActionListener(e -> startBatch());
// MigLayout "h 40!" matches BenchmarkControlPanel's start button exactly, including DPI scaling.
JPanel startBtnWrapper = new JPanel(new net.miginfocom.swing.MigLayout("insets 0, fillx", "[grow]", "[]"));
startBtnWrapper.setAlignmentX(0);
startBtnWrapper.add(startBtn, "growx, h 40!");
startBtnWrapper.add(startBatchButton, "growx, h 40!");
startBtnWrapper.setMaximumSize(new Dimension(Integer.MAX_VALUE, startBtnWrapper.getPreferredSize().height));
profileColumn.add(startBtnWrapper);

Expand Down Expand Up @@ -283,13 +286,7 @@ private JPanel buildResultsCard() {
JPanel panel = new JPanel(new BorderLayout(8, 8));
panel.setBorder(new EmptyBorder(12, 12, 12, 12));

JPanel headerPanel = new JPanel(new BorderLayout());
JLabel titleLabel = new JLabel("Batch Mode — Results");
titleLabel.setFont(titleLabel.getFont().deriveFont(Font.BOLD, 16f));
headerPanel.add(titleLabel, BorderLayout.WEST);
durationLabel = new JLabel("");
headerPanel.add(durationLabel, BorderLayout.EAST);
panel.add(headerPanel, BorderLayout.NORTH);


chartContainer = new JPanel(new BorderLayout());
chartContainer.setPreferredSize(new Dimension(600, 280));
Expand Down Expand Up @@ -317,6 +314,10 @@ public void mouseClicked(java.awt.event.MouseEvent e) {
cm.getColumn(3).setPreferredWidth(80); // Read MB/s
cm.getColumn(4).setPreferredWidth(70); // Latency (ms)
cm.getColumn(5).setPreferredWidth(80); // Status
cm.getColumn(2).setCellRenderer(new RightTableCellRenderer());
cm.getColumn(3).setCellRenderer(new RightTableCellRenderer());
cm.getColumn(4).setCellRenderer(new RightTableCellRenderer());
cm.getColumn(5).setCellRenderer(new CenterTableCellRenderer());
JScrollPane tableScroll = new JScrollPane(summaryTable);
tableScroll.setPreferredSize(new Dimension(600, 120));

Expand All @@ -327,10 +328,13 @@ public void mouseClicked(java.awt.event.MouseEvent e) {

panel.add(splitPane, BorderLayout.CENTER);

JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
JButton newBatchBtn = new JButton("New Batch");
newBatchBtn.addActionListener(e -> resetToSetup());
buttonPanel.add(newBatchBtn);
JPanel buttonPanel = new JPanel(new BorderLayout());
durationLabel = new JLabel("");
buttonPanel.add(durationLabel, BorderLayout.WEST);
newBatchButton = new JButton("New Batch");
newBatchButton.putClientProperty("FlatLaf.style", org.metricus.jdm.ui.ButtonStyles.DEFAULT_START);
newBatchButton.addActionListener(e -> resetToSetup());
buttonPanel.add(newBatchButton, BorderLayout.EAST);
panel.add(buttonPanel, BorderLayout.SOUTH);

return panel;
Expand Down Expand Up @@ -398,10 +402,7 @@ public void showBatchFromHistory(UUID batchId) {
Duration dur = Duration.between(first, last);
durationText = String.format("Duration: %dm %ds", dur.toMinutes(), dur.toSecondsPart());
}
String profileNames = profiles.stream()
.map(p -> p != null ? p.getName() : "—")
.collect(Collectors.joining(", "));
durationLabel.setText("Profiles: " + profileNames + " | " + durationText);
durationLabel.setText(durationText);

cardLayout.show(cardPanel, CARD_RESULTS);
}
Expand Down Expand Up @@ -442,6 +443,15 @@ public void refreshChartTheme() {
}
}

public void applyStartButtonStyle(String style) {
if (startBatchButton != null) startBatchButton.putClientProperty("FlatLaf.style", style);
if (newBatchButton != null) newBatchButton.putClientProperty("FlatLaf.style", style);
}

public void applyCancelButtonStyle(String style) {
if (cancelButton != null) cancelButton.putClientProperty("FlatLaf.style", style);
}

// ── Drive Population ────────────────────────────────────────────────────

private void populateDrives() {
Expand Down Expand Up @@ -486,6 +496,9 @@ protected void done() {
driveLabels.add(entry.model());
driveListPanel.add(cb);
}
if (!driveCheckBoxes.isEmpty()) {
driveCheckBoxes.getFirst().setSelected(true);
}

driveListPanel.revalidate();
driveListPanel.repaint();
Expand Down Expand Up @@ -666,12 +679,12 @@ private void showResults(BatchResult result) {
Duration dur = result.getTotalDuration();
long mins = dur.toMinutes();
long secs = dur.toSecondsPart();
String profileNames = String.join(", ", result.getProfiles().stream().map(BenchmarkProfile::getName).toList());
String durationText = String.format("Profiles: %s | Duration: %dm %ds", profileNames, mins, secs);
String durationText = String.format("Duration: %dm %ds", mins, secs);
durationLabel.setText(durationText);

cardLayout.show(cardPanel, CARD_RESULTS);
App.msg("Batch complete — " + durationText);
String profileNames = String.join(", ", result.getProfiles().stream().map(BenchmarkProfile::getName).toList());
App.msg("Batch complete — " + profileNames + " | " + durationText);
}

private void buildChart(List<BenchmarkProfile> profiles, List<BatchResult.RunResult> successful) {
Expand Down Expand Up @@ -760,12 +773,12 @@ private void buildSummaryTable(List<BatchResult.RunResult> results) {
for (BenchmarkOperation op : rr.benchmark().getOperations()) {
switch (op.ioMode) {
case WRITE -> {
writeBw = DF.format(op.bwAvg);
if (latency.equals("—")) latency = DF.format(op.accAvg);
writeBw = DF2.format(op.bwAvg);
if (latency.equals("—")) latency = DF2.format(op.accAvg);
}
case READ -> {
readBw = DF.format(op.bwAvg);
latency = DF.format(op.accAvg);
readBw = DF2.format(op.bwAvg);
latency = DF2.format(op.accAvg);
}
}
}
Expand Down
9 changes: 4 additions & 5 deletions jdm-core/src/main/java/jdiskmark/BatchWorker.java
Original file line number Diff line number Diff line change
Expand Up @@ -117,14 +117,14 @@ private BatchResult.RunResult runSingle(int runIndex, UUID batchId, File drive,
}
dataDir.mkdirs();

if (!DriveChecker.validateTargetDirectory(drive, false)) {
if (!DriveChecker.validateTargetDirectory(App.locationDir, false)) {
Comment thread
jamesmarkchan marked this conversation as resolved.
return new BatchResult.RunResult(drive, driveModel, profile, null,
isRetry ? BatchResult.DriveStatus.RETRIED_THEN_SKIPPED
: BatchResult.DriveStatus.SKIPPED,
"Target directory validation failed");
}

if (!DriveChecker.checkDiskSpace(drive)) {
if (!DriveChecker.checkDiskSpace(App.locationDir)) {
return new BatchResult.RunResult(drive, driveModel, profile, null,
isRetry ? BatchResult.DriveStatus.RETRIED_THEN_SKIPPED
: BatchResult.DriveStatus.SKIPPED,
Expand Down Expand Up @@ -193,9 +193,8 @@ public void attemptCacheDrop() {
: BatchResult.DriveStatus.SKIPPED,
err);
} finally {
File dataDir = new File(drive.getAbsolutePath() + File.separator + App.DATADIRNAME);
if (dataDir.exists()) {
Util.deleteDirectory(dataDir);
if (App.dataDir != null && App.dataDir.exists()) {
Util.deleteDirectory(App.dataDir);
}
}
}
Expand Down
24 changes: 24 additions & 0 deletions jdm-core/src/main/java/jdiskmark/DriveChecker.java
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,30 @@ public static boolean checkDiskSpace(File locationDir) {
return false;
}

/**
* Resolves a drive root (mount point) to a user-writable location suitable
* for benchmark data. On Linux the root filesystem ({@code /}) is not
* writable by normal users, so when {@code root} equals the root component
* of {@code user.home} (e.g., {@code /} or {@code C:\\}) we return the home
* directory instead.
* @param root the drive mount point
* @return a writable directory on that drive, or {@code null} if none found
*/
public static File resolveLocationForRoot(File root) {
File home = new File(System.getProperty("user.home", ""));
if (home.exists()) {
java.nio.file.Path homeRoot = home.toPath().getRoot();
if (homeRoot != null && homeRoot.equals(root.toPath())) {
File candidate = new File(home, App.DATADIRNAME);
if (candidate.exists() ? candidate.canWrite() : home.canWrite()) {
return home;
}
}
}
if (root.canRead() && root.canWrite()) return root;
return null;
}

private static String formatBytes(long bytes) {
if (bytes >= App.GIGABYTE) {
return String.format("%.1f GB", bytes / (double) App.GIGABYTE);
Expand Down
13 changes: 1 addition & 12 deletions jdm-core/src/main/java/jdiskmark/DrivePanel.java
Original file line number Diff line number Diff line change
Expand Up @@ -423,18 +423,7 @@ private void applySelectedDrive() {
}

private static File resolveLocationForRoot(File root) {
File home = new File(System.getProperty("user.home", ""));
if (home.exists()) {
java.nio.file.Path homeRoot = home.toPath().getRoot();
if (homeRoot != null && homeRoot.equals(root.toPath())) {
File candidate = new File(home, App.DATADIRNAME);
if (candidate.exists() ? candidate.canWrite() : home.canWrite()) {
return home;
}
}
}
if (root.canRead() && root.canWrite()) return root;
return null;
return DriveChecker.resolveLocationForRoot(root);
}

/**
Expand Down
13 changes: 11 additions & 2 deletions jdm-core/src/main/java/jdiskmark/Gui.java
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,7 @@ public static void applyTheme(Theme t) {
mainFrame.getGraphPaletteMenu().setAllItemsEnabled(!t.hasLinkedPalette());
}
applyStartButtonStyle(t);
applyCancelButtonStyle(t);
applyIconToWindow(t);
if (batchPanel != null) batchPanel.refreshChartTheme();
}
Expand All @@ -416,6 +417,9 @@ public static void applyStartButtonStyle(Theme t) {
if (smartPanel != null) {
smartPanel.runButton.putClientProperty("FlatLaf.style", style.replace("font: bold ", "font: "));
}
if (batchPanel != null) {
batchPanel.applyStartButtonStyle(style);
}
}

/**
Expand All @@ -441,10 +445,14 @@ private static void applyIconToWindow(Theme t) {
* a theme-coherent "stop" colour; the default falls back to amber.
*/
public static void applyCancelButtonStyle(Theme t) {
if (controlPanel == null) return;
String style = t.definition().cancelButtonStyle();
if (style == null) style = ButtonStyles.CANCEL;
controlPanel.startButton.putClientProperty("FlatLaf.style", style);
if (controlPanel != null && "Cancel".equals(controlPanel.startButton.getText())) {
controlPanel.startButton.putClientProperty("FlatLaf.style", style);
}
if (batchPanel != null) {
batchPanel.applyCancelButtonStyle(style);
}
}


Expand Down Expand Up @@ -542,6 +550,7 @@ public static void init() {
// panel exists. Without this, the button always opens GitHub-green
// because BenchmarkControlPanel seeds DEFAULT_START in its constructor.
applyStartButtonStyle(theme);
applyCancelButtonStyle(theme);

// On macOS, replace the default system-provided About dialog (which shows
// the Java runtime info) with our own branded dialog.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ public String startButtonStyle() {
"#D86413",
"#A44606",
"#F47B2088",
"#ffffff"
"#1a1a1a"
);
}

Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<!-- CI-friendly version placeholder. Override with -Drevision=<value> at build time.
Default produces the current release build used by NetBeans and plain mvn.
CI workflows set this to a labelled version (e.g. 0.8.0-ci.d.47). -->
<revision>0.9.0-rc1</revision>
<revision>0.9.0-rc2</revision>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>25</maven.compiler.release>
<exec.mainClass>jdiskmark.App</exec.mainClass>
Expand Down
Loading