package gdsc.smlm.ij.plugins; /*----------------------------------------------------------------------------- * GDSC Plugins for ImageJ * * Copyright (C) 2017 Alex Herbert * Genome Damage and Stability Centre * University of Sussex, UK * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. *---------------------------------------------------------------------------*/ import java.util.ArrayList; import java.util.Arrays; /** * Allow processing work in stages, repeating only the stages necessary to render new results given changes to settings. * This class is designed to be used to allow live display of results upon settings changes by running the analysis on * worker threads. * * @author Alex Herbert */ public class Workflow<S, R> { /** * Default delay (in milliseconds) to use for dialog previews */ public static final long DELAY = 500; private class Work { public final long timeout; public final S settings; public final R results; Work(long time, S settings, R results) { if (settings == null) throw new NullPointerException("Settings cannot be null"); this.timeout = time; this.settings = settings; this.results = results; } Work(S settings, R results) { this(0, settings, results); } } /** * Allow work to be added to a FIFO stack in a synchronised manner */ private class WorkStack { // We only support a stack size of 1 private Work work = null; synchronized void setWork(Work work) { this.work = work; } synchronized void addWork(Work work) { this.work = work; this.notify(); } @SuppressWarnings("unused") synchronized void close() { this.work = null; this.notify(); } synchronized Work getWork() { Work work = this.work; this.work = null; return work; } boolean isEmpty() { return work == null; } } public class RunnableWorker implements Runnable { private final WorkflowWorker<S, R> worker; private boolean running = true; private Work lastWork = null; private Work result; private WorkStack inbox; private Object[] outbox; RunnableWorker(WorkflowWorker<S, R> worker) { this.worker = worker; } @SuppressWarnings("unchecked") public void run() { // Note: We check the condition for loop termination within the loop while (true) { try { Work work = null; synchronized (inbox) { if (inbox.isEmpty()) { debug("Inbox empty, waiting ..."); inbox.wait(); } work = inbox.getWork(); if (work != null) debug(" Found work"); } if (work == null) { debug(" No work, stopping"); break; } // Delay processing the work. Allows the work to be updated before we process it. if (work.timeout != 0) { debug(" Checking delay"); long timeout = work.timeout; while (System.currentTimeMillis() < timeout) { debug(" Delaying"); Thread.sleep(50); // Assume new work can be added to the inbox. Here we are peaking at the inbox // so we do not take ownership with synchronized if (inbox.work != null) timeout = inbox.work.timeout; } // If we intend to modify the inbox then we should take ownership synchronized (inbox) { if (!inbox.isEmpty()) { work = inbox.getWork(); debug(" Found updated work"); } } } if (!equals(work, lastWork)) { // Create a new result debug(" Creating new result"); R results = worker.createResults(work.settings, work.results); result = new Work(work.settings, results); } else { // Pass through the new settings with the existing results debug(" Updating existing result"); result = new Work(work.settings, result.results); } lastWork = work; // Add the result to the output if (outbox != null) { debug(" Posting result"); for (int i = outbox.length; i-- > 0;) ((WorkStack) outbox[i]).addWork(result); } } catch (InterruptedException e) { debug(" Interrupted, stopping"); break; } if (!running) { debug(" Shutdown"); break; } } } private void debug(String msg) { if (debug) System.out.println(worker.getClass().getSimpleName() + msg); } private boolean equals(Work work, Work lastWork) { if (lastWork == null) return false; // We must selectively compare this as not all settings changes matter. if (compareNulls(work.settings, lastWork.settings)) return false; if (!worker.equalSettings(work.settings, lastWork.settings)) return false; // We can compare these here using object references. // Any new results passed in will trigger equals to fail. boolean result = worker.equalResults(work.results, lastWork.results); if (!result) worker.newResults(); return result; } private boolean compareNulls(Object o1, Object o2) { if (o1 == null) return o2 != null; return o2 == null; } } private WorkStack inputStack = new WorkStack(); private ArrayList<Thread> threads; private ArrayList<RunnableWorker> workers = new ArrayList<RunnableWorker>(); private long delay = 0; /** The debug flag. Set to true to allow print statements during operation. */ public boolean debug = false; /** * Adds the worker. Connect the inbox to the previous worker outbox, or the primary input if the previous is null. * * @param worker * the worker * @return the worker id */ public int add(WorkflowWorker<S, R> worker) { return add(worker, -1); } /** * Adds the worker. Connect the inbox to the previous worker outbox, or the primary input if the previous is null. * <p> * Use this method to add workers that can operate in parallel on the output from a previous worker. * * @param worker * the worker * @param previous * the previous worker id * @return the worker id */ public int add(WorkflowWorker<S, R> worker, int previous) { if (previous <= 0 || previous > workers.size()) return addToChain(worker); else return addToChain(worker, workers.get(previous - 1)); } /** * Adds the worker. Connect the inbox to the previous worker outbox, or the primary input. * * @param worker * the worker * @return the worker id */ private int addToChain(WorkflowWorker<S, R> worker) { if (workers.isEmpty()) { return addToChain(worker, null); } else { // Chain together RunnableWorker previous = workers.get(workers.size() - 1); return addToChain(worker, previous); } } /** * Adds the worker. Connect the inbox to the previous worker outbox, or the primary input. * * @param worker * the worker * @param previous * the previous worker from which to take work * @return the worker id */ @SuppressWarnings("unchecked") private int addToChain(WorkflowWorker<S, R> inputWorker, RunnableWorker previous) { RunnableWorker worker = new RunnableWorker(inputWorker); if (previous == null) { // Take the primary input worker.inbox = inputStack; } else { // Chain together int size = (previous.outbox == null) ? 0 : previous.outbox.length; if (size == 0) previous.outbox = new Object[1]; else previous.outbox = Arrays.copyOf(previous.outbox, size + 1); previous.outbox[size] = new WorkStack(); worker.inbox = (WorkStack) previous.outbox[size]; } workers.add(worker); return workers.size(); } public synchronized void start() { shutdown(true); threads = startWorkers(workers); } public synchronized void shutdown(boolean now) { if (threads != null) { finishWorkers(workers, threads, now); threads = null; } } private ArrayList<Thread> startWorkers(ArrayList<RunnableWorker> workers) { ArrayList<Thread> threads = new ArrayList<Thread>(); for (RunnableWorker w : workers) { Thread t = new Thread(w); w.running = true; t.setDaemon(true); t.start(); threads.add(t); } return threads; } private void finishWorkers(ArrayList<RunnableWorker> workers, ArrayList<Thread> threads, boolean now) { // Finish work for (int i = 0; i < threads.size(); i++) { Thread t = threads.get(i); RunnableWorker w = workers.get(i); if (now) { // Stop immediately any running worker try { t.interrupt(); } catch (SecurityException e) { // We should have permission to interrupt this thread. e.printStackTrace(); } } else { // Stop after the current work in the inbox w.running = false; // Notify a workers waiting on the inbox. // Q. How to check if the worker is sleeping? synchronized (w.inbox) { w.inbox.notify(); } // Leave to finish their current work try { t.join(0); } catch (InterruptedException e) { } } } } /** * Add the work settings into the workflow queue and run. * * @param settings * the settings */ public void run(S settings) { run(settings, null); } /** * Add the work settings into the workflow queue and run. * * @param settings * the settings * @param results * the results */ public void run(S settings, R results) { inputStack.addWork(new Work(getTimeout(), settings, results)); } /** * Stage the work settings into the workflow queue but do not run. * * @param settings * the settings */ public void stage(S settings) { stage(settings, null); } /** * Stage the work settings into the workflow queue but do not run. * * @param settings * the settings * @param results * the results */ public void stage(S settings, R results) { inputStack.setWork(new Work(getTimeout(), settings, results)); } /** * Checks if work is staged. * * @return true, if is staged */ public boolean isStaged() { return !inputStack.isEmpty(); } /** * Run the staged work. */ public void runStaged() { inputStack.addWork(inputStack.work); } /** * Gets the delay to allow before running new work. * * @return the delay */ public long getDelay() { return delay; } /** * Sets the delay to allow before running new work. All work is added to the queue with a timeout equals to the * current system time plus the delay. If additional work is added before the delay elapses then the * preceding work is ignored and the timeout reset. * * @param delay * the new delay */ public void setDelay(long delay) { this.delay = Math.max(0, delay); } private long getTimeout() { if (delay == 0) return 0; return System.currentTimeMillis() + delay; } /** * Start preview mode. This sets the delay to the default delay time (see {@link #DELAY}). It should be called when * all new work should respect the timeout delay. */ public void startPreview() { setDelay(DELAY); } /** * Stop preview. This sets the delay to zero. */ public void stopPreview() { setDelay(0); } }