Inserting some characters in JTextPane causes performance problems and memory leak

My chat client has a JTextPane in which text is inserted, which can be up to several lines per second. It usually works fine even for a longer period of time (for example, per hour), but sometimes it just becomes incredibly slow, using a lot of CPU and memory, sometimes up to 1 GB and almost completely freezing.

I added the option β€œ-Xrunhprof: heap = sites” to find out what memory is using, and from what I could collect, it has something to do with text rendering, although I really don't know about it, so it's more educated a guess. This is part of the result made at a time when memory usage was unusually high. I included an appropriate trace in each entry. Other heap heaps looked a bit different, but always pointed to the same or similar classes (something with a character in the name). You do not know how to correctly interpret this, and if it is really useful for solving this problem.

percent live alloc'ed stack class rank self accum bytes objs bytes objs trace name 1 16.33% 16.33% 11209120 350285 99416352 3106761 319103 java.awt.geom.Rectangle2D$Float TRACE 319103: java.awt.geom.RectangularShape.<init>(RectangularShape.java:56) java.awt.geom.Rectangle2D.<init>(Rectangle2D.java:511) java.awt.geom.Rectangle2D$Float.<init>(Rectangle2D.java:111) sun.font.StandardGlyphVector$GlyphStrike.getGlyphOutlineBounds(StandardGlyphVector.java:1790) 2 14.28% 30.61% 9799744 3958 52026864 49485 319095 float[] TRACE 319095: sun.font.StandardGlyphVector.getGlyphInfo(StandardGlyphVector.java:851) sun.font.ExtendedTextSourceLabel.createCharinfo(ExtendedTextSourceLabel.java:583) sun.font.ExtendedTextSourceLabel.getCharinfo(ExtendedTextSourceLabel.java:509) sun.font.ExtendedTextSourceLabel.getLineBreakIndex(ExtendedTextSourceLabel.java:455) 3 8.17% 38.77% 5604560 350285 49708176 3106761 319110 sun.font.DelegatingShape TRACE 319110: sun.font.DelegatingShape.<init>(DelegatingShape.java:43) sun.font.StandardGlyphVector.getGlyphVisualBounds(StandardGlyphVector.java:586) sun.font.StandardGlyphVector.getGlyphInfo(StandardGlyphVector.java:864) sun.font.ExtendedTextSourceLabel.createCharinfo(ExtendedTextSourceLabel.java:583) 4 7.96% 46.74% 5466576 9933 40683104 164341 319090 float[] TRACE 319090: sun.font.GlyphLayout$GVData.createGlyphVector(GlyphLayout.java:596) sun.font.GlyphLayout.layout(GlyphLayout.java:476) sun.font.ExtendedTextSourceLabel.createGV(ExtendedTextSourceLabel.java:325) sun.font.ExtendedTextSourceLabel.getGV(ExtendedTextSourceLabel.java:311) 5 4.07% 50.81% 2795304 9933 21434888 164341 319089 int[] TRACE 319089: sun.font.GlyphLayout$GVData.createGlyphVector(GlyphLayout.java:591) sun.font.GlyphLayout.layout(GlyphLayout.java:476) sun.font.ExtendedTextSourceLabel.createGV(ExtendedTextSourceLabel.java:325) sun.font.ExtendedTextSourceLabel.getGV(ExtendedTextSourceLabel.java:311) 6 3.71% 54.52% 2544072 106003 183421728 7642572 319087 java.awt.geom.Point2D$Float TRACE 319087: java.awt.geom.Point2D.<init>(Point2D.java:237) java.awt.geom.Point2D$Float.<init>(Point2D.java:69) sun.font.FileFontStrike.getGlyphMetrics(FileFontStrike.java:791) sun.font.FileFontStrike.getGlyphMetrics(FileFontStrike.java:787) 7 3.70% 58.22% 2539560 105815 182834016 7618084 319088 java.awt.geom.Point2D$Float TRACE 319088: java.awt.geom.Point2D.<init>(Point2D.java:237) java.awt.geom.Point2D$Float.<init>(Point2D.java:69) sun.font.FileFontStrike.getGlyphMetrics(FileFontStrike.java:809) sun.font.FileFontStrike.getGlyphMetrics(FileFontStrike.java:787) 8 2.20% 60.42% 1512888 6109 14728808 123309 319100 java.awt.Shape[] TRACE 319100: sun.font.StandardGlyphVector.getGlyphVisualBounds(StandardGlyphVector.java:580) sun.font.StandardGlyphVector.getGlyphInfo(StandardGlyphVector.java:864) sun.font.ExtendedTextSourceLabel.createCharinfo(ExtendedTextSourceLabel.java:583) sun.font.ExtendedTextSourceLabel.getCharinfo(ExtendedTextSourceLabel.java:509) 9 2.20% 62.62% 1507120 2151 49362432 73824 319503 float[] TRACE 319503: sun.font.StandardGlyphVector.getGlyphInfo(StandardGlyphVector.java:851) sun.font.ExtendedTextSourceLabel.createCharinfo(ExtendedTextSourceLabel.java:583) sun.font.ExtendedTextSourceLabel.getCharinfo(ExtendedTextSourceLabel.java:509) sun.font.ExtendedTextSourceLabel.getCharX(ExtendedTextSourceLabel.java:353) 10 2.09% 64.71% 1437120 44910 99416352 3106761 319111 java.awt.geom.Rectangle2D$Float TRACE 319111: java.awt.geom.RectangularShape.<init>(RectangularShape.java:56) java.awt.geom.Rectangle2D.<init>(Rectangle2D.java:511) java.awt.geom.Rectangle2D$Float.<init>(Rectangle2D.java:128) java.awt.geom.Rectangle2D$Float.getBounds2D(Rectangle2D.java:251) 11 1.84% 66.55% 1262456 6 1707160 18 307780 char[] TRACE 307780: javax.swing.text.GapContent.allocateArray(GapContent.java:94) javax.swing.text.GapVector.resize(GapVector.java:214) javax.swing.text.GapVector.shiftEnd(GapVector.java:229) javax.swing.text.GapContent.shiftEnd(GapContent.java:345) 12 1.16% 67.71% 794640 9933 13147280 164341 319092 sun.font.StandardGlyphVector TRACE 319092: java.awt.font.GlyphVector.<init>(GlyphVector.java:109) sun.font.StandardGlyphVector.<init>(StandardGlyphVector.java:185) sun.font.GlyphLayout$GVData.createGlyphVector(GlyphLayout.java:607) sun.font.GlyphLayout.layout(GlyphLayout.java:476) 

I also tracked the program using JConsole and noticed that only when it started to use more resources in the chat that I did not recognize were there some characters (for example, a smiley face, some Indian character and some Thai character, were used as part of the emoticon). I tried to insert the same characters into the JTextPane myself, which took an unusually long character, and also led to the fact that subsequent text inserts were much slower.

I created SSCCE with which I could reproduce the problem:

  • After entering a character that seems to have broken something.
    • .. it becomes much slower after several hundred lines if no more lines are inserted.
    • .. it becomes much slower when changing the style that was added to the StyledDocument with each insert, if there are already several hundred lines.
    • .. otherwise, it becomes a little slower (several percent more CPU usage), but gradually uses more and more memory.

I suppose that not adding linebreak treats all inserted text as one object, but changing the style added to StyledDocument can somehow update the whole document, although I did not know about it, since it does not actually change the style of already inserted text.

Now here is SSCCE (tested with jdk1.7.0_21), with a simple command input: "test" adds several identical lines, "insert1" or "insert2" adds a character that slows everything down, "style" "changes between changing the style that was added to StyledDocument, and the other, "linebreak" toggles between adding lines and not. Another input is just added to JTextPane.

 import java.awt.BorderLayout; import java.awt.Color; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.*; import javax.swing.text.*; public class JTextPaneTest extends JFrame implements Runnable, ActionListener { JTextPane textPane; JTextField input; Style styleA; SimpleAttributeSet styleB; StyledDocument doc; boolean setStyleA = false; boolean linebreak = true; public JTextPaneTest() { SwingUtilities.invokeLater(this); } @Override public void run() { // Text Pane textPane = new JTextPane(); doc = textPane.getStyledDocument(); JScrollPane scrollPane = new JScrollPane(textPane); // Styles styleA = doc.addStyle("styleA", null); styleB = new SimpleAttributeSet(); // Input input = new JTextField(); input.addActionListener(this); // Add everything to the window this.getContentPane().add(scrollPane, BorderLayout.CENTER); getContentPane().add(input, BorderLayout.SOUTH); // Prepare and show window this.setDefaultCloseOperation(EXIT_ON_CLOSE); pack(); this.setSize(400, 300); setVisible(true); } public static void main(String[] args) { new JTextPaneTest(); } void insert(final String text) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { try { if (setStyleA) { // Changing styleA, which is added to the StyledDocument // seems to make the problem worse StyleConstants.setForeground(styleA, Color.blue); } else { StyleConstants.setForeground(styleB, Color.blue); } // Not adding a linebreak seems to make the problem worse String addLinebreak = ""; if (linebreak) { addLinebreak = "\n"; } doc.insertString(doc.getLength(), text+addLinebreak, null); } catch (BadLocationException ex) { Logger.getLogger(JTextPaneTest.class.getName()).log(Level.SEVERE, null, ex); } } }); } @Override public void actionPerformed(ActionEvent e) { String text = input.getText(); if (text.equals("test")) { new Thread(new Runnable() { @Override public void run() { // Insert some text to kind of simulate chat messages coming in for (int i = 0; i < 500; i++) { try { Thread.sleep(250); } catch (InterruptedException ex) { Logger.getLogger(JTextPaneTest.class.getName()).log(Level.SEVERE, null, ex); } insert(i + " Test text to sort of simulate a chat message"); } } }).start(); } // Insert text that seems to break something // Example 1: else if (text.equals("insert1")) { insert("\uD83D\uDE3A"); } // Example 2: else if (text.equals("insert2")) { insert("\u0E07"); } // Toggle changing styleA or styleB else if (text.equals("style")) { if (this.setStyleA) { setStyleA = false; insert("Style: B"); } else { setStyleA = true; insert("Style: A"); } } // Toggle printing a linebreak after each insert else if (text.equals("linebreak")) { if (this.linebreak) { linebreak = false; insert("Linebreak: OFF"); } else { linebreak = true; insert("Linebreak: ON"); } } // Output entered text else { insert(input.getText()); input.setText(""); } } } 

The question is what happens there. Is this a known bug? Am I doing something wrong? It seems strange that adding one character will have such an effect. Even if it would be a little more expensive, it should not cause big problems.

If this is a Java error, what can I do as a workaround? Maybe filter the affected characters? But I don’t even know what it is. If I do something wrong, what is it? Maybe I need to somehow prepare the text before embedding it? Change its encoding? Maybe this is something very simple and simple I need to change? Please help. :)

Update: The following figure shows what happens when you enter 5,000 lines of text (which takes about 20 minutes), on the left, without doing anything special, on the right after inserting one of the unpleasant characters. I asked for garbage collection in JConsole after its completion, and the left one is up to 10 MB, and the right one is only about 45 MB, which is much more, given that the only difference is the one character inserted. After that, JConsole simply shuts down. You can also see that CPU usage is about 0.5 percentage points higher on the right. I repeated this test several times, the result was always the same. This is without linebreak / Style elements, which makes the problem even more noticeable.

Memory leak

+6
source share
1 answer

Here is what I did:

  • Run the SSCCE program
  • Attach JVisualVM and start the memory profiler
  • Let the program initialize and stabilize the heap; launch GC and take a snapshot from the profiler.
  • Enter a β€œtest” into the program and let it add new content
  • Force GC from JVisualVM and take a snapshot from the profiler
  • Enter "insert1" and "insert2" into the program to generate problem characters.
  • Enter the β€œtest” in the program to create additional, normal content and complete it.
  • Extract the GC from JVisualVM and take a snapshot from the profiler, also create a bunch of dump JVisualVM dumps

I see what you mentioned in your question, but would like to add:

  • Special DO characters use a separate rendering path from your regular sample text. Comparing the differences between snapshots (3) and (5), for example, shows only one class from the sun.font.* package sun.font.* . The difference between snapshots (5) and (8) shows that an additional 40 classes are now used. These include the classes you mentioned: sun.font.StandardGlyphVector , sun.font.ExtendedTextSourceLabel , sun.font.StandardTextSource and sun.font.DelegatingShape .

  • Of the above classes, most of them have 850 living objects, each in my profile. But sun.font.DelegatingShape is an outlier with ~ 20,000 + live objects.

  • I used JVisualVM to learn the last heap of the heap and focused on the DelegatingShape class. These objects contain references to various java.awt.geom.Rectangle2D$Float objects. Both are supported by the Shape[] array inside StandardGlyphVector and are separated using ExtendedTextSourceLabel . Each array contained ~ 49 nonzero elements.

  • Looking at the source code, these arrays are stored as soft links, as a cache type for visual bounding blocks for individual glyphs (see StandardGlyphVector.getGlyphVisualBounds() ). The good news is that objects accessible only through Soft References can be garbage collected and do not themselves represent a memory leak. The VM will leave them in memory (increasing the heap) as long as possible. If STRONGLY objects are held in any other way, they will never be collected; I currently do not notice any explicit links.

But why are there so many ExtendedTextSourceLabels? In short, your JTextPane implemented on top of javax.swing.text.BoxView , which after inserting ~ 1002 lines through your document contains ~ 4004 ParagraphView child objects. Each view contains its own TextLayoutStrategy and, after going through a large number of other objects, contains those ExtendedTextSourceLabel instances.

Thus, support for some Unicode subsets can be more expensive, both in rendering time and in memory consumption. I did not find any signs of a memory leak, except when your example stores the entire history of the "chat conversation" in a stylized document of your JTextPane. What can you do?

  • Show a limited portion of chat history in JTextPane, for example, only the last N entries.

  • Keep chat history in a different data structure outside of the Swing render graph. you will need to control the scrolling of itself to the "on page" pages and "exit the page" of the text to / from JTextPane, so it should only display part of the whole story.

EDIT: Profiling # 2

 "AWT-EventQueue-0" prio=10 tid=0x00007ff38028c000 nid=0x5f74 runnable [0x00007ff3745db000] java.lang.Thread.State: RUNNABLE at javax.swing.text.AbstractDocument$BranchElement.getElementIndex(AbstractDocument.java:2389) at javax.swing.text.CompositeView.getViewIndexAtPosition(CompositeView.java:579) at javax.swing.text.FlowView$LogicalView.getViewIndexAtPosition(FlowView.java:692) at javax.swing.text.CompositeView.getViewIndex(CompositeView.java:497) at javax.swing.text.TextLayoutStrategy$AttributedSegment.getAttribute(TextLayoutStrategy.java:520) at sun.text.bidi.BidiBase.setPara(BidiBase.java:2711) at java.text.Bidi.<init>(Bidi.java:134) at java.awt.font.TextMeasurer.initAll(TextMeasurer.java:208) at java.awt.font.TextMeasurer.<init>(TextMeasurer.java:167) at java.awt.font.LineBreakMeasurer.<init>(LineBreakMeasurer.java:310) 

With craters "linebreaks OFF" to a dead stop. I took a few dumps of flows, and the common point is LineBreakMeasurer; I chose the trace above because it shows that he has to deal with bidirectional bidi characters.

This does not seem to be a problem for me until I touch on the style options or linebreak.

+2
source

Source: https://habr.com/ru/post/945115/


All Articles