{"id":18,"date":"2007-03-18T16:53:54","date_gmt":"2007-03-18T21:53:54","guid":{"rendered":"http:\/\/wp.javatechniques.com\/blog\/faster-jtextpane-text-insertion-part-ii\/"},"modified":"2007-06-25T13:06:07","modified_gmt":"2007-06-25T18:06:07","slug":"faster-jtextpane-text-insertion-part-ii","status":"publish","type":"page","link":"http:\/\/javatechniques.com\/blog\/faster-jtextpane-text-insertion-part-ii\/","title":{"rendered":"Faster JTextPane Text Insertion (Part II)"},"content":{"rendered":"<p>In <A HREF=\"http:\/\/javatechniques.com\/blog\/faster-jtextpane-text-insertion-part-i\/\">Part I<\/A> we briefly examined two of the reasons why inserting large quantities of text with different styles (attributes) into a Swing <CODE>JTextPane<\/CODE> can be very slow: Each update can trigger a UI refresh, and the thread-safe design of <CODE>DefaultStyledDocument<\/CODE> imposes a small amount of locking overhead on each update. <\/p>\n<p>As shown in <A HREF=\"http:\/\/javatechniques.com\/blog\/faster-jtextpane-text-insertion-part-i\/\">Part I<\/A>, simply &#8220;detaching&#8221; the document from its text pane before modifying it avoid the UI refresh problem. This can, for example, improve the speed of initializing a large, multi-style document by a factor of three or more, depending on document complexity. For large documents, however, this may not be enough. A little rummaging through the internals of <CODE>DefaultStyledDocument<\/CODE> reveals a workaround for the second speed issue, internal locking overhead.<\/p>\n<p><B>Batch Text Insertion<\/B><\/p>\n<p>A common way to initialize multi-styled content in a <CODE>DefaultStyledDocument<\/CODE> is to parse data from a file (or other external source) into a series of substrings and corresponding <CODE>Attributes<\/CODE> objects, where the attributes contain the font, color, and other style information for each substring. For example, a code editor for an IDE might provide syntax highlighting by parsing a source file to determine language keywords, variable names, and other relevant constructs, and give each a unique style. Each substring would then be added by calling the <CODE>insertString(int offset, String str, Attributes attrs)<\/CODE> method on the document object.<\/p>\n<p>Since document objects are thread-safe, <CODE>insertString(&#8230;)<\/CODE> first acquires a write-lock to ensure that only a single thread is modifying the underlying data representation, then makes the update, and finally releases the lock when it is finished. For modifications made by user input from the keyboard, this processs is sufficiently fast. For the kind of batch updates needed to initialize a large document, however, the lock management overhead is significant.<\/p>\n<p>In <CODE>DefaultStyledDocument<\/CODE>, most of the work of actually updating the document contents when a string is inserted is done by the <CODE>protected<\/CODE> method <CODE>insertUpdate(&#8230;)<\/CODE>. The string and attributes to be inserted are used to create one or more instances of the <CODE>ElementSpec<\/CODE> class, which are then used to actually effect the modifications.<\/p>\n<p><CODE>ElementSpec<\/CODE> is a static inner class within <CODE>DefaultStyledDocument<\/CODE>. A quick serach for its uses within <CODE>DefaultStyledDocument<\/CODE> reveals the method:<\/p>\n<pre>\r\n    protected void insert(int offset, ElementSpec[] data) throws \r\n        BadLocationException\r\n<\/pre>\n<p>This version of <CODE>insert<\/CODE> is also thread-safe (like <CODE>insertString(&#8230;)<\/CODE>), but processes a list of <CODE>ElementSpec<\/CODE> objects that are to be inserted at a given offset. Unlike <CODE>insertString(&#8230;)<\/CODE>, the lock is only acquired once, rather than once for each modification. This gives us the tools we need to construct a custom subclass that supports batch inserts. Figure 1 shows a possible implementation of such a Document subclass.<\/p>\n<p><HR><\/p>\n<pre>\r\n\r\nimport java.util.ArrayList;\r\nimport javax.swing.text.Element;\r\nimport javax.swing.text.AttributeSet;\r\nimport javax.swing.text.BadLocationException;\r\nimport javax.swing.text.DefaultStyledDocument;\r\n\r\n\/**\r\n * DefaultDocument subclass that supports batching inserts.\r\n *\/\r\npublic class BatchDocument extends DefaultStyledDocument {\r\n    \/**\r\n     * EOL tag that we re-use when creating ElementSpecs\r\n     *\/\r\n    private static final char[] EOL_ARRAY = { '\\n' };\r\n\r\n    \/**\r\n     * Batched ElementSpecs\r\n     *\/\r\n    private ArrayList batch = null;\r\n\r\n    public BatchDocument() {\r\n        batch = new ArrayList();\r\n    }\r\n\r\n    \/**\r\n     * Adds a String (assumed to not contain linefeeds) for \r\n     * later batch insertion.\r\n     *\/\r\n    public void appendBatchString(String str, \r\n        AttributeSet a) {\r\n        \/\/ We could synchronize this if multiple threads \r\n        \/\/ would be in here. Since we're trying to boost speed, \r\n        \/\/ we'll leave it off for now.\r\n\r\n        \/\/ Make a copy of the attributes, since we will hang onto \r\n        \/\/ them indefinitely and the caller might change them \r\n        \/\/ before they are processed.\r\n        a = a.copyAttributes();\r\n        char[] chars = str.toCharArray();\r\n        batch.add(new ElementSpec(\r\n            a, ElementSpec.ContentType, chars, 0, str.length()));\r\n    }\r\n\r\n    \/**\r\n     * Adds a linefeed for later batch processing\r\n     *\/\r\n    public void appendBatchLineFeed(AttributeSet a) {\r\n        \/\/ See sync notes above. In the interest of speed, this \r\n        \/\/ isn't synchronized.\r\n\r\n        \/\/ Add a spec with the linefeed characters\r\n        batch.add(new ElementSpec(\r\n                a, ElementSpec.ContentType, EOL_ARRAY, 0, 1));\r\n\r\n        \/\/ Then add attributes for element start\/end tags. Ideally \r\n        \/\/ we'd get the attributes for the current position, but we \r\n        \/\/ don't know what those are yet if we have unprocessed \r\n        \/\/ batch inserts. Alternatives would be to get the last \r\n        \/\/ paragraph element (instead of the first), or to process \r\n        \/\/ any batch changes when a linefeed is inserted.\r\n        Element paragraph = getParagraphElement(0);\r\n        AttributeSet pattr = paragraph.getAttributes();\r\n        batch.add(new ElementSpec(null, ElementSpec.EndTagType));\r\n        batch.add(new ElementSpec(pattr, ElementSpec.StartTagType));\r\n    }\r\n\r\n    public void processBatchUpdates(int offs) throws \r\n        BadLocationException {\r\n        \/\/ As with insertBatchString, this could be synchronized if\r\n        \/\/ there was a chance multiple threads would be in here.\r\n        ElementSpec[] inserts = new ElementSpec[batch.size()];\r\n        batch.toArray(inserts);\r\n\r\n        \/\/ Process all of the inserts in bulk\r\n        super.insert(offs, inserts);\r\n    }\r\n}\r\n<\/pre>\n<p><HR><br \/>\n<CENTER>Figure 1. <CODE>BatchDocument<\/CODE>, a document subclass that supports batch insertion of text with different styles.<\/CENTER><\/p>\n<p>Use of this class differs slightly from a normal <CODE>DefaultStyledDocument<\/CODE>. Strings (and their attributes) that are to be inserted should be added by calling the <CODE>appendBatchString(&#8230;)<\/CODE> method. When a new line should be inserted, <CODE>appendBatchLinefeed(&#8230;)<\/CODE> should be called. Once all of the batched content has been added, <CODE>processBatchUpdates(&#8230;)<\/CODE> should be called to actually insert the text into the document. Note that it would be possible to add methods that would parse arbitrary strings and handle linefeeds automatically.<\/p>\n<p><B>Testing BatchDocument<\/B><\/p>\n<p>Figure 2 shows an example of a test class that initialized a document with a long, multi-format string (using either a standard <CODE>DefaultStyledDocument<\/CODE> or a <CODE>BatchDocument<\/CODE>, and making updates while the document is either attached and visible or detached) and computes the time required.<\/p>\n<p><HR><\/p>\n<pre>\r\n\r\nimport java.awt.Color;\r\nimport java.awt.BorderLayout;\r\nimport javax.swing.JFrame;\r\nimport javax.swing.JTextPane;\r\nimport javax.swing.JScrollPane;\r\nimport javax.swing.text.StyleConstants;\r\nimport javax.swing.text.SimpleAttributeSet;\r\nimport javax.swing.text.BadLocationException;\r\nimport javax.swing.text.DefaultStyledDocument;\r\n\r\n\/**\r\n * Demonstration class for BatchDocuments. This class creates a\r\n * randomly formatted string and adds it to a document.\r\n *\/\r\npublic class Test {\r\n\r\n    public static void main(String[] args) throws \r\n        BadLocationException {\r\n        if (args.length != 3) {\r\n            System.err.println(\"Please give 3 arguments:\");\r\n            System.err.println(\" [true\/false] for use batch \" +\r\n                \"(true) vs. use default doc [false]\");\r\n            System.err.println(\r\n                \" [true\/false] for update while visible\");\r\n            System.err.println(\r\n                \" [int] for number of strings to insert\");\r\n            System.exit(-1);\r\n        }\r\n\r\n        boolean useBatch = args[0].toLowerCase().equals(\"true\");\r\n        boolean updateWhileVisible = args[1].equals(\"true\");\r\n        int iterations = Integer.parseInt(args[2]);\r\n        System.out.println(\"Using batch = \" + useBatch);\r\n        System.out.println(\"Updating while pane visible = \" + \r\n            updateWhileVisible);\r\n        System.out.println(\"Strings to insert = \" + iterations);\r\n\r\n        JFrame f = new JFrame(\"Document Speed Test\");\r\n        f.getContentPane().setLayout(new BorderLayout());\r\n        JTextPane jtp = new JTextPane();\r\n        f.getContentPane().add(\r\n            new JScrollPane(jtp), BorderLayout.CENTER);\r\n        f.setSize(400, 400); f.show();\r\n\r\n        \/\/ Make one of each kind of document. \r\n        BatchDocument bDoc = new BatchDocument();\r\n        DefaultStyledDocument doc = new DefaultStyledDocument();\r\n        if (updateWhileVisible) {\r\n            if (useBatch)\r\n                jtp.setDocument(bDoc);\r\n            else\r\n                jtp.setDocument(doc);\r\n        }\r\n        long start = System.currentTimeMillis();\r\n\r\n        \/\/ Make some test data. Normally the text pane \r\n        \/\/ content would come from other source, be parsed, and \r\n        \/\/ have styles applied based on appropriate application \r\n        \/\/ criteria. Here we are interested in the speed of updating \r\n        \/\/ a document, rather than parsing, so we pre-parse the data.\r\n        String[] str = new String[] {\r\n            \"The \", \"quick \", \"brown \", \"fox \", \"jumps \", \r\n            \"over \", \"the \", \"lazy \", \"dog. \" };\r\n        Color[] colors = \r\n            new Color[] { Color.red, Color.blue, Color.green };\r\n        int[] sizes = new int[] { 10, 14, 12, 9, 16 };\r\n\r\n        \/\/ Add the test repeatedly\r\n        int offs = 0;\r\n        int count = 0;\r\n        SimpleAttributeSet attrs = new SimpleAttributeSet();\r\n        for (int i = 0; i &lt; iterations; i++) {\r\n            for (int j = 0; j &lt; str.length; j++) {\r\n                \/\/ Make some random style changes\r\n                StyleConstants.setFontSize(\r\n                    attrs, sizes[count % sizes.length]);\r\n                StyleConstants.setForeground(\r\n                    attrs, colors[count % colors.length]);\r\n\r\n                if (useBatch)\r\n                    bDoc.appendBatchString(str[j], attrs);\r\n                else\r\n                    doc.insertString(offs, str[j], attrs);\r\n\r\n                \/\/ Update out counters\r\n                count++;\r\n                offs += str[j].length();\r\n            }\r\n\r\n            \/\/ Add a linefeed after each instance of the string\r\n            if (useBatch)\r\n                bDoc.appendBatchLineFeed(attrs);\r\n            else\r\n                doc.insertString(offs, \"\\n\", attrs);\r\n            offs++;\r\n        }\r\n\r\n        \/\/ If we're testing the batch document, process all \r\n        \/\/ of the updates now.\r\n        if (useBatch)\r\n            bDoc.processBatchUpdates(0);\r\n\r\n        System.out.println(\"Time to update = \" +\r\n            (System.currentTimeMillis() - start));\r\n        System.out.println(\"Text size = \" + offs);\r\n\r\n        if (! updateWhileVisible) {\r\n            if (useBatch) {\r\n                jtp.setDocument(bDoc);\r\n            }\r\n            else {\r\n                jtp.setDocument(doc);\r\n            }\r\n        }\r\n    }\r\n\r\n}\r\n<\/pre>\n<p><HR><br \/>\n<CENTER>Figure 2. Simple test class for comparing large document initialization times<\/CENTER><\/p>\n<p>The <CODE>Test<\/CODE> class in Figure 2 should be run with three parameters:<br \/>\n<OL><br \/>\n<LI> &#8220;<CODE>true<\/CODE>&#8221; if the <CODE>BatchDocument<\/CODE> class should be used or &#8220;<CODE>false<\/CODE>&#8221; if a <CODE>DefaultStyledDocument<\/CODE> should be used.<br \/>\n<LI> &#8220;<CODE>true<\/CODE>&#8221; if the updates should be made while the document is attached to a visible <CODE>JTextPane<\/CODE>, or &#8220;<CODE>false<\/CODE>&#8221; if the updates should be made while the document is unattached.<br \/>\n<LI> The number of times that the test string should be repeated to build the document.<br \/>\n<\/OL><\/p>\n<p>Figure 3 shows some sample results from running the <CODE>Test<\/CODE> class.<\/p>\n<p><HR><br \/>\n<CENTER><TABLE BORDER=0 CELLPADDING=3 VALIGN=TOP><TR><TH> Test <\/TH><TH> Command line <\/TH><TH> Time (milliseconds) <\/TH><\/TR><TR VALIGN=TOP><TD>Default document, updated while visible<\/TD><TD><CODE>java Test false true 10000<\/CODE><\/TD><TD ALIGN=RIGHT>460827<\/TD><\/TR><TR VALIGN=TOP><TD>Default document, updated while detached<\/TD><TD><CODE>java&nbsp;Test&nbsp;false&nbsp;false&nbsp;10000<\/CODE><\/TD><TD ALIGN=RIGHT>151591<\/TD><\/TR><TR VALIGN=TOP><TD>Batch document, updated while visible<\/TD><TD><CODE>java Test true true 10000<\/CODE><\/TD><TD ALIGN=RIGHT>30185<\/TD><\/TR><TR VALIGN=TOP><TD>Batch document, updated while detached<\/TD><TD><CODE>java Test true false 10000<\/CODE><\/TD><TD ALIGN=RIGHT>29444<\/TD><\/TR><\/TABLE><\/p>\n<p><HR><br \/>\nFigure 3. Sample results from running the <CODE>Test<\/CODE> class.<br \/>\n<\/CENTER><br \/>\nAs we noted in <A HREF=\"http:\/\/javatechniques.com\/blog\/faster-jtextpane-text-insertion-part-i\/\">Part I<\/A>, simply detaching a <CODE>DefaultStyledDocument<\/CODE> before inserting the text is roughly three times faster for this particular test. Switching to <CODE>BatchDocument<\/CODE> boosts the speed by another <I>five<\/I> times, producing an initialization time that is roughly 15 times faster than initializing a visible <CODE>DefaultStyledDocument<\/CODE>. With <CODE>BatchDocument<\/CODE>, visibility is less of a factor since only a single UI refresh will be triggered. The difference between the visible vs. detached times for <CODE>BatchDocument<\/CODE> shown in the results above is simply &#8220;noise&#8221;.<\/p>\n<p>Results will, of course, vary considerably based on machine speed and memory, JDK version, document size, and (perhaps most importantly) the complexity of the document content.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>In Part I we briefly examined two of the reasons why inserting large quantities of text with different styles (attributes) into a Swing JTextPane can be very slow: Each update can trigger a UI refresh, and the thread-safe design of DefaultStyledDocument imposes a small amount of locking overhead on each update. As shown in Part &hellip; <\/p>\n<p class=\"link-more\"><a href=\"http:\/\/javatechniques.com\/blog\/faster-jtextpane-text-insertion-part-ii\/\" class=\"more-link\">Continue reading<span class=\"screen-reader-text\"> &#8220;Faster JTextPane Text Insertion (Part II)&#8221;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"open","template":"","meta":{"footnotes":""},"class_list":["post-18","page","type-page","status-publish","hentry","entry"],"_links":{"self":[{"href":"http:\/\/javatechniques.com\/blog\/wp-json\/wp\/v2\/pages\/18","targetHints":{"allow":["GET"]}}],"collection":[{"href":"http:\/\/javatechniques.com\/blog\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"http:\/\/javatechniques.com\/blog\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"http:\/\/javatechniques.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"http:\/\/javatechniques.com\/blog\/wp-json\/wp\/v2\/comments?post=18"}],"version-history":[{"count":0,"href":"http:\/\/javatechniques.com\/blog\/wp-json\/wp\/v2\/pages\/18\/revisions"}],"wp:attachment":[{"href":"http:\/\/javatechniques.com\/blog\/wp-json\/wp\/v2\/media?parent=18"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}