Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8337853

AttributedString + LineBreakMeasurer prevents deletion of temp font files

XMLWordPrintable

    • 2d
    • b01
    • 11
    • generic
    • generic

      A DESCRIPTION OF THE PROBLEM :
      When a Font is created using an InputStream, a temporary font file is created behind the scenes. This font file is managed and cleaned up by the JVM when the Font is no longer in use. However, when a Font is used in an AttributedString, and the AttributedString is then used to create a LineBreakMeasurer, these temporary font files are never cleaned up (until the VM process ends).

      The temp font file cleanup was working correctly in this scenario in Java 8, but is broken in Java 11, Java 17, Java 21 and Java 22. I have not had a chance to test Java 9 or 10.

      This bug can completely exhaust disk space in long-running processes which use AttributedStrings.

      REGRESSION : Last worked in version 8u421

      STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
      Run the provided test case.

      EXPECTED VERSUS ACTUAL BEHAVIOR :
      EXPECTED -
      Most temporary font files created during the test run are deleted before the VM exits.
      ACTUAL -
      None of the temporary font files are deleted before the VM exits.

      ---------- BEGIN SOURCE ----------
      import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
      import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;

      import java.awt.Font;
      import java.awt.Graphics2D;
      import java.awt.font.LineBreakMeasurer;
      import java.awt.font.TextAttribute;
      import java.awt.font.TextLayout;
      import java.awt.image.BufferedImage;
      import java.io.ByteArrayInputStream;
      import java.io.File;
      import java.io.IOException;
      import java.nio.file.FileSystems;
      import java.nio.file.Path;
      import java.nio.file.WatchEvent;
      import java.nio.file.WatchKey;
      import java.nio.file.WatchService;
      import java.text.AttributedCharacterIterator;
      import java.text.AttributedString;
      import java.util.ArrayList;
      import java.util.Base64;
      import java.util.Collections;
      import java.util.List;
      import java.util.concurrent.TimeUnit;

      public class TempFontFileDeletionTest {

          // test TTF font created just for this test, nothing special about it
          // contains placeholder glyphs for a-z and 0-9
          private static String FONT = "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";

          private Thread thread;
          private volatile boolean stopWatching = false;
          private List< Path > created = Collections.synchronizedList(new ArrayList<>());
          private List< Path > deleted = Collections.synchronizedList(new ArrayList<>());

          public static void main(String[] args) throws Exception {
              new TempFontFileDeletionTest().createFonts();
          }

          public TempFontFileDeletionTest() {
              this.thread = new Thread(this::watchTempDir);
              this.thread.start();
          }

          private void createFonts() throws Exception {

              int count = 500;
              byte[] bytes = Base64.getDecoder().decode(FONT);
              BufferedImage img = new BufferedImage(50, 50, BufferedImage.TYPE_BYTE_GRAY);
              Graphics2D g2d = img.createGraphics();

              for (int i = 0; i < count; i++) {

                  Font font = Font.createFont(Font.TRUETYPE_FONT, new ByteArrayInputStream(bytes));
                  
                  /* NOTE: this commented out code can be included and will NOT affect the deletion of temp files

                  g2d.setFont(font);
                  g2d.drawString("iul 105", 10, 30);

                  FontMetrics fontMetrics = g2d.getFontMetrics(font);
                  LineMetrics lineMetrics = fontMetrics.getLineMetrics("zas 123", g2d);
                  float lineHeight = lineMetrics.getHeight();

                  Font f2 = font.deriveFont(16f).deriveFont(AffineTransform.getRotateInstance(Math.PI));
                  g2d.setFont(f2);
                  g2d.drawString("abc 678", 10, lineHeight);
                  */

                  // NOTE: if this block is commented out, all of the temporary font files are auto-deleted correctly;
                  // something about this code block is preventing correct cleanup of temp font files
                  AttributedString string = new AttributedString("xyz 456");
                  string.addAttribute(TextAttribute.FONT, font);
                  AttributedCharacterIterator paragraph = string.getIterator();
                  LineBreakMeasurer lineMeasurer = new LineBreakMeasurer(paragraph, g2d.getFontRenderContext());
                  while (lineMeasurer.getPosition() < paragraph.getEndIndex()) {
                      TextLayout lineLayout = lineMeasurer.nextLayout(100, 100, false);
                      lineLayout.draw(g2d, 10, 20);
                  }
              }

              g2d.dispose();
              System.gc();
              Thread.sleep(5000);
              stopWatching = true;
              thread.join();

              List< Path > remaining = new ArrayList<>(created);
              remaining.removeAll(deleted);
              System.out.println(created.size() + " created - " + deleted.size() +
                  " deleted = " + remaining.size() + " remaining temporary font files");

              if (created.size() < count) {
                  throw new RuntimeException("Expected to see at least " + count +
                      " temporary font files, but only observed " + created.size());
              }

              if (deleted.size() < count * 0.1) {
                  throw new RuntimeException("Expected most temp font files to be auto-deleted, but only " +
                      deleted.size() + " files were deleted");
              }
          }

          private void watchTempDir() {
              Path temp = new File(System.getProperty("java.io.tmpdir")).toPath();
              System.out.println("Watching " + temp);
              try (WatchService watcher = FileSystems.getDefault().newWatchService()) {
                  temp.register(watcher, ENTRY_CREATE, ENTRY_DELETE);
                  while (true) {
                      WatchKey key = watcher.poll(1, TimeUnit.SECONDS);
                      if (key != null) {
                          for (WatchEvent< ? > event : key.pollEvents()) {
                              Path path = (Path) event.context();
                              if (path.getFileName().toString().startsWith("+~JF")) {
                                  String prefix;
                                  if (event.kind() == ENTRY_CREATE) {
                                      created.add(path);
                                      prefix = "Created ";
                                  } else {
                                      deleted.add(path);
                                      prefix = "Deleted ";
                                  }
                                  System.out.println(prefix + path);
                              }
                          }
                          boolean valid = key.reset();
                          if (!valid) {
                              break;
                          }
                      }
                      if (stopWatching) {
                          break;
                      }
                  }
              } catch (IOException | InterruptedException e) {
                  throw new RuntimeException(e);
              } finally {
                  System.out.println("Finished watching " + temp);
              }
          }
      }
      ---------- END SOURCE ----------

      FREQUENCY : always


            prr Philip Race
            webbuggrp Webbug Group
            Votes:
            0 Vote for this issue
            Watchers:
            4 Start watching this issue

              Created:
              Updated: