From 35057c509e710618215cba31ed055b1dddda2d04 Mon Sep 17 00:00:00 2001 From: stepan <stepan.sindelar@oracle.com> Date: Mon, 18 Jun 2018 10:05:07 +0200 Subject: [PATCH] Implemented grid-device server. --- .../fastrGrid/server/RemoteDeviceServer.java | 545 +++++++++++++++ .../r/library/fastrGrid/GridContext.java | 20 +- .../r/library/fastrGrid/WindowDevice.java | 34 +- .../NotSupportedImageFormatException.java | 34 + .../device/awt/BufferedImageDevice.java | 9 +- .../fastrGrid/device/remote/RemoteDevice.java | 653 ++++++++++++++++++ .../remote/RemoteDeviceDataExchange.java | 268 +++++++ .../grDevices/InitWindowedDevice.java | 7 +- .../oracle/truffle/r/runtime/FastRConfig.java | 17 +- mx.fastr/mx_fastr.py | 6 + mx.fastr/native-image.properties | 5 +- mx.fastr/suite.py | 28 + 12 files changed, 1587 insertions(+), 39 deletions(-) create mode 100644 com.oracle.truffle.r.library.fastrGrid.server/src/com/oracle/truffle/r/library/fastrGrid/server/RemoteDeviceServer.java create mode 100644 com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/NotSupportedImageFormatException.java create mode 100644 com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/remote/RemoteDevice.java create mode 100644 com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/remote/RemoteDeviceDataExchange.java diff --git a/com.oracle.truffle.r.library.fastrGrid.server/src/com/oracle/truffle/r/library/fastrGrid/server/RemoteDeviceServer.java b/com.oracle.truffle.r.library.fastrGrid.server/src/com/oracle/truffle/r/library/fastrGrid/server/RemoteDeviceServer.java new file mode 100644 index 0000000000..dd8c181226 --- /dev/null +++ b/com.oracle.truffle.r.library.fastrGrid.server/src/com/oracle/truffle/r/library/fastrGrid/server/RemoteDeviceServer.java @@ -0,0 +1,545 @@ +/* + * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 3 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 3 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.truffle.r.library.fastrGrid.server; + +import java.io.InputStream; +import java.io.OutputStream; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.time.LocalTime; +import java.util.concurrent.ConcurrentHashMap; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.oracle.truffle.r.library.fastrGrid.device.DrawingContext; +import com.oracle.truffle.r.library.fastrGrid.device.GridColor; +import com.oracle.truffle.r.library.fastrGrid.device.GridDevice; +import com.oracle.truffle.r.library.fastrGrid.device.GridDevice.DeviceCloseException; +import com.oracle.truffle.r.library.fastrGrid.device.GridDevice.ImageInterpolation; +import com.oracle.truffle.r.library.fastrGrid.device.NotSupportedImageFormatException; +import com.oracle.truffle.r.library.fastrGrid.device.remote.RemoteDevice; +import com.oracle.truffle.r.library.fastrGrid.device.remote.RemoteDevice.DeviceType; +import com.oracle.truffle.r.library.fastrGrid.device.remote.RemoteDeviceDataExchange; +import com.oracle.truffle.r.library.fastrGrid.GridContext; +import com.oracle.truffle.r.library.fastrGrid.WindowDevice; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; + +public class RemoteDeviceServer { + + private static final Logger log = Logger.getLogger(RemoteDevice.class.getName()); + + private static final String STATUS_HANDLER = "/status"; + private static final String QUIT_HANDLER = "/quit"; + + private static HttpServer server; + + private static int lastDeviceId = 0; + + private static Map<Integer, GridDevice> id2Device = new ConcurrentHashMap<>(); + + private static int lastDrawingContextId = 0; + + private static Map<Integer, ServerDrawingContext> id2DrawingContext = new ConcurrentHashMap<>(); + + private static Map<DrawingContext, Integer> drawingContext2id = new ConcurrentHashMap<>(); + + private static long totalRequestsServiced; + + private static long totalBytesRead; + + private static long totalBytesWritten; + + private static synchronized ServerDrawingContext getDrawingContext(Integer ctxId) { + return getDrawingContextImpl(ctxId); + } + + private static ServerDrawingContext getDrawingContextImpl(Integer ctxId) { + ServerDrawingContext ctx = id2DrawingContext.get(ctxId); + assert (ctx != null) : "Unknown or GCed drawing context id=" + ctxId + ", lastDrawingContextId=" + lastDrawingContextId; + return ctx; + } + + private static void releaseDrawingContextImpl(Integer ctxId) { + ServerDrawingContext ctx = getDrawingContextImpl(ctxId); + if (ctx.decRefCount()) { + id2DrawingContext.remove(ctxId); + drawingContext2id.remove(ctx); + } + } + + public static void main(String[] args) throws IOException { + server = HttpServer.create(new InetSocketAddress(RemoteDevice.SERVER_PORT), 0); + server.createContext(RemoteDevice.COMMAND_HANDLER, new CommandHandler()); + server.createContext(STATUS_HANDLER, new StatusAndQuitHandler(false)); + server.createContext(QUIT_HANDLER, new StatusAndQuitHandler(true)); + server.setExecutor(null); // creates a default executor + server.start(); + } + + private static class CommandHandler implements HttpHandler { + + private RemoteDeviceDataExchange resultEncoder = new RemoteDeviceDataExchange(); + + @Override + public void handle(HttpExchange exchange) throws IOException { + try { + handleImpl(exchange); + } catch (Throwable t) { + t.printStackTrace(); + System.exit(1); + } + } + + private void handleImpl(HttpExchange exchange) throws IOException { + totalRequestsServiced++; + InputStream is = exchange.getRequestBody(); + byte[] isBuf = new byte[is.available() + 1]; + int off = 0; + int len; + try { + while ((len = is.read(isBuf, off, isBuf.length - off)) != -1) { + off += len; + if (off == isBuf.length) { + byte[] newBuf = new byte[isBuf.length + Math.max(isBuf.length, is.available() + 1)]; + System.arraycopy(isBuf, 0, newBuf, 0, off); + isBuf = newBuf; + } + } + } finally { + is.close(); + } + if (log.isLoggable(Level.FINER)) { + log.finer(RemoteDeviceDataExchange.bytesToString("Server Input: ", isBuf, off)); + } + + if (off == 0) { + throw new IOException("Empty request to grid server"); + } + totalBytesRead += off; + RemoteDeviceDataExchange paramsDecoder = new RemoteDeviceDataExchange(isBuf, off); + byte commandId = paramsDecoder.readByte(); + // Optimistically write ok status for all ops (revert if necessary) + resultEncoder.writeByte(RemoteDevice.STATUS_OK); + boolean checkServerClose = false; + RuntimeException rExc = null; + try { + if (commandId == RemoteDevice.CREATE_IMAGE) { + DeviceType type = DeviceType.values()[paramsDecoder.readInt()]; + String filename = paramsDecoder.readString(); + String fileType = paramsDecoder.readString(); + int width = paramsDecoder.readInt(); + int height = paramsDecoder.readInt(); + int deviceId; + synchronized (RemoteDeviceServer.class) { + deviceId = ++lastDeviceId; + } + GridDevice device; + switch (type) { + case BUFFERED_IMAGE: + try { + device = GridContext.openLocalOrRemoteDevice(filename, fileType, width, height); + } catch (NotSupportedImageFormatException ex) { + deviceId = -1; + device = null; + } + break; + case WINDOW: + device = WindowDevice.createWindowDevice(true, width, height); + break; + default: + throw new AssertionError(); + } + if (deviceId != -1) { + id2Device.put(deviceId, device); + } + resultEncoder.writeInt(deviceId); + } else if (commandId == RemoteDevice.CREATE_DRAWING_CONTEXT) { + ServerDrawingContext ctx = new ServerDrawingContext(paramsDecoder); + Integer ctxId = drawingContext2id.get(ctx); + if (ctxId == null) { + synchronized (RemoteDeviceServer.class) { + ctxId = ++lastDrawingContextId; + id2DrawingContext.put(ctxId, ctx); + drawingContext2id.put(ctx, ctxId); + } + } else { + synchronized (RemoteDeviceServer.class) { + ctx = getDrawingContextImpl(ctxId); + ctx.incRefCount(); + } + } + resultEncoder.writeInt(ctxId); + } else if (commandId == RemoteDevice.RELEASE_DRAWING_CONTEXT) { + int ctxId = paramsDecoder.readInt(); + synchronized (RemoteDeviceServer.class) { + releaseDrawingContextImpl(ctxId); + } + } else { + Integer deviceId = paramsDecoder.readInt(); + GridDevice device = id2Device.get(deviceId); + if (device == null) { + throw new IllegalStateException("Grid device for id=" + deviceId + " does not exist on server."); + } + switch (commandId) { + case RemoteDevice.OPEN_NEW_PAGE: { + device.openNewPage(); + break; + } + case RemoteDevice.HOLD: { + device.hold(); + break; + } + case RemoteDevice.FLUSH: { + device.flush(); + break; + } + case RemoteDevice.CLOSE: { + int[] releaseDrawingContextIds = paramsDecoder.readIntArray(); + synchronized (RemoteDeviceServer.class) { + for (int i = 0; i < releaseDrawingContextIds.length; i++) { + int ctxId = releaseDrawingContextIds[i]; + if (ctxId != 0) { + releaseDrawingContextImpl(ctxId); + } + } + } + id2Device.remove(deviceId); + checkServerClose = true; + String exMsg = null; + try { + device.close(); + } catch (DeviceCloseException ex) { + exMsg = ex.getMessage(); + } + resultEncoder.writeString(exMsg); + break; + } + case RemoteDevice.DRAW_RECT: { + DrawingContext ctx = getDrawingContext(paramsDecoder.readInt()); + double leftX = paramsDecoder.readDouble(); + double bottomY = paramsDecoder.readDouble(); + double width = paramsDecoder.readDouble(); + double height = paramsDecoder.readDouble(); + double rotationAnticlockWise = paramsDecoder.readDouble(); + device.drawRect(ctx, leftX, bottomY, width, height, rotationAnticlockWise); + break; + } + case RemoteDevice.DRAW_POLY_LINES: { + DrawingContext ctx = getDrawingContext(paramsDecoder.readInt()); + double[] x = paramsDecoder.readDoubleArray(); + double[] y = paramsDecoder.readDoubleArray(); + int startIndex = paramsDecoder.readInt(); + int length = paramsDecoder.readInt(); + device.drawPolyLines(ctx, x, y, startIndex, length); + break; + } + case RemoteDevice.DRAW_POLYGON: { + DrawingContext ctx = getDrawingContext(paramsDecoder.readInt()); + double[] x = paramsDecoder.readDoubleArray(); + double[] y = paramsDecoder.readDoubleArray(); + int startIndex = paramsDecoder.readInt(); + int length = paramsDecoder.readInt(); + device.drawPolygon(ctx, x, y, startIndex, length); + break; + } + case RemoteDevice.DRAW_CIRCLE: { + DrawingContext ctx = getDrawingContext(paramsDecoder.readInt()); + double centerX = paramsDecoder.readDouble(); + double centerY = paramsDecoder.readDouble(); + double radius = paramsDecoder.readDouble(); + device.drawCircle(ctx, centerX, centerY, radius); + break; + } + case RemoteDevice.DRAW_RASTER: { + double leftX = paramsDecoder.readDouble(); + double bottomY = paramsDecoder.readDouble(); + double width = paramsDecoder.readDouble(); + double height = paramsDecoder.readDouble(); + int[] pixels = paramsDecoder.readIntArray(); + int pixelsColumnsCount = paramsDecoder.readInt(); + ImageInterpolation interpolation = ImageInterpolation.values()[paramsDecoder.readInt()]; + device.drawRaster(leftX, bottomY, width, height, pixels, pixelsColumnsCount, interpolation); + break; + } + case RemoteDevice.DRAW_STRING: { + DrawingContext ctx = getDrawingContext(paramsDecoder.readInt()); + double leftX = paramsDecoder.readDouble(); + double bottomY = paramsDecoder.readDouble(); + double rotationAnticlockWise = paramsDecoder.readDouble(); + String text = paramsDecoder.readString(); + device.drawString(ctx, leftX, bottomY, rotationAnticlockWise, text); + break; + } + case RemoteDevice.GET_WIDTH: { + resultEncoder.writeDouble(device.getWidth()); + break; + } + case RemoteDevice.GET_HEIGHT: { + resultEncoder.writeDouble(device.getHeight()); + break; + } + case RemoteDevice.GET_NATIVE_WIDTH: { + resultEncoder.writeDouble(device.getNativeWidth()); + break; + } + case RemoteDevice.GET_NATIVE_HEIGHT: { + resultEncoder.writeDouble(device.getNativeHeight()); + break; + } + case RemoteDevice.GET_STRING_WIDTH: { + DrawingContext ctx = getDrawingContext(paramsDecoder.readInt()); + String text = paramsDecoder.readString(); + resultEncoder.writeDouble(device.getStringWidth(ctx, text)); + break; + } + case RemoteDevice.GET_STRING_HEIGHT: { + DrawingContext ctx = getDrawingContext(paramsDecoder.readInt()); + String text = paramsDecoder.readString(); + resultEncoder.writeDouble(device.getStringHeight(ctx, text)); + break; + } + default: + throw new IllegalStateException("Invalid requestId=" + commandId); + } + } + } catch (RuntimeException ex) { + rExc = ex; + } + byte[] osBuf = resultEncoder.resetWrite(); + if (rExc != null) { + resultEncoder.writeByte(RemoteDevice.STATUS_SERVER_ERROR); + osBuf = resultEncoder.resetWrite(); + } + if (log.isLoggable(Level.FINER)) { + log.finer(RemoteDeviceDataExchange.bytesToString("Server Output: ", osBuf, osBuf.length)); + } + exchange.sendResponseHeaders(200, osBuf.length); + OutputStream os = exchange.getResponseBody(); + try { + os.write(osBuf); + } finally { + os.close(); + } + totalBytesWritten += osBuf.length; + exchange.close(); + if (checkServerClose && rExc == null && id2Device.isEmpty()) { + log.fine("Server closing automatically after last device was closed."); + server.stop(0); + System.exit(0); + } + if (rExc != null) { + throw rExc; + } + } + + } + + private static final class ServerDrawingContext implements DrawingContext { + + private final byte[] lineType; + private final double lineWidth; + private final GridLineJoin lineJoin; + private final GridLineEnd lineEnd; + private final double lineMitre; + private final GridColor color; + private final double fontSize; + private final GridFontStyle fontStyle; + private final String fontFamily; + private final double lineHeight; + private final GridColor fillColor; + + private int hash; + private int refCount = 1; + + ServerDrawingContext(RemoteDeviceDataExchange paramsDecoder) { + byte[] lineTypeRead = paramsDecoder.readByteArray(); + if (lineTypeRead == null) { + lineType = DrawingContext.GRID_LINE_BLANK; + } else if (lineTypeRead.length == 0) { + lineType = DrawingContext.GRID_LINE_SOLID; + } else { + lineType = lineTypeRead; + } + lineWidth = paramsDecoder.readDouble(); + lineJoin = GridLineJoin.values()[paramsDecoder.readInt()]; + lineEnd = GridLineEnd.values()[paramsDecoder.readInt()]; + lineMitre = paramsDecoder.readDouble(); + color = GridColor.fromRawValue(paramsDecoder.readInt()); + fontSize = paramsDecoder.readDouble(); + fontStyle = GridFontStyle.values()[paramsDecoder.readInt()]; + fontFamily = paramsDecoder.readString(); + lineHeight = paramsDecoder.readDouble(); + fillColor = GridColor.fromRawValue(paramsDecoder.readInt()); + } + + @Override + public byte[] getLineType() { + return lineType; + } + + @Override + public double getLineWidth() { + return lineWidth; + } + + @Override + public GridLineJoin getLineJoin() { + return lineJoin; + } + + @Override + public GridLineEnd getLineEnd() { + return lineEnd; + } + + @Override + public double getLineMitre() { + return lineMitre; + } + + @Override + public GridColor getColor() { + return color; + } + + @Override + public double getFontSize() { + return fontSize; + } + + @Override + public GridFontStyle getFontStyle() { + return fontStyle; + } + + @Override + public String getFontFamily() { + return fontFamily; + } + + @Override + public double getLineHeight() { + return lineHeight; + } + + @Override + public GridColor getFillColor() { + return fillColor; + } + + @Override + public int hashCode() { + int h = hash; + if (h == 0) { + h = lineType.length; + for (int i = 0; i < lineType.length; i++) { + h = (h << 8) ^ lineType[i]; + } + h ^= Double.hashCode(lineWidth); + h ^= lineJoin.ordinal(); + h ^= lineEnd.ordinal(); + h ^= Double.hashCode(lineMitre); + h ^= color.getRawValue(); + h ^= Double.hashCode(fontSize); + h ^= fontStyle.ordinal(); + h ^= (fontFamily != null) ? fontFamily.hashCode() : 0; + h ^= Double.hashCode(lineHeight); + h ^= fillColor.getRawValue(); + hash = h; + } + return h; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof ServerDrawingContext) { + ServerDrawingContext ctx = (ServerDrawingContext) obj; + byte[] ctxLT = ctx.lineType; + if (lineType != ctxLT) { + if (lineType != null && ctxLT != null && lineType.length == ctxLT.length) { + for (int i = ctxLT.length - 1; i >= 0; i--) { + if (lineType[i] != ctxLT[i]) { + return false; + } + } + } else { + return false; + } + } + if (lineWidth != ctx.lineWidth || lineJoin != ctx.lineJoin || + lineEnd != ctx.lineEnd || lineMitre != ctx.lineMitre || + !color.equals(ctx.color) || fontSize != ctx.fontSize || + fontStyle != ctx.fontStyle || lineHeight != ctx.lineHeight || + !fillColor.equals(ctx.fillColor)) { + return false; + } + if (fontFamily != ctx.fontFamily) { + return (fontFamily != null && fontFamily.equals(ctx.fontFamily)); + } + return true; + } + return false; + } + + void incRefCount() { + refCount++; + } + + boolean decRefCount() { + return (--refCount == 0); + } + + } + + private static class StatusAndQuitHandler implements HttpHandler { + + private final boolean handleQuit; + + StatusAndQuitHandler(boolean handleQuitArg) { + this.handleQuit = handleQuitArg; + } + + @Override + public void handle(HttpExchange exchange) throws IOException { + OutputStream os = exchange.getResponseBody(); + String response = (handleQuit ? LocalTime.now().toString() + ": Grid server stopped. Exit.\n" : "") + + "Total devices created: " + lastDeviceId + ", active: " + id2Device.size() + + "\nTotal DrawingContexts created: " + lastDrawingContextId + ", active: " + id2DrawingContext.size() + + "\nTotal requests serviced: " + totalRequestsServiced + + "\nTotal bytes read: " + totalBytesRead + ", written: " + totalBytesWritten; + byte[] responseBytes = response.getBytes(); + exchange.sendResponseHeaders(200, responseBytes.length); + os.write(responseBytes); + os.close(); + if (handleQuit) { + server.stop(0); + System.exit(0); + } + } + + } + +} diff --git a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/GridContext.java b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/GridContext.java index 7776efd084..60d4bd8c91 100644 --- a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/GridContext.java +++ b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/GridContext.java @@ -33,7 +33,8 @@ import com.oracle.truffle.r.library.fastrGrid.device.GridDevice; import com.oracle.truffle.r.library.fastrGrid.device.GridDevice.DeviceCloseException; import com.oracle.truffle.r.library.fastrGrid.device.SVGDevice; import com.oracle.truffle.r.library.fastrGrid.device.awt.BufferedImageDevice; -import com.oracle.truffle.r.library.fastrGrid.device.awt.BufferedImageDevice.NotSupportedImageFormatException; +import com.oracle.truffle.r.library.fastrGrid.device.NotSupportedImageFormatException; +import com.oracle.truffle.r.library.fastrGrid.device.remote.RemoteDevice; import com.oracle.truffle.r.library.fastrGrid.grDevices.FileDevUtils; import com.oracle.truffle.r.library.fastrGrid.graphics.RGridGraphicsAdapter; import com.oracle.truffle.r.runtime.FastRConfig; @@ -106,6 +107,14 @@ public final class GridContext { return getContext(RContext.getInstance()); } + public static GridDevice openLocalOrRemoteDevice(String filename, String fileType, int width, int height) throws NotSupportedImageFormatException { + if (FastRConfig.UseRemoteGridAwtDevice) { + return RemoteDevice.open(filename, fileType, width, height); + } else { + return BufferedImageDevice.open(filename, fileType, width, height); + } + } + @TruffleBoundary public GridState getGridState() { gridState.setDeviceState(devices.get(currentDeviceIdx).state); @@ -153,7 +162,7 @@ public final class GridContext { if (!FastRConfig.InternalGridAwtSupport) { throw awtNotSupported(); } - setCurrentDevice(defaultDev, WindowDevice.createWindowDevice()); + setCurrentDevice(defaultDev, WindowDevice.createWindowDevice(false, GridDevice.DEFAULT_WIDTH, GridDevice.DEFAULT_HEIGHT)); } else if (defaultDev.equals("svg")) { String filename = "Rplot%03d.svg"; SVGDevice svgDevice = new SVGDevice(FileDevUtils.formatInitialFilename(filename), GridDevice.DEFAULT_WIDTH, GridDevice.DEFAULT_HEIGHT); @@ -200,12 +209,9 @@ public final class GridContext { } private void safeOpenImageDev(String filename, String formatName) { - if (!FastRConfig.InternalGridAwtSupport) { - throw awtNotSupported(); - } - BufferedImageDevice dev = null; + GridDevice dev = null; try { - dev = BufferedImageDevice.open(FileDevUtils.formatInitialFilename(filename), formatName, GridDevice.DEFAULT_WIDTH, GridDevice.DEFAULT_HEIGHT); + dev = openLocalOrRemoteDevice(FileDevUtils.formatInitialFilename(filename), formatName, GridDevice.DEFAULT_WIDTH, GridDevice.DEFAULT_HEIGHT); } catch (NotSupportedImageFormatException e) { throw RInternalError.shouldNotReachHere("Device format " + formatName + " should be supported."); } diff --git a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/WindowDevice.java b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/WindowDevice.java index 63e453ec48..da39c52da9 100644 --- a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/WindowDevice.java +++ b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/WindowDevice.java @@ -24,7 +24,9 @@ package com.oracle.truffle.r.library.fastrGrid; import com.oracle.truffle.r.library.fastrGrid.device.GridDevice; import com.oracle.truffle.r.library.fastrGrid.device.awt.JFrameDevice; +import com.oracle.truffle.r.library.fastrGrid.device.remote.RemoteDevice; import com.oracle.truffle.r.nodes.function.PromiseHelperNode; +import com.oracle.truffle.r.runtime.FastRConfig; import com.oracle.truffle.r.runtime.RCaller; import com.oracle.truffle.r.runtime.RError; import com.oracle.truffle.r.runtime.RError.Message; @@ -41,20 +43,23 @@ public final class WindowDevice { // only static members } - public static GridDevice createWindowDevice() { - return createWindowDevice(GridDevice.DEFAULT_WIDTH, GridDevice.DEFAULT_HEIGHT); - } - - public static GridDevice createWindowDevice(int width, int height) { - JFrameDevice frameDevice = new JFrameDevice(width, height); - RContext ctx = RContext.getInstance(); - if (ctx.hasExecutor()) { - frameDevice.setResizeListener(() -> redrawAll(ctx)); - frameDevice.setCloseListener(() -> devOff(ctx)); - } else { + public static GridDevice createWindowDevice(boolean byGridServer, int width, int height) { + if (FastRConfig.UseRemoteGridAwtDevice) { noSchedulingSupportWarning(); + return RemoteDevice.createWindowDevice(width, height); + } else { + JFrameDevice frameDevice = new JFrameDevice(width, height); + RContext ctx; + if (!byGridServer && ((ctx = RContext.getInstance()) != null) && ctx.hasExecutor() && !FastRConfig.UseRemoteGridAwtDevice) { + frameDevice.setResizeListener(() -> redrawAll(ctx)); + frameDevice.setCloseListener(() -> devOff(ctx)); + } else { + if (!byGridServer) { + noSchedulingSupportWarning(); + } + } + return frameDevice; } - return frameDevice; } public static RError awtNotSupported() { @@ -103,7 +108,8 @@ public final class WindowDevice { } private static void noSchedulingSupportWarning() { - // Note: the PolyglotEngine was not built with an Executor - RError.warning(RError.NO_CALLER, Message.GENERIC, "Grid cannot resize the drawings. If you resize the window, the content will be lost."); + // Note: the PolyglotEngine was not built with an Executor or we use remote grid device + RError.warning(RError.NO_CALLER, Message.GENERIC, "Grid cannot resize the drawings. If you resize the window, the content will be lost. " + + "You can redraw the contents using: 'popViewport(0, recording = FALSE); grid:::draw.all()'."); } } diff --git a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/NotSupportedImageFormatException.java b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/NotSupportedImageFormatException.java new file mode 100644 index 0000000000..2396f7ad0f --- /dev/null +++ b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/NotSupportedImageFormatException.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 3 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 3 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.truffle.r.library.fastrGrid.device; + +public class NotSupportedImageFormatException extends Exception { + + private static final long serialVersionUID = 1182697755931636217L; + + @Override + public synchronized Throwable fillInStackTrace() { + return this; + } + +} diff --git a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/awt/BufferedImageDevice.java b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/awt/BufferedImageDevice.java index 9d9f175900..4c1aafb4fb 100644 --- a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/awt/BufferedImageDevice.java +++ b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/awt/BufferedImageDevice.java @@ -22,6 +22,7 @@ */ package com.oracle.truffle.r.library.fastrGrid.device.awt; +import com.oracle.truffle.r.library.fastrGrid.device.NotSupportedImageFormatException; import static java.awt.image.BufferedImage.TYPE_INT_RGB; import java.awt.Color; @@ -102,12 +103,4 @@ public final class BufferedImageDevice extends Graphics2DDevice implements FileG return false; } - public static class NotSupportedImageFormatException extends Exception { - private static final long serialVersionUID = 1182697755931636217L; - - @Override - public synchronized Throwable fillInStackTrace() { - return this; - } - } } diff --git a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/remote/RemoteDevice.java b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/remote/RemoteDevice.java new file mode 100644 index 0000000000..6749450102 --- /dev/null +++ b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/remote/RemoteDevice.java @@ -0,0 +1,653 @@ +/* + * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 3 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 3 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.truffle.r.library.fastrGrid.device.remote; + +import java.io.InputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import java.net.HttpURLConnection; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketTimeoutException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.Map; +import java.util.WeakHashMap; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.r.library.fastrGrid.device.DrawingContext; +import com.oracle.truffle.r.library.fastrGrid.device.GridDevice; +import com.oracle.truffle.r.library.fastrGrid.device.NotSupportedImageFormatException; +import com.oracle.truffle.r.runtime.REnvVars; +import com.oracle.truffle.r.runtime.RInternalError; + +public final class RemoteDevice implements GridDevice { + + private static final Logger log = Logger.getLogger(RemoteDevice.class.getName()); + + /** Marker bit for commands required to return a result from server. */ + static final byte RESULT_MASK = 64; + + /** + * Create new image with filename, fileType, width and height params. Server returns integer + * imageId of the new image. Other requests encode command-type byte followed by imagedId int + * followed by command-specific parameters. + */ + public static final byte CREATE_IMAGE = RESULT_MASK | 1; + public static final byte OPEN_NEW_PAGE = 2; + public static final byte HOLD = 3; + public static final byte FLUSH = 4; + public static final byte CLOSE = RESULT_MASK | 5; // Result is exception msg or "" + public static final byte DRAW_RECT = 6; + public static final byte DRAW_POLY_LINES = 7; + public static final byte DRAW_POLYGON = 8; + public static final byte DRAW_CIRCLE = 9; + public static final byte DRAW_RASTER = 10; + public static final byte DRAW_STRING = 11; + public static final byte GET_WIDTH = RESULT_MASK | 12; + public static final byte GET_HEIGHT = RESULT_MASK | 13; + public static final byte GET_NATIVE_WIDTH = RESULT_MASK | 14; + public static final byte GET_NATIVE_HEIGHT = RESULT_MASK | 15; + public static final byte GET_STRING_WIDTH = RESULT_MASK | 16; + public static final byte GET_STRING_HEIGHT = RESULT_MASK | 17; + public static final byte CREATE_DRAWING_CONTEXT = RESULT_MASK | 18; + public static final byte RELEASE_DRAWING_CONTEXT = 19; + + /** Status is sent back from server as first byte of the response stream. */ + public static final byte STATUS_OK = 0; + public static final byte STATUS_SERVER_ERROR = 1; + + public static final int SERVER_PORT = 8011; + + public static final String COMMAND_HANDLER = "/command"; + + private static final int SERVER_CONNECT_RETRIES = 3; + private static final int SERVER_CONNECT_TIMEOUT = 2000; // in ms + private static final int SERVER_CONNECT_RETRY_DELAY = 1000; // in ms + private static final int SERVER_AFTER_RUN_DELAY = 500; // in ms + + private static final String SERVER_JAR_NAME = "grid-device-remote-server.jar"; + + private static LinkedBlockingDeque<RemoteRequest> queue = new LinkedBlockingDeque<>(); + + private static Thread queueWorker; + + private static ReferenceQueue<DrawingContext> drawingContextRefQueue = new ReferenceQueue<>(); + + private static Process serverProcess; + + private static Path javaCmd; + + private final int remoteDeviceId; + + private boolean closed; + + private final RemoteDeviceDataExchange paramsEncoder = new RemoteDeviceDataExchange(); + + private final Map<DrawingContext, DrawingContextWeakRef> drawingContext2Ref = new WeakHashMap<>(); + + public static RemoteDevice open(String filename, String fileType, int width, int height) throws NotSupportedImageFormatException { + return new RemoteDevice(DeviceType.BUFFERED_IMAGE, filename, fileType, width, height); + } + + public static RemoteDevice createWindowDevice(int width, int height) { + try { + return new RemoteDevice(DeviceType.WINDOW, null, null, width, height); + } catch (NotSupportedImageFormatException ex) { // Should never happen for this device type + throw new AssertionError(); + } + } + + private static Path javaCmd() { + if (javaCmd == null) { + String javaHome = System.getenv("JAVA_HOME"); + if (javaHome == null) { + throw new RInternalError("JAVA_HOME is null"); + } + javaCmd = Paths.get(javaHome, "bin", "java"); + if (!Files.exists(javaCmd)) { + throw new RInternalError("Non-existent path '" + javaCmd + "'."); + } + } + return javaCmd; + } + + private static void checkQueueInited() { + if (queueWorker == null) { + Runnable queueWorkerRun = new Runnable() { + @Override + public void run() { + while (true) { + RemoteRequest request; + try { + request = queue.take(); + } catch (InterruptedException ex) { + break; + } + + do { + try { + String url = "http://localhost:" + SERVER_PORT + COMMAND_HANDLER; + HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); + sendRequest(request, conn); + } catch (IOException ex) { + if (!checkServerConnectable()) { + destroyServer(); + request.finish(new byte[]{STATUS_SERVER_ERROR}, ex); + } + } + } while (!request.isFinished()); + } + } + }; + queueWorker = new Thread(queueWorkerRun, "Grid-Remote-Device-Queue-Worker"); + // queueWorker.setDaemon(true); + queueWorker.setPriority(Thread.NORM_PRIORITY + 2); + queueWorker.start(); + + Runnable dcRefGC = new Runnable() { + RemoteDeviceDataExchange paramsEncoder = new RemoteDeviceDataExchange(); + + @Override + public void run() { + while (true) { + try { + DrawingContextWeakRef ref = (DrawingContextWeakRef) drawingContextRefQueue.remove(); + assert (paramsEncoder.isEmpty()); + paramsEncoder.writeByte(RELEASE_DRAWING_CONTEXT); + paramsEncoder.writeInt(ref.getContextId()); + addRequestImpl(paramsEncoder.resetWrite()); + if (log.isLoggable(Level.FINE)) { + log.fine("Drawing context with contextRefIHC=" + System.identityHashCode(ref) + " and id=" + ref.getContextId() + " released."); + } + } catch (InterruptedException ex) { + } + } + } + }; + Thread dcRefQueueWorker = new Thread(dcRefGC, "Grid-Drawing-Context-GC"); + dcRefQueueWorker.setDaemon(true); + dcRefQueueWorker.start(); + } + } + + private static boolean checkServerConnectable() { + for (int i = SERVER_CONNECT_RETRIES - 1; i >= 0; i--) { + if (serverProcess != null && !serverProcess.isAlive()) { + serverProcess.destroy(); + serverProcess = null; + } + if (serverProcess == null) { + String rHome = REnvVars.rHome(); + Path serverJar = Paths.get(rHome, SERVER_JAR_NAME); + if (!Files.exists(serverJar)) { + Path buildServerJar = Paths.get(rHome, "mxbuild", "dists", SERVER_JAR_NAME); + if (!Files.exists(buildServerJar)) { + RInternalError.shouldNotReachHere( + "Remote grid server jar " + serverJar + " nor " + buildServerJar + " not found."); + } + serverJar = buildServerJar; + } + ProcessBuilder pb = new ProcessBuilder( + javaCmd().toAbsolutePath().toString(), + "-Dsun.net.httpserver.nodelay=true", + "-jar", + serverJar.toAbsolutePath().toString()); + pb.inheritIO(); + try { + serverProcess = pb.start(); + } catch (IOException ex) { + throw new RInternalError(ex, "Cannot start remote grid server process."); + } + try { + // Wait for an estimate of how long it takes to the server process + // to start listening on server port. + Thread.sleep(SERVER_AFTER_RUN_DELAY); + } catch (InterruptedException ex) { + } + } + + // Check that server is connectable + Socket socket = new Socket(); + try { + socket.connect(new InetSocketAddress(SERVER_PORT), SERVER_CONNECT_TIMEOUT); + return true; + } catch (SocketTimeoutException ex) { + } catch (IOException ex) { + } finally { + try { + socket.close(); + } catch (IOException ex) { + } + } + + if (serverProcess != null) { + // In case the server start delay timeout was + // insufficient this (repetitive) timeout should + // ensure that the client will finally connect. + try { + Thread.sleep(SERVER_CONNECT_RETRY_DELAY); + } catch (InterruptedException ex) { + } + } + } + return false; + } + + private static void destroyServer() { + if (serverProcess != null) { + serverProcess.destroy(); + serverProcess = null; + } + } + + private static RInternalError serverError() { + return new RInternalError("Grid Server communication error."); + } + + private RemoteDevice(DeviceType type, String filename, String fileType, int width, int height) throws NotSupportedImageFormatException { + checkQueueInited(); + paramsEncoder.writeByte(CREATE_IMAGE); + paramsEncoder.writeInt(type.ordinal()); + paramsEncoder.writeString(filename); + paramsEncoder.writeString(fileType); + paramsEncoder.writeInt(width); + paramsEncoder.writeInt(height); + RemoteDeviceDataExchange resultDecoder = addResultRequest(true); + remoteDeviceId = resultDecoder.readInt(); + if (remoteDeviceId == -1) { + throw new NotSupportedImageFormatException(); + } + if (log.isLoggable(Level.FINE)) { + log.fine("New remote device id=" + remoteDeviceId + " created."); + } + } + + private void encodeOp(byte opId) { + if (closed) { + throw new RInternalError("Operation opId=" + opId + " with closed remote grid device (id=" + remoteDeviceId + ") prohibited."); + } + assert (paramsEncoder.isEmpty()); + paramsEncoder.writeByte(opId); + assert (remoteDeviceId != 0) : "Remote device not obtained yet."; + paramsEncoder.writeInt(remoteDeviceId); + } + + private boolean encodeOpAndDrawingContext(byte opId, DrawingContext ctx) { + DrawingContextWeakRef ctxRef; + synchronized (drawingContext2Ref) { + ctxRef = drawingContext2Ref.get(ctx); + } + if (ctxRef == null) { // Precede op with a request for drawing context creation + assert (paramsEncoder.isEmpty()); + paramsEncoder.writeByte(CREATE_DRAWING_CONTEXT); + paramsEncoder.writeByteArray(ctx.getLineType()); + paramsEncoder.writeDouble(ctx.getLineWidth()); + paramsEncoder.writeInt(ctx.getLineJoin().ordinal()); + paramsEncoder.writeInt(ctx.getLineEnd().ordinal()); + paramsEncoder.writeDouble(ctx.getLineMitre()); + paramsEncoder.writeInt(ctx.getColor().getRawValue()); + paramsEncoder.writeDouble(ctx.getFontSize()); + paramsEncoder.writeInt(ctx.getFontStyle().ordinal()); + paramsEncoder.writeString(ctx.getFontFamily()); + paramsEncoder.writeDouble(ctx.getLineHeight()); + paramsEncoder.writeInt(ctx.getFillColor().getRawValue()); + RemoteDeviceDataExchange resultDecoder = addResultRequest(false); + if (resultDecoder.readByte() == STATUS_OK) { + ctxRef = new DrawingContextWeakRef(ctx, resultDecoder.readInt()); + if (log.isLoggable(Level.FINE)) { + log.fine("Drawing context id=" + ctxRef.getContextId() + " for contextRefIHC=" + System.identityHashCode(ctxRef)); + } + assert resultDecoder.isReadFinished(); + synchronized (drawingContext2Ref) { + drawingContext2Ref.put(ctx, ctxRef); + } + } else { + return false; + } + } + encodeOp(opId); + paramsEncoder.writeInt(ctxRef.getContextId()); + return true; + } + + @TruffleBoundary + RemoteDeviceDataExchange addResultRequest(boolean decodeReturnStatus) { + RemoteRequest request = addRequestImpl(paramsEncoder.resetWrite()); + assert (request.params[0] & RESULT_MASK) == RESULT_MASK : "Unexpected no-result command-id " + request.params[0]; + while (true) { + synchronized (request) { + if (request.isFinished()) { + RemoteDeviceDataExchange resultDecoder = request.resultDecoder(); + if (decodeReturnStatus) { + if (resultDecoder.readByte() != STATUS_OK) { + throw serverError(); + } + } + return resultDecoder; + } + try { + request.wait(); + if (request.errorCause != null) { + throw new RInternalError(request.errorCause, "Grid Server communication error"); + } + } catch (InterruptedException ex) { + throw new RInternalError("Waiting for result interrupted"); + } + } + } + } + + @TruffleBoundary + void addNoResultRequest() { + RemoteRequest request = addRequestImpl(paramsEncoder.resetWrite()); + assert (request.params[0] & RESULT_MASK) == 0 : "Unexpected result command-id " + request.params[0]; + } + + private static RemoteRequest addRequestImpl(byte[] params) { + RemoteRequest request = new RemoteRequest(params); + queue.add(request); + return request; + } + + private static void sendRequest(RemoteRequest request, HttpURLConnection conn) throws IOException { + conn.setRequestMethod("GET"); + conn.setRequestProperty("User-Agent", "R-Grid-Remote-Client"); + conn.setDoOutput(true); + OutputStream os = conn.getOutputStream(); + byte[] osBuf = request.params; + if (log.isLoggable(Level.FINER)) { + log.finer(RemoteDeviceDataExchange.bytesToString("Data to server:", osBuf, osBuf.length)); + } + try { + os.write(osBuf); + } finally { + os.close(); + } + int responseCode = conn.getResponseCode(); + if (responseCode == 200) { // Status OK + InputStream is = conn.getInputStream(); + byte[] isBuf = new byte[is.available() + 1]; + int off = 0; + int len; + try { + while ((len = is.read(isBuf, off, isBuf.length - off)) != -1) { + off += len; + if (off == isBuf.length) { + byte[] newBuf = new byte[isBuf.length + Math.max(isBuf.length, is.available() + 1)]; + System.arraycopy(isBuf, 0, newBuf, 0, off); + isBuf = newBuf; + } + } + } finally { + is.close(); + } + if (log.isLoggable(Level.FINER)) { + log.finer(RemoteDeviceDataExchange.bytesToString("Response from server:", isBuf, off)); + } + byte[] result; + if (off != isBuf.length) { + result = new byte[off]; + System.arraycopy(isBuf, 0, result, 0, off); + } else { + result = isBuf; + } + request.finish(result, null); + } + } + + @Override + public void openNewPage() { + encodeOp(OPEN_NEW_PAGE); + addNoResultRequest(); + } + + @Override + public void close() throws DeviceCloseException { + encodeOp(CLOSE); + int[] releaseDrawingContextIds; + synchronized (RemoteDevice.class) { + releaseDrawingContextIds = new int[drawingContext2Ref.size()]; + int i = 0; + for (DrawingContextWeakRef ref : drawingContext2Ref.values()) { + int ctxId = ref.invalidateContextId(); + assert (ctxId != -1); + releaseDrawingContextIds[i++] = ctxId; + } + drawingContext2Ref.clear(); + } + paramsEncoder.writeIntArray(releaseDrawingContextIds); // Possible zero ids skipped on + // server + RemoteDeviceDataExchange resultDecoder = addResultRequest(true); + String excMsg = resultDecoder.readString(); + closed = true; + if (excMsg != null) { + throw new DeviceCloseException(new IOException(excMsg)); // wrap into IOException for + // now + } + if (log.isLoggable(Level.FINE)) { + log.fine("Remote device id=" + remoteDeviceId + " closed successfully."); + } + } + + @Override + public void drawRect(DrawingContext ctx, double leftX, double bottomY, double width, double height, double rotationAnticlockWise) { + if (encodeOpAndDrawingContext(DRAW_RECT, ctx)) { + paramsEncoder.writeDouble(leftX); + paramsEncoder.writeDouble(bottomY); + paramsEncoder.writeDouble(width); + paramsEncoder.writeDouble(height); + paramsEncoder.writeDouble(rotationAnticlockWise); + addNoResultRequest(); + } else { + throw serverError(); + } + } + + @Override + public void drawPolyLines(DrawingContext ctx, double[] x, double[] y, int startIndex, int length) { + if (encodeOpAndDrawingContext(DRAW_POLY_LINES, ctx)) { + paramsEncoder.writeDoubleArray(x); + paramsEncoder.writeDoubleArray(y); + paramsEncoder.writeInt(startIndex); + paramsEncoder.writeInt(length); + addNoResultRequest(); + } else { + throw serverError(); + } + } + + @Override + public void drawPolygon(DrawingContext ctx, double[] x, double[] y, int startIndex, int length) { + if (encodeOpAndDrawingContext(DRAW_POLYGON, ctx)) { + paramsEncoder.writeDoubleArray(x); + paramsEncoder.writeDoubleArray(y); + paramsEncoder.writeInt(startIndex); + paramsEncoder.writeInt(length); + addNoResultRequest(); + } else { + throw serverError(); + } + } + + @Override + public void drawCircle(DrawingContext ctx, double centerX, double centerY, double radius) { + if (encodeOpAndDrawingContext(DRAW_CIRCLE, ctx)) { + paramsEncoder.writeDouble(centerX); + paramsEncoder.writeDouble(centerY); + paramsEncoder.writeDouble(radius); + addNoResultRequest(); + } else { + throw serverError(); + } + } + + @Override + public void drawRaster(double leftX, double bottomY, double width, double height, int[] pixels, int pixelsColumnsCount, ImageInterpolation interpolation) { + encodeOp(DRAW_RASTER); + paramsEncoder.writeDouble(leftX); + paramsEncoder.writeDouble(bottomY); + paramsEncoder.writeDouble(width); + paramsEncoder.writeDouble(height); + paramsEncoder.writeIntArray(pixels); + paramsEncoder.writeInt(pixelsColumnsCount); + paramsEncoder.writeInt(interpolation.ordinal()); + addNoResultRequest(); + } + + @Override + public void drawString(DrawingContext ctx, double leftX, double bottomY, double rotationAnticlockWise, String text) { + if (encodeOpAndDrawingContext(DRAW_STRING, ctx)) { + paramsEncoder.writeDouble(leftX); + paramsEncoder.writeDouble(bottomY); + paramsEncoder.writeDouble(rotationAnticlockWise); + paramsEncoder.writeString(text); + addNoResultRequest(); + } else { + throw serverError(); + } + } + + @Override + public double getWidth() { + encodeOp(GET_WIDTH); + RemoteDeviceDataExchange resultDecoder = addResultRequest(true); + return resultDecoder.readDouble(); + } + + @Override + public double getHeight() { + encodeOp(GET_HEIGHT); + RemoteDeviceDataExchange resultDecoder = addResultRequest(true); + return resultDecoder.readDouble(); + } + + @Override + public int getNativeWidth() { + encodeOp(GET_NATIVE_WIDTH); + RemoteDeviceDataExchange resultDecoder = addResultRequest(true); + return resultDecoder.readInt(); + } + + @Override + public int getNativeHeight() { + encodeOp(GET_NATIVE_HEIGHT); + RemoteDeviceDataExchange resultDecoder = addResultRequest(true); + return resultDecoder.readInt(); + } + + @Override + public double getStringWidth(DrawingContext ctx, String text) { + if (encodeOpAndDrawingContext(GET_STRING_WIDTH, ctx)) { + paramsEncoder.writeString(text); + RemoteDeviceDataExchange resultDecoder = addResultRequest(true); + return resultDecoder.readDouble(); + } else { + throw serverError(); + } + } + + @Override + public double getStringHeight(DrawingContext ctx, String text) { + if (encodeOpAndDrawingContext(GET_STRING_HEIGHT, ctx)) { + paramsEncoder.writeString(text); + RemoteDeviceDataExchange resultDecoder = addResultRequest(true); + return resultDecoder.readDouble(); + } else { + throw serverError(); + } + } + + static final class RemoteRequest { + + static final byte[] EMPTY_RESULT = new byte[0]; + + final byte[] params; + + byte[] result; + + Exception errorCause; + + RemoteRequest(byte[] params) { + this.params = params; + } + + RemoteDeviceDataExchange resultDecoder() { + return (result != null) ? new RemoteDeviceDataExchange(result, result.length) : null; + } + + void finish(byte[] resultArg, Exception errorCauseArg) { + assert (this.result == null) : "Result already assigned"; + byte[] resultArg2 = resultArg; + if (resultArg2 != null) { + assert (params[0] | RESULT_MASK) != 0 : "Attempt to assign result to non-result request"; + } else { + resultArg2 = EMPTY_RESULT; + } + synchronized (this) { + this.result = resultArg2; + this.errorCause = errorCauseArg; + notifyAll(); + } + } + + boolean isFinished() { + return (result != null); + } + + } + + public enum DeviceType { + WINDOW, + BUFFERED_IMAGE; + } + + private static final class DrawingContextWeakRef extends WeakReference<DrawingContext> { + + private int contextId; + + DrawingContextWeakRef(DrawingContext drawingContext, int contextIdArg) { + super(drawingContext, drawingContextRefQueue); + this.contextId = contextIdArg; + } + + int getContextId() { + return contextId; + } + + int invalidateContextId() { + int id = contextId; + contextId = -1; + return id; + } + + } + +} diff --git a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/remote/RemoteDeviceDataExchange.java b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/remote/RemoteDeviceDataExchange.java new file mode 100644 index 0000000000..5a6674094a --- /dev/null +++ b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/device/remote/RemoteDeviceDataExchange.java @@ -0,0 +1,268 @@ +/* + * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 3 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 3 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.oracle.truffle.r.library.fastrGrid.device.remote; + +import com.oracle.truffle.r.runtime.RInternalError; +import java.nio.charset.StandardCharsets; + +public class RemoteDeviceDataExchange { + + public static String bytesToString(String title, byte[] buf, int limit) { + StringBuilder sb = new StringBuilder(title.length() + (limit << 2)); + sb.append(title).append("0x"); + for (int i = 0; i < limit; i++) { + int b = buf[i] & 0xFF; + sb.append("0123456789ABCDEF".charAt(b >>> 4)); + sb.append("0123456789ABCDEF".charAt(b & 0x0F)); + } + return sb.toString(); + } + + private static final int DEFAULT_BUF_SIZE = 64; + + private byte[] buf; + + private int index; + + private int limit; + + public RemoteDeviceDataExchange() { + this.buf = new byte[DEFAULT_BUF_SIZE]; + } + + public RemoteDeviceDataExchange(byte[] readBuf, int limit) { + this.buf = readBuf; + this.limit = limit; + } + + public void writeInt(int value) { + ensureCapacity(4); + buf[index++] = (byte) (value >>> 24); + buf[index++] = (byte) (value >> 16); + buf[index++] = (byte) (value >> 8); + buf[index++] = (byte) value; + } + + public int readInt() { + ensureData(4); + return ((buf[index++] & 0xff) << 24 | + (buf[index++] & 0xff) << 16 | + (buf[index++] & 0xff) << 8 | + (buf[index++] & 0xff)); + } + + public void writeIntArray(int[] value) { + if (value != null) { + int len = value.length; + ensureCapacity(4 + (len << 2)); + writeInt(len); + for (int i = 0; i < len; i++) { + writeInt(value[i]); + } + } else { + writeInt(-1); + } + } + + public int[] readIntArray() { + int len = readInt(); + if (len == -1) { + return null; + } + ensureData(len << 2); + int[] arr = new int[len]; + for (int i = 0; i < len; i++) { + arr[i] = readInt(); + } + return arr; + } + + public void writeByte(byte value) { + ensureCapacity(1); + buf[index++] = value; + } + + public byte readByte() { + ensureData(1); + return buf[index++]; + } + + public void writeByteArray(byte[] value) { + if (value != null) { + int len = value.length; + ensureCapacity(4 + len); + writeInt(len); + System.arraycopy(value, 0, buf, index, len); + index += len; + } else { + writeInt(-1); + } + } + + public byte[] readByteArray() { + int len = readInt(); + if (len == -1) { + return null; + } + ensureData(len); + byte[] arr = new byte[len]; + System.arraycopy(buf, index, arr, 0, len); + index += len; + return arr; + } + + public void writeDouble(double value) { + ensureCapacity(8); + long valueBits = Double.doubleToRawLongBits(value); + buf[index++] = (byte) (valueBits >>> 56); + buf[index++] = (byte) ((valueBits >> 48) & 0xff); + buf[index++] = (byte) ((valueBits >> 40) & 0xff); + buf[index++] = (byte) ((valueBits >> 32) & 0xff); + buf[index++] = (byte) ((valueBits >> 24) & 0xff); + buf[index++] = (byte) ((valueBits >> 16) & 0xff); + buf[index++] = (byte) ((valueBits >> 8) & 0xff); + buf[index++] = (byte) (valueBits & 0xff); + } + + public double readDouble() { + long bits = ((long) (buf[index++] & 0xff) << 56 | (long) (buf[index++] & 0xff) << 48 | (long) (buf[index++] & 0xff) << 40 | (long) (buf[index++] & 0xff) << 32 | + (long) (buf[index++] & 0xff) << 24 | (long) (buf[index++] & 0xff) << 16 | (long) (buf[index++] & 0xff) << 8 | buf[index++] & 0xff); + return Double.longBitsToDouble(bits); + } + + public void writeDoubleArray(double[] value) { + if (value != null) { + int len = value.length; + ensureCapacity(4 + (len << 3)); + writeInt(len); + for (int i = 0; i < len; i++) { + writeDouble(value[i]); + } + } else { + writeInt(-1); + } + } + + public double[] readDoubleArray() { + int len = readInt(); + if (len == -1) { + return null; + } + ensureData(len << 3); + double[] arr = new double[len]; + for (int i = 0; i < len; i++) { + arr[i] = readDouble(); + } + return arr; + } + + public void writeString(String value) { + if (value != null) { + boolean simple = true; + for (int i = 0; i < value.length(); i++) { + if (value.charAt(i) >= 0x80) { + simple = false; + break; + } + } + if (simple && value.length() <= buf.length) { + writeInt(value.length()); + ensureCapacity(value.length()); + for (int i = 0; i < value.length(); i++) { + buf[index++] = (byte) value.charAt(i); + } + } else { + byte[] bytes = value.getBytes(); + int bytesLen = bytes.length; + ensureCapacity(bytesLen + 4); + writeInt(bytesLen); + System.arraycopy(bytes, 0, buf, index, bytesLen); + index += bytesLen; + } + } else { // value == null + writeInt(-1); + } + } + + public String readString() { + int strLen = readInt(); + if (strLen == -1) { + return null; + } + boolean simple = true; + for (int i = 0; i < strLen; i++) { + byte b = buf[index + i]; + if (b < 0) { + simple = false; + break; + } + } + String result; + if (simple) { + @SuppressWarnings("deprecation") + String s = new String(buf, 0, index, strLen); + result = s; + } else { + result = new String(buf, index, strLen, StandardCharsets.UTF_8); + } + index += strLen; + return result; + } + + /** + * Grab all bytes written so far and return them as byte array and then reset write index to + * zero for fresh writing. + * + * @return bytes written prior call to this method. + */ + public byte[] resetWrite() { + byte[] result = new byte[index]; + System.arraycopy(buf, 0, result, 0, index); + index = 0; + return result; + } + + public boolean isEmpty() { + return (index == 0); + } + + public boolean isReadFinished() { + return (index == buf.length); + } + + private void ensureCapacity(int nBytes) { + int requireLen = index + nBytes; + if (requireLen > buf.length) { + byte[] newBuf = new byte[Math.max(requireLen, buf.length << 1)]; + System.arraycopy(buf, 0, newBuf, 0, index); + buf = newBuf; + } + } + + private void ensureData(int nBytes) { + if (index + nBytes > limit) { + throw RInternalError.unimplemented("Unexpected EOF: " + (nBytes - limit) + " more bytes expected."); + } + } + +} diff --git a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/grDevices/InitWindowedDevice.java b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/grDevices/InitWindowedDevice.java index 73a6bf369a..a80d86a008 100644 --- a/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/grDevices/InitWindowedDevice.java +++ b/com.oracle.truffle.r.library/src/com/oracle/truffle/r/library/fastrGrid/grDevices/InitWindowedDevice.java @@ -31,8 +31,7 @@ import com.oracle.truffle.api.TruffleLanguage; import com.oracle.truffle.r.library.fastrGrid.GridContext; import com.oracle.truffle.r.library.fastrGrid.WindowDevice; import com.oracle.truffle.r.library.fastrGrid.device.GridDevice; -import com.oracle.truffle.r.library.fastrGrid.device.awt.BufferedImageDevice; -import com.oracle.truffle.r.library.fastrGrid.device.awt.BufferedImageDevice.NotSupportedImageFormatException; +import com.oracle.truffle.r.library.fastrGrid.device.NotSupportedImageFormatException; import com.oracle.truffle.r.library.fastrGrid.device.awt.Graphics2DDevice; import com.oracle.truffle.r.nodes.builtin.RExternalBuiltinNode; import com.oracle.truffle.r.runtime.FastRConfig; @@ -94,7 +93,7 @@ public final class InitWindowedDevice extends RExternalBuiltinNode { } // otherwise create the window ourselves - GridDevice device = WindowDevice.createWindowDevice(width, height); + GridDevice device = WindowDevice.createWindowDevice(false, width, height); String name = isFastRDevice ? "awt" : "X11cairo"; GridContext.getContext().setCurrentDevice(name, device); return RNull.instance; @@ -104,7 +103,7 @@ public final class InitWindowedDevice extends RExternalBuiltinNode { String formatName = name.substring(0, name.indexOf("::")); String filename = name.substring(name.lastIndexOf(':') + 1); try { - BufferedImageDevice device = BufferedImageDevice.open(FileDevUtils.formatInitialFilename(filename), formatName, width, height); + GridDevice device = GridContext.openLocalOrRemoteDevice(FileDevUtils.formatInitialFilename(filename), formatName, width, height); GridContext.getContext().setCurrentDevice(formatName.toUpperCase(), device, filename); } catch (NotSupportedImageFormatException e) { throw error(Message.GENERIC, String.format("Format '%s' is not supported.", formatName)); diff --git a/com.oracle.truffle.r.runtime/src/com/oracle/truffle/r/runtime/FastRConfig.java b/com.oracle.truffle.r.runtime/src/com/oracle/truffle/r/runtime/FastRConfig.java index a91d29ca1c..955d4969f1 100644 --- a/com.oracle.truffle.r.runtime/src/com/oracle/truffle/r/runtime/FastRConfig.java +++ b/com.oracle.truffle.r.runtime/src/com/oracle/truffle/r/runtime/FastRConfig.java @@ -28,6 +28,8 @@ public final class FastRConfig { */ public static final boolean InternalGridAwtSupport; + public static final boolean UseRemoteGridAwtDevice; + /** * Umbrella option, which changes default values of other options in a way that FastR will not * invoke any native code directly and other potentially security sensitive operations are @@ -59,10 +61,12 @@ public final class FastRConfig { InternalGridAwtSupport = false; UseMXBeans = false; UseNativeEventLoop = false; + UseRemoteGridAwtDevice = false; } else { - InternalGridAwtSupport = getBoolean("fastr.internal.grid.awt.support"); - UseMXBeans = getBoolean("fastr.internal.usemxbeans"); - UseNativeEventLoop = getBoolean("fastr.internal.usenativeeventloop"); + InternalGridAwtSupport = getBooleanOrTrue("fastr.internal.grid.awt.support"); + UseMXBeans = getBooleanOrTrue("fastr.internal.usemxbeans"); + UseNativeEventLoop = getBooleanOrTrue("fastr.internal.usenativeeventloop"); + UseRemoteGridAwtDevice = getBooleanOrFalse("fastr.use.remote.grid.awt.device"); } DefaultDownloadMethod = System.getProperty("fastr.internal.defaultdownloadmethod"); } @@ -71,7 +75,12 @@ public final class FastRConfig { // only static fields } - private static boolean getBoolean(String propName) { + private static boolean getBooleanOrFalse(String propName) { + String val = System.getProperty(propName); + return val != null && val.equals("true"); + } + + private static boolean getBooleanOrTrue(String propName) { String val = System.getProperty(propName); return val == null || val.equals("true"); } diff --git a/mx.fastr/mx_fastr.py b/mx.fastr/mx_fastr.py index beb02cb20e..70cce2fe7e 100644 --- a/mx.fastr/mx_fastr.py +++ b/mx.fastr/mx_fastr.py @@ -107,6 +107,11 @@ def do_run_r(args, command, extraVmArgs=None, jdk=None, **kwargs): vmArgs.extend(_command_class_dict[command.lower()]) return mx.run_java(vmArgs + args, jdk=jdk, **kwargs) +def run_grid_server(args, **kwargs): + vmArgs = mx.get_runtime_jvm_args(['GRID_DEVICE_REMOTE_SERVER'], jdk=get_default_jdk()) + vmArgs.append('com.oracle.truffle.r.library.fastrGrid.device.remote.server.RemoteDeviceServer') + return mx.run_java(vmArgs + args, jdk=get_default_jdk(), **kwargs) + def r_classpath(args): print mx.classpath('FASTR', jdk=mx.get_jdk()) @@ -594,6 +599,7 @@ _commands = { 'R' : [rshell, '[options]'], 'rscript' : [rscript, '[options]'], 'Rscript' : [rscript, '[options]'], + 'gridserver' : [run_grid_server, ''], 'rtestgen' : [testgen, ''], 'rgate' : [rgate, ''], 'rutsimple' : [ut_simple, ['options']], diff --git a/mx.fastr/native-image.properties b/mx.fastr/native-image.properties index b0cc90d91a..9c953e3666 100644 --- a/mx.fastr/native-image.properties +++ b/mx.fastr/native-image.properties @@ -7,11 +7,11 @@ Requires = tool:truffle JavaArgs = \ -Dfastr.resource.factory.class=com.oracle.truffle.r.nodes.builtin.EagerResourceHandlerFactory \ - -Dfastr.internal.grid.awt.support=false \ -Dfastr.internal.usemxbeans=false \ -Dfastr.internal.usenativeeventloop=false \ -Dfastr.internal.defaultdownloadmethod=wget \ -Dfastr.internal.ignorejvmargs=true \ + -Dfastr.use.remote.grid.awt.device=true \ -Xmx6G LauncherClass = com.oracle.truffle.r.launcher.RMain @@ -19,4 +19,5 @@ LauncherClassPath = lib/graalvm/launcher-common.jar:languages/R/fastr-launcher.j Args = -H:MaxRuntimeCompileMethods=8000 \ -H:-TruffleCheckFrameImplementation \ - -H:+TruffleCheckNeverPartOfCompilation + -H:+TruffleCheckNeverPartOfCompilation \ + -H:EnableURLProtocols=http diff --git a/mx.fastr/suite.py b/mx.fastr/suite.py index 727d1f1a43..06ba950ab0 100644 --- a/mx.fastr/suite.py +++ b/mx.fastr/suite.py @@ -311,6 +311,20 @@ suite = { }, + "com.oracle.truffle.r.library.fastrGrid.server" : { + "sourceDirs" : ["src"], + "dependencies" : [ + "com.oracle.truffle.r.library", + ], + "annotationProcessors" : [ + ], + "checkstyle" : "com.oracle.truffle.r.runtime", + "javaCompliance" : "1.8", + "workingSets" : "FastR", + "jacoco" : "include", + + }, + "com.oracle.truffle.r.release" : { "sourceDirs" : ["src"], "buildDependencies" : ["com.oracle.truffle.r.native.recommended"], @@ -396,6 +410,20 @@ suite = { ], }, + "GRID_DEVICE_REMOTE_SERVER" : { + "description" : "remote server for grid device", + "dependencies" : [ + "com.oracle.truffle.r.library.fastrGrid.server", + ], + "mainClass" : "com.oracle.truffle.r.library.fastrGrid.server.RemoteDeviceServer", + "exclude" : [ + "truffle:JLINE", + "ANTLR-3.5", + "GNUR", + "XZ-1.6", + ], + }, + "FASTR_UNIT_TESTS" : { "description" : "unit tests", "dependencies" : [ -- GitLab