Search code examples
javajavafxjavafx-webengine

Javafx WebEngine: what really happens in a background worker? UI hangs on loadContent(big HTML document)


From the WebEngine docs:

Loading always happens on a background thread. Methods that initiate loading return immediately after scheduling a background job. To track progress and/or cancel a job, use the Worker instance available from the getLoadWorker() method.

I have an HTML string which I load on the WebView via WebEngine.loadContent(String). That string is about 5 million chars long. Upon running that in Platform.runLater() (and I have to run it in the JavaFX thread, otherwise I get an error) my UI hangs for about a minute.

If I don't run it in Platform.runLater() I get:

java.lang.IllegalStateException: Not on FX application thread; currentThread = populator
    at com.sun.javafx.tk.Toolkit.checkFxUserThread(Toolkit.java:236)
    at com.sun.javafx.tk.quantum.QuantumToolkit.checkFxUserThread(QuantumToolkit.java:423)
    at javafx.scene.web.WebEngine.checkThread(WebEngine.java:1216)
    at javafx.scene.web.WebEngine.loadContent(WebEngine.java:931)
    at javafx.scene.web.WebEngine.loadContent(WebEngine.java:919)
    ...

I can't query the webEngine via webEngine.getLoadWorker().getProgress() nor cancel via webEngine.getLoadWorker().cancel() because I have to, again, run that on the JavaFX thread, which is hanged...

So I have to wait until the page loads, and then any Platform.runLater(()->webEngine.getLoadWorker().getProgress()) submitted previously (during the webpage loading process) will run, giving me 1.0 each time...

The code I'm using to query the Worker:

// WebView
wvIn.getEngine().getLoadWorker().stateProperty().addListener(new ChangeListener<Worker.State>() {
    class ProgressThread extends Thread {
        private Worker.State loadWorkerState;

        synchronized Worker.State getLoadWorkerState() {
            return loadWorkerState;
        }

        synchronized void setLoadWorkerState(Worker.State loadWorkerState) {
            this.loadWorkerState = loadWorkerState;
        }

        {
            setDaemon(true);
            setName("LoadingWebpageProgressThread");
        }

        public void run() {
            while (true) {
                try {
                    if (getLoadWorkerState() == Worker.State.RUNNING)
                        // piWv ProgressIndicator (WebView loading)
                        Platform.runLater(() -> piWv.setVisible(true));
                    while (getLoadWorkerState() == Worker.State.RUNNING) {
                        Platform.runLater(() -> {
                            piWv.setProgress(wvIn.getEngine().getLoadWorker().getProgress());
                            // TODO delete
                            System.out.println(wvIn.getEngine().getLoadWorker().getProgress());
                        });
                        Thread.sleep(100);
                    }
                    if (getLoadWorkerState() == Worker.State.SUCCEEDED) {
                        Platform.runLater(() -> piWv.setProgress(1d));
                        Thread.sleep(100);
                        Platform.runLater(() -> {
                            piWv.setVisible(false);
                            piWv.setProgress(0d);
                        });
                    }
                    synchronized (this) {
                        wait();
                    }
                } catch (InterruptedException e) {
                }
            }
        }
    };

    final ProgressThread progressThread = new ProgressThread();
    {
        progressThread.start();
    }

    // executed on JavaFX Thread
    @Override
    public void changed(ObservableValue<? extends State> observable, State oldValue, State newValue) {
        if (newValue == State.SUCCEEDED) {
            JSObject window = (JSObject) wvIn.getEngine().executeScript("window");
            window.setMember("controller", mainController);
            progressThread.setLoadWorkerState(newValue);
            progressThread.interrupt();
        } else if (newValue == State.RUNNING) {
            progressThread.setLoadWorkerState(newValue);
            progressThread.interrupt();
        }
        // TODO delete
        System.out.println(oldValue + "->" + newValue);
    }
});

Is there anyway to force the loading in a background thread?

What exactly is happening in the JavaFX thread? Is it the process of populating the WebView?


Solution

  • That HTML file you showed in your question loads almost instantly on my machine.

    In my opinion, the problem is in your code. Well, it's quite cumbersome. Please let me point out that it doesn't look very good, since it is deadlock-prone, and uses exceptions to implement a form of guarded-blocks.

    It looks like you don't fully understand the concept of events. Unless you have cut off the code snippet in your question in some way for the sake of brevity, the ProgressThread instance is completely useless. You already have a ChangeListener dispatching events as needed, executing callbacks right on the JavaFX Application Thread; just use it.

    indicator.setVisible(false); // Start hidden
    engine.getLoadWorker().progressProperty().addListener((observable, oldValue, newValue) -> {
        indicator.setProgress(newValue.doubleValue());
        indicator.setVisible(indicator.getProgress() != 1D);
    });
    

    Alternatively, you could just use the power of binding:

    Worker<Void> worker = engine.getLoadWorker();
    indicator.visibleProperty().bind(worker.stateProperty().isEqualTo(Worker.State.RUNNING));
    indicator.progressProperty().bind(worker.progressProperty());
    

    For your JavaScript code, I suggest you to listen to document events instead of worker state events: they are more meaningful in your case, and they are fired when the page has actually finished rendering, not just when the page data has finished downloading.

    engine.documentProperty().addListener((observable, oldValue, newValue) -> {
        if (newValue == null) {
            return;
        }
        JSObject window = (JSObject)engine.executeScript("window");
        window.setMember("controller", mainController);
    });
    

    Please note that on my machine, the progress appears always to be zero while loading is in progress. I think that the problem is in your HTML code: if you try to load some other resource, such us the webpage you are reading now, the progress indicator will deliver a much more enjoyable experience, being updated several times before the page contents have finished loading.