简体   繁体   中英

JavaFX Application unresponsive after drawing multiple canvas

I have an application where I want to show a FlowPane with PDF pages thumbnails drawn into canvases. I'm using PDFBox and FXGraphics2D to render pages. My current implementation creates a number of canvases as the number of pages, adds them to the FlowPane and then spins a number or async tasks to draw the pages content into the canvas.

I'm not sure if the async drawing is the recommended way but the idea is to not use the JavaFX thread to do the PDF parsing to avoid freezing the application. Now, my issue is this, I can see from the logs that all the rendering tasks have finished and the document is closed. The UI shows some rendered pages but it stays unresponsive for ~10 seconds. After that the application revives, all pages are rendered and everything works nicely.

I tried to profile and I think this is the relevant part: 剖析器 But I have limited knowledge of what is going on under the hood and I couldn't figure out what I'm doing wrong. Do you have an idea or hint on what is wrong with my approach/code and how it can be improved? Ideally I'd like to have the application fast and responsive while the pages thumbnails are filled.

I'm on Linux with a pretty decent machine but I also tested on Windows and got the same behavior. I also tried to replace FlowPane with HBox or VBox but still the same happened.

Here is some ugly code to reproduce the behavior:

public class TestApp extends Application {
    public static void main(String[] args) {
        Application.launch(TestApp.class, args);
    }

    @Override
    public void start(Stage primaryStage) {
        try {
            BorderPane root = new BorderPane();
            Scene scene = new Scene(root, 400, 400);

            var flow = new FlowPane();
            flow.setOnDragOver(e -> {
                if (e.getDragboard().hasFiles()) {
                    e.acceptTransferModes(TransferMode.COPY);
                }
                e.consume();
            });

            flow.setOnDragDropped(e -> {
                var tasks = new ArrayList<TestApp.RenderTask>();
                try {
                    var document = PDDocument.load(e.getDragboard()
                                                    .getFiles().get(0));
                    var renderer = new PDFRenderer(document);
                    for (int i = 1; i <= document.getNumberOfPages(); i++) {
                        var page = document.getPage(i - 1);
                        var cropbox = page.getCropBox();
                        var thumbnailDimension = new Dimension2D(400,
                                                                 400 * (cropbox.getHeight() / cropbox.getWidth()));
                        var thumbnail = new Canvas(thumbnailDimension.getWidth(), thumbnailDimension.getHeight());
                        var gs = thumbnail.getGraphicsContext2D();
                        var pop = gs.getFill();
                        gs.setFill(Color.WHITE);
                        gs.fillRect(0, 0, thumbnailDimension.getWidth(), thumbnailDimension.getHeight());
                        gs.setFill(pop);

                        tasks.add(new TestApp.RenderTask(renderer, thumbnail, i));
                        flow.getChildren().add(new Group(thumbnail));
                    }

                    var exec = Executors.newSingleThreadExecutor();
                    tasks.forEach(exec::submit);
                    exec.submit(()-> {
                        try {
                            document.close();
                            System.out.println("close");
                        } catch (IOException ioException) {
                            ioException.printStackTrace();
                        }
                    });
                } catch (Exception ioException) {
                    ioException.printStackTrace();
                }

                e.setDropCompleted(true);
                e.consume();
            });
            var scroll = new ScrollPane(flow);
            scroll.setFitToHeight(true);
            scroll.setFitToWidth(true);
            root.setCenter(scroll);

            primaryStage.setScene(scene);
            primaryStage.show();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static record RenderTask(PDFRenderer renderer, Canvas canvas, int page) implements Runnable {

        @Override
        public void run() {
            var gs = new FXGraphics2D(canvas.getGraphicsContext2D());
            gs.setBackground(WHITE);
            try {
                renderer.renderPageToGraphics(page - 1, gs);
            } catch (IOException e) {
                e.printStackTrace();
            }
            gs.dispose();
        }
    }
}

And this is the 310 pages PDF file I'm using to test it. Thanks

I finally managed to get what I wanted, the application responds, the thumbnails are populated as they are ready and the memory usage is limited.

The issue
I thought I was drawing the canvases off of the FX thread but how Canvas works is that you fill a buffer of drawing instructions and they are executed when the canvas becomes part of the scene (I think). What was happening is that I was quickly filling the canvases with a lot of drawing instructions, adding all the canvases to the FlowPane and the the application was spending a lot of time actually executing the drawing instructions for 310 canvas and becoming unresponsive for ~10 seconds.

After some reading and suggestions from the JavaFX community here and on Twitter, I tried to switch to an ImageView implementation, telling PDFBox to create a BufferedImage of the page and use it to create an ImageView . It worked nicely but memory usage was 10 times compared to the Canvas impl: ImageView 内存使用情况

The best of both worlds
In my final solution I add white canvases to the FlowPane, create a BufferedImage of the page and convert it to an Image off of the FX thread, draw the image to the Canvas and discard the Image . This is memory usage for the same 310 pages PDF file: 画布内存使用情况

And this is the application responsiveness: 在此处输入图像描述

EDIT (added code to reproduce)
Here the code I used (with SAMBox instead of PDFBox)

public class TestAppCanvasImageMixed extends Application {
    public static void main(String[] args) {
        Application.launch(TestAppCanvas.class, args);
    }

    @Override
    public void start(Stage primaryStage) {
        try {
            BorderPane root = new BorderPane();
            Scene scene = new Scene(root, 400, 400);

            var button = new Button("render");
            button.setDisable(true);
            root.setTop(new HBox(button));
            var flow = new FlowPane();
            flow.setOnDragOver(e -> {
                if (e.getDragboard().hasFiles()) {
                    e.acceptTransferModes(TransferMode.COPY);
                }
                e.consume();
            });

            flow.setOnDragDropped(e -> {
                ArrayList<TestAppCanvasImageMixed.RenderTaskToImage> tasks = new ArrayList<>();
                try {
                    var document = PDDocument.load(e.getDragboard()
                                                    .getFiles().get(0));
                    var renderer = new PDFRenderer(document);
                    for (int i = 1; i <= document.getNumberOfPages(); i++) {
                        var page = document.getPage(i - 1);
                        var cropbox = page.getCropBox();
                        var thumbnailDimension = new Dimension2D(400,
                                                                 400 * (cropbox.getHeight() / cropbox.getWidth()));
                        var thumbnail = new Canvas(thumbnailDimension.getWidth(), thumbnailDimension.getHeight());
                        var gs = thumbnail.getGraphicsContext2D();
                        var clip = new Rectangle(thumbnailDimension.getWidth(), thumbnailDimension.getHeight());
                        clip.setArcHeight(15);
                        clip.setArcWidth(15);
                        thumbnail.setClip(clip);
                        var pop = gs.getFill();
                        gs.setFill(WHITE);
                        gs.fillRect(0, 0, thumbnailDimension.getWidth(), thumbnailDimension.getHeight());
                        gs.setFill(pop);
                        var g = new Group();
                        tasks.add(new TestAppCanvasImageMixed.RenderTaskToImage(renderer, 400 / cropbox.getWidth(), i, thumbnail,
                                                                                () -> g.getChildren().setAll(thumbnail)));
                        flow.getChildren().add(new Group());
                    }

                    button.setOnAction(a -> {
                        var exec = Executors.newFixedThreadPool(1);
                        tasks.forEach(exec::submit);
                        exec.submit(() -> {
                            try {
                                document.close();
                                System.out.println("close");
                            } catch (IOException ioException) {
                                ioException.printStackTrace();
                            }
                        });
                    });
                    button.setDisable(false);

                } catch (Exception ioException) {
                    ioException.printStackTrace();
                }

                e.setDropCompleted(true);
                e.consume();
            });
            var scroll = new ScrollPane(new StackPane(flow));
           scroll.setFitToHeight(true);
           scroll.setFitToWidth(true);
            root.setCenter(scroll);

            primaryStage.setScene(scene);
            primaryStage.show();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static record RenderTaskToImage(PDFRenderer renderer, float scale, int page, Canvas canvas, Runnable r) implements Runnable {

        @Override
        public void run() {
            try {
                var bufferedImage = renderer.renderImage(page - 1, scale, ImageType.ARGB, RenderDestination.VIEW);
                var image = SwingFXUtils.toFXImage(bufferedImage, null);
                var gc = canvas.getGraphicsContext2D();
                gc.drawImage(image, 0, 0);
                Platform.runLater(() -> r.run());
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM