Tutoriel de mesure des performances de la bibliothèque JFreeChart sous SWT pour un affichage temps réel

Auteur

Frank Buloup

Je vous propose dans cet article de mettre au point une méthode de mesure des temps d'affichage de la bibliothèque JFreeChart. Les bases d'une bibliothèque 2D SWT puis SWT/OpenGL seront ensuite posées dans un second article et la méthode de mesure sera utilisée pour effectuer des comparaisons.

- Titre : Tutoriel de mesure des performances de la bibliothèque JFreeChart sous SWT pour un affichage temps réel

- Auteur : Frank Buloup

- Parution : 20 mars 2015

- Mise à jour : 11 mai 2015

- Public visé :

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2015 Frank Buloup. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts. Droits de diffusion permanents accordés à Developpez LLC.

Sommaire

- I. Introduction

- II. Installation et configuration des outils

- II.1. Eclipse

- II.2. JFreeChart

- III. Développement et tests qualitatifs

- III.1. Création de la fenêtre des graphes

- III.2. Création de la classe de génération de signaux

- IV. Mesures

- IV.1. Configurations préalables

- IV.2. Automatisation des mesures

- V. Conclusion

I - Introduction

Il existe de nombreuses bibliothèques graphique 2D libres en Java basées sur AWT/Swing :

- JFreeChart

- OpenChart2 (JOpenChart)

- GRAL

- XChart

- ...

Les seules bibliothèques graphiques basées sur SWT que je connaisse sont SWTChart et swt-xy-graph (qui utilise Draw2D).

Toutes ces bibliothèques offrent de grandes fonctionnalités et de très bons rendus mais pour avoir testé JFreeChart, une référence en matière de bibliothèque graphique 2D, et SWTChart, j'ai pu me rendre compte qu'elles ne possédaient pas de bonnes performances en affichage dynamique de données ou de signaux.

Pour vous faire une idée sur l'affichage dynamique de signaux, vous pouvez suivre ce lien qui présente, sous la forme d'une courte vidéo, l'utilisation et le réglage d'un oscilloscope. Ces appareils sont capables d'afficher des signaux qui varient rapidement dans le temps. Ce sont des outils très utilisés pour afficher et analyser des signaux en temps réel.

Il est bien sûr difficile voire impossible d'obtenir ce type de performance sur un ordinateur, mais il est par contre possible d'observer le comportement de ces bibliothèques en affichage temps réel. Pour avoir fait des tests avec JFreeChart et SWTChart, force est de constater que ces bibliothèques excellent dans l'affichage d'un ensemble de valeurs préétablies ou changeant très lentement, mais qu'elles deviennent assez poussives dès que vous souhaitez ajouter de nouvelles informations et rafraichir le graphe à une cadence régulière et relativement élevée : plusieurs dizaines de points toutes les cinquante millisecondes par exemple.

Je ne suis pas le seul à faire cette constatation et une petite recherche sur internet vous montrera que cet avis est partagé. On trouve d'ailleurs dans la FAQ de JFreeChart la réponse suivante à la question : Does JFreeChart support real-time charting ? En substance, cela signifie simplement que cette bibliothèque n'a pas été développée pour faire de l'affichage temps réel.

Ce premier article vous propose donc une méthode de mesure des performances de JFreeChart en affichage temps réel. Après avoir installé et configuré les outils, nous réaliserons le développement de l'interface graphique de présentation des courbes dynamiques avant de mettre au point la méthode de mesure. Nous nous servirons de ces résultats comme référence dans le prochain article.

II - Installation et configuration des outils

Nous allons installer et utiliser Eclipse et JFreeChart. Nous créerons un projet Java sous Eclipse et nous le configurerons afin d'utiliser les bibliothèques SWT et JFreeChart.

II.1 - Eclipse

Si vous ne connaissez pas du tout ce logiciel, je vous invite à suivre ce tutoriel de DVP.COM qui vous guidera dans son installation et utilisation. Vous pouvez bien sûr télécharger une version d'Eclipse plus récente, les explications contenues dans l'article n'étant pas obsolètes.

Télécharger Eclipse en suivant ce lien. Vous pouvez utiliser Eclipse IDE for Java Developers par exemple. Je vous propose de lancer Eclipse dans un répertoire de travail nommé RealTimeCharting.

Maintenant qu'Eclipse est installé, lancé et que vous êtes en perspective de développement Java ; dans le menu File, cliquer sur New -> Java Project pour créer le projet nommé RealTimeCharting. Laisser les options par défaut et cliquer sur le bouton Next.

Création du project RealTimeCharting - Étape 1

Sélectionner alors l'onglet Libraries puis cliquer sur le bouton Add Variable...

Création du project RealTimeCharting - Étape 2

La fenêtre ci-dessous apparait. Sélectionner la variable ECLIPSE_HOME et cliquer sur le bouton Extend...

Création du project RealTimeCharting - Étape 3

Dans la zone de texte de la nouvelle fenêtre, taper *swt*, dérouler le répertoire plugins et sélectionner la bibliothèque SWT. Dans mon cas, cette bibliothèque se nomme org.eclipse.swt.cocoa.macosx.x86_64_3.8.0.v3833.jar, mais tout dépend de votre système d'exploitation et de la version d'Eclipse que vous utilisez.

Création du project RealTimeCharting - Étape 4

Cliquer sur les boutons OK puis Finish pour terminer la création du projet. Nous venons d'ajouter une dépendance à la bibliothèque SWT à notre projet.

II.2 - JFreeChart

Télécharger la version 1.0.19 de JFreeChart en suivant ce lien. Une fois le fichier décompressé, vous devriez obtenir, dans le répertoire jfreechart-1.0.19, l'arborescence présentée dans l'image ci-dessous.

Arborescence du répertoire jfreechart-1.0.19

Dans la suite de cet article nous allons utiliser des bibliothèques et le code source de JFreeChart. Nous allons donc importer entièrement ce répertoire dans le projet. Pour ce faire, sélectionner le projet RealTimeCharting dans la vue Package Explorer puis dans le menu File cliquer sur Import.... La fenêtre ci-dessous apparait, sélectionner l'élément File System puis cliquer sur le bouton Next.

Importation de JFreeChart - Étape 1

Cliquer sur le bouton Browse... pour sélectionner le répertoire de JFreeChart et cocher ce répertoire comme le montre la figure ci-dessous. Ajouter JFreeChart au chemin par défaut pour tout importer dans ce répertoire et cliquer finalement sur le bouton Finish pour terminer l'importation.

Importation de JFreeChart - Étape 2

Vous devriez obtenir le résultat présenté dans l'image de la vue Package Explorer ci-dessous.

Importation de JFreeChart - Étape 3

III - Développement et tests qualitatifs

Nous allons, dans un premier temps, écrire le code de l'interface, c'est à dire la création du contenu de la fenêtre des graphes. Pour afficher des données en temps réel dans ces graphes, il nous faudra, dans un deuxième temps, créer une classe utilitaire chargé d'envoyer des nouvelles informations régulièrement à tous les graphes. Le but est de tester qualitativement la bibliothèque JFreeChart en affichage temps réel, nous ferons des mesures par la suite.

III.1 - Création de la fenêtre des graphes

Cette création va se faire en deux étapes. La première consiste simplement à créer le Shell et la boucle évènementielle classique de SWT. Le code de création des graphes et des courbes de ces graphes sera traité dans la deuxième étape.

Si vous ne connaissez pas la bibliothèque SWT, je vous invite à suivre ce cours de DVP.COM.

PREMIÈRE ÉTAPE : création de la classe JFreeChartRTCTest

C'est parti pour la première étape. Sélectionner le répertoire src du projet puis dans le menu File, cliquer sur New -> Class. Donner com.developpez.realtimecharting.jfreechart comme nom de package et JFreeChartRTCTest comme nom de classe. N'oubliez pas de cocher la boîte pour ajouter la méthode main à cette nouvelle classe avant de cliquer sur le bouton Finish.

Création de la classe de test

Récupérer le code présenté dans le listing ci-dessous.

Code de la classe JFreeChartRTCTest - Étape 1

1. package com.developpez.realtimecharting.jfreechart;

3. import org.eclipse.swt.widgets.Display;

4. import org.eclipse.swt.widgets.Shell;

6. public class JFreeChartRTCTest {

8. /**

9. * The application shell

10. */

11. private static Shell shell;

13. /**

14. * The entry point method for the test.

15. * @param args : not used

16. */

17. public static void main(String[] args) {

18. /*

19. * Create display and shell.

20. * Assign application name, shell title and size.

21. */

22. Display.setAppName("JFreeChart RTC SWT");

23. Display display = new Display();

24. shell = new Shell(display);

25. shell.setText("JFreeChart Real Time Charting with SWT");

26. shell.setMaximized(true);

28. /* Open the shell maximized */

29. shell.open();

31. /* SWT display loop */

32. while (!shell.isDisposed()) {

33. if (!display.readAndDispatch()) display.sleep();

34. }

35. display.dispose();

37. }

39. }

Vous pouvez lancer ce code en cliquant sur Run As -> Java Application dans le menu contextuel de l'éditeur de la classe JFreeChartRTCTest. Ce code ne crée pour l'instant qu'une simple fenêtre avec le titre JFreeChart Real Time Charting with SWT.

DEUXIÈME ÉTAPE : création des graphes et courbes

Nous allons maintenant ajouter les trois bibliothèques suivantes pour construire les graphes dans cette fenêtre :

- jfreechart-1.0.19.jar : bibliothèque de référence de JFreeChart

- jfreechart-1.0.19-swt.jar : bibliothèque permettant d'utiliser JFreeChart dans SWT

- jcommon-1.0.23.jar : bibliothèque utilitaire pour JFreeChart

Pour ce faire, sélectionner ces trois bibliothèques comme le montre l'image de la vue Package Explorer ci-dessous. Puis dans le menu contextuel, cliquer sur Build Path -> Add to Build Path. Ces nouvelles bibliothèques devraient maintenant être visibles dans la liste des bibliothèques référencées.

Ajout de bibliothèques au chemin des classes

Modifier maintenant cette classe pour ajouter la création des graphes et séries. Vous pouvez directement copier le code présenté dans le listing ci-dessous. Les deux paramètres nbCharts et nbSeriesPerChart permettent de régler le nombre de graphes et le nombre de courbes dans chaque graphe. Si par exemple vous modifiez nbCharts à 4, vous aurez le résultat présenté dans l'image d'après. La méthode createChartsArea() crée l'ensemble des graphes disposés dans une disposition de type grille. Elle appelle la méthode createSeries() pour créer le nombre de courbes spécifiées dans chaque graphe.

Code de la classe JFreeChartRTCTest - Étape 2

1. package com.developpez.realtimecharting.jfreechart;

3. import java.awt.Color;

5. import org.eclipse.swt.SWT;

6. import org.eclipse.swt.layout.GridData;

7. import org.eclipse.swt.layout.GridLayout;

8. import org.eclipse.swt.widgets.Display;

9. import org.eclipse.swt.widgets.Shell;

10. import org.jfree.chart.ChartFactory;

11. import org.jfree.chart.JFreeChart;

12. import org.jfree.chart.plot.XYPlot;

13. import org.jfree.data.Range;

14. import org.jfree.data.xy.XYSeries;

15. import org.jfree.data.xy.XYSeriesCollection;

16. import org.jfree.experimental.chart.swt.ChartComposite;

18. public class JFreeChartRTCTest {

20. /**

21. * The number of charts that will be displayed

22. */

23. private static int nbCharts = 1;

25. /**

26. * The number of series per chart

27. */

28. private static int nbSeriesPerChart = 1;

30. /**

31. * An array of chart composite containing each JFree chart

32. */

33. private static ChartComposite[] charts;

35. /**

36. * The application shell

37. */

38. private static Shell shell;

40. /**

41. * This method create JFree chart XYSeries

42. * @param nbSeries number of series to be created in the chart

43. * @param chartComposite SWT chart composite containing the chart

44. */

45. private static void createSeries(int nbSeries, ChartComposite chartComposite) {

46. //Create XY Collection as usual with JFree chart

47. XYSeriesCollection xySeriesCollection = new XYSeriesCollection();

48. //Loop on series number

49. for (int i = 0; i < nbSeries; i++) {

50. //Create series

51. XYSeries xySeries = new XYSeries("Serie" + (i+1), false, false);

52. //Add created series to the collection of chart XY Series collection

53. xySeriesCollection.addSeries(xySeries);

54. }

55. //Assign XY Series collection to chart data set

56. ((XYPlot)chartComposite.getChart().getPlot()).setDataset(xySeriesCollection);

57. }

59. /**

60. * This method creates charts area. A grid layout is assigned to the shell.

61. * If more than one chart will be displayed, grid layout will contain two columns.

62. * In each cell of the grid, a JFree chart XY Line is created and wrapped into a

63. * ChartComposite in order to be displayed with SWT. Finally, we create series

64. * in the current JFree chart.

65. */

66. private static void createChartsArea() {

67. shell.setLayout(new GridLayout((nbCharts>1?2:1), true));

68. charts = new ChartComposite[nbCharts];

69. //Loop on charts

70. for (int i = 0; i < charts.length; i++) {

71. //Create XY line chart

72. JFreeChart jFreeChart = ChartFactory.createXYLineChart(null, null, null, null);

73. //Some appearance

74. jFreeChart.setBackgroundPaint(Color.BLACK);

75. jFreeChart.getLegend().setBackgroundPaint(Color.BLACK);

76. jFreeChart.getLegend().setItemPaint(Color.WHITE);

77. ((XYPlot)jFreeChart.getPlot()).setBackgroundPaint(Color.BLACK);

78. ((XYPlot)jFreeChart.getPlot()).getDomainAxis().setAutoRange(false);

79. ((XYPlot)jFreeChart.getPlot()).getDomainAxis().setRange(new Range(0, 10));

80. ((XYPlot)jFreeChart.getPlot()).getDomainAxis().setTickLabelPaint(Color.WHITE);

81. ((XYPlot)jFreeChart.getPlot()).getRangeAxis().setTickLabelPaint(Color.WHITE);

82. ((XYPlot)jFreeChart.getPlot()).setDomainGridlinesVisible(false);

83. ((XYPlot)jFreeChart.getPlot()).setRangeGridlinesVisible(false);

84. jFreeChart.setAntiAlias(false);

85. //Wrap JFree chart in a Chart composite

86. charts[i] = new ChartComposite(shell, SWT.NONE, jFreeChart, false);

87. charts[i].setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));

88. //Create series in this chart

89. createSeries(nbSeriesPerChart, charts[i]);

90. }

91. }

93. /**

94. * The entry point method for the test.

95. * @param args : not used

96. */

97. public static void main(String[] args) {

98. /*

99. * Create display and shell.

100. * Assign application name, shell title, size and layout.

101. */

102. Display.setAppName("JFreeChart RTC SWT");

103. Display display = new Display ();

104. shell = new Shell (display);

105. shell.setText("JFreeChart Real Time Charting with SWT");

106. shell.setMaximized(true);

108. /* Create charts area */

109. createChartsArea();

111. /* Open the shell to display charts */

112. shell.open ();

114. /* SWT display loop */

115. while (!shell.isDisposed()) {

116. if (!display.readAndDispatch()) display.sleep();

117. }

118. display.dispose();

120. }

122. }

Résultat obtenu à la fin de l'étape 2 (nbCharts = 4 et nbSeriesPerChart = 1)

III.2 - Création de la classe de génération de signaux

À ce niveau du développement, rien n'est dynamique. Il nous faut maintenant ajouter un mécanisme qui permet de générer des données automatiquement et régulièrement. Ce sera le rôle de la classe GenerateData que nous allons créer dès maintenant.

La classe GenerateData

Créer une classe privée, statique et interne à la classe JFreeChartRTCTest nommée GenerateData. Cette classe étend TimerTask. Elle sera utilisée par une instance de la classe Timer afin de fournir un ensemble de valeurs à afficher à toutes les séries de tous les graphes présents dans la fenêtre de l'application. Cette mise à jour de l'affichage sera faite à une certaine cadence, spécifiée dans la méthode schedule(TimerTask task, long delay, long period) de la classe Timer. La méthode run(), responsable de cet affichage, de la classe GenerateData sera appelée à une cadence approximative de 10Hz (c.a.d. toutes les 100ms). Par défaut, la génération comporte cent échantillons d'une sinusoïde, simulant ainsi une acquisition à 1000Hz. Au bout de dix secondes, la génération de données s'arrête et l'application se ferme.

Attention !
Si L'affichage prend trop de temps, c.a.d. plus de 100ms, il serait en pratique impossible d'afficher tous les points acquis. Dans le cas de cette simulation, tous les évènements successifs seront décalés dans le temps comme il est spécifié dans la documentation de la classe Timer

Voici le code de la classe GenerateData :

Attention !
Ce code ne comprend pas les "includes". Si vous copiez ce code tel quel, vous aurez des erreurs de compilation que vous pourrez résoudre en ajoutant les includes correspondants.

Code de la classe GenerateData

1. /**

2. * This class generates data for each series of all charts.

3. * It simulates 1000Hz sin data acquisition. Generation is called every

4. * 100ms, producing 100 samples.

5. */

6. private static class GenerateData extends TimerTask {

8. /**

9. * Samples number

10. */

11. private final int nbSamples = 100;

13. /**

14. * Time (domain in JFreeChart) values

15. */

16. private final Double[] time = new Double[nbSamples];

18. /**

19. * Amplitude (range in JFreeChart) values

20. */

21. private final Double[] y = new Double[nbSamples];

23. /**

24. * Initial time values. Used when new samples are received.

25. */

26. private double initialTime = -1/1000.0;

28. /**

29. * This is the main method of the timer task. It generates 100 samples

30. * for each series that will be displayed in charts at a rate of 100ms.

31. * When this rate is not supported, each events will be delayed, as

32. * specified in Timer class documentation (<a href=

33. * "https://docs.oracle.com/javase/6/docs/api/java/util/Timer.html#schedule(java.util.TimerTask,%20long,%20long)"

34. * >see here</a>). These Delays will be visible on screen (display will jerk).

35. */

36. @Override

37. public void run() {

38. //Loop on charts

39. for (int numChart = 0; numChart < charts.length; numChart++) {

41. final ChartComposite chart = charts[numChart];

43. //Build time array

44. for (int i = 0; i < time.length; i++) time[i] = initialTime + i/1000.0;

46. //Updates must be done in SWT UI thread

47. shell.getDisplay().syncExec(new Runnable() {

48. @Override

49. public void run() {

50. //Loop on series

51. final XYSeriesCollection xySeriesCollection = (XYSeriesCollection) ((XYPlot)chart.getChart().getPlot()).getDataset();

52. for (int j = 0; j < xySeriesCollection.getSeriesCount(); j++) {

53. final XYSeries serie = xySeriesCollection.getSeries(j);

54. //Get series number in chart : it will be used as sin's frequency

55. int frequency = Integer.parseInt(serie.getKey().toString().substring(serie.getKey().toString().length()-1));

56. //Generate sin's samples

57. for (int i = 0; i < y.length; i++) {

58. y[i] = 5*Math.sin(2*Math.PI*(frequency/10.0)*time[i]);

59. //Add sample to corresponding series, Update only when all samples have been added

60. serie.add(time[i], y[i], ((i == y.length - 1)?true:false));

61. }

62. }

63. }

64. });

66. }

67. //Update initialTime for next series samples

68. initialTime = time[time.length - 1] + 1/1000.0;

70. //Stop when 10s have been acquired

71. if(initialTime > 10) {

72. cancel();

73. shell.getDisplay().asyncExec(new Runnable() {

74. @Override

75. public void run() {

76. shell.close();

77. }

78. });

79. }

80. }

82. }

Utilisation de GenerateData

Il nous faut maintenant utiliser cette classe. Quelques modifications dans la méthode main() permettent de lancer le Timer à la cadence voulue et de l'arrêter lorsque l'application se termine.

Modification de la méthode main

1. public static void main(String[] args) {

2. /*

3. * Create display and shell.

4. * Assign application name, shell title, size and layout.

5. */

6. Display.setAppName("JFreeChart SWT");

7. Display display = new Display ();

8. shell = new Shell (display);

9. shell.setText("Real Time JFreeChart SWT");

10. shell.setMaximized(true);

12. /* Create charts area */

13. createChartsArea();

15. /* Open the shell to display charts */

16. shell.open();

18. /* Launch Data timer generation : generates data each 100ms */

19. Timer timer = new Timer();

20. GenerateData generateData = new GenerateData();

21. timer.schedule(generateData, 1000, 100);

23. /* SWT display loop */

24. while (!shell.isDisposed()) {

25. if (!display.readAndDispatch()) display.sleep();

26. }

27. display.dispose();

29. /* Stop data timer */

30. timer.cancel();

32. }

Vous pouvez tester avec un seul graphe et une seule série dans ce graphe, l'affichage semble assez correct. Vous pouvez jouer avec les paramètres nbCharts et nbSeriesPerChart : passez par exemple à six graphes avec deux courbes par graphe. Vous constaterez que ce n'est plus du tout la même chose. Tout dépend bien évidement de votre hardware et nous sommes ici en évaluation qualitative, mais on peut déjà constater que les performances ne sont pas au rendez-vous dès qu'on augmente le nombre de graphes et de courbes. En acquisition de données, il n'est pas rare d'avoir plusieurs graphes avec éventuellement plusieurs courbes par graphe. Dans la dernière partie de cet article, nous allons modifier le code afin de faire des mesures sur les temps d'affichage.

IV - Mesures

Avant de mettre en place une automatisation des mesures, quelques configurations sont nécessaires.

IV.1 - Configurations préalables

Pour réaliser des mesures de temps d'affichage de la bibliothèque JFreeChart, il nous faut modifier le code source de cette bibliothèque. Nous allons donc, dans un premier temps, créer un projet, nommé jfreechart, dans notre répertoire de travail d'Eclipse. Ce projet, une fois configuré, contiendra la code source opérationnel de JFreeChart, légèrement modifié. Nous supprimerons ensuite le lien vers la bibliothèque jfreechart-1.0.19.jar pour en ajouter un vers notre nouveau projet jfreechart avant de rajouter le code de mesure des temps d'affichage.

Création du projet Java jfreechart

- Créer un nouveau projet Java nommé jfreechart

- Copier le contenu du répertoire source de JFreeChart dans le répertoire src du projet jfreechart nouvellement créé

Vous aurez des erreurs de compilation, comme le montre l'image ci-dessous, qui seront résolues un peu plus tard.

Création du projet Java jfreechart

Continuons dans la création de ce projet jfreechart :

- Créer un répertoire lib dans jfreechart

- Copier la bibliothèque jcommon-1.0.23.jar dans ce répertoire à partir du répertoire lib de RealTimeCharting

- Copier également la bibliothèque servlet.jar dans ce répertoire à partir du répertoire lib de RealTimeCharting

- Ajouter ces deux bibliothèques au Build Path de jfreechart

L'image suivante vous montre le résultat : le répertoire lib et les références aux deux bibliothèques.

Les deux bibliothèques et les références

Vous aurez maintenant moins d'erreurs de compilation mais des packages utilisant JavaFX posent encore problèmes. Dans notre cas, il suffit de supprimer ces trois packages : clic droit sur les trois packages sélectionnés, puis cliquer sur Delete et le tour est joué !

Suppression de trois packages

À ce stade, vous ne devriez plus avoir d'erreurs de compilation. Pour utiliser ce nouveau projet Java jfreechart, il nous faut maintenant enlever la référence à la bibliothèque jfreechart-1.0.19.jar dans le projet RealTimeCharting (menu contextuel sur jfreechart-1.0.19.jar dans Referenced Libraries, puis Build Path et Remove From Build Path), pour lui rajouter une référence au projet jfreechart, comme le montre l'image ci-dessous :

Ajout du projet jfreechart au Build Path de RealTimeCharting

Vérifier que l'application fonctionne toujours.

Modification du code pour la mesure des temps d'affichage

Dans le package org.jfree.chart.plot du projet jfreechart, ouvrir la classe XYPlot. Localiser la méthode draw() pour ajouter en tout début et fin quelques instructions de mesure temporelle, comme le montre le code suivant. Il faudra également ajouter la déclaration du tableau drawTimes et son accesseur.

Modification de la méthode main

1. private ArrayList<Long> drawTimes = new ArrayList<Long>(0);

3. /**

4. * Return draw times : elapsed times between start and end of chart drawing

5. *

6. * @return an array of double containing elapsed times, in ms

7. */

8. public double[] getDrawTimes() {

9. double[] values = new double[drawTimes.size()];

10. for (int i = 0; i < values.length; i++) {

11. values[i] = drawTimes.get(i);

12. }

13. return values;

14. }

16. /**

17. ...

18. */

19. @Override public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor,

20. PlotState parentState, PlotRenderingInfo info) {

22. long time = System.nanoTime();

23. ...

24. ...

25. drawTimes.add(System.nanoTime() - time);

27. }

Nous avons maintenant la possibilité de récupérer tous les temps d'affichage de chacun des graphes pour effectuer des calculs statistiques de type moyenne, écart type etc. Pour ce faire, modifier le code de la méthode run() de la classe GenerateData afin qu'elle effectue ces calculs avant la fermeture définitive du programme. Ce code récupère tous les tableaux drawTimes de tous les graphes et les compile dans un HasMap nommé drawTimesMap avant d'appeler la méthode statique compute() de la classe com.developpez.realtimecharting.Statistics avec, entre autres, ce HasMap en paramètre. Le listing suivant présente les modifications apportées à la méthode run().

Modification de la méthode run() de la classe GenerateData

1. ...

2. //Stop when 10s have been acquired

3. if(initialTime > 10) {

4. cancel();

6. //Retrieve all draw times of each chart, saving them in an HashMap

7. HashMap<Integer, double[]> drawTimesMap = new HashMap<Integer, double[]>();

8. for (int numChart = 0; numChart < charts.length; numChart++) {

9. final ChartComposite chart = charts[numChart];

10. double[] drawTimes = chart.getChart().getXYPlot().getDrawTimes();

11. drawTimesMap.put(numChart, drawTimes);

12. }

14. //Compute some statistics on these values

15. Statistics.compute(drawTimesMap, nbCharts, nbSeriesPerChart, "JF");

17. //Close application

18. shell.getDisplay().asyncExec(new Runnable() {

19. @Override

20. public void run() {

21. shell.close();

22. }

23. });

24. }

25. ...

Il faut maintenant créer la classe Statistics dans la package com.developpez.realtimechart avec la méthode statique compute(), comme le montre le code suivant :

Code de la classe Statistics

1. package com.developpez.realtimecharting;

3. import java.io.File;

4. import java.io.FileOutputStream;

5. import java.io.IOException;

6. import java.io.PrintWriter;

7. import java.text.DecimalFormat;

8. import java.text.NumberFormat;

9. import java.util.HashMap;

11. import org.apache.commons.math3.stat.StatUtils;

13. /**

14. * Compute statistics like mean, variance and quartiles

15. */

16. public final class Statistics {

18. /**

19. *

20. * @param drawTimesMap : hashMap containing all the display elasped times

21. * @param nbCharts : the number of charts in the shell

22. * @param nbSeriesPerChart : the number of series per chart

23. * @param library : the library for which we compute these statistics ("JF" : JFreeChart)

24. */

25. public static void compute(HashMap<Integer, double[]> drawTimesMap, int nbCharts, int nbSeriesPerChart, String library) {

26. //Assume there are always the same number of drawing events for each chart

27. int nbDrawEvents = ((double[])drawTimesMap.get(0)).length;

29. //An array which will contains these values,

30. //summed when there are more than one chart

31. double[] totalValues = new double[nbDrawEvents];

32. for (double[] values : drawTimesMap.values()) {

33. for (int i = 0; i < values.length; i++) {

34. totalValues[i] = totalValues[i]  + values[i];

35. }

36. }

38. //Compute statistics

39. double mean = StatUtils.mean(totalValues);

40. double stdDev = Math.sqrt(StatUtils.variance(totalValues));

41. double min = StatUtils.min(totalValues);

42. double max = StatUtils.max(totalValues);

43. double quartile1 = StatUtils.percentile(totalValues, 25);

44. double median = StatUtils.percentile(totalValues, 50);

45. double quartile3 = StatUtils.percentile(totalValues, 75);

46. String condition = "C" + nbCharts + "S" + nbSeriesPerChart + library;

48. //Display results in console

49. System.out.printf("Condition : %s\n", condition);

50. System.out.printf("nbDrawEvents : %d\n", nbDrawEvents);

51. System.out.printf("min : %f ms\n", + min);

52. System.out.printf("quartile1 : %f ms\n", quartile1);

53. System.out.printf("mean : %f ms\n", mean);

54. System.out.printf("median : %f ms\n", median);

55. System.out.printf("quartile3 : %f ms\n", quartile3);

56. System.out.printf("max : %f ms\n", max);

57. System.out.printf("stdDev : %f ms", stdDev);

59. }

61. }

Pour effectuer les calculs dans la méthode statique compute(), j'ai utilisé la bibliothèque Apache Commons Math que vous pouvez télécharger ici. Créer ensuite un répertoire lib dans le projet RealTimeCharting pour y copier le fichier commons-math3-3.4.1.jar. Ajouter cette bibliothèque au Build Path du projet RealTimeCharting.

Vous pouvez maintenant lancer des mesures avec différentes conditions de graphes et de courbes :

- Un graphe et une courbe : C1S1JF (JF pour JFreeChart)

- Un graphe et deux courbes : C1S2JF

- Deux graphes et une courbe : C2S1JF

- Quatre graphes et deux courbes : C4S2JF

- ...

en changeant les valeurs de nbCharts et de nbSeriesPerChart. Des résultats dans la condition C1S1 sont présentés dans l'image ci-dessous : JfreeChart met en moyenne 120ms pour afficher les données avec un écart type de 20ms.

Résultats pour la condition C1S1 : un graphe avec une courbe

Ces valeurs peuvent évidement changer suivant la machine que vous avez, l'essentiel étant de comparer les résultats sur la même machine. Dans le cas de l'exemple précédent, c'est un MacBook Pro, OS10.7.5, CPU 2.3GHz Intel Core i5, RAM 4Go 1.333GHz DDR3, GPU Intel HD Graphics 3000 384MB.

L'inconvénient à ce niveau de développement vient du fait qu'il est nécessaire de changer les paramètres à la main pour faire des mesures dans différentes conditions. L'étape suivante consiste à automatiser ces changements et à enregistrer les mesures dans un fichier texte.

IV.2 - Automatisation des mesures

Pour automatiser les mesures, je vous propose de passer en paramètre de la méthode main() de la classe JFreeChartRTCTest :

- le nombre de graphes

- le nombre de courbes

- le nom du fichier de sauvegarde des mesures

- un booléen permettant de savoir si on ajoute les mesures dans un fichier existant ou si on crée ce fichier

Ajoutons le traitement de ces paramètres à la méthode main() de JFreeChartRTCTest :

Ajout du traitement des paramètre de la méthode main() de la classe JFreeChartRTCTest

1. /*

2. *  Arguments (if not null) : nb chart, nb series per chart,

3. * file name and create or append to results file

4. */

5. public static void main(String[] args) {

7. /* Arguments (if not null) : nb chart, nb series per chart,

8. file name and create or append to results file */

9. if(args != null && args.length == 4) {

10. nbCharts = Integer.parseInt(args[0]);

11. nbSeriesPerChart = Integer.parseInt(args[1]);

12. resultFileName = args[2];

13. createResultFile = Boolean.parseBoolean(args[3]);

14. }

15. ...

N'oubliez pas de rajouter les champs statiques privés resultFileName et createResultFile dans la classe JFreeChartRTCTest, en tant que chaine de caractères et booléen respectivement. Modifions maintenant la méthode compute() de la classe Statistics afin de prendre en charge l'enregistrement des mesures :

Ajout de la création du fichier résultats dans la méthode computeStatictics() de la classe JFreeChartRTCTest

1. ...

2. /**

3. *

4. * @param drawTimesMap : hashMap containing all the display elasped times

5. * @param nbCharts : the number of charts in the shell

6. * @param nbSeriesPerChart : the nomber of series per chart

7. * @param resultFileName : the results file's name, or null

8. * @param createResultFile : create or append to results file's name. No used if resultFileName is null

9. * @param library : the library for which we compute these statistics ("JF" : JFreeChart)

10. */

11. public static void compute(HashMap<Integer, double[]> drawTimesMap, int nbCharts, int nbSeriesPerChart, String resultFileName, boolean createResultFile, String library) {

12. //Assume there are always the same number of drawing events for each chart

13. int nbDrawEvents = ((double[])drawTimesMap.get(0)).length;

15. ...

17. System.out.printf("max : %f ms\n", max);

18. System.out.printf("stdDev : %f ms", stdDev);

20. /* Create or append to result file if necessary */

21. if(resultFileName != null) {

22. try {

23. //We only need two decimals

24. NumberFormat formatter = new DecimalFormat("#0.00");

25. File resultFile = new File(resultFileName);

26. //If results file must be created, delete it if it exists

27. if(createResultFile && resultFile.exists()) resultFile.delete();

28. //Create PrintWriter for results file

29. PrintWriter printWriter = new PrintWriter(new FileOutputStream(resultFile, !createResultFile));

30. //If results file must be created, print header

31. if(createResultFile) printWriter.println("Condition\tnbDrawEvents\tmin(ms)\tQ1(ms)\tmean(ms)\tmedian(ms)\tQ3(ms)\tmax(ms)\tstdDev(ms)");

32. //Print results

33. printWriter.printf("%s\t%d\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", condition, nbDrawEvents , formatter.format(min), formatter.format(quartile1),

34. formatter.format(median), formatter.format(quartile3), formatter.format(max),

35. formatter.format(mean), formatter.format(stdDev));

36. //Close file

37. printWriter.flush();

38. printWriter.close();

39. } catch (IOException e) {

40. e.printStackTrace();

41. }

42. }

43. ...

Pour terminer, créons une classe de tests AllTests dans le package com.developpez.realtimecharting qui regroupera tous les appels aux tests élémentaires :

Création de la classe de tests

1. package com.developpez.realtimecharting;

3. import com.developpez.realtimecharting.jfreechart.JFreeChartRTCTest;

5. /**

6. *

7. * @author frank buloup

8. *

9. */

10. public class AllTests {

12. public static void main(String[] args) {

14. try {

16. String resultsFile = "results.txt";

18. //Condition C1S1

19. JFreeChartRTCTest.main(new String[]{"1", "1", resultsFile, "true"});

20. Thread.sleep(100);

21. //Condition C1S2

22. JFreeChartRTCTest.main(new String[]{"1", "2", resultsFile, "false"});

23. Thread.sleep(100);

24. //Condition C1S3

25. JFreeChartRTCTest.main(new String[]{"1", "3", resultsFile, "false"});

26. Thread.sleep(100);

27. //Condition C1S4

28. JFreeChartRTCTest.main(new String[]{"1", "4", resultsFile, "false"});

29. Thread.sleep(100);

30. //Condition C2S1

31. JFreeChartRTCTest.main(new String[]{"2", "1", resultsFile, "false"});

32. Thread.sleep(100);

33. //Condition C2S2

34. JFreeChartRTCTest.main(new String[]{"2", "2", resultsFile, "false"});

35. Thread.sleep(100);

36. //Condition C2S3

37. JFreeChartRTCTest.main(new String[]{"2", "3", resultsFile, "false"});

38. Thread.sleep(100);

39. //Condition C2S4

40. JFreeChartRTCTest.main(new String[]{"2", "4", resultsFile, "false"});

41. Thread.sleep(100);

42. //Condition C4S1

43. JFreeChartRTCTest.main(new String[]{"4", "1", resultsFile, "false"});

44. Thread.sleep(100);

45. //Condition C4S2

46. JFreeChartRTCTest.main(new String[]{"4", "2", resultsFile, "false"});

47. Thread.sleep(100);

48. //Condition C4S3

49. JFreeChartRTCTest.main(new String[]{"4", "3", resultsFile, "false"});

50. Thread.sleep(100);

51. //Condition C4S4

52. JFreeChartRTCTest.main(new String[]{"4", "4", resultsFile, "false"});

53. Thread.sleep(100);

54. //Condition C6S1

55. JFreeChartRTCTest.main(new String[]{"6", "1", resultsFile, "false"});

56. Thread.sleep(100);

57. //Condition C6S2

58. JFreeChartRTCTest.main(new String[]{"6", "2", resultsFile, "false"});

59. Thread.sleep(100);

60. //Condition C6S3

61. JFreeChartRTCTest.main(new String[]{"6", "3", resultsFile, "false"});

62. Thread.sleep(100);

63. //Condition C6S4

64. JFreeChartRTCTest.main(new String[]{"6", "4", resultsFile, "false"});

65. Thread.sleep(100);

67. System.exit(0);

69. } catch (InterruptedException e) {

70. e.printStackTrace();

71. }

73. }

75. }

Vous devriez obtenir le type de fichier suivant :

Exemple de fichier resultats

(cliquez ici pour aggrandir l'image)

Nous sommes maintenant armés pour analyser les résultats en conclusion de ce premier article.

V - Conclusion

Les résultats qui suivent ont été obtenus sur un iMac, OS X.9.5, CPU 3.1GHz Intel Core i7, RAM 16Go 1.6GHz DDR3, GPU NVIDIA GeForce GT 750M 1024 Mo.

La figure ci-dessous vous présente le diagramme en boîte issu de ces mesures. On constate évidement que les temps d'affichage augmentent avec la complexité du graphique. La valeur médiane de ce temps, dans la condition C1S1JF, c.a.d. la condition la plus simple, est d'environ 70ms. Elle est déjà proche de la cadence à laquelle on demande un renouvellement de l'affichage. Toujours dans les cas où l'on ne considère qu'un seul graphe, la valeur médiane du temps d'affichage augmente avec le nombre de courbes : elle dépasse les 150ms pour C1S4JF ! C'est à dire que JFreeChart met souvent plus de 150ms à afficher les quatre courbes dans le graphe ! Cette analyse peut être renouvelée dans les conditions comportant plus d'un graphe : C2, C3 et C4. La valeur médiane du temps d'affichage pouvant approcher la seconde.

On peut aussi constater que la variabilité augmente également avec la complexité. Les premier et troisième quartiles s'écartent de la valeur médiane et il en est de même pour les valeurs maximales. Toujours sur le diagramme en boîte, il semble que les valeurs minimales ne varient pas, mais c'est un effet d'échelle : elles restent comprises entre 20ms et 65ms, comme le montre l'histogramme des minima.

Le dernier histogramme nous montre les valeurs moyennes en fonction des conditions. L'allure générale est identique au diagramme en boîte et les valeurs moyennes sont proches des médianes pour chaque condition. Par exemple, dans le cas C4S1JF, quatre graphes avec une seule courbe par graphe, cette valeur moyenne est d'environ 180ms ! Pourtant afficher quatre signaux, ça ne devrait pas prendre autant de temps ! JFreeChart n'est effectivement pas une solution pour faire de l'affichage temps réel.

Diagramme en boîtes

(cliquez ici pour aggrandir l'image)

Histogramme des minima

(cliquez ici pour aggrandir l'image)

Histogramme moyenne et erreur standard

(cliquez ici pour aggrandir l'image)

Voici les codes R qui ont été utilisés pour obtenir ces figures :

Code R pour produire le diagramme en boîtes

1. # Once results.txt file has been imported in R environment (with heading),

2. # you can launch this script in order to plot results as boxplot

3. range = 1:16;

4. # Put data in specific vars

5. x = t(results[range,3:7]);

6. cols = c("red","red","red","red","green","green","green","green","blue","blue",

7. "blue","blue","sienna","sienna","sienna","sienna");

8. boxplot(x, main = "Drawing Times (in ms) vs Conditions", names=results$Condition[range], las=2, col=cols, range = 0, cex.axis=0.75);

Code R pour produire l'histogramme des minima

1. # Once results.txt file has been imported in Environment, you can launch this script

2. # Data range to plot

3. rangeCond = 1:16;

4. # Put data in specific vars

5. conditions = results$Condition[rangeCond]

6. means = results$min.ms.[rangeCond]

7. stddev = results$stdDev.ms.[rangeCond]

8. # Prepare bar plot colors and angles

9. colors = c("red", "red", "red", "red", "green", "green", "green", "green", "blue", "blue",

10. "blue", "blue", "sienna", "sienna", "sienna", "sienna")

11. mp <- barplot(means, names.arg = conditions, las = 2, col=colors, ylim=c(0,80), cex.names=0.75);

12. # Plot

13. axis(1, at = mp, labels=FALSE)

14. # Put a title

15. title("Minimum Draw times (in ms) vs Conditions")

Code R pour produire l'histogramme moyenne et erreur standard

1. # Once results.txt file has been imported in Environment, you can launch this script

2. # Data range to plot

3. range = 1:16;

4. # Put data in specific vars

5. conditions = results$Condition[range]

6. means = results$mean.ms.[range]

7. stddev = results$stdDev.ms.[range]

8. # Prepare bar plot colors and angles

9. colors = c("red", "red", "red", "red", "green", "green", "green", "green", "blue",

10. "blue", "blue", "blue", "sienna", "sienna", "sienna", "sienna")

11. mp <- barplot(means, names.arg=conditions, las=2, col=colors, ylim=c(0,2000), cex.names=0.75);

12. # Plot

13. axis(1, at = mp, labels=FALSE)

14. # Put a title

15. title(expression(paste("Mean Draw times (in ms \U00b1", frac(sigma, sqrt(N)), ") vs Conditions")))

16. # Plot errors bars

17. # Standard error of the mean

18. sem = stddev/sqrt(results$nbDrawEvents[range])

19. segments(mp, means - sem, mp, means + sem, lwd=2)

20. segments(mp - 0.1, means - sem, mp + 0.1, means - sem, lwd=2)

21. segments(mp - 0.1, means + sem, mp + 0.1, means + sem, lwd=2)

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article