Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JavaFx WebView callback from Javascript failing after Garbage Collection

I am currently working on a JavaFX based application, where users can interact with places that are marked on a world map. To do this, I am using an approach similiar to the one described in http://captaincasa.blogspot.de/2014/01/javafx-and-osm-openstreetmap.html ([1]).

However, I am facing a hard-to-debug problem related to the Javascript callback variable injected to the embedded HTML-page using the WebEngine's setMember() method (see also https://docs.oracle.com/javase/8/javafx/embedded-browser-tutorial/js-javafx.htm ([2]) for an official tutorial).

When running the program for a while, the callback variable is loosing its state unpredictably! To demonstrate this behaviour, I developed a minimal working/failing example. I am using jdk1.8.0_121 64-bit on a Windows 10 machine.

The JavaFx App looks as follows:

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;

import javafx.application.Application;
import javafx.concurrent.Worker.State;
import javafx.scene.Scene;
import javafx.scene.layout.AnchorPane;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import netscape.javascript.JSObject;

public class WebViewJsCallbackTest extends Application {

    private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");

    public static void main(String[] args) {
        launch(args);
    }

    public class JavaScriptBridge {
        public void callback(String data) {
            System.out.println("callback retrieved: " + data);
        }
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        WebView webView = new WebView();
        primaryStage.setScene(new Scene(new AnchorPane(webView)));
        primaryStage.show();

        final WebEngine webEngine = webView.getEngine();
        webEngine.load(getClass().getClassLoader().getResource("page.html").toExternalForm());

        webEngine.getLoadWorker().stateProperty().addListener((observableValue, oldValue, newValue) -> {
            if (newValue == State.SUCCEEDED) {
                JSObject window = (JSObject) webEngine.executeScript("window");
                window.setMember("javaApp", new JavaScriptBridge());
            }
        });

        webEngine.setOnAlert(event -> {
            System.out.println(DATE_FORMAT.format(new Date()) + " alerted: " + event.getData());
        });
    }

}

The HTML file "page.html" looks as follows:

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<!-- use for in-browser debugging -->
<!-- <script type='text/javascript' src='http://getfirebug.com/releases/lite/1.2/firebug-lite-compressed.js'></script> -->
<script type="text/javascript">
    var javaApp = null;

    function javaCallback(data) {           
        try {
            alert("javaApp=" + javaApp + "(type=" + typeof javaApp + "), data=" + data);
            javaApp.callback(data);
        } catch (e) {
            alert("caugt exception: " + e);
        }
    }
</script>
</head>
<body>
    <button onclick="javaCallback('Test')">Send data to Java</button>
    <button onclick="setInterval(function(){ javaCallback('Test'); }, 1000)">Send data to Java in endless loop</button>
</body>
</html>

The state of the callback variable javaApp can be observed by clicking on the "Send data to Java in endless loop" button. It will continuously try to run the callback method via javaApp.callback, which produces some logging message in the Java app. Alerts are used as an additional communication channel to back things up (always seems to work and currently used as work-around, but that's not how things are ment to be...).

If everything is working as supposed, each time logging similiar to the following lines should be printed:

callback retrieved: Test
2017/01/27 21:26:11 alerted: javaApp=webviewtests.WebViewJsCallbackTest$JavaScriptBridge@51fac693(type=object), data=Test

However, after a while (anything from 2-7 minutes), no more callbacks are retrieved, but only loggings like the following line are printed:

2017/01/27 21:32:01 alerted: javaApp=undefined(type=object), data=Test

Printing the variable now gives 'undefined' instead of the Java instance path. A strange observation is that the state of javaApp is not truly "undefined". using typeof returnsobject, javaApp === undefined evaluates to false. This is in accordance with the fact that the callback-call does not throw an exception (otherwise, an alert starting with "caugt exception: " would be printed).

Using Java VisualVM showed that the time of failure happens to coincide with the time the Garbage Collector is activated. This can be seen by observing the Heap memory consumption, which drops from approx. 60MB to 16MB due to GC.

What's goining on there? Do you have any idea how I can further debug the issue? I could not find any related know bug...

Thanks a lot for your advice!

PS: the problem was reproduced much faster when including Javascript code to display a world map via Leaflet (cf [1]). Loading or shifting the map most of the time instantly caused the GC to do its job. While debugging this original issue, I traced the problem to the minimal example presented here.

like image 978
luddwich_r Avatar asked Jan 27 '17 21:01

luddwich_r


1 Answers

I solved the problem by creating an instance variable bridge in Java that holds the JavaScriptBridge instance sent to Javascript via setMember(). This way, Gargbage Collection of the instance is prevented.

Relevant code snippet:

public class JavaScriptBridge {
    public void callback(String data) {
        System.out.println("callback retrieved: " + data);
    }
}

private JavaScriptBridge bridge;

@Override
public void start(Stage primaryStage) throws Exception {
    WebView webView = new WebView();
    primaryStage.setScene(new Scene(new AnchorPane(webView)));
    primaryStage.show();

    final WebEngine webEngine = webView.getEngine();
    webEngine.load(getClass().getClassLoader().getResource("page.html").toExternalForm());

    bridge = new JavaScriptBridge();
    webEngine.getLoadWorker().stateProperty().addListener((observableValue, oldValue, newValue) -> {
        if (newValue == State.SUCCEEDED) {
            JSObject window = (JSObject) webEngine.executeScript("window");
            window.setMember("javaApp", bridge);
        }
    });

    webEngine.setOnAlert(event -> {
        System.out.println(DATE_FORMAT.format(new Date()) + " alerted: " + event.getData());
    });
}

Altough the code now works smoothly (also in conjunction with Leaflet), I am still irritated of this unexpected behaviour...

Edit: The explanation for this behaviour is documented since Java 9 (thanks @dsh for your clarifying comment! I was working with Java 8 at the time and unfortunately didn't have this information at hand...)

like image 147
luddwich_r Avatar answered Oct 06 '22 03:10

luddwich_r