3

I've written a drive wiping program designed to securely overwrite the free space of drives. Everything is working great at first, then over time the speed reduced dramatically. I have one 1TB drive which starts out at around 120MB/s then drops slowly to 70. At first I thought it was the drive, so I tested it on my RAID0 velociraptor drives, which got 160MB/s for almost half a minute before slowly dropping to around 110. It doesn't seem like just the cache filling up, because it takes a minute or so to fully slow down.

First, is the problem in the way that I'm writing data to the disk potentially, or is it actually normal function for HDDs in other languages as well?

Secondly, would I see any potential benefit from switching to NIO related to speed? Using the ISAAC wipe is multithreaded, so the bottleneck is really just the write speed it seems.

Lastly, maybe it could be in my speed calculations. But I left it very simple, so I don't see how that could be.

EDIT: (Some info)

Both are regular magnetic drives. The 1TB drive is a WD 7200rpm. The raid0 setup is two WD 10,000rpm. Running Windows 7 Ultimate.

java version "1.8.0_45". Java(TM) SE Runtime Environment (build 1.8.0_45-b15). Java HotSpot(TM) 64-Bit Server VM (build 25.45-b02, mixed mode).

You can test this running example: (cleaner.DriveCleaner)

package cleaner;

import java.awt.EventQueue;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.security.SecureRandom;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JProgressBar;
import javax.swing.text.NumberFormatter;

/**
 * @author Colby
 */
public class DriveCleaner extends javax.swing.JFrame {

    public DriveCleaner() {
        initComponents();
        refreshDrives();
    }

    protected static boolean running = false;
    private Thread worker;

    private void refreshDrives() {
        File[] roots = File.listRoots();
        drives.setModel(new DefaultComboBoxModel(roots));
    }

    @SuppressWarnings("unchecked")
    // <editor-fold defaultstate="collapsed" desc="Generated Code">                          
    private void initComponents() {

        buttonGroup1 = new javax.swing.ButtonGroup();
        jLabel1 = new javax.swing.JLabel();
        jSeparator1 = new javax.swing.JSeparator();
        drives = new javax.swing.JComboBox();
        normSelect = new javax.swing.JRadioButton();
        randSelect = new javax.swing.JRadioButton();
        jLabel2 = new javax.swing.JLabel();
        jLabel3 = new javax.swing.JLabel();
        passes = new javax.swing.JComboBox();
        jLabel4 = new javax.swing.JLabel();
        runButton = new javax.swing.JButton();
        jSeparator2 = new javax.swing.JSeparator();
        jSeparator3 = new javax.swing.JSeparator();
        progress = new javax.swing.JProgressBar();
        jMenuBar1 = new javax.swing.JMenuBar();
        jMenu1 = new javax.swing.JMenu();
        jMenuItem1 = new javax.swing.JMenuItem();

        setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
        setTitle("DriveCleaner V1.0");

        jLabel1.setFont(new java.awt.Font("Consolas", 2, 17)); // NOI18N
        jLabel1.setText("DriveCleaner");

        buttonGroup1.add(normSelect);
        normSelect.setText("Simple Clean");
        normSelect.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                normSelectActionPerformed(evt);
            }
        });

        buttonGroup1.add(randSelect);
        randSelect.setSelected(true);
        randSelect.setText("ISAAC 256 Clean");
        randSelect.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                randSelectActionPerformed(evt);
            }
        });

        jLabel2.setText("Drive:");

        jLabel3.setText("Passes:");

        passes.setModel(new javax.swing.DefaultComboBoxModel(new String[] { "1", "2", "4", "8", "16", "32", "64", "128" }));
        passes.setSelectedIndex(2);

        jLabel4.setText("Method:");

        runButton.setText("Clean");
        runButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                runButtonActionPerformed(evt);
            }
        });

        jSeparator2.setOrientation(javax.swing.SwingConstants.VERTICAL);

        progress.setString("");
        progress.setStringPainted(true);

        jMenu1.setText("File");

        jMenuItem1.setAccelerator(javax.swing.KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_F5, 0));
        jMenuItem1.setText("Refresh Drives");
        jMenuItem1.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jMenuItem1ActionPerformed(evt);
            }
        });
        jMenu1.add(jMenuItem1);

        jMenuBar1.add(jMenu1);

        setJMenuBar(jMenuBar1);

        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
        getContentPane().setLayout(layout);
        layout.setHorizontalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addComponent(jSeparator1, javax.swing.GroupLayout.Alignment.TRAILING)
            .addGroup(layout.createSequentialGroup()
                .addContainerGap()
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addComponent(jSeparator3)
                    .addGroup(layout.createSequentialGroup()
                        .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                            .addComponent(jLabel1)
                            .addGroup(layout.createSequentialGroup()
                                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                                    .addComponent(drives, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                                    .addComponent(jLabel2))
                                .addGap(18, 18, 18)
                                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                                    .addComponent(passes, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                                    .addComponent(jLabel3))
                                .addGap(18, 18, 18)
                                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                                    .addGroup(layout.createSequentialGroup()
                                        .addComponent(randSelect)
                                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                                        .addComponent(normSelect))
                                    .addComponent(jLabel4))
                                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 31, Short.MAX_VALUE)
                                .addComponent(jSeparator2, javax.swing.GroupLayout.PREFERRED_SIZE, 12, javax.swing.GroupLayout.PREFERRED_SIZE)))
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                        .addComponent(runButton, javax.swing.GroupLayout.PREFERRED_SIZE, 75, javax.swing.GroupLayout.PREFERRED_SIZE))
                    .addComponent(progress, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
                .addContainerGap())
        );
        layout.setVerticalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(layout.createSequentialGroup()
                .addContainerGap()
                .addComponent(jLabel1)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(jSeparator1, javax.swing.GroupLayout.PREFERRED_SIZE, 10, javax.swing.GroupLayout.PREFERRED_SIZE)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false)
                    .addGroup(layout.createSequentialGroup()
                        .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                            .addComponent(jLabel2)
                            .addComponent(jLabel3)
                            .addComponent(jLabel4))
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                        .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                            .addComponent(drives, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                            .addComponent(passes, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                            .addComponent(randSelect)
                            .addComponent(normSelect)))
                    .addComponent(jSeparator2)
                    .addComponent(runButton, javax.swing.GroupLayout.DEFAULT_SIZE, 43, Short.MAX_VALUE))
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(jSeparator3, javax.swing.GroupLayout.PREFERRED_SIZE, 10, javax.swing.GroupLayout.PREFERRED_SIZE)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(progress, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
        );

        pack();
    }// </editor-fold>                        

    private void runButtonActionPerformed(java.awt.event.ActionEvent evt) {                                          

        if (running) {

            runButton.setText("Halting");
            runButton.setEnabled(false);

            new Thread() {

                @Override
                public void run() {
                    try {
                        running = false;
                        worker.join();

                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    EventQueue.invokeLater(new Runnable() {

                        @Override
                        public void run() {
                            progress.setString("");
                            progress.setValue(0);

                            runButton.setEnabled(true);
                            runButton.setText("Clean");
                        }
                    });
                }
            }.start();

        } else {
            running = true;
            runButton.setText("Stop");
            worker = new Thread(new Runnable() {

                @Override
                public void run() {

                    try {
                        Wipe.wipe(progress, (File) drives.getSelectedItem(), Integer.parseInt((String) passes.getSelectedItem()), useRandomData);

                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
            worker.start();
        }
    }                                         

    private void jMenuItem1ActionPerformed(java.awt.event.ActionEvent evt) {                                           
        refreshDrives();
    }                                          

    private void randSelectActionPerformed(java.awt.event.ActionEvent evt) {                                           
        useRandomData = true;
    }                                          

    private void normSelectActionPerformed(java.awt.event.ActionEvent evt) {                                           
        useRandomData = false;
    }                                          

    protected static boolean useRandomData = true;

    public static void main(String args[]) {
        /* Set the Nimbus look and feel */
        //<editor-fold defaultstate="collapsed" desc=" Look and feel setting code (optional) ">
        /* If Nimbus (introduced in Java SE 6) is not available, stay with the default look and feel.
         * For details see http://download.oracle.com/javase/tutorial/uiswing/lookandfeel/plaf.html 
         */
        try {
            for (javax.swing.UIManager.LookAndFeelInfo info : javax.swing.UIManager.getInstalledLookAndFeels()) {
                if ("Nimbus".equals(info.getName())) {
                    javax.swing.UIManager.setLookAndFeel(info.getClassName());
                    break;
                }
            }
        } catch (ClassNotFoundException ex) {
            java.util.logging.Logger.getLogger(DriveCleaner.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
        } catch (InstantiationException ex) {
            java.util.logging.Logger.getLogger(DriveCleaner.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
        } catch (IllegalAccessException ex) {
            java.util.logging.Logger.getLogger(DriveCleaner.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
        } catch (javax.swing.UnsupportedLookAndFeelException ex) {
            java.util.logging.Logger.getLogger(DriveCleaner.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
        }
        //</editor-fold>

        /* Create and display the form */
        java.awt.EventQueue.invokeLater(new Runnable() {
            public void run() {
                new DriveCleaner().setVisible(true);
            }
        });
    }

    // Variables declaration - do not modify                     
    private javax.swing.ButtonGroup buttonGroup1;
    private javax.swing.JComboBox drives;
    private javax.swing.JLabel jLabel1;
    private javax.swing.JLabel jLabel2;
    private javax.swing.JLabel jLabel3;
    private javax.swing.JLabel jLabel4;
    private javax.swing.JMenu jMenu1;
    private javax.swing.JMenuBar jMenuBar1;
    private javax.swing.JMenuItem jMenuItem1;
    private javax.swing.JSeparator jSeparator1;
    private javax.swing.JSeparator jSeparator2;
    private javax.swing.JSeparator jSeparator3;
    private javax.swing.JRadioButton normSelect;
    private javax.swing.JComboBox passes;
    private javax.swing.JProgressBar progress;
    private javax.swing.JRadioButton randSelect;
    private javax.swing.JButton runButton;
    // End of variables declaration                   
}

class ISAAC {

    public ISAAC(int ai[]) {
        cryptArray = new int[256];
        keySetArray = new int[256];
        System.arraycopy(ai, 0, keySetArray, 0, ai.length);

        initializeKeySet();
    }

    public int getNextKey() {
        if (keyArrayIdx-- == 0) {
            generateNextKeySet();
            keyArrayIdx = 255;
        }
        return keySetArray[keyArrayIdx];
    }

    public void generateNextKeySet() {
        cryptVar2 += ++cryptVar3;
        for (int i = 0; i < 256; i++) {
            int j = cryptArray[i];
            if ((i & 3) == 0) {
                cryptVar1 ^= cryptVar1 << 13;
            } else if ((i & 3) == 1) {
                cryptVar1 ^= cryptVar1 >>> 6;
            } else if ((i & 3) == 2) {
                cryptVar1 ^= cryptVar1 << 2;
            } else if ((i & 3) == 3) {
                cryptVar1 ^= cryptVar1 >>> 16;
            }
            cryptVar1 += cryptArray[i + 128 & 0xff];
            int k;
            cryptArray[i] = k = cryptArray[(j & 0x3fc) >> 2] + cryptVar1 + cryptVar2;
            keySetArray[i] = cryptVar2 = cryptArray[(k >> 8 & 0x3fc) >> 2] + j;
        }
    }

    public void initializeKeySet() {
        int i1;
        int j1;
        int k1;
        int l1;
        int i2;
        int j2;
        int k2;
        int l = i1 = j1 = k1 = l1 = i2 = j2 = k2 = 0x9e3779b9;
        for (int i = 0; i < 4; i++) {
            l ^= i1 << 11;
            k1 += l;
            i1 += j1;
            i1 ^= j1 >>> 2;
            l1 += i1;
            j1 += k1;
            j1 ^= k1 << 8;
            i2 += j1;
            k1 += l1;
            k1 ^= l1 >>> 16;
            j2 += k1;
            l1 += i2;
            l1 ^= i2 << 10;
            k2 += l1;
            i2 += j2;
            i2 ^= j2 >>> 4;
            l += i2;
            j2 += k2;
            j2 ^= k2 << 8;
            i1 += j2;
            k2 += l;
            k2 ^= l >>> 9;
            j1 += k2;
            l += i1;
        }

        for (int j = 0; j < 256; j += 8) {
            l += keySetArray[j];
            i1 += keySetArray[j + 1];
            j1 += keySetArray[j + 2];
            k1 += keySetArray[j + 3];
            l1 += keySetArray[j + 4];
            i2 += keySetArray[j + 5];
            j2 += keySetArray[j + 6];
            k2 += keySetArray[j + 7];
            l ^= i1 << 11;
            k1 += l;
            i1 += j1;
            i1 ^= j1 >>> 2;
            l1 += i1;
            j1 += k1;
            j1 ^= k1 << 8;
            i2 += j1;
            k1 += l1;
            k1 ^= l1 >>> 16;
            j2 += k1;
            l1 += i2;
            l1 ^= i2 << 10;
            k2 += l1;
            i2 += j2;
            i2 ^= j2 >>> 4;
            l += i2;
            j2 += k2;
            j2 ^= k2 << 8;
            i1 += j2;
            k2 += l;
            k2 ^= l >>> 9;
            j1 += k2;
            l += i1;
            cryptArray[j] = l;
            cryptArray[j + 1] = i1;
            cryptArray[j + 2] = j1;
            cryptArray[j + 3] = k1;
            cryptArray[j + 4] = l1;
            cryptArray[j + 5] = i2;
            cryptArray[j + 6] = j2;
            cryptArray[j + 7] = k2;
        }

        for (int k = 0; k < 256; k += 8) {
            l += cryptArray[k];
            i1 += cryptArray[k + 1];
            j1 += cryptArray[k + 2];
            k1 += cryptArray[k + 3];
            l1 += cryptArray[k + 4];
            i2 += cryptArray[k + 5];
            j2 += cryptArray[k + 6];
            k2 += cryptArray[k + 7];
            l ^= i1 << 11;
            k1 += l;
            i1 += j1;
            i1 ^= j1 >>> 2;
            l1 += i1;
            j1 += k1;
            j1 ^= k1 << 8;
            i2 += j1;
            k1 += l1;
            k1 ^= l1 >>> 16;
            j2 += k1;
            l1 += i2;
            l1 ^= i2 << 10;
            k2 += l1;
            i2 += j2;
            i2 ^= j2 >>> 4;
            l += i2;
            j2 += k2;
            j2 ^= k2 << 8;
            i1 += j2;
            k2 += l;
            k2 ^= l >>> 9;
            j1 += k2;
            l += i1;
            cryptArray[k] = l;
            cryptArray[k + 1] = i1;
            cryptArray[k + 2] = j1;
            cryptArray[k + 3] = k1;
            cryptArray[k + 4] = l1;
            cryptArray[k + 5] = i2;
            cryptArray[k + 6] = j2;
            cryptArray[k + 7] = k2;
        }

        generateNextKeySet();
        keyArrayIdx = 256;
    }
    public int keyArrayIdx;
    public int keySetArray[];
    public int cryptArray[];
    public int cryptVar1;
    public int cryptVar2;
    public int cryptVar3;
}

class Wipe {

    private static BlockingQueue<byte[]> buffers, randata;

    private static class SecureDataCreator implements Runnable {

        @Override
        public void run() {
            try {
                SecureRandom seeder = new SecureRandom();
                ISAAC rand = new ISAAC(new int[]{seeder.nextInt(), seeder.nextInt(), seeder.nextInt(), seeder.nextInt()});
                do {
                    byte[] next = buffers.take();
                    for (int i = 0; i < next.length; i++) {
                        next[i] = (byte) rand.getNextKey();
                    }
                    randata.add(next);

                } while (true);

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void wipe(JProgressBar prog, File drive, int numPasses, boolean random) throws IOException, InterruptedException, ParseException {

        NumberFormat format = NumberFormat.getPercentInstance();
        format.setMinimumFractionDigits(2);
        NumberFormatter formatter = new NumberFormatter(format);

        prog.setValue(0);
        prog.setString("Opening file handle");

        File wipeFile = new File(drive, "wipefile.dat");
        wipeFile.deleteOnExit();

        try (RandomAccessFile raf = new RandomAccessFile(wipeFile, "rw")) {

            try {
                while (wipeFile.getFreeSpace() > raf.length()) {
                    try {
                        raf.setLength(drive.getFreeSpace());

                    } catch (IOException e) {
                        raf.setLength(0);
                    }
                }

                int dataSize = 1024 * 1024 * 32;
                int numCores = Runtime.getRuntime().availableProcessors();

                boolean needWorkers = buffers == null && random;

                if (needWorkers) {
                    for (int i = 0; i < numCores; i++) {
                        Thread worker = new Thread(new SecureDataCreator());
                        worker.setPriority(Thread.MIN_PRIORITY);
                        worker.start();
                    }

                    buffers = new ArrayBlockingQueue<>(numCores + 1);
                    randata = new ArrayBlockingQueue<>(numCores + 1);

                    for (int i = 0; i < numCores + 1; i++) {
                        buffers.add(new byte[dataSize]);
                    }
                }

                long startTime = System.nanoTime();
                byte[] data = random ? null : new byte[dataSize];
                for (int pass = 0; DriveCleaner.running && (pass < numPasses); pass++) {
                    raf.seek(0);

                    do {
                        long writeLen = dataSize;
                        if (raf.getFilePointer() + writeLen > raf.length()) {
                            writeLen = raf.length() - raf.getFilePointer();
                        }

                        if (random) {
                            data = randata.take();
                        }
                        raf.write(data, 0, (int) writeLen);
                        if (random) {
                            buffers.add(data);
                        }

                        double total = numPasses * raf.length();
                        double done = (pass * (raf.length() - 1)) + raf.getFilePointer();
                        float percent = (float) (done / total);

                        double elapsed = (System.nanoTime() - startTime) / (1000000D * 1000D);
                        float bytesPerSec = (float) (done / elapsed) / (1024F * 1024F);

                        prog.setValue((int) percent);
                        prog.setString("Cleaning  " + drive + ".  Pass #" + pass + "/" + numPasses + ".  "
                                + formatter.valueToString(new Float(percent))
                                + "  @" + (int) bytesPerSec + "mBps");

                    } while (raf.getFilePointer() < raf.length() && DriveCleaner.running);
                    prog.setString("Complete.");
                }
                System.out.println("done");

            } finally {
                raf.setLength(0);
            }

        } finally {
            wipeFile.delete();
        }
    }
}
Colby
  • 452
  • 4
  • 19
  • 1
    90% of the code is GUI and an implementation of isaac. The only relevant code is in the clean.Wipe, however it is critical to the question to include all of the code. – Colby Jun 26 '15 at 01:07
  • Do you mean 'consecutive' writes? – user207421 Jun 26 '15 at 03:34
  • 2
    use visualvm to profile this; the code looks like it is doing a lot of different things so it is not a given that it is actually bottlenecked on IO as you seem to be assuming. Visualvm will tell you where it spends its time. – Jilles van Gurp Jun 26 '15 at 13:10

1 Answers1

3

I have a few suggestions to isolate the source of the problem, as there is not yet any indication whether it is occuring from the JVM, OS, or hardware:

  • Your random number generator may be running out of entropy. As a test, use zeroes instead of calling random.
  • Measure the JVM garbage collection times. It is possible that the construction of many temporary array objects is causing GC pauses.
  • Try running your Java program on Linux (e.g., from a bootable USB or CD) to see if the same issue occurs.
  • Try a different JVM implementation (e.g. OpenJDK vs. Oracle JDK) to see if the same issue occurs.
Community
  • 1
  • 1
Parker
  • 7,244
  • 12
  • 70
  • 92
  • 2
    I want to add, that a slowdown on writes may be normal when the disk gets full and it becomes more and more difficult for the filesystem to find empty blocks (This might or might not be a reason depending on the underlying file system). Otherwise, very good suggestions to track down the problem -> upvote! – loonytune Jun 26 '15 at 13:05
  • 2
    Hello, i have already tried using only zero data. Also, i do not creare new byte arrays. Instead, there is one per core +1 and they are passed between workers and the writer using a queue system. Garbage collection has also been ruled out already. The issue appears to be the same on both a linux virtual box and openjdk on windows. – Colby Jun 26 '15 at 18:46
  • also @loonytune the disks are both less than 50% full also. – Colby Jun 26 '15 at 18:48
  • but you have to fill them up for your program to work, right? – loonytune Jun 28 '15 at 08:05