我尝试在https://github.com/GSI-CS-CO/chart-fx中结合 Chart-FX和 JavaFX 中的 TableView 合二为一,满足我的要求。目前我已经写了一个demo来实现。但是现在发现当图表中DataSet的点数越来越多的时候,会阻塞UI线程,导致TableView的刷新变得停滞。而且,我的刷新频率是毫秒级的(每5ms刷新一次表格,每秒刷新一次累计数据到图表中)。我尝试了很多方法。首先,平台。runLater 不是一个选项,因为刷新率太高会阻塞 UI 线程。然后我尝试用服务替换runLater,这明显减少了内存使用。但是Chart和TableView同时刷新时阻塞的问题一直没有解决。你能告诉我如何解决这个问题吗?
代码如下:
package de.gsi.chart.samples;
import java.time.ZoneOffset;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
import de.gsi.chart.plugins.DataPointTooltip;
import de.gsi.chart.plugins.TableViewer;
import de.gsi.chart.plugins.Zoomer;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.concurrent.Worker;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.stage.Stage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import de.gsi.chart.XYChart;
import de.gsi.chart.axes.spi.DefaultNumericAxis;
import de.gsi.chart.axes.spi.format.DefaultTimeFormatter;
import de.gsi.chart.plugins.EditAxis;
import de.gsi.chart.renderer.ErrorStyle;
import de.gsi.chart.renderer.datareduction.DefaultDataReducer;
import de.gsi.chart.renderer.spi.ErrorDataSetRenderer;
import de.gsi.chart.ui.ProfilerInfoBox;
import de.gsi.chart.ui.ProfilerInfoBox.DebugLevel;
import de.gsi.chart.ui.geometry.Side;
import de.gsi.dataset.event.AddedDataEvent;
import de.gsi.dataset.spi.CircularDoubleErrorDataSet;
import de.gsi.dataset.utils.ProcessingProfiler;
/**
* @author rstein
*/
public class RollingBufferSample extends Application {
private static final Logger LOGGER = LoggerFactory.getLogger(RollingBufferSample.class);
public static final int DEBUG_UPDATE_RATE = 5000;
// 0: just drop points that are drawn on the same pixel '3' points need to be at least 3 pixel apart to be drawn
protected static final int MIN_PIXEL_DISTANCE = 0;
public static int N_SAMPLES = 30; // default: 1000000
public static int UPDATE_PERIOD = 1000; // [ms]
public static int BUFFER_CAPACITY = 50000; // 750 samples @ 25 Hz <-> 30 s
public final CircularDoubleErrorDataSet rollingBufferDipoleCurrent = new CircularDoubleErrorDataSet(
"dipole current [A]", RollingBufferSample.BUFFER_CAPACITY);
public final CircularDoubleErrorDataSet rollingBufferBeamIntensity = new CircularDoubleErrorDataSet(
"beam intensity [ppp-1]", RollingBufferSample.BUFFER_CAPACITY);
public final CircularDoubleErrorDataSet rollingBufferBeamIntensity2 = new CircularDoubleErrorDataSet(
"beam intensity [ppp-2]", RollingBufferSample.BUFFER_CAPACITY);
public final CircularDoubleErrorDataSet rollingBufferBeamIntensity3 = new CircularDoubleErrorDataSet(
"beam intensity [ppp-3]", RollingBufferSample.BUFFER_CAPACITY);
public final CircularDoubleErrorDataSet rollingBufferBeamIntensity4 = new CircularDoubleErrorDataSet(
"beam intensity [ppp-4]", RollingBufferSample.BUFFER_CAPACITY);
public final CircularDoubleErrorDataSet rollingBufferBeamIntensity5 = new CircularDoubleErrorDataSet(
"beam intensity [ppp-5]", RollingBufferSample.BUFFER_CAPACITY);
public final CircularDoubleErrorDataSet rollingBufferBeamIntensity6 = new CircularDoubleErrorDataSet(
"beam intensity [ppp-6]", RollingBufferSample.BUFFER_CAPACITY);
public final CircularDoubleErrorDataSet rollingBufferBeamIntensity7 = new CircularDoubleErrorDataSet(
"beam intensity [ppp-7]", RollingBufferSample.BUFFER_CAPACITY);
public final CircularDoubleErrorDataSet rollingBufferBeamIntensity8 = new CircularDoubleErrorDataSet(
"beam intensity [ppp-8]", RollingBufferSample.BUFFER_CAPACITY);
public final CircularDoubleErrorDataSet rollingBufferBeamIntensity9 = new CircularDoubleErrorDataSet(
"beam intensity [ppp-9]", RollingBufferSample.BUFFER_CAPACITY);
public final CircularDoubleErrorDataSet rollingBufferBeamIntensity10 = new CircularDoubleErrorDataSet(
"beam intensity [ppp-10]", RollingBufferSample.BUFFER_CAPACITY);
private final ErrorDataSetRenderer beamIntensityRenderer = new ErrorDataSetRenderer();
private final ErrorDataSetRenderer beamIntensityRenderer2 = new ErrorDataSetRenderer();
private final ErrorDataSetRenderer beamIntensityRenderer3 = new ErrorDataSetRenderer();
private final ErrorDataSetRenderer beamIntensityRenderer4 = new ErrorDataSetRenderer();
private final ErrorDataSetRenderer beamIntensityRenderer5 = new ErrorDataSetRenderer();
private final ErrorDataSetRenderer beamIntensityRenderer6 = new ErrorDataSetRenderer();
private final ErrorDataSetRenderer beamIntensityRenderer7 = new ErrorDataSetRenderer();
private final ErrorDataSetRenderer beamIntensityRenderer8 = new ErrorDataSetRenderer();
private final ErrorDataSetRenderer beamIntensityRenderer9 = new ErrorDataSetRenderer();
private final ErrorDataSetRenderer beamIntensityRenderer10 = new ErrorDataSetRenderer();
private final ErrorDataSetRenderer dipoleCurrentRenderer = new ErrorDataSetRenderer();
private final DefaultNumericAxis yAxis1 = new DefaultNumericAxis("beam intensity", "ppp");
private final DefaultNumericAxis yAxis2 = new DefaultNumericAxis("dipole current", "A");
protected Timer[] timer;
private int i=0;
private void generateBeamIntensityData() {
final long startTime = ProcessingProfiler.getTimeStamp();
final double now = System.currentTimeMillis() / 1000.0 + 1;
// N.B. '+1' to check for resolution
if (rollingBufferBeamIntensity.getDataCount() == 0) {
// suppress auto notification since we plan to add multiple data points
// N.B. this is for illustration of the 'setAutoNotification(..)' functionality
// one may use also the add(double[], double[], ...) method instead
boolean oldState = rollingBufferBeamIntensity.autoNotification().getAndSet(false);
for (int n = RollingBufferSample.N_SAMPLES; n >= 0; --n) {
final double t = now - n * RollingBufferSample.UPDATE_PERIOD / 1000.0;
final double y = 100 * RollingBufferSample.rampFunctionBeamIntensity(t);
final double ey = 1;
if(i<3500){
// rollingBufferBeamIntensity.add(t, (int)(Math.random()*1000), ey, ey);
}
i++;
// N.B. update events suppressed by 'setAutoNotification(false)' above
}
rollingBufferBeamIntensity.autoNotification().set(oldState);
// need to issue a separate update notification
rollingBufferBeamIntensity.fireInvalidated(new AddedDataEvent(rollingBufferBeamIntensity));
} else {
final double t = now;
final double y2 = 100 * RollingBufferSample.rampFunctionBeamIntensity(t);
final double ey = 1;
// single add automatically fires update event/update of chart
// rollingBufferBeamIntensity.add(t, (int)(Math.random()*1000), ey, ey);
}
ProcessingProfiler.getTimeDiff(startTime, "adding data into DataSet");
}
private void generateDipoleCurrentData() {
System.out.println(Thread.currentThread().getName());
final long startTime = ProcessingProfiler.getTimeStamp();
final double now = System.currentTimeMillis() / 1000.0 + 1; // N.B. '+1'
// to check
// for
// resolution
if (rollingBufferDipoleCurrent.getDataCount() == 0) {
// suppress auto notification since we plan to add multiple data points
// N.B. this is for illustration of the 'setAutoNotification(..)' functionality
// one may use also the add(double[], double[], ...) method instead
boolean oldState = rollingBufferDipoleCurrent.autoNotification().getAndSet(false);
for (int n = RollingBufferSample.N_SAMPLES; n >= 0; --n) {
final double t = now - n * RollingBufferSample.UPDATE_PERIOD / 1000.0;
final double y = 25 * RollingBufferSample.rampFunctionDipoleCurrent(t);
final double ey = 1;
rollingBufferDipoleCurrent.add(t, (int)(Math.random()*1000), ey, ey);
// N.B. update events suppressed by 'setAutoNotification(false)' above
}
rollingBufferDipoleCurrent.autoNotification().set(oldState);
// need to issue a separate update notification
rollingBufferDipoleCurrent.fireInvalidated(new AddedDataEvent(rollingBufferDipoleCurrent));
} else {
boolean oldState = rollingBufferDipoleCurrent.autoNotification().getAndSet(false);
for (int j = 0; j < 200; j++) {
final double t = now;
final double y = 25 * RollingBufferSample.rampFunctionDipoleCurrent(t);
final double ey = 1;
// single add automatically fires update event/update of chart
rollingBufferBeamIntensity.add(System.currentTimeMillis(), (int)(Math.random()*1000), ey, ey);
rollingBufferBeamIntensity2.add(System.currentTimeMillis(), (int)(Math.random()*1000), ey, ey);
rollingBufferBeamIntensity3.add(System.currentTimeMillis(), (int)(Math.random()*1000), ey, ey);
rollingBufferBeamIntensity4.add(System.currentTimeMillis(), (int)(Math.random()*1000), ey, ey);
rollingBufferBeamIntensity5.add(System.currentTimeMillis(), (int)(Math.random()*1000), ey, ey);
rollingBufferBeamIntensity6.add(System.currentTimeMillis(), (int)(Math.random()*1000), ey, ey);
rollingBufferBeamIntensity7.add(System.currentTimeMillis(), (int)(Math.random()*1000), ey, ey);
rollingBufferBeamIntensity8.add(System.currentTimeMillis(), (int)(Math.random()*1000), ey, ey);
rollingBufferBeamIntensity9.add(System.currentTimeMillis(), (int)(Math.random()*1000), ey, ey);
rollingBufferBeamIntensity10.add(System.currentTimeMillis(), (int)(Math.random()*1000), ey, ey);
System.out.println("当前计数"+i++);
}
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
rollingBufferDipoleCurrent.autoNotification().set(oldState);
// need to issue a separate update notification
rollingBufferDipoleCurrent.fireInvalidated(new AddedDataEvent(rollingBufferDipoleCurrent));
}
ProcessingProfiler.getTimeDiff(startTime, "adding data into DataSet");
}
private HBox getHeaderBar(Scene scene) {
final Button newDataSet = new Button("new DataSet");
newDataSet.setOnAction(evt -> {
// getTask(0).run();
// getTask(1).run();
Service<Integer> service=new Service() {
@Override
protected Task createTask() {
return new Task() {
@Override
protected Integer call() throws Exception {
int i=0;
while (true){
Thread.sleep(1000);
updateValue(i++);
}
}
};
}
};
service.valueProperty().addListener(new ChangeListener<Integer>() {
@Override
public void changed(ObservableValue<? extends Integer> observable, Integer oldValue, Integer newValue) {
generateDipoleCurrentData();
}
});
service.start();
});
final Button startTimer = new Button("timer");
startTimer.setOnAction(evt -> {
if (timer == null) {
timer = new Timer[2];
timer[0] = new Timer("sample-update-timer", true);
rollingBufferBeamIntensity.reset();
timer[0].scheduleAtFixedRate(getTask(0), 0, UPDATE_PERIOD);
timer[1] = new Timer("sample-update-timer", true);
rollingBufferDipoleCurrent.reset();
timer[1].scheduleAtFixedRate(getTask(1), 0, UPDATE_PERIOD);
} else {
timer[0].cancel();
timer[1].cancel();
timer = null; // NOPMD
}
});
// H-Spacer
Region spacer = new Region();
spacer.setMinWidth(Region.USE_PREF_SIZE);
HBox.setHgrow(spacer, Priority.ALWAYS);
final ProfilerInfoBox profilerInfoBox = new ProfilerInfoBox(DEBUG_UPDATE_RATE);
profilerInfoBox.setDebugLevel(DebugLevel.VERSION);
return new HBox(newDataSet, startTimer, spacer, profilerInfoBox);
}
protected TimerTask getTask(final int updateItem) {
return new TimerTask() {
private int updateCount;
@Override
public void run() {
if (updateItem == 0) {
generateBeamIntensityData();
} else {
generateDipoleCurrentData();
}
if (updateCount % 20 == 0 && LOGGER.isDebugEnabled()) {
LOGGER.atDebug().addArgument(updateCount).log("update iteration #{}");
}
updateCount++;
}
};
}
public BorderPane initComponents(Scene scene) {
final BorderPane root = new BorderPane();
generateBeamIntensityData();
generateDipoleCurrentData();
initErrorDataSetRenderer(beamIntensityRenderer);
initErrorDataSetRenderer(beamIntensityRenderer2);
initErrorDataSetRenderer(beamIntensityRenderer3);
initErrorDataSetRenderer(beamIntensityRenderer4);
initErrorDataSetRenderer(beamIntensityRenderer5);
initErrorDataSetRenderer(beamIntensityRenderer6);
initErrorDataSetRenderer(beamIntensityRenderer7);
initErrorDataSetRenderer(beamIntensityRenderer8);
initErrorDataSetRenderer(beamIntensityRenderer9);
initErrorDataSetRenderer(beamIntensityRenderer10);
initErrorDataSetRenderer(dipoleCurrentRenderer);
final DefaultNumericAxis xAxis1 = new DefaultNumericAxis("time");
xAxis1.setAutoRangeRounding(false);
xAxis1.setTickLabelRotation(45);
xAxis1.setMinorTickCount(30);
xAxis1.invertAxis(false);
xAxis1.setTimeAxis(true);
yAxis2.setSide(Side.RIGHT);
yAxis2.setAnimated(false);
// N.B. it's important to set secondary axis on the 2nd renderer before
// adding the renderer to the chart
dipoleCurrentRenderer.getAxes().add(yAxis2);
final XYChart chart = new XYChart(xAxis1, yAxis1);
chart.legendVisibleProperty().set(true);
chart.setAnimated(false);
chart.getRenderers().set(0, beamIntensityRenderer);
// chart.getRenderers().add(beamIntensityRenderer2);
// chart.getRenderers().add(beamIntensityRenderer3);
// chart.getRenderers().add(beamIntensityRenderer4);
// chart.getRenderers().add(beamIntensityRenderer5);
// chart.getRenderers().add(beamIntensityRenderer6);
// chart.getRenderers().add(beamIntensityRenderer7);
// chart.getRenderers().add(beamIntensityRenderer8);
// chart.getRenderers().add(beamIntensityRenderer9);
// chart.getRenderers().add(beamIntensityRenderer10);
chart.getPlugins().add(new EditAxis());
chart.getPlugins().add(new DataPointTooltip());
chart.getPlugins().add(new Zoomer());//工具栏
chart.getPlugins().add(new TableViewer());
beamIntensityRenderer.getDatasets().add(rollingBufferBeamIntensity);
beamIntensityRenderer.getDatasets().add(rollingBufferBeamIntensity2);
beamIntensityRenderer.getDatasets().add(rollingBufferBeamIntensity3);
beamIntensityRenderer.getDatasets().add(rollingBufferBeamIntensity4);
beamIntensityRenderer.getDatasets().add(rollingBufferBeamIntensity5);
beamIntensityRenderer.getDatasets().add(rollingBufferBeamIntensity6);
beamIntensityRenderer.getDatasets().add(rollingBufferBeamIntensity7);
beamIntensityRenderer.getDatasets().add(rollingBufferBeamIntensity8);
beamIntensityRenderer.getDatasets().add(rollingBufferBeamIntensity9);
beamIntensityRenderer.getDatasets().add(rollingBufferBeamIntensity10);
dipoleCurrentRenderer.getDatasets().add(rollingBufferDipoleCurrent);
// set localised time offset
if (xAxis1.isTimeAxis() && xAxis1.getAxisLabelFormatter() instanceof DefaultTimeFormatter) {
final DefaultTimeFormatter axisFormatter = (DefaultTimeFormatter) xAxis1.getAxisLabelFormatter();
axisFormatter.setTimeZoneOffset(ZoneOffset.UTC);
axisFormatter.setTimeZoneOffset(ZoneOffset.ofHoursMinutes(5, 0));
}
yAxis1.setForceZeroInRange(true);
yAxis2.setForceZeroInRange(true);
yAxis1.setAutoRangeRounding(true);
yAxis2.setAutoRangeRounding(true);
// init menu bar
root.setTop(getHeaderBar(scene));
long startTime = ProcessingProfiler.getTimeStamp();
ProcessingProfiler.getTimeDiff(startTime, "adding data to chart");
startTime = ProcessingProfiler.getTimeStamp();
root.setCenter(chart);
TableView<Person> pane = getPane();
root.setBottom(pane);
ProcessingProfiler.getTimeDiff(startTime, "adding chart into StackPane");
return root;
}
protected void initErrorDataSetRenderer(final ErrorDataSetRenderer eRenderer) {
eRenderer.setErrorType(ErrorStyle.ERRORSURFACE);
// for higher performance w/o error bars, enable this for comparing with
// the standard JavaFX charting library (which does not support error
// handling, etc.)
eRenderer.setErrorType(ErrorStyle.NONE);
eRenderer.setDashSize(RollingBufferSample.MIN_PIXEL_DISTANCE); // plot pixel-to-pixel distance
eRenderer.setPointReduction(true);
eRenderer.setDrawMarker(false);
final DefaultDataReducer reductionAlgorithm = (DefaultDataReducer) eRenderer.getRendererDataReducer();
reductionAlgorithm.setMinPointPixelDistance(RollingBufferSample.MIN_PIXEL_DISTANCE);
}
@Override
public void start(final Stage primaryStage) {
ProcessingProfiler.setVerboseOutputState(true);
ProcessingProfiler.setLoggerOutputState(true);
ProcessingProfiler.setDebugState(false);
final BorderPane root = new BorderPane();
final Scene scene = new Scene(root, 1800, 1000);
root.setCenter(initComponents(scene));
final long startTime = ProcessingProfiler.getTimeStamp();
primaryStage.setTitle(this.getClass().getSimpleName());
primaryStage.setScene(scene);
primaryStage.setOnCloseRequest(evt -> Platform.exit());
primaryStage.show();
ProcessingProfiler.getTimeDiff(startTime, "for showing");
}
/**
* @param args the command line arguments
*/
public static void main(final String[] args) {
Application.launch(args);
}
public static double rampFunctionBeamIntensity(final double t) {
final int second = (int) Math.floor(t);
final double subSecond = t - second;
double offset = 0.3;
final double y = (1 - 0.1 * subSecond) * 1e9;
double gate = RollingBufferSample.square(2, subSecond - offset)
* RollingBufferSample.square(1, subSecond - offset);
// every 5th cycle is a booster mode cycle
if (second % 5 == 0) {
offset = 0.1;
gate = Math.pow(RollingBufferSample.square(3, subSecond - offset), 2);
}
if (gate <= 0 || subSecond < offset) {
gate = 0;
}
return gate * y;
}
public static double rampFunctionDipoleCurrent(final double t) {
final int second = (int) Math.floor(t);
final double subSecond = t - second;
double offset = 0.3;
double y = 100 * RollingBufferSample.sine(1, subSecond - offset);
// every 5th cycle is a booster mode cycle
if (second % 5 == 0) {
offset = 0.1;
y = 100 * Math.pow(RollingBufferSample.sine(1.5, subSecond - offset), 2);
}
if (y <= 0 || subSecond < offset) {
y = 0;
}
return y + 10;
}
private static double sine(final double frequency, final double t) {
return Math.sin(2.0 * Math.PI * frequency * t);
}
private static double square(final double frequency, final double t) {
final double sine = 100 * Math.sin(2.0 * Math.PI * frequency * t);
final double squarePoint = Math.signum(sine);
return squarePoint >= 0 ? squarePoint : 0.0;
}
ObservableList<Person> realTimeDataObservableList = FXCollections.observableArrayList();
public TableView<Person> getPane(){
//创建一个表格来模仿实际业务刷新
TableView<Person> tableView = new TableView<>();
TableColumn<Person, String> name = new TableColumn<>("Firstname");
name.setPrefWidth(200);
name.setCellValueFactory(new PropertyValueFactory<>("firstName"));
TableColumn<Person, String> lastName = new TableColumn<>("lastName");
lastName.setPrefWidth(200);
lastName.setCellValueFactory(person -> person.getValue().lastNameProperty());
TableColumn<Person, String> email = new TableColumn<>("email");
email.setPrefWidth(200);
email.setCellValueFactory(new PropertyValueFactory<>("email"));
//noinspection unchecked
tableView.getColumns().addAll(name, lastName, email);
for (int i = 0; i < 10; i++) {
Person person = new Person("firstName" + i, "lastName" + i, "email" + i);
realTimeDataObservableList.add(person);
}
tableView.setItems(realTimeDataObservableList);
//创建一个service用来高频刷新表格
Service<Integer> service = new Service<Integer>() {
@Override
protected Task<Integer> createTask() {
return new Task<Integer>() {
@Override
protected Integer call() throws Exception {
for (int i = 0; i < 1000000000; i++) {
TimeUnit.MILLISECONDS.sleep(1);
updateValue(i);
}
return 1000000000;
}
};
}
};
//监听service的value属性更改
service.valueProperty().addListener((o, oldValue, newValue) -> {
// try {
// Thread.sleep(100);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
for (Person person : realTimeDataObservableList) {
person.setFirstName("firstname" + newValue);
person.setLastName("lastname" + newValue);
person.setEmail("email" + newValue);
// System.out.println("正在更新" + newValue);
}
});
service.stateProperty().addListener(new ChangeListener<Worker.State>() {
@Override
public void changed(ObservableValue<? extends Worker.State> observable, Worker.State oldValue, Worker.State newValue) {
System.out.println(newValue);
}
});
service.start();
return tableView;
}
//tableview里面的工具类
public static class Person {
private final StringProperty firstName;
private final StringProperty lastName;
private final StringProperty email;
private Person(String fName, String lName, String email) {
this.firstName = new SimpleStringProperty(fName);
this.lastName = new SimpleStringProperty(lName);
this.email = new SimpleStringProperty(email);
}
public String getFirstName() {
return firstName.get();
}
public void setFirstName(String fName) {
firstName.set(fName);
}
public StringProperty firstNameProperty() {
return firstName;
}
public String getLastName() {
return lastName.get();
}
public void setLastName(String lName) {
lastName.set(lName);
}
public StringProperty lastNameProperty() {
return lastName;
}
public String getEmail() {
return email.get();
}
public void setEmail(String inMail) {
email.set(inMail);
}
public StringProperty emailProperty() {
return email;
} // if this method is commented out then the tableview will not refresh when the email is set.
}
}``