Help with converting BigDecimal to double.

This is a inputstream that has some statistics built in like bytes/ps.

I think its a performance drain because of bigdecimal churn. The algoritm to estimate the velocity is also the most simple i could find. I did find more smooth algorithms, but i couldn’t get them to work, and i only could get the algorithm not to jump around by using bigdecimals.

I’m asking for help on this, since it combines both areas that i’m a absolute noob on, floating point errors, timers and concurrency.

Main class (bigdecimal churn)


package downloads;

import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;

public class StatisticsInputStream extends InputStream {

    private long totalBytesRead;
    private long startTimeMillis;
    private InputStream delegate;
    private volatile long expectedSize;
    private BigDecimal bytesPerMillisecond = BigDecimal.ZERO;
    private BigDecimal thousand = BigDecimal.valueOf(1000);
    private BigDecimal timeRemaing = BigDecimal.valueOf(-1);


    public StatisticsInputStream(InputStream delegate, long expectedSize) {
        super();
        this.expectedSize = expectedSize;
        this.delegate = delegate;
    }

    @Override
    public long skip(long n) throws IOException {
        return delegate.skip(n);
    }

    @Override
    public synchronized void reset() throws IOException {
        delegate.reset();
    }

    @Override
    public int read(byte[] b, int off, int len) throws IOException {
        if (startTimeMillis == 0) {
            startTimeMillis = System.currentTimeMillis();
        }
        int read = delegate.read(b, off, len);
        if (read != -1) {
            updateMeasure(read);
        }
        return read;
    }

    @Override
    public int read(byte[] b) throws IOException {
        if (startTimeMillis == 0) {
            startTimeMillis = System.currentTimeMillis();
        }
        int read = delegate.read(b);
        if (read != -1) {
            updateMeasure(read);
        }
        return read;
    }

    public int read() throws IOException {
        if (startTimeMillis == 0) {
            startTimeMillis = System.currentTimeMillis();
        }
        int byteRead = delegate.read();
        if (byteRead != -1) {
            updateMeasure(1);
        }
        return byteRead;
    }

    @Override
    public boolean markSupported() {
        return delegate.markSupported();
    }

    @Override
    public synchronized void mark(int readlimit) {
        delegate.mark(readlimit);
    }

    @Override
    public void close() throws IOException {
        delegate.close();
    }

    @Override
    public int available() throws IOException {
        return delegate.available();
    }

    protected void updateMeasure(int bytesRead) {
        totalBytesRead += bytesRead;
        long now = System.currentTimeMillis();
        long delta_T = now - startTimeMillis;
        //You'd think negative intervals couldn't happen. You'd be wrong.
        if (delta_T <= 0) {
            return;
        }
        //(time elapsed/dl'ed size)*size left
        BigDecimal remainingBytes = BigDecimal.valueOf(getExpectedSize() - getBytesRead().longValue());
        BigDecimal timeElapsed = BigDecimal.valueOf(delta_T);
        BigDecimal bytes = BigDecimal.valueOf(totalBytesRead);
        timeRemaing = timeElapsed.divide(bytes, 5, RoundingMode.HALF_UP).multiply(remainingBytes);
    }

    public BigDecimal getBytesPerSecond() {
        return bytesPerMillisecond.multiply(thousand);
    }

    public BigInteger getBytesRead() {
        return BigInteger.valueOf(totalBytesRead);
    }

    public BigDecimal getTimeRemaining() {
        return timeRemaing;
    }

    /**
     * @return the expectedSize
     */
    public long getExpectedSize() {
        return expectedSize;
    }

    /**
     * @param expectedSize the expectedSize to set
     */
    protected void setExpectedSize(long expectedSize) {
        this.expectedSize = expectedSize;
    }
}

And the class that uses it - the main problem with this class is the painfully long time until
the observer gets notified the first time (and i suspect getBytesRead() and getBytesPerSecond() churn).
A way to remove the synchronization there would be great also. I just slapdashed synchronize allover the place without
thinking it through. :stuck_out_tongue:


package downloads;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLDecoder;
import java.util.Observable;
import java.util.Timer;
import java.util.TimerTask;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.SwingUtilities;

public final class Download extends Observable {

    private BigInteger size;
    private File localFile;
    private volatile StatisticsInputStream stream;
    private final URL url;
    private boolean cancelled = false, started = false;
    private final String mimeType;

    public Download(URL downloadURL, File localFile, long expectedSizeInBytes, String mimeType) throws DownloadException {
        super();
        this.size = BigInteger.valueOf(expectedSizeInBytes);
        this.localFile = localFile;
        this.url = downloadURL;
        this.mimeType = mimeType;
    }

    /**
     * reate a download with a unknown mimeType
     * @param downloadURL
     * @param localFile
     * @param expectedSizeInBytes
     * @throws DownloadException
     */
    public Download(URL downloadURL, File localFile, long expectedSizeInBytes) throws DownloadException {
        this(downloadURL, localFile, -1, null);
    }

    /**
     * Create a download with a unknown size and a unknown mimeType
     * @param downloadURL
     * @param localFile
     * @throws DownloadException
     */
    public Download(URL downloadURL, File localFile) throws DownloadException {
        this(downloadURL, localFile, -1, null);
    }

    /**
     * The local file where the download is being put
     * @return
     */
    public File getDownloadedFile() {
        return localFile;
    }

    /**
     * @return the mimeType null if unknown
     */
    public String getMimeType() {
        return mimeType;
    }

    /**
     * @return the expected download size or -1
     * if not known.
     */
    public synchronized BigInteger getExpectedSize() {
        return size;
    }

    /**
     * The origin of the download
     * @return
     */
    public URL getURL() {
        return url;
    }

    /**
     * Cancel the download, only cancels if it already started
     * @return if it was cancelled
     */
    public synchronized boolean cancel() {
        if (!isDone()) {
            if (started) {
                cancelled = true;
                try {
                    stream.close();
                } catch (Exception ex) {
                    //if failed closing, bah
                }
            }
            setChanged();
            notifyObservers();
            return !isDone();
        }
        setChanged();
        notifyObservers();
        return false;
    }

    /**
     * Retry the download (only if canceled)
     * @return
     */
    public synchronized boolean retry() {
        if (!cancelled) {
            return false;
        }
        try {
            started = false;
            start();
            cancelled = false;
        } catch (IOException ex) {
            setChanged();
            notifyObservers();
            Logger.getLogger(Download.class.getName()).log(Level.SEVERE, "Couldn't start download", ex);
            return false;
        }
        setChanged();
        notifyObservers();
        return true;
    }

    /**
     * Returns if the download is done.
     * @return
     */
    public synchronized boolean isDone() {
        //if size is unknown it will be set at the end of the download
        return getBytesRead().longValue() == size.longValue();
    }

    /**
     * Returns if the download is cancelled
     * @return
     */
    public synchronized boolean isCancelled() {
        return cancelled;
    }

    /**
     * Percentage of the download from 0 to 100
     * Only valid if getExpectedSize != -1
     * @return
     */
    public synchronized BigInteger getProgress() {
        BigInteger hundred = BigInteger.valueOf(100);
        return getBytesRead().multiply(hundred).divide(getExpectedSize());
    }

    /**
     * Name of the downloaded url
     * @return
     */
    public String getName() {
        String realpath;
        try {
            realpath = URLDecoder.decode(url.getPath(), "UTF-8");
        } catch (UnsupportedEncodingException ex) {
            throw new AssertionError("UTF-8 is always a supported encoding.");
        }
        //avoid directories path seperator indexes.
        int index = realpath.lastIndexOf('/');

        if (index == (realpath.length() - 1)) {//directory
            int index2 = realpath.lastIndexOf('/', realpath.length() - 2);
            return realpath.substring(index2 + 1, index);
        } else {//file
            return realpath.substring(index + 1);
        }
    }

    /**
     * The number of bytes read so far from the download
     * @return
     */
    public synchronized BigInteger getBytesRead() {
        return (stream == null) ? BigInteger.ZERO : stream.getBytesRead();
    }

    /**
     * The number of bytes per second
     * @return
     */
    public synchronized BigDecimal getBytesPerSecond() {
        return (stream == null) ? BigDecimal.ZERO : stream.getBytesPerSecond();
    }

    /**
     * The time remaining
     * @return
     */
    public synchronized BigDecimal getTimeRemaining() {

        return (stream == null) ? BigDecimal.ZERO : stream.getTimeRemaining();
    }
    private static Timer observersTimedNotify = new Timer(true);

    /**
     * Start the download
     * @throws IOException
     */
    public synchronized void start() throws IOException {
        if (started) {
            throw new IllegalStateException("Can't start a download twice, try retry");
        }
        started = true;

        final URLConnection conn = url.openConnection();
        //conn.setConnectTimeout(1500);
        if (size.longValue() == -1) {
            //may or may not work
            size = BigInteger.valueOf(conn.getContentLength());
        }

        //start the stream in the same thread that starts the download(for visibility)
        try {
            stream = new ObservableStatisticsInputStream(conn.getInputStream(), Download.this.getExpectedSize().longValue());
        } catch (IOException ex) {
            cancel();
            return;
        } finally {
            setChanged();
            notifyObservers();
        }

        Runnable task = new Runnable() {

            public void run() {
                try {
                    writeInto(stream, true, new FileOutputStream(localFile), 1024, true);
                    //not finished prematurely
                    if (!isCancelled()) {
                        size = stream.getBytesRead();
                    }
                } catch (IOException ex) {
                    Logger.getLogger(Download.class.getName()).log(Level.SEVERE, "Couldn't start download", ex);
                    cancel();
                } finally {
                    setChanged();
                    notifyObservers();
                }
            }
        };

        if (SwingUtilities.isEventDispatchThread()) {
            new Thread(task).start();
        } else {
            task.run();
        }

        class RecursiveNotify extends TimerTask {

            @Override
            public void run() {
                notifyObservers();
                if (!isDone() && !isCancelled()) {
                    observersTimedNotify.schedule(new RecursiveNotify(), 100);
                }
            }
        }

        observersTimedNotify.schedule(new RecursiveNotify(), 100);
    }

    private static void writeInto(final InputStream input, boolean closeInput, final OutputStream output, final int bufferSize, boolean closeOutput) throws IOException {
        final byte[] buffer = new byte[bufferSize];
        int n = 0;
        try {
            while (-1 != (n = input.read(buffer))) {
                output.write(buffer, 0, n);
            }
        } finally {
            if (closeInput && input != null) {
                input.close();
            }
            if (closeOutput && output != null) {
                output.close();
            }
        }
    }

    private final class ObservableStatisticsInputStream extends StatisticsInputStream {

        public ObservableStatisticsInputStream(InputStream delegate, long expectedSize) {
            super(delegate, expectedSize);
        }

        @Override
        protected void updateMeasure(int bytesRead) {
            super.updateMeasure(bytesRead);
            setChanged();
        }
    }
}

[quote=“i30817,post:1,topic:34321”]
Floating point errors: just don’t do any subtraction in floats. The number of bytes read is an int or a long; the time elapsed is an int or a long; cast to double for a division when you query.
Concurrency: why are there concurrency issues? I assume you’re not calling read() from multiple threads.