performance - Aplicación de lienzo JavaFX de sincronización
swing graphics (1)
Ordené practicar JavaFX, construí una aplicación simple que dibuja Triángulos Sierpinski .
import javafx.application.Application;
import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.geometry.Insets;
import javafx.geometry.Point2D;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
public class SierpinskiTriangles extends Application {
private final int PADDING = 5;
private static int numberOfLevels;
public static void launch(String... args){
numberOfLevels = 8;
if((args != null) && (args.length > 0)) {
int num = -1;
try {
num = Integer.parseInt(args[0]);
} catch (NumberFormatException ex) {
ex.printStackTrace();
return;
}
numberOfLevels = (num > 0) ? num : numberOfLevels;
}
Application.launch(args);
}
@Override
public void start(Stage stage) {
stage.setOnCloseRequest((ae) -> {
Platform.exit();
System.exit(0);
});
stage.setTitle("Sierpinski Triangles (fx)");
BorderPane mainPane = new BorderPane();
mainPane.setPadding(new Insets(PADDING));
Pane triPanel = new Triangles();
BorderPane.setAlignment(triPanel, Pos.CENTER);
mainPane.setCenter(triPanel);
Scene scene = new Scene(mainPane);
stage.setScene(scene);
stage.centerOnScreen();
stage.setResizable(false);
stage.show();
}
class Triangles extends AnchorPane{
private static final int PANEL_WIDTH =600, PANEL_HEIGHT = 600;
private static final int TRI_WIDTH= 500, TRI_HEIGHT= 500;
private static final int SIDE_GAP = (PANEL_WIDTH - TRI_WIDTH)/2;
private static final int TOP_GAP = (PANEL_HEIGHT - TRI_HEIGHT)/2;
private int countTriangles;
private long startTime;
private Point2D top, left, right;
private Canvas canvas;
private GraphicsContext gc;
Triangles(){
setPrefSize(PANEL_WIDTH, PANEL_HEIGHT);
canvas = getCanvas();
gc = canvas.getGraphicsContext2D();
getChildren().add(canvas);
draw(numberOfLevels);
}
void draw(int numberLevels) {
Platform.runLater(new Runnable() {
@Override
public void run() {
clearCanvas();
setStartPoints();
startTime = System.currentTimeMillis();
countTriangles = 0;
RunTask task = new RunTask(numberLevels, top, left, right);
Thread thread = new Thread(task);
thread.setDaemon(true);
thread.start();
}
});
}
private void drawTriangle( int levels, Point2D top, Point2D left, Point2D right) {
if(levels < 0) {//add stop criteria
return ;
}
gc.strokePolygon( //implementing with strokeLine did not make much difference
new double[]{
top.getX(),left.getX(),right.getX()
},
new double[]{
top.getY(),left.getY(), right.getY()
},3
);
countTriangles++;
//Get the midpoint on each edge in the triangle
Point2D p12 = midpoint(top, left);
Point2D p23 = midpoint(left, right);
Point2D p31 = midpoint(right, top);
// recurse on 3 triangular areas
drawTriangle(levels - 1, top, p12, p31);
drawTriangle(levels - 1, p12, left, p23);
drawTriangle(levels - 1, p31, p23, right);
}
private void setStartPoints() {
top = new Point2D(getPrefWidth()/2, TOP_GAP);
left = new Point2D(SIDE_GAP, TOP_GAP + TRI_HEIGHT);
right = new Point2D(SIDE_GAP + TRI_WIDTH, TOP_GAP + TRI_WIDTH);
}
private Point2D midpoint(Point2D p1, Point2D p2) {
return new Point2D((p1.getX() + p2.getX()) /
2, (p1.getY() + p2.getY()) / 2);
}
private void updateGraphics(boolean success){
if(success) {
gc.fillText("Number of triangles: "+ countTriangles,5,15);
gc.fillText("Time : "+ (System.currentTimeMillis()- startTime)+ " mili seconds", 5,35);
gc.fillText("Levels: "+ numberOfLevels,5,55);
}
System.out.println("Completed after: "+
(System.currentTimeMillis()- startTime)+ " mili seconds"
+" Triangles: " + countTriangles +" Failed: "+ !success );
}
private Canvas getCanvas() {
Canvas canvas = new Canvas();
canvas.widthProperty().bind(widthProperty());
canvas.heightProperty().bind(heightProperty());
canvas.getGraphicsContext2D().setStroke(Color.RED);
canvas.getGraphicsContext2D().setLineWidth(0.3f);
return canvas;
}
private void clearCanvas() {
gc.clearRect(0, 0, canvas.getWidth(), canvas.getHeight());
}
class RunTask extends Task<Void>{
private int levels;
private Point2D top, left;
private Point2D right;
RunTask(int levels, Point2D top, Point2D left, Point2D right){
this.levels = levels;
this.top = top;
this.left = left;
this.right = right;
startTime = System.currentTimeMillis();
countTriangles = 0;
}
@Override public Void call() {
drawTriangle(levels,top, left, right);
return null;
}
@Override
protected void succeeded() {
updateGraphics(true);
super.succeeded();
}
@Override
protected void failed() {
updateGraphics(false);
}
}
}
public static void main(String[] args) {
launch("13");
}
}
Los problemas que tengo:
a.
La impresión de tiempo en
updateGraphics()
muestra
mucho
antes (8 segundos en mi máquina) que se completa el dibujo de los triángulos, por lo tanto, no mide el proceso completo.
¿Cómo lo mejoro?
segundo. En mi máquina, tarda 30-35 segundos hasta que el panel esté completamente dibujado. Una aplicación de swing similar dura 4 segundos. Puede sugerir que hay algo fundamentalmente incorrecto con mi implementación de javafx.
Su
Task
invoca
drawTriangle()
en el fondo para actualizar un
Canvas
.
El
GraphicsContext
asociado requiere que "una vez que un nodo
Canvas
se adjunta a una escena, se debe modificar en el subproceso de aplicación JavaFX".
Su llamada profundamente recursiva bloquea el hilo de aplicación JavaFX, evitando una actualización de pantalla oportuna.
Por el contrario, la implementación de
System.out.println()
su plataforma puede
allow
informar de manera oportuna.
La disparidad de tiempo se ve incluso sin una
Task
en absoluto.
Felizmente para
Canvas
, "Si no está adjunto a ninguna escena, puede ser modificado por cualquier hilo, siempre y cuando solo se use de un hilo a la vez".
Se puede sugerir un enfoque en
Task
.
Cree una
Task<Image>
nocional
Task<Image>
que actualice un
Canvas
separado en segundo plano.
Periódicamente, tal vez en cada nivel de recursión,
copy
el
Canvas
y publique una instantánea a través de
updateValue()
.
El
Pane
adjunto puede escuchar la propiedad de
value
la tarea y actualizar un
Canvas
adjunto a través de
drawImage()
sin bloquear el subproceso de aplicación JavaFX.
Lamentablemente, la instantánea "Lanza
IllegalStateException
si este método se llama en un subproceso que no sea el subproceso de aplicación JavaFX"
En la alternativa que se muestra a continuación,
CanvasTask
extiende la
Task<Canvas>
y publica un nuevo
Canvas
en cada iteración de un bucle.
CanvasTaskTest
escucha la propiedad
value
y reemplaza el
Canvas
anterior cada vez que llega uno nuevo.
El siguiente ejemplo muestra una serie de árboles fractales de profundidad creciente y el tiempo necesario para componer cada uno.
Tenga en cuenta que en un
GraphicsContext
, "Cada llamada empuja los parámetros necesarios en el búfer donde luego serán procesados en la imagen del nodo
Canvas
por el hilo de procesamiento al final de un pulso".
Esto permite que JavaFX aproveche la
canalización de representación de
una plataforma, pero puede imponer una sobrecarga adicional para una gran cantidad de trazos.
En la práctica, decenas de miles de trazos ralentizan la representación imperceptible, mientras que millones de trazos superpuestos pueden ser superfluos.
import javafx.application.Application;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
/**
* @see https://.com/a/44056730/230513
*/
public class CanvasTaskTest extends Application {
private static final int W = 800;
private static final int H = 600;
@Override
public void start(Stage stage) {
stage.setTitle("CanvasTaskTest");
StackPane root = new StackPane();
Canvas canvas = new Canvas(W, H);
root.getChildren().add(canvas);
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
CanvasTask task = new CanvasTask();
task.valueProperty().addListener((ObservableValue<? extends Canvas> observable, Canvas oldValue, Canvas newValue) -> {
root.getChildren().remove(oldValue);
root.getChildren().add(newValue);
});
Thread thread = new Thread(task);
thread.setDaemon(true);
thread.start();
}
private static class CanvasTask extends Task<Canvas> {
private int strokeCount;
@Override
protected Canvas call() throws Exception {
Canvas canvas = null;
for (int i = 1; i < 15; i++) {
canvas = new Canvas(W, H);
GraphicsContext gc = canvas.getGraphicsContext2D();
strokeCount = 0;
long start = System.nanoTime();
drawTree(gc, W / 2, H - 50, -Math.PI / 2, i);
double dt = (System.nanoTime() - start) / 1_000d;
gc.fillText("Depth: " + i
+ "; Strokes: " + strokeCount
+ "; Time : " + String.format("%1$07.1f", dt) + " µs", 8, H - 8);
Thread.sleep(200); // simulate rendering latency
updateValue(canvas);
}
return canvas;
}
private void drawTree(GraphicsContext gc, int x1, int y1, double angle, int depth) {
if (depth == 0) {
return;
}
int x2 = x1 + (int) (Math.cos(angle) * depth * 5);
int y2 = y1 + (int) (Math.sin(angle) * depth * 5);
gc.strokeLine(x1, y1, x2, y2);
strokeCount++;
drawTree(gc, x2, y2, angle - Math.PI / 8, depth - 1);
drawTree(gc, x2, y2, angle + Math.PI / 8, depth - 1);
}
}
public static void main(String[] args) {
launch(args);
}
}