001/**
002 * Copyright (c) 2011, The University of Southampton and the individual contributors.
003 * All rights reserved.
004 *
005 * Redistribution and use in source and binary forms, with or without modification,
006 * are permitted provided that the following conditions are met:
007 *
008 *   *  Redistributions of source code must retain the above copyright notice,
009 *      this list of conditions and the following disclaimer.
010 *
011 *   *  Redistributions in binary form must reproduce the above copyright notice,
012 *      this list of conditions and the following disclaimer in the documentation
013 *      and/or other materials provided with the distribution.
014 *
015 *   *  Neither the name of the University of Southampton nor the names of its
016 *      contributors may be used to endorse or promote products derived from this
017 *      software without specific prior written permission.
018 *
019 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
020 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
021 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
022 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
023 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
024 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
025 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
026 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
027 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
028 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
029 */
030package org.openimaj.tools.globalfeature;
031
032import java.io.File;
033import java.io.IOException;
034import java.lang.reflect.Method;
035import java.net.MalformedURLException;
036import java.net.URL;
037import java.net.URLClassLoader;
038import java.util.ArrayList;
039import java.util.List;
040import java.util.concurrent.Callable;
041import java.util.concurrent.ExecutorService;
042import java.util.concurrent.Executors;
043
044import org.kohsuke.args4j.CmdLineException;
045import org.kohsuke.args4j.CmdLineParser;
046import org.kohsuke.args4j.Option;
047
048/**
049 * A tool for running other tools in parallel, in a similar manner to a UNIX
050 * Makefile.
051 * 
052 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
053 * 
054 */
055public class ParallelExecutor {
056        @Option(name = "--input", aliases = "-i", usage = "input directory", required = true)
057        private File inputBase;
058
059        @Option(name = "--output", aliases = "-o", usage = "output directory", required = true)
060        private File outputBase;
061
062        @Option(name = "--output-ext", aliases = "-e", usage = "output extension", required = true)
063        private File outputExt;
064
065        @Option(name = "--input-regex", aliases = "-r", usage = "input regex")
066        private String inputRegex;
067
068        @Option(name = "--class", aliases = "-c", usage = "class to run", required = true)
069        private String commandClass;
070
071        @Option(name = "--args", aliases = "-a", usage = "arguments to pass", required = true)
072        private String commandArgs;
073
074        @Option(name = "--force", aliases = "-f", usage = "force regenerate")
075        private boolean force = false;
076
077        @Option(name = "-j", usage = "n paralled jobs", required = false)
078        private int njobs = 1;
079
080        @Option(name = "--timing", aliases = "-t", usage = "print timing information")
081        private boolean timing = false;
082
083        @Option(name = "--verbose", aliases = "-v", usage = "print timing information")
084        private boolean verbose = false;
085
086        // private TLongObjectHashMap<URLClassLoader> classLoaders = new
087        // TLongObjectHashMap<URLClassLoader>();
088
089        synchronized Class<?> loadClass(String clzName) throws ClassNotFoundException {
090                // long id = Thread.currentThread().getId();
091                URLClassLoader tmp;
092
093                // if (classLoaders.containsKey(id)) {
094                // tmp = classLoaders.get(id);
095                // } else {
096                tmp = new URLClassLoader(new URL[] { getClassPath() }) {
097                        @Override
098                        public synchronized Class<?> loadClass(String name) throws ClassNotFoundException {
099                                Class<?> cl = null;
100                                try {
101                                        cl = findLoadedClass(name);
102                                        if (cl != null)
103                                                return cl;
104                                        cl = findClass(name);
105                                } catch (final ClassNotFoundException e) {
106                                        cl = super.loadClass(name);
107                                }
108
109                                return cl;
110                        }
111                };
112
113                // classLoaders.put(id, tmp);
114                // }
115
116                return tmp.loadClass(clzName);
117        }
118
119        private static URL getClassPath() {
120                final String resName = ParallelExecutor.class.getName().replace('.', '/') + ".class";
121                final String loc = ParallelExecutor.class.getClassLoader().getResource(resName).toExternalForm();
122                URL cp;
123                try {
124                        cp = new URL(loc.substring(0, loc.length() - resName.length()));
125                } catch (final MalformedURLException e) {
126                        throw new RuntimeException(e);
127                }
128                return cp;
129        }
130
131        class Job implements Callable<Boolean> {
132                File file;
133
134                public Job(File file) {
135                        this.file = file;
136                }
137
138                @Override
139                public Boolean call() throws Exception {
140                        try {
141                                Class<?> clz = null;
142                                try {
143                                        clz = loadClass(commandClass);
144                                        // clz = Class.forName(commandClass);
145                                } catch (final ClassNotFoundException e) {
146                                        clz = loadClass("org.openimaj.tools.globalfeature." + commandClass);
147                                        // clz =
148                                        // Class.forName("uk.ac.soton.ecs.jsh2."+commandClass);
149                                }
150
151                                final Method m = clz.getMethod("main", String[].class);
152
153                                final File output = getOutput(file);
154                                output.getParentFile().mkdirs();
155
156                                String theCommandArgs = commandArgs.replace("__IN__", file.getAbsolutePath());
157                                theCommandArgs = theCommandArgs.replace("__OUT__", output.getAbsolutePath());
158
159                                if (verbose)
160                                        System.err.println("java " + commandClass + " " + theCommandArgs);
161
162                                m.invoke(null, new Object[] { theCommandArgs.split(" ") });
163                        } catch (final Throwable e) {
164                                e.printStackTrace();
165                                return false;
166                        }
167
168                        return true;
169                }
170        }
171
172        void execute() throws InterruptedException {
173                final double t1 = System.currentTimeMillis();
174                final List<File> inputFiles = getInputs();
175                final double t2 = System.currentTimeMillis();
176                if (timing) {
177                        System.out.println("Input files:\t" + inputFiles.size());
178                        System.out.println("Search time:\t" + ((t2 - t1) / 1000.0) + " secs");
179                }
180
181                final List<Job> jobs = new ArrayList<Job>();
182
183                for (final File f : inputFiles)
184                        jobs.add(new Job(f));
185                final double t3 = System.currentTimeMillis();
186                if (timing)
187                        System.out.println("Job create:\t" + ((t3 - t2) / 1000.0) + " secs");
188
189                final ExecutorService es = Executors.newFixedThreadPool(njobs);
190                es.invokeAll(jobs);
191                es.shutdown();
192
193                final double t4 = System.currentTimeMillis();
194                if (timing) {
195                        System.out.println("Proc time total:\t" + ((t4 - t3) / 1000.0) + " secs");
196                        System.out.println("Proc time per file:\t" + (((t4 - t3) / 1000.0) / inputFiles.size()) + " secs");
197
198                        System.out.println("Norm Proc time total:\t" + (njobs * (t4 - t3) / 1000) + " secs");
199                        System.out.println("Norm Proc time per file:\t" + (njobs * ((t4 - t3) / 1000.0) / inputFiles.size())
200                                        + " secs");
201                }
202        }
203
204        private List<File> getInputs() {
205                final List<File> files = new ArrayList<File>();
206
207                getInputs(files, inputBase);
208
209                return files;
210        }
211
212        private void getInputs(List<File> files, File dir) {
213                for (final File f : dir.listFiles()) {
214                        if (f.isDirectory()) {
215                                getInputs(files, f);
216                        } else {
217                                // check matches regex
218                                if (inputRegex == null || f.getName().matches(inputRegex)) {
219                                        // check output
220                                        final File output = getOutput(f);
221
222                                        if (!output.exists() || force) {
223                                                files.add(f);
224                                        }
225                                }
226                        }
227                }
228        }
229
230        private File getOutput(File f) {
231                final File tmp = new File(outputBase, f.getAbsolutePath().replace(inputBase.getAbsolutePath(), ""));
232                String outputName = tmp.getName();
233                if (tmp.getName().contains(".")) {
234                        outputName = tmp.getName().substring(0, tmp.getName().lastIndexOf("."));
235                }
236                return new File(tmp.getParent(), outputName + outputExt);
237        }
238
239        /**
240         * The main method of the tool.
241         * 
242         * @param args
243         * @throws IOException
244         * @throws InterruptedException
245         */
246        public static void main(String[] args) throws IOException, InterruptedException {
247                final ParallelExecutor tool = new ParallelExecutor();
248
249                final CmdLineParser parser = new CmdLineParser(tool);
250
251                try {
252                        parser.parseArgument(args);
253                } catch (final CmdLineException e) {
254                        System.err.println(e.getMessage());
255                        System.err
256                                        .println("Usage: java -cp GlobalFeaturesTool.jar uk.ac.soton.ecs.jsh2.ParallelExecutor [options...]");
257                        parser.printUsage(System.err);
258                        return;
259                }
260
261                tool.execute();
262        }
263}