1

I use JFreeChart to create various kinds of charts en masse, based on huge amounts of data. Those charts are only meant to be written to PNG files on the hard drive; no JavaFX, Swing, AWT or other GUI is involved at all.

In order to speed up the generation of those charts, I decided to implement parallelisation with one thread per processor core:

final int cores = Runtime.getRuntime().availableProcessors();

try (final ExecutorService threadPool = Executors.newFixedThreadPool(cores)) {
    for (final YearMonth yearMonth : dataset.keySet()) {
        final Runnable task = () -> {
            // Call JFreeChart …
        };

        threadPool.submit(task);
    }

    threadPool.shutdown();
    threadPool.awaitTermination(Long.MAX_VALUE, java.util.concurrent.TimeUnit.NANOSECONDS);
}

When looking at the timeline in order to verify optimal throughput (i.e. CPU usage as high as possible), I noticed something odd: Between two parallelised phases (with one thread per core running) there was a long phase where only one thread was running and all others sleeping.

In order to check that I didn't accidentally break my own multi-threading, I created the following MRE:

    @Test
    public void testMultiThreadingClassic() throws InterruptedException {
        final int cores = Runtime.getRuntime().availableProcessors();
        final CountDownLatch latch = new CountDownLatch(cores);

        for (int i = 0; i < cores; i++) {
            final int index = i;
            new Thread(() -> {
                try {
                    this.createFakeChart(
                        "Fake Chart " + index,
                        new File("fake-chart-%s.png".formatted(index))
                    );
                } catch (final IOException e) {
                    LOG.error("Could not create fake chart", e);
                    System.exit(1);
                } finally {
                    latch.countDown();
                }
            }).start();
        }

        latch.await();
    }

    private void createFakeChart(String title, File outputFile) throws IOException {
        final TimeSeries timeSeries = new TimeSeries("Test Series");

        final LocalDateTime startDate = LocalDateTime.of(2020, 1, 1, 0, 0);
        final LocalDateTime endDate = LocalDateTime.of(2024, 1, 1, 0, 0);
        LocalDateTime currentDate = startDate;

        while (!currentDate.isAfter(endDate)) {
            timeSeries.add(
                new org.jfree.data.time.Day(
                    currentDate.getDayOfMonth(),
                    currentDate.getMonthValue(),
                    currentDate.getYear()
                ),
                Math.random() * 100
            );

            currentDate = currentDate.plusDays(1);
        }

        final TimeSeriesCollection dataset = new TimeSeriesCollection();
        dataset.addSeries(timeSeries);
        final IntervalXYDataset xyBarDataset = new XYBarDataset(dataset, 24 * 60 * 60 * 1000L);

        final JFreeChart chart = ChartFactory.createXYBarChart(
            title,
            "Day",
            true,
            "Random number",
            xyBarDataset
        );

        final DateAxis dateAxis = new DateAxis();
        dateAxis.setDateFormatOverride(new SimpleDateFormat("yyyy-MM-dd"));
        dateAxis.setTickUnit(new DateTickUnit(DateTickUnitType.YEAR, 1));
        chart.getXYPlot().setDomainAxis(dateAxis);

        ChartUtils.saveChartAsPNG(
            outputFile,
            chart,
            1000,
            500
        );
    }

Here's the timeline at two points in time:

enter image description here

enter image description here

Here's the first stack trace (Thread-0, 00:00:05.565):

createFontConfiguration:690, X11FontManager (sun.awt)
<init>:341, SunFontManager (sun.font)
<init>:35, FcFontManager (sun.awt)
<init>:55, X11FontManager (sun.awt)
createFontManager:37, PlatformFontInfo (sun.font)
getInstance:51, FontManagerFactory (sun.font)
getInstance:240, SunFontManager (sun.font)
getMetrics:260, FontDesignMetrics (sun.font)
getFontMetrics:862, SunGraphics2D (sun.java2d)
getStringWidth:64, G2TextMeasurer (org.jfree.chart.text)
nextLineBreak:227, TextUtils (org.jfree.chart.text)
createTextBlock:168, TextUtils (org.jfree.chart.text)
arrangeRR:569, TextTitle (org.jfree.chart.title)
arrange:443, TextTitle (org.jfree.chart.title)
drawTitle:1233, JFreeChart (org.jfree.chart)
draw:1149, JFreeChart (org.jfree.chart)
createBufferedImage:1316, JFreeChart (org.jfree.chart)
createBufferedImage:1297, JFreeChart (org.jfree.chart)
writeChartAsPNG:146, ChartUtils (org.jfree.chart)
saveChartAsPNG:272, ChartUtils (org.jfree.chart)
saveChartAsPNG:247, ChartUtils (org.jfree.chart)
createFakeChart:215, JFreeChartTest (com.example.jfreechart)
lambda$testMultiThreadingClassic$0:191, JFreeChartTest (com.example.jfreechart)
runWith:1460, Thread (java.lang)
run:1447, Thread (java.lang)

Here's the second stack trace (Thread-4, 00:00:06.455):

getFontPathNative:-1, FcFontManager (sun.awt)
getFontPath:710, X11FontManager (sun.awt)
getPlatformFontPath:2878, SunFontManager (sun.font)
loadFonts:2904, SunFontManager (sun.font)
loadFonts:423, X11FontManager (sun.awt)
findFont2D:2059, SunFontManager (sun.font)
getFont2D:526, Font (java.awt)
getFont2D:262, Font$FontAccessImpl (java.awt)
getFont2D:141, FontUtilities (sun.font)
initMatrixAndMetrics:353, FontDesignMetrics (sun.font)
<init>:346, FontDesignMetrics (sun.font)
getMetrics:298, FontDesignMetrics (sun.font)
getFontMetrics:862, SunGraphics2D (sun.java2d)
getStringWidth:63, G2TextMeasurer (org.jfree.chart.text)
nextLineBreak:227, TextUtils (org.jfree.chart.text)
createTextBlock:168, TextUtils (org.jfree.chart.text)
arrangeRR:569, TextTitle (org.jfree.chart.title)
arrange:443, TextTitle (org.jfree.chart.title)
drawTitle:1233, JFreeChart (org.jfree.chart)
draw:1149, JFreeChart (org.jfree.chart)
createBufferedImage:1316, JFreeChart (org.jfree.chart)
createBufferedImage:1297, JFreeChart (org.jfree.chart)
writeChartAsPNG:146, ChartUtils (org.jfree.chart)
saveChartAsPNG:272, ChartUtils (org.jfree.chart)
saveChartAsPNG:247, ChartUtils (org.jfree.chart)
createFakeChart:244, JFreeChartTest (com.example.jfreechart)
lambda$testMultiThreadingClassic$0:191, JFreeChartTest (com.example.jfreechart)
runWith:1460, Thread (java.lang)
run:1447, Thread (java.lang)

As you can see in the stack trace, SunFontManager#loadFonts:2904 is called, and that line is inside a giant synchronized block:

    protected void loadFonts() {
        if (discoveredAllFonts) {
            return;
        }
        /* Use lock specific to the font system */
        synchronized (this) {
            // …
            if (fontPath != null) {
                if (! gotFontsFromPlatform()) {
                    registerFontsOnPath(fontPath, false,
                                        Font2D.UNKNOWN_RANK,
                                        false, true);
                    // …
                }
            }
            // …
        }
    }

It's very frustrating to see that my parallelisation of the charts' generation is bottle-necked by font-related I/O.

Can I speed up that process of discovering and loading the system's fonts somehow? For example by bringing a custom font? Or did I overlook something in the first place?

3

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.