1 /*
2  * Copyright 2008-2019 by Emeric Vernat
3  *
4  *     This file is part of Java Melody.
5  *
6  * Licensed under the Apache License, Version 2.0 (the "License");
7  * you may not use this file except in compliance with the License.
8  * You may obtain a copy of the License at
9  *
10  *     http://www.apache.org/licenses/LICENSE-2.0
11  *
12  * Unless required by applicable law or agreed to in writing, software
13  * distributed under the License is distributed on an "AS IS" BASIS,
14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  * See the License for the specific language governing permissions and
16  * limitations under the License.
17  */

18 package net.bull.javamelody.internal.model;
19
20 import java.awt.Color;
21 import java.awt.Font;
22 import java.awt.GradientPaint;
23 import java.awt.Paint;
24 import java.io.File;
25 import java.io.FileNotFoundException;
26 import java.io.FilenameFilter;
27 import java.io.IOException;
28 import java.io.OutputStream;
29 import java.nio.charset.StandardCharsets;
30 import java.util.ArrayList;
31 import java.util.Arrays;
32 import java.util.Calendar;
33 import java.util.Collections;
34 import java.util.Comparator;
35 import java.util.HashMap;
36 import java.util.List;
37 import java.util.Locale;
38 import java.util.Map;
39 import java.util.Timer;
40
41 import javax.imageio.ImageIO;
42
43 import org.jrobin.core.ConsolFuns;
44 import org.jrobin.core.FetchRequest;
45 import org.jrobin.core.RrdBackendFactory;
46 import org.jrobin.core.RrdDb;
47 import org.jrobin.core.RrdDbPool;
48 import org.jrobin.core.RrdDef;
49 import org.jrobin.core.RrdException;
50 import org.jrobin.core.Sample;
51 import org.jrobin.core.Util;
52 import org.jrobin.data.DataProcessor;
53 import org.jrobin.data.Plottable;
54 import org.jrobin.graph.RrdGraph;
55 import org.jrobin.graph.RrdGraphDef;
56
57 import net.bull.javamelody.Parameter;
58 import net.bull.javamelody.internal.common.I18N;
59 import net.bull.javamelody.internal.common.LOG;
60 import net.bull.javamelody.internal.common.Parameters;
61
62 /**
63  * Stockage RRD et graphiques statistiques.
64  * Cette classe utilise <a href='http://www.jrobin.org/index.php/Main_Page'>JRobin</a>
65  * qui est une librairie Java opensource (LGPL) similaire à <a href='http://oss.oetiker.ch/rrdtool/'>RRDTool</a>.
66  * L'API et le tutorial JRobin sont à http://oldwww.jrobin.org/api/index.html
67  * @author Emeric Vernat
68  */

69 public final class JRobin {
70     public static final int SMALL_HEIGHT = 50;
71     private static final Color PERCENTILE_COLOR = new Color(200, 50, 50);
72     private static final Color LIGHT_RED = Color.RED.brighter().brighter();
73     private static final Paint SMALL_GRADIENT = new GradientPaint(0, 0, LIGHT_RED, 0, SMALL_HEIGHT,
74             Color.GREEN, false);
75     private static final int HOUR = 60 * 60;
76     private static final int DAY = 24 * HOUR;
77     private static final int DEFAULT_OBSOLETE_GRAPHS_DAYS = 90;
78     private static final int DEFAULT_MAX_RRD_DISK_USAGE_MB = 20;
79
80     // pool of open RRD files
81     private final RrdDbPool rrdPool = getRrdDbPool();
82     private final String application;
83     private final String name;
84     private final String rrdFileName;
85     private final int step;
86     private final String requestName;
87
88     private static final class AppContextClassLoaderLeakPrevention {
89         private AppContextClassLoaderLeakPrevention() {
90             super();
91         }
92
93         static {
94             // issue 476: appContextProtection is disabled by default in JreMemoryLeakPreventionListener since Tomcat 7.0.42,
95             // so protect from sun.awt.AppContext ourselves
96             final ClassLoader loader = Thread.currentThread().getContextClassLoader();
97             try {
98                 // Use the system classloader as the victim for this ClassLoader pinning we're about to do.
99                 Thread.currentThread().setContextClassLoader(ClassLoader.getSystemClassLoader());
100
101                 // Trigger a call to sun.awt.AppContext.getAppContext().
102                 // This will pin the system class loader in memory but that shouldn't be an issue.
103                 ImageIO.getCacheDirectory();
104             } catch (final Throwable t) { // NOPMD
105                 LOG.info("prevention of AppContext ClassLoader leak failed, skipping");
106             } finally {
107                 Thread.currentThread().setContextClassLoader(loader);
108             }
109         }
110
111         static void dummy() {
112             // just to initialize the class
113         }
114     }
115
116     private JRobin(String application, String name, File rrdFile, int step, String requestName)
117             throws RrdException, IOException {
118         super();
119         assert application != null;
120         assert name != null;
121         assert rrdFile != null;
122         assert step > 0;
123         // requestName est null pour un compteur
124
125         this.application = application;
126         this.name = name;
127         this.rrdFileName = rrdFile.getPath();
128         this.step = step;
129         this.requestName = requestName;
130
131         init();
132     }
133
134     public static void stop() {
135         if (RrdNioBackend.getFileSyncTimer() != null) {
136             RrdNioBackend.getFileSyncTimer().cancel();
137         }
138     }
139
140     /**
141      * JavaMelody uses a custom RrdNioBackendFactory,
142      * in order to use its own and cancelable file sync timer.
143      * @param timer Timer
144      * @throws IOException e
145      */

146     public static void initBackendFactory(Timer timer) throws IOException {
147         RrdNioBackend.setFileSyncTimer(timer);
148
149         try {
150             if (!RrdBackendFactory.getDefaultFactory().getFactoryName()
151                     .equals(RrdNioBackendFactory.FACTORY_NAME)) {
152                 RrdBackendFactory.registerAndSetAsDefaultFactory(new RrdNioBackendFactory());
153             }
154         } catch (final RrdException e) {
155             throw createIOException(e);
156         }
157     }
158
159     static JRobin createInstance(String application, String name, String requestName)
160             throws IOException {
161         final File rrdFile = getRrdFile(application, name);
162         final int step = Parameters.getResolutionSeconds();
163         try {
164             return new JRobin(application, name, rrdFile, step, requestName);
165         } catch (final RrdException e) {
166             throw createIOException(e);
167         }
168     }
169
170     static JRobin createInstanceIfFileExists(String application, String name, String requestName)
171             throws IOException {
172         final File rrdFile = getRrdFile(application, name);
173         if (rrdFile.exists()) {
174             final int step = Parameters.getResolutionSeconds();
175             try {
176                 return new JRobin(application, name, rrdFile, step, requestName);
177             } catch (final RrdException e) {
178                 throw createIOException(e);
179             }
180         }
181         return null;
182     }
183
184     private static File getRrdFile(String application, String name) {
185         final File dir = Parameters.getStorageDirectory(application);
186         return new File(dir, name + ".rrd");
187     }
188
189     private void init() throws IOException, RrdException {
190         final File rrdFile = new File(rrdFileName);
191         final File rrdDirectory = rrdFile.getParentFile();
192         if (!rrdDirectory.mkdirs() && !rrdDirectory.exists()) {
193             throw new IOException(
194                     "JavaMelody directory can't be created: " + rrdDirectory.getPath());
195         }
196         // cf issue 41: rrdFile could have been created with length 0 if out of disk space
197         // (fix IOException: Read failed, file xxx.rrd not mapped for I/O)
198         if (!rrdFile.exists() || rrdFile.length() == 0) {
199             // create RRD file since it does not exist (or is empty)
200             final RrdDef rrdDef = new RrdDef(rrdFileName, step);
201             // "startTime" décalé de "step" pour éviter que addValue appelée juste
202             // après ne lance l'exception suivante la première fois
203             // "Bad sample timestamp x. Last update time was x, at least one second step is required"
204             rrdDef.setStartTime(Util.getTime() - step);
205             // single gauge datasource
206             final String dsType = "GAUGE";
207             // max time before "unknown value"
208             final int heartbeat = step * 2;
209             rrdDef.addDatasource(getDataSourceName(), dsType, heartbeat, 0, Double.NaN);
210             // several archives
211             final String average = ConsolFuns.CF_AVERAGE;
212             final String max = ConsolFuns.CF_MAX;
213             // 1 jour
214             rrdDef.addArchive(average, 0.25, 1, DAY / step);
215             rrdDef.addArchive(max, 0.25, 1, DAY / step);
216             // 1 semaine
217             rrdDef.addArchive(average, 0.25, HOUR / step, 7 * 24);
218             rrdDef.addArchive(max, 0.25, HOUR / step, 7 * 24);
219             // 1 mois
220             rrdDef.addArchive(average, 0.25, 6 * HOUR / step, 31 * 4);
221             rrdDef.addArchive(max, 0.25, 6 * HOUR / step, 31 * 4);
222             // 2 ans (1 an pour période "1 an" et 2 ans pour période "tout")
223             rrdDef.addArchive(average, 0.25, 8 * 6 * HOUR / step, 2 * 12 * 15);
224             rrdDef.addArchive(max, 0.25, 8 * 6 * HOUR / step, 2 * 12 * 15);
225             // create RRD file in the pool
226             final RrdDb rrdDb = rrdPool.requestRrdDb(rrdDef);
227             rrdPool.release(rrdDb);
228         }
229     }
230
231     private void resetFile() throws IOException {
232         deleteFile();
233         try {
234             init();
235         } catch (final RrdException e) {
236             throw createIOException(e);
237         }
238     }
239
240     public byte[] graph(Range range, int width, int height) throws IOException {
241         return graph(range, width, height, false);
242     }
243
244     public byte[] graph(Range range, int width, int height, boolean maxHidden) throws IOException {
245         // static init of the AppContext ClassLoader
246         AppContextClassLoaderLeakPrevention.dummy();
247
248         try {
249             // Rq : il pourrait être envisagé de récupérer les données dans les fichiers rrd ou autre stockage
250             // puis de faire des courbes en sparklines html (écrites dans la page html) ou jfreechart
251
252             // create common part of graph definition
253             final RrdGraphDef graphDef = new RrdGraphDef();
254             if (Locale.CHINESE.getLanguage()
255                     .equals(I18N.getResourceBundle().getLocale().getLanguage())) {
256                 graphDef.setSmallFont(new Font(Font.MONOSPACED, Font.PLAIN, 10));
257                 graphDef.setLargeFont(new Font(Font.MONOSPACED, Font.BOLD, 12));
258             }
259
260             initGraphSource(graphDef, height, maxHidden, range);
261
262             initGraphPeriodAndSize(range, width, height, graphDef);
263
264             graphDef.setImageFormat("png");
265             graphDef.setFilename("-");
266             // il faut utiliser le pool pour les performances
267             // et pour éviter des erreurs d'accès concurrents sur les fichiers
268             // entre différentes générations de graphs et aussi avec l'écriture des données
269             graphDef.setPoolUsed(true);
270             return new RrdGraph(graphDef).getRrdGraphInfo().getBytes();
271         } catch (final RrdException e) {
272             throw createIOException(e);
273         }
274     }
275
276     private void initGraphPeriodAndSize(Range range, int width, int height, RrdGraphDef graphDef) {
277         // ending timestamp is the (current) timestamp in seconds
278         // starting timestamp will be adjusted for each graph
279         final long endTime = range.getJRobinEndTime();
280         final long startTime = range.getJRobinStartTime();
281         final String label = getLabel();
282         final String titleStart;
283         if (label.length() > 31 && width <= 200) {
284             // si le label est trop long, on raccourci le titre sinon il ne rentre pas
285             titleStart = label;
286         } else {
287             titleStart = label + " - " + range.getLabel();
288         }
289         final String titleEnd;
290         if (width > 400) {
291             if (range.getPeriod() == null) {
292                 titleEnd = " - " + I18N.getFormattedString("sur", getApplication());
293             } else {
294                 titleEnd = " - " + I18N.getCurrentDate() + ' '
295                         + I18N.getFormattedString("sur", getApplication());
296             }
297         } else {
298             titleEnd = "";
299             if (range.getPeriod() == null) {
300                 // si période entre 2 dates et si pas de zoom,
301                 // alors on réduit de 2 point la fonte du titre pour qu'il rentre dans le cadre
302                 graphDef.setLargeFont(graphDef.getLargeFont()
303                         .deriveFont(graphDef.getLargeFont().getSize2D() - 2f));
304             }
305         }
306         graphDef.setStartTime(startTime);
307         graphDef.setEndTime(endTime);
308         graphDef.setTitle(titleStart + titleEnd);
309         graphDef.setFirstDayOfWeek(
310                 Calendar.getInstance(I18N.getCurrentLocale()).getFirstDayOfWeek());
311         // or if the user locale patch is merged we should do:
312         // (https://sourceforge.net/tracker/?func=detail&aid=3403733&group_id=82668&atid=566807)
313         //graphDef.setLocale(I18N.getCurrentLocale());
314
315         // rq : la largeur et la hauteur de l'image sont plus grandes que celles fournies
316         // car jrobin ajoute la largeur et la hauteur des textes et autres
317         graphDef.setWidth(width);
318         graphDef.setHeight(height);
319         if (width <= 100) {
320             graphDef.setNoLegend(true);
321             graphDef.setUnitsLength(0);
322             graphDef.setShowSignature(false);
323             graphDef.setTitle(null);
324         }
325         //        graphDef.setColor(RrdGraphConstants.COLOR_BACK, new GradientPaint(0, 0,
326         //                RrdGraphConstants.DEFAULT_BACK_COLOR.brighter(), 0, height,
327         //                RrdGraphConstants.DEFAULT_BACK_COLOR));
328     }
329
330     private void initGraphSource(RrdGraphDef graphDef, int height, boolean maxHidden, Range range)
331             throws IOException {
332         final String dataSourceName = getDataSourceName();
333         final String average = "average";
334         graphDef.datasource(average, rrdFileName, dataSourceName, ConsolFuns.CF_AVERAGE);
335         graphDef.setMinValue(0);
336         final String moyenneLabel = I18N.getString("Moyenne");
337         graphDef.area(average, getPaint(height), moyenneLabel);
338         graphDef.gprint(average, ConsolFuns.CF_AVERAGE, moyenneLabel + ": %9.0f %S\\r");
339         //graphDef.gprint(average, ConsolFuns.CF_MIN, "Minimum: %9.0f %S\\r");
340         if (!maxHidden) {
341             final String max = "max";
342             graphDef.datasource(max, rrdFileName, dataSourceName, ConsolFuns.CF_MAX);
343             final String maximumLabel = I18N.getString("Maximum");
344             graphDef.line(max, Color.BLUE, maximumLabel);
345             graphDef.gprint(max, ConsolFuns.CF_MAX, maximumLabel + ": %9.0f %S\\r");
346         }
347         if (height > 200) {
348             final double percentileValue = get95PercentileValue(range);
349             final Plottable constantDataSource = new Plottable() {
350                 @Override
351                 public double getValue(long timestamp) {
352                     return percentileValue;
353                 }
354             };
355             final String percentile = "percentile";
356             graphDef.datasource(percentile, constantDataSource);
357             final String percentileLabel = I18N.getString("95percentile");
358             graphDef.line(percentile, PERCENTILE_COLOR, percentileLabel);
359             graphDef.gprint(percentile, ConsolFuns.CF_MAX, ":%9.0f %S\\r");
360         }
361         // graphDef.comment("JRobin :: RRDTool Choice for the Java World");
362     }
363
364     private static Paint getPaint(int height) {
365         // si on avait la moyenne globale/glissante des valeurs et l'écart type
366         // on pourrait mettre vert si < moyenne + 1 écart type puis orange puis rouge si > moyenne + 2 écarts types,
367         // en utilisant LinearGradientPaint par exemple, ou bien selon paramètres de plages de couleurs par graphe
368         if (height == SMALL_HEIGHT) {
369             // design pattern fly-weight (poids-mouche) pour le cas des 9 ou 12 graphs
370             // dans la page html de départ
371             return SMALL_GRADIENT;
372         }
373         return new GradientPaint(0, 0, LIGHT_RED, 0, height, Color.GREEN, false);
374     }
375
376     void addValue(double value) throws IOException {
377         try {
378             // request RRD database reference from the pool
379             final RrdDb rrdDb = rrdPool.requestRrdDb(rrdFileName);
380             synchronized (rrdDb) {
381                 try {
382                     // create sample with the current timestamp
383                     final Sample sample = rrdDb.createSample();
384                     // test pour éviter l'erreur suivante au redéploiement par exemple:
385                     // org.jrobin.core.RrdException:
386                     // Bad sample timestamp x. Last update time was x, at least one second step is required
387                     if (sample.getTime() > rrdDb.getLastUpdateTime()) {
388                         // set value for load datasource
389                         sample.setValue(getDataSourceName(), value);
390                         // update database
391                         sample.update();
392                     }
393                 } finally {
394                     // release RRD database reference
395                     rrdPool.release(rrdDb);
396                 }
397             }
398         } catch (final FileNotFoundException e) {
399             if (e.getMessage() != null && e.getMessage().endsWith("[non existent]")) {
400                 // cf issue 255
401                 LOG.debug("A JRobin file was deleted and created again: "
402                         + new File(rrdFileName).getPath());
403                 resetFile();
404                 addValue(value);
405             }
406         } catch (final RrdException e) {
407             if (e.getMessage() != null && e.getMessage().startsWith("Invalid file header")) {
408                 // le fichier RRD a été corrompu, par exemple en tuant le process java au milieu
409                 // d'un write, donc on efface le fichier corrompu et on le recrée pour corriger
410                 // le problème
411                 LOG.debug("A JRobin file was found corrupted and was reset: "
412                         + new File(rrdFileName).getPath());
413                 resetFile();
414                 addValue(value);
415             }
416             throw createIOException(e);
417         } catch (final IllegalArgumentException | ArithmeticException e) {
418             // catch IllegalArgumentException for issue 533:
419             //            java.lang.IllegalArgumentException
420             //            at java.nio.Buffer.position(Buffer.java:244)
421             //            at net.bull.javamelody.RrdNioBackend.read(RrdNioBackend.java:147)
422             //          ...
423             //            at org.jrobin.core.RrdDbPool.requestRrdDb(RrdDbPool.java:103)
424             //            at net.bull.javamelody.JRobin.addValue(JRobin.java:334)
425
426             // ou catch ArithmeticException for issue 139 / JENKINS-51590:
427             //        java.lang.ArithmeticException : / by zero
428             //        at org.jrobin.core.Archive.archive(Archive.java:129)
429             //        at org.jrobin.core.RrdDb.archive(RrdDb.java:720)
430             //        at org.jrobin.core.Datasource.process(Datasource.java:201)
431             //        at org.jrobin.core.RrdDb.store(RrdDb.java:593)
432             //        at org.jrobin.core.Sample.update(Sample.java:228)
433             //        at net.bull.javamelody.internal.model.JRobin.addValue(JRobin.java:374)
434
435             // le fichier RRD a été corrompu, par exemple en tuant le process java au milieu
436             // d'un write, donc on efface le fichier corrompu et on le recrée pour corriger
437             // le problème
438             LOG.debug("A JRobin file was found corrupted and was reset: "
439                     + new File(rrdFileName).getPath());
440             resetFile();
441             addValue(value);
442             throw createIOException(e);
443         }
444     }
445
446     public double getLastValue() throws IOException {
447         try {
448             // request RRD database reference from the pool
449             final RrdDb rrdDb = rrdPool.requestRrdDb(rrdFileName);
450             try {
451                 return rrdDb.getLastDatasourceValue(getDataSourceName());
452             } finally {
453                 // release RRD database reference
454                 rrdPool.release(rrdDb);
455             }
456         } catch (final RrdException e) {
457             throw createIOException(e);
458         }
459     }
460
461     public void dumpXml(OutputStream output, Range range) throws IOException {
462         try {
463             // request RRD database reference from the pool
464             final RrdDb rrdDb = rrdPool.requestRrdDb(rrdFileName);
465             try {
466                 if (range.getPeriod() == Period.TOUT) {
467                     rrdDb.exportXml(output);
468                 } else {
469                     final FetchRequest fetchRequest = rrdDb.createFetchRequest(
470                             ConsolFuns.CF_AVERAGE, range.getJRobinStartTime(),
471                             range.getJRobinEndTime());
472                     final String xml = fetchRequest.fetchData().exportXml()
473                             .replaceFirst("<file>.*</file>""");
474                     output.write(xml.getBytes(StandardCharsets.UTF_8));
475                 }
476             } finally {
477                 // release RRD database reference
478                 rrdPool.release(rrdDb);
479             }
480         } catch (final RrdException e) {
481             throw createIOException(e);
482         }
483     }
484
485     public String dumpTxt(Range range) throws IOException {
486         try {
487             // request RRD database reference from the pool
488             final RrdDb rrdDb = rrdPool.requestRrdDb(rrdFileName);
489             try {
490                 if (range.getPeriod() == Period.TOUT) {
491                     return rrdDb.dump();
492                 }
493                 final FetchRequest fetchRequest = rrdDb.createFetchRequest(ConsolFuns.CF_AVERAGE,
494                         range.getJRobinStartTime(), range.getJRobinEndTime());
495                 return fetchRequest.fetchData().dump();
496             } finally {
497                 // release RRD database reference
498                 rrdPool.release(rrdDb);
499             }
500         } catch (final RrdException e) {
501             throw createIOException(e);
502         }
503     }
504
505     double getMeanValue(Range range) throws IOException {
506         assert range.getPeriod() == null;
507         try {
508             final DataProcessor dproc = processData(range);
509             return dproc.getAggregate("average", ConsolFuns.CF_AVERAGE);
510         } catch (final RrdException e) {
511             throw createIOException(e);
512         }
513     }
514
515     private double get95PercentileValue(Range range) throws IOException {
516         try {
517             final DataProcessor dproc = processData(range);
518             // 95th percentile et non un autre percentile par choix
519             return dproc.get95Percentile("average");
520         } catch (final RrdException e) {
521             throw createIOException(e);
522         }
523     }
524
525     private DataProcessor processData(Range range) throws IOException, RrdException {
526         final String dataSourceName = getDataSourceName();
527         final long endTime = range.getJRobinEndTime();
528         final long startTime = range.getJRobinStartTime();
529         final DataProcessor dproc = new DataProcessor(startTime, endTime);
530         dproc.addDatasource("average", rrdFileName, dataSourceName, ConsolFuns.CF_AVERAGE);
531         dproc.setPoolUsed(true);
532         dproc.processData();
533         return dproc;
534     }
535
536     boolean deleteFile() {
537         return new File(rrdFileName).delete();
538     }
539
540     private String getApplication() {
541         return application;
542     }
543
544     public String getName() {
545         return name;
546     }
547
548     private String getDataSourceName() {
549         // RrdDef.addDatasource n'accepte pas un nom de datasource supérieur à 20 caractères
550         return name.substring(0, Math.min(20, name.length()));
551     }
552
553     public String getLabel() {
554         if (requestName == null) {
555             // c'est un jrobin global issu soit de JavaInformations soit d'un Counter dans le Collector
556             return I18N.getString(getName());
557         }
558         // c'est un jrobin issu d'un CounterRequest dans le Collector
559         final String shortRequestName = requestName.substring(0,
560                 Math.min(30, requestName.length()));
561         // plus nécessaire:  if (getName().startsWith("error")) {
562         // c'est un jrobin issu d'un CounterRequest du Counter "error"
563         // return I18N.getString("Erreurs_par_minute_pour") + ' ' + shortRequestName; }
564         return I18N.getFormattedString("Temps_moyens_de", shortRequestName);
565     }
566
567     private static IOException createIOException(Exception e) {
568         // Rq: le constructeur de IOException avec message et cause n'existe qu'en jdk 1.6
569         return new IOException(e.getMessage(), e);
570     }
571
572     static long deleteObsoleteJRobinFiles(String application) {
573         final Calendar nowMinusThreeMonthsAndADay = Calendar.getInstance();
574         nowMinusThreeMonthsAndADay.add(Calendar.DAY_OF_YEAR, -getObsoleteGraphsDays());
575         nowMinusThreeMonthsAndADay.add(Calendar.DAY_OF_YEAR, -1);
576         final long timestamp = Util.getTimestamp(nowMinusThreeMonthsAndADay);
577         final int counterRequestIdLength = new CounterRequest("""").getId().length();
578         long diskUsage = 0;
579         final Map<String, Long> lastUpdateTimesByPath = new HashMap<>();
580         final List<File> rrdFiles = new ArrayList<>(listRrdFiles(application));
581         for (final File file : rrdFiles) {
582             // on ne supprime que les fichiers rrd de requêtes (les autres sont peu nombreux)
583             if (file.getName().length() > counterRequestIdLength
584                     && file.lastModified() < nowMinusThreeMonthsAndADay.getTimeInMillis()) {
585                 final long lastUpdateTime = getLastUpdateTime(file);
586                 lastUpdateTimesByPath.put(file.getPath(), lastUpdateTime);
587                 final boolean obsolete = lastUpdateTime < timestamp;
588                 boolean deleted = false;
589                 if (obsolete) {
590                     deleted = file.delete();
591                 }
592                 if (!deleted) {
593                     diskUsage += file.length();
594                 }
595             } else {
596                 diskUsage += file.length();
597             }
598         }
599         final long maxRrdDiskUsage = getMaxRrdDiskUsageMb() * 1024L * 1024L;
600         if (diskUsage > maxRrdDiskUsage) {
601             // sort rrd files from least to most recently used
602             for (final File file : rrdFiles) {
603                 if (lastUpdateTimesByPath.get(file.getPath()) == null) {
604                     lastUpdateTimesByPath.put(file.getPath(), getLastUpdateTime(file));
605                 }
606             }
607             final Comparator<File> comparatorByLastUpdateTime = new Comparator<File>() {
608                 @Override
609                 public int compare(File o1, File o2) {
610                     return lastUpdateTimesByPath.get(o1.getPath())
611                             .compareTo(lastUpdateTimesByPath.get(o2.getPath()));
612                 }
613             };
614             Collections.sort(rrdFiles, comparatorByLastUpdateTime);
615             // delete least recently used rrd files until rrd disk usage < 20 MB
616             for (final File file : rrdFiles) {
617                 if (diskUsage < maxRrdDiskUsage) {
618                     break;
619                 }
620                 if (file.getName().length() > counterRequestIdLength) {
621                     final long length = file.length();
622                     if (file.delete()) {
623                         diskUsage -= length;
624                     }
625                 }
626             }
627         }
628
629         return diskUsage;
630     }
631
632     private static long getLastUpdateTime(File file) {
633         try {
634             final RrdDbPool rrdPool = getRrdDbPool();
635             final RrdDb rrdDb = rrdPool.requestRrdDb(file.getPath());
636             final long lastUpdateTime = rrdDb.getLastUpdateTime();
637             rrdPool.release(rrdDb);
638             return lastUpdateTime;
639         } catch (final IOException | RrdException e) {
640             return file.lastModified() / 1000L;
641         }
642     }
643
644     private static long getMaxRrdDiskUsageMb() {
645         final String param = Parameters.getParameterValue(Parameter.MAX_RRD_DISK_USAGE_MB);
646         if (param != null) {
647             // lance une NumberFormatException si ce n'est pas un nombre
648             final int result = Integer.parseInt(param);
649             if (result <= 0) {
650                 throw new IllegalStateException(
651                         "The parameter max-rrd-disk-usage-mb should be > 0 (20 recommended)");
652             }
653             return result;
654         }
655         return DEFAULT_MAX_RRD_DISK_USAGE_MB;
656     }
657
658     /**
659      * @return Nombre de jours avant qu'un fichier de graphique JRobin (extension .rrd) qui n'est plus utilisé,
660      * soit considéré comme obsolète et soit supprimé automatiquement, à minuit (90 par défaut, soit 3 mois).
661      */

662     private static int getObsoleteGraphsDays() {
663         final String param = Parameter.OBSOLETE_GRAPHS_DAYS.getValue();
664         if (param != null) {
665             // lance une NumberFormatException si ce n'est pas un nombre
666             final int result = Integer.parseInt(param);
667             if (result <= 0) {
668                 throw new IllegalStateException(
669                         "The parameter obsolete-graphs-days should be > 0 (90 recommended)");
670             }
671             return result;
672         }
673         return DEFAULT_OBSOLETE_GRAPHS_DAYS;
674     }
675
676     private static List<File> listRrdFiles(String application) {
677         final File storageDir = Parameters.getStorageDirectory(application);
678         // filtre pour ne garder que les fichiers d'extension .rrd et pour éviter d'instancier des File inutiles
679         final FilenameFilter filenameFilter = new FilenameFilter() {
680             /** {@inheritDoc} */
681             @Override
682             public boolean accept(File dir, String fileName) {
683                 return fileName.endsWith(".rrd");
684             }
685         };
686         final File[] files = storageDir.listFiles(filenameFilter);
687         if (files == null) {
688             return Collections.emptyList();
689         }
690         return Arrays.asList(files);
691     }
692
693     private static RrdDbPool getRrdDbPool() throws IOException {
694         try {
695             return RrdDbPool.getInstance();
696         } catch (final RrdException e) {
697             throw createIOException(e);
698         }
699     }
700
701     @Override
702     public String toString() {
703         return getClass().getSimpleName() + "[application=" + getApplication() + ", name="
704                 + getName() + ']';
705     }
706
707     //  public void test() throws RrdException, IOException {
708     //    for (int i = 1000; i > 0; i--) {
709     //      // request RRD database reference from the pool
710     //      RrdDb rrdDb = rrdPool.requestRrdDb(rrdFileName);
711     //      // create sample with the current timestamp
712     //      Sample sample = rrdDb.createSample(Util.getTime() - 120 * i);
713     //      // set value for load datasource
714     //      // println(i + " " + new byte[5000]);
715     //      sample.setValue(name, Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory());
716     //      // update database
717     //      sample.update();
718     //      // release RRD database reference
719     //      rrdPool.release(rrdDb);
720     //    }
721     //
722     //    graph(Period.JOUR);
723     //    graph(Period.SEMAINE);
724     //    graph(Period.MOIS);
725     //    graph(Period.ANNEE);
726     //  }
727     //
728     //  public static void main(String[] args) throws IOException, RrdException {
729     //    new JRobin("Mémoire""jrobin", 120).test();
730     //  }
731 }
732