Commit 588376f1 authored by Daniel Vogel's avatar Daniel Vogel

responsiveness demos

parent 8bb2c7df
/**
* AbstractPrimesModel contains the infrastructure for calculating
* a set of primes, including the code to support MVC. Subclasses illustrate
* the right and wrong ways to support a long task in an interactive
* application.
*
* @author bwbecker
* @date 22-Mar-2012 factored out common code
*/
import java.util.Vector;
public abstract class AbstractModel {
protected int min;
protected int max;
protected Vector<Integer> primes = new Vector<Integer>();
protected Vector<IView> views = new Vector<IView>();
/** Instantiate a class to calculate all the prime numbers between min and max. */
protected AbstractModel(int min, int max) {
this.min = min;
this.max = max;
}
/** Determine whether n is prime. For the purposes of a long-running task
* demo, we DO NOT want to optimize this! */
protected final boolean isPrime(int n) {
for (int i = 2; i < n; i++) {
if (n % i == 0)
return false;
}
return true;
}
public abstract void calculatePrimes();
protected void addPrime(int i) {
this.primes.add(new Integer(i));
}
/** Get the list of prime numbers (returns a copy). */
public Vector<Integer> getPrimes() {
return new Vector<Integer>(this.primes);
}
/*
* Stuff to support the observers.
*/
/** Add a view/observer. */
public void addView(IView view) {
this.views.add(view);
view.update();
}
/** Remove a view/observer. */
public void removeView(IView view) {
this.views.remove(view);
}
/** Update all views/observers with details about a change to the model. */
protected void updateAllViews() {
for (IView view : this.views) {
view.update();
}
}
}
/**
* An abstract model class for views of the prime numbers model. It provides a
* start/stop button, a progress meter, and a text area in which the primes
* are viewed. Subclasses must override update() to update the interface
* appropriately, and registerControllers() to register a controller with the
* start/stop button.
*
* @author Byron Weber Becker
* @date 22-Mar-2012 factored out common code
*/
import java.awt.BorderLayout;
import java.awt.FlowLayout;
import javax.swing.*;
import java.awt.Font;
@SuppressWarnings("serial")
abstract public class AbstractView<M extends AbstractModel> extends JPanel implements IView {
protected M model;
// our UI widgets
protected JButton startStopButton = new JButton("Start");
protected JProgressBar progressBar = new JProgressBar(0, 100);
protected JList primesList = new JList();
public AbstractView(M m) {
this.model = m;
this.primesList.setFont(new Font(Font.SANS_SERIF, 3, 12));
this.layoutView();
this.registerControllers();
this.model.addView(this);
}
private void layoutView() {
this.setLayout(new BorderLayout());
JPanel north = new JPanel(new FlowLayout());
north.add(this.startStopButton);
north.add(this.progressBar);
this.add(north, BorderLayout.NORTH);
this.add(new JScrollPane(primesList), BorderLayout.CENTER);
}
abstract protected void registerControllers();
abstract public void update();
}
/*
* An application to calculate and display prime numbers
* up to 250,000. To demonstrate the wrong way to do it.
* The task takes so long that it freezes the user interface.
*
* @author Byron Weber Becker
* @date 19-June-2009
* @date 22-Mar-2012 factored out common code
*/
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.*;
public class Demo1 {
public static void main(String[] args) {
Model1 model = new Model1(1, 250000);
View1 view = new View1(model);
JFrame frame = new JFrame("Demo1: UI Thread Blocks");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(view);
frame.pack();
frame.setVisible(true);
}
}
@SuppressWarnings("serial")
class View1 extends AbstractView<Model1> {
public View1(Model1 model) {
super(model);
}
public void update() {
primesList.setListData(model.getPrimes());
}
protected void registerControllers() {
// Handle presses of the start button
this.startStopButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
startStopButton.setText("Stop");
model.calculatePrimes();
}
});
}
}
/**
* Find all the prime numbers between min and max in the event handling thread.
* No attempt to break up the job.
*/
class Model1 extends AbstractModel {
public Model1(int min, int max) {
super(min, max);
}
public void calculatePrimes() {
for (int i = this.min; i < this.max; i++) {
if (this.isPrime(i)) {
this.addPrime(i);
}
}
updateAllViews();
}
}
/*
* An application to calculate and display prime numbers
* up to 250,000. The task takes a long time, so we just
* do a little bit of the calculation at a time. If the
* calculation isn't done yet, invokeLater is used to
* let it run a little longer. All this processing
* happens in the Event dispatch thread, but a little
* at a time so the UI doesn't block.
*
* There is a hard-coded constant in FindPrimesFP that
* determines how long it can compute before quiting to
* give the UI a chance to update. It should probably be
* about 100 rather than 500.
*
* @author Byron Weber Becker
* @date 19-June-2009
* @date 22-Mar-2012 factored out common code
*/
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.*;
public class Demo2 {
public static void main(String[] args) {
Model2 model = new Model2(1, 250000);
View2 view = new View2(model);
JFrame frame = new JFrame("Demo2: UI Thread Works");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(view);
frame.pack();
frame.setVisible(true);
}
}
@SuppressWarnings("serial")
class View2 extends AbstractView<Model2> {
public View2(Model2 model) {
super(model);
}
public void update() {
progressBar.setValue(model.progress());
primesList.setListData(model.getPrimes());
if (model.isDone()) {
startStopButton.setText("Done");
startStopButton.setEnabled(false);
} else if (model.wasCancelled()) {
startStopButton.setText("Cancelled");
startStopButton.setEnabled(false);
} else if (model.isRunning()) {
startStopButton.setText("Stop");
}
}
protected void registerControllers() {
this.startStopButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (model.isRunning()) {
model.cancel();
} else {
model.calculatePrimes();
}
}
});
}
}
class Model2 extends AbstractModel {
private boolean cancelled = false;
private boolean running = false;
private int current; // progress so far
public Model2(int min, int max) {
super(min, max);
this.current = this.min;
}
/*
* Calculate some primes in the event thread. If necessary, schedule
* ourselves to calculate some more a little bit later.
*/
public void calculatePrimes() {
this.running = true;
SwingUtilities.invokeLater(new Runnable() {
public void run() {
calculateSomePrimes(100);
if (!cancelled && current <= max) {
calculatePrimes();
}
}
});
}
/**
* Calculate some prime numbers for duration ms.
* Quit when we run out of time or we're
* cancelled or we've reached the maximum prime to look for.
*/
private void calculateSomePrimes(long duration) {
long start = System.currentTimeMillis();
while (true) {
if (this.current > this.max) {
this.running = false;
updateAllViews();
return;
} else if (System.currentTimeMillis() - start >= duration) {
updateAllViews();
return;
} else if (isPrime(this.current)) {
this.addPrime(current);
}
current += 1;
}
}
public boolean isRunning() {
return this.running;
}
public boolean isDone() {
return this.current > this.max;
}
public boolean wasCancelled() {
return this.cancelled;
}
public void cancel() {
this.cancelled = true;
}
public int progress() {
return (this.current * 100) / (this.max - this.min);
}
}
/**
* An application to calculate and display prime numbers
* up to 250,000. The task takes a long time, so we put
* it in a thread that periodically checks if has been
* canceled. Updates to the UI are done with invokeLater.
*
* @author Byron Weber Becker
* @date 19-June-2009
* @date 22-Mar-2012 factored out common code
*/
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Vector;
import javax.swing.*;
public class Demo3 {
public static void main(String[] args) {
Model3 model = new Model3(1, 250000);
View3 view = new View3(model);
JFrame frame = new JFrame("Demo3: Separate Thread");
frame.getContentPane().add(view);
frame.pack();
frame.setVisible(true);
}
}
@SuppressWarnings("serial")
class View3 extends AbstractView<Model3> {
public View3(Model3 model) {
super(model);
}
public void update() {
progressBar.setValue(model.progress());
primesList.setListData(model.getPrimes());
if (model.isDone()) {
startStopButton.setText("Done");
startStopButton.setEnabled(false);
} else if (model.wasCancelled()) {
startStopButton.setText("Cancelled");
startStopButton.invalidate();
startStopButton.setEnabled(false);
} else if (model.isRunning()) {
startStopButton.setText("Stop");
}
}
protected void registerControllers() {
this.startStopButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (model.isRunning()) {
model.cancel();
} else {
model.calculatePrimes();
}
}
});
}
}
class Model3 extends AbstractModel {
private boolean cancelled = false;
private boolean running = false;
private int current;
public Model3(int min, int max) {
super(min, max);
this.current = this.min;
}
public void calculatePrimes() {
// create a new thread and do task there ...
new Thread() {
public void run() {
running = true;
long start = System.currentTimeMillis();
while (true) {
if (cancelled || current > max) {
running = false;
updateUI();
return;
} else if (isPrime(current)) {
addPrime(current);
}
current += 1;
// update UI every 100 ms
if (System.currentTimeMillis() - start >= 100) {
updateUI();
start = System.currentTimeMillis();
}
}
}
private void updateUI() {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
updateAllViews();
}
});
}
}.start();
}
// the synchronized keyword is needed to share Vector across two threads
protected synchronized void addPrime(int i) {
super.addPrime(i);
}
public synchronized Vector<Integer> getPrimes() {
return super.getPrimes();
}
public boolean isRunning() {
return this.running;
}
public boolean isDone() {
return this.current > this.max;
}
public boolean wasCancelled() {
return this.cancelled;
}
public void cancel() {
this.cancelled = true;
}
public int progress() {
return (this.current * 100) / (this.max - this.min);
}
}
/**
* Classes implement this interface to observe another object; that
* is, the update method will be called when something of significance
* happens in the observed object.
*/
public interface IView {
/**
* Something of significance happened in the observed object.
*/
public void update();
}
# super simple makefile
# call it using 'make NAME=name_of_code_file_without_extension'
# (assumes a .java extension)
NAME = "Main"
all:
@echo "Compiling..."
javac *.java
run: all
@echo "Running..."
java $(NAME)
clean:
rm -rf *.class
# Responsiveness Demos
* `Demo1.java` Wrong way to handle long tasks: task blocks UI thread.
* `Demo2.java` Better way: task is broken up into smaller subtasks and still run in the UI thread.
* `Demo3.java` Best way: task runs in a different thread, and communicates with the UI thread in a safe way
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment