Monday, May 30, 2011

Leveraging Java from LiveCode - experiments with multi-threading

For some time now, I've been promoting the idea of extending LiveCode using Java - both are wonderful cross-platform technologies, each with their own strengths and weaknesses. One area where Java has really made an impact is its built-in approach to multi-threading, by offering a way to split work into chunks that can be executed separately and simultaneously. In a world where chip makers turn to multiple cores to speed up processing, this is a very important technology to incorporate into our software designs.

LiveCode is single-threaded - well, mostly single-threaded: script execution happens on a single thread, but some parts, like socket communication, can be used with event callbacks. This means that when new data arrives, an event message is sent to a control and handled on the script execution thread; but the actual reading and writing can happen on other threads. This is a solid middle ground, as coders don't have to worry about data races and the other un-fun things that come with full-blown multi-threaded programming.

Now my earlier postrs on extending LiveCode with Java were based on a strategy of handing off some work to a Java process (via shell calls or process communication via pipes) and waiting for the result to come back. This is all nice and dandy if the work is done quickly, but what if the processing may take a long time to complete? One of the main reasons to employ multi-threading, after all, is to make sure we can update the user interface while some heavy processing continues in the background.

One approach could be to use the 'in time' variant of the 'read from process' command, and poll the other side for content, but I wanted to show a different approach, by wrapping our Java code in an ExecutorService. The basic idea: we can either 'call' a command immedialtely, or 'submit' it as a task to process in the background. Submitting a command returns a pending-result-id, which we can use to 'cancel' (if it hasn't started or finished yet), check on its 'status' (done, cancelled, pending) or even 'get' its result (waiting for the command to complete).

I know that's a lot of things in one paragraph, so let's put it in Java code:


import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicInteger;


public class LiveCodeJavaExecutor {

private final static String EXIT_COMMAND = "exit";
private final static String CALL_COMMAND = "call";
private final static String SUBMIT_COMMAND = "submit";
private final static String CANCEL_COMMAND = "cancel";
private final static String STATUS_COMMAND = "status";
private final static String GET_COMMAND = "get";

private final static ExecutorService EXECUTOR_SERVICE;
private final static Map<Integer,Future<String>> PENDING_RESULTS;
private final static AtomicInteger NEXT_PENDING_ID;

static {
EXECUTOR_SERVICE = Executors.newCachedThreadPool();
NEXT_PENDING_ID = new AtomicInteger();
PENDING_RESULTS = new HashMap<Integer,Future<String>>();
}

/**
* @param args
*/
public static void main(String[] args) {
// Make sure to run in head-less mode
System.setProperty("java.awt.headless", "true");

String commandLine = ""; // the last received command line
System.out.println("LiveCodeJavaExecutor started. Type the command you wish to execute (type 'exit' to stop):");
final InputStreamReader isr = new InputStreamReader(System.in);
final BufferedReader bir = new BufferedReader(isr);

InfiniteLoop:
while(!EXIT_COMMAND.equalsIgnoreCase(commandLine)) {
try {
commandLine = bir.readLine();
} catch (IOException e) {
continue InfiniteLoop;
}
final String[] commandParts = commandLine.split("\t");
final String commandName = commandParts[0];
if (EXIT_COMMAND.equalsIgnoreCase(commandName)) {
System.out.println("LiveCodeJavaExecutor is shutting down");
EXECUTOR_SERVICE.shutdown();
System.out.println("LiveCodeJavaExecutor is exiting");
} else if (CALL_COMMAND.equalsIgnoreCase(commandName)) {
String result = "";
final CallableCommand callableCommand = new CallableCommand(commandParts);
try {
result = callableCommand.call();
} catch (Exception e) {
result = e.getMessage().trim();
}
System.out.println(result);
} else if (SUBMIT_COMMAND.equalsIgnoreCase(commandName)){
String result = "";
try {
final CallableCommand callableCommand = new CallableCommand(commandParts);
final Integer pendingResultId = Integer.valueOf(NEXT_PENDING_ID.incrementAndGet());
final Future pendingResult = EXECUTOR_SERVICE.submit(callableCommand);
PENDING_RESULTS.put(pendingResultId, pendingResult);
result = pendingResultId.toString();
} catch (RejectedExecutionException e) {
result = "error - could not submit command";
}
System.out.println(result);
} else if (CANCEL_COMMAND.equalsIgnoreCase(commandName)) {
String result = "";
try {
final Integer pendingResultId = Integer.valueOf(commandParts[1]);
final Future pendingResult = PENDING_RESULTS.get(pendingResultId);
if (pendingResult != null) {
result = String.valueOf(pendingResult.cancel(true));
} else {
result = "unknown pending result id: " + pendingResultId.toString();
}
} catch (ArrayIndexOutOfBoundsException e) {
result = "invalid parameters - expected: cancel<tab><pending-result-id>";
} catch (Exception e) {
result = e.getMessage().trim();
}
System.out.println(result);
} else if (STATUS_COMMAND.equalsIgnoreCase(commandName)) {
String result = "";
try {
final Integer pendingResultId = Integer.valueOf(commandParts[1]);
final Future pendingResult = PENDING_RESULTS.get(pendingResultId);
if (pendingResult != null) {
if (pendingResult.isCancelled()) {
result = "cancelled";
} else if (pendingResult.isDone()) {
result = "done";
} else {
result = "pending";
}
} else {
result = "unknown pending result id: " + pendingResultId.toString();
}
} catch (ArrayIndexOutOfBoundsException e) {
result = "invalid parameters - expected: status<tab><pending-result-id>";
} catch (Exception e) {
result = e.getMessage().trim();
}
System.out.println(result);
} else if (GET_COMMAND.equalsIgnoreCase(commandName)) {
String result = "";
try {
final Integer pendingResultId = Integer.valueOf(commandParts[1]);
final Future pendingResult = PENDING_RESULTS.get(pendingResultId);
if (pendingResult != null) {
result = pendingResult.get();
PENDING_RESULTS.remove(pendingResultId);
} else {
result = "unknown pending result id: " + pendingResultId.toString();
}
} catch (ArrayIndexOutOfBoundsException e) {
result = "invalid parameters - expected: get<tab><pending-result-id>";
} catch (Exception e) {
result = e.getMessage().trim();
}
System.out.println(result);
} else {
System.out.println("unrecognized command: " + commandLine);
}
System.out.println('.');
}
}

private static class CallableCommand implements Callable<String> {
private final String[] commandParts;
public CallableCommand(final String[] commandParts) {
super();
this.commandParts = commandParts;
}
public String call() throws Exception {
String result = null;
result = dispatchCommand(commandParts);
return result;
}
}

public static String dispatchCommand(String[] commandParts) throws Exception {
String commandName = commandParts[1];
Thread.sleep(10000);
return commandName;
}

}


Admittedly, the actual command execution part is a bit silly to say the least: sleeping for ten seconds and then just returning the command name is not exactly business logic. But you can take a look at some of my previous examples, to put in code for concatenating PDF files, validating XML data using XSD schemas, image processing or anything else that could take quite a bit of time to finish processing.

Turning our attention to the LiveCode side, we would have a stack script looking something like this:

local sProcess

on JExecutor_StartHelper
if sProcess is empty then
local tDefaultFolder, tStackFolder
put ImageIO_StackFolder() into tStackFolder
put the defaultFolder into tDefaultFolder
set the defaultFolder to tStackFolder
put "java LiveCodeJavaExecutor" into sProcess
open process sProcess for update
set the defaultFolder to tDefaultFolder
--> make sure we're speaking with the right helper
read from process sProcess for 1 line
if it begins with "LiveCodeJavaExecutor started." then
--> good to go
else
close process sProcess
put empty into sProcess
end if
end if
end JExecutor_StartHelper

on JExecutor_StopHelper
write ("exit") & return to process sProcess
close process sProcess
put empty into sProcess
end JExecutor_StopHelper

command JExecutor_CallCommand pCommand
write("call" & tab & pCommand) & return to process sProcess
read from process sprocess for 1 line
return line 1 of it
end JExecutor_CallCommand

command JExecutor_SubmitCommand pCommand
write("submit" & tab & pCommand) & return to process sProcess
read from process sprocess for 1 line
return line 1 of it
end JExecutor_SubmitCommand

command JExecutor_CancelRequest pRequestId
write("cancel" & tab & pRequestId) & return to process sProcess
read from process sprocess for 1 line
return line 1 of it
end JExecutor_CancelRequest

function JExecutor_StatusOfRequest pRequestId
write("status" & tab & pRequestId) & return to process sProcess
read from process sprocess for 1 line
return line 1 of it
end JExecutor_StatusOfRequest

function JExecutor_GetResultOfRequest pRequestId
write("get" & tab & pRequestId) & return to process sProcess
read from process sprocess for 1 line
return line 1 of it
end JExecutor_GetResultOfRequest


Now we have the commands and functions we need to start the separate Java process, 'call' commands, 'submit' commands, 'cancel' pending requests, checking the 'status' of pending requests, and fetching the result with a 'get'. Enough to control the lifecycle of such submitted commands a.k.a. pending requests.

If we're sure the command is just a small bit of work, we use the 'call' command and wait for the result to appear; but if it may take a long time to complete, we can 'submit' a command and use the returned 'pending request id' to track its progress - and while the Java side is churning away, we can update our LiveCode user interface with a lovely indeterminate progress display, polling every second or so until the 'status' returns "done" and then use 'get' to fetch the completed command output.

So perhaps the title was a little misleading, as I've only given you a skeleton. Hopefully it will inspire you to combine this technique with code that I posted previously, and actually use it in your own projects. Happy coding!

Monday, May 9, 2011

Quartam PDF Library 1.1.1 Available

This maintenance update to Quartam PDF Library fixes two bugs specific to LiveCode Server environments, and extends the WriteTextTable command to allow more control over border drawing.

The cross-platform .zip archive can be downloaded at: http://downloads.quartam.com/qrtpdflib_111_xplatform.zip
A web page with LiveCode Server / On-Rev demos is available at: http://quartam.on-rev.com/qrtpdfdemos.irev

Quartam PDF Library for LiveCode - version 1.1 introduced support for transformations, transparency and blendmodes, gradients, clipping, text box fitting, inserting pages, compression, experimental support for including EPS files, as well as support for LiveCode Server and On-Rev. It is released as open source under a dual license (GNU Affero General Public License / Commercial License).

Sunday, May 8, 2011

Presenting at RunRevLive.11 - the code

As I mentioned earlier, I presented two topics at the RunRevLive.11 developer conference. After returning home, I got clobbered with a backlog of work at the day-job; but today, I finally got around to uploading the example code.

- Advanced databases (or: migrating from a single-user desktop application to a multi-user networked database application)
- Extending LiveCode with Java (or: leveraging Java libraries from within LiveCode scripts)

Enjoy - and don't forget that if you couldn't attend it in person and bought a SimulCast pass, you can see the whole presentation through the RunRevLive SimulCast - or wait for the DVD to come out :-)