Building Python Document 30% faster.

稲田直哉 <[email protected]>
Newsgroups gmane.comp.python.documentation
Message-ID <[email protected]>
Hi, all.

I'm a member of Japanese translate of Python document Project.
We complete translating Python 2.5 document last year and now
work for Python 2.6 Document.

I feel building document is slow a little. So I try to tune docutils
and Sphinx.

Attached patches make building document 30% faster.
(In my environ. 330sec -> 220sec roughly)

I post sphinx.patch to bitbucket, but I don't know where to post docutils.patch.
Could anyone review these patch?

These patches changes following:

1. Use PyStemmer instead of PorterStemmer.
PorterStemmer is implemented Python and consumes about 50seconds
during buid.
PyStemmer <http://pypi.python.org/pypi/PyStemmer/1.0.1> implemented in C
and consumes only 7 seconds.

But searchindex.js with PyStemmer is different to one with PorterStemmer.

2. Avoid building OptionParser many times.
Sphinx uses docutils.core.publish_parts() without `settings` argument
many times.
This causes building docutils.frontend.OptionParser many times and consumes
29 seconds.

3. Avoid building NestedStateMachine many times.
NestedStateMachine is built and destroyed many times.
Recycling that SM make significant performance gain.

== before ==
   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
25720/459    0.997    0.000  134.085    0.292
tools/docutils/statemachine.py:178(run)
92281/1513    1.420    0.000  133.935    0.089
tools/docutils/statemachine.py:384(check_line)
    25720    0.184    0.000   89.628    0.003
tools/docutils/statemachine.py:129(__init__)
    25720    0.632    0.000   89.444    0.003
tools/docutils/statemachine.py:448(add_states)
   385800    1.665    0.000   88.813    0.000
tools/docutils/statemachine.py:436(add_state)
   385800    2.356    0.000   85.287    0.000
tools/docutils/statemachine.py:928(__init__)
   385800    1.793    0.000   82.931    0.000
tools/docutils/statemachine.py:566(__init__)

== after ==
   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
25720/459    1.051    0.000   68.175    0.149
tools/docutils/statemachine.py:178(run)
92281/1513    1.405    0.000   68.024    0.045
tools/docutils/statemachine.py:384(check_line)
     6862    0.031    0.000   24.241    0.004
tools/docutils/statemachine.py:129(__init__)
     6862    0.174    0.000   24.210    0.004
tools/docutils/statemachine.py:448(add_states)
   102930    0.430    0.000   24.036    0.000
tools/docutils/statemachine.py:436(add_state)
   102930    0.633    0.000   23.162    0.000
tools/docutils/statemachine.py:928(__init__)
   102930    0.549    0.000   22.529    0.000
tools/docutils/statemachine.py:566(__init__)

_______________________________________________
Doc-SIG maillist  -  [email protected]
http://mail.python.org/mailman/listinfo/doc-sig
sphinx.patch (application/octet-stream, 3.8 KB)
Index: search.py
===================================================================
--- search.py	(リビジョン 71045)
+++ search.py	(作業コピー)
@@ -14,8 +14,14 @@
 
 from docutils.nodes import Text, NodeVisitor
 
-from sphinx.util.stemmer import PorterStemmer
 from sphinx.util import jsdump, rpartition
+try:
+    # PyStemmer is wrapper for stemmer in c
+    import Stemmer as PyStemmer
+    PYSTEMMER = True
+except ImportError:
+    from sphinx.util.stemmer import PorterStemmer
+    PYSTEMMER = False
 
 
 word_re = re.compile(r'\w+(?u)')
@@ -62,17 +68,31 @@
 js_index = _JavaScriptIndex()
 
 
-class Stemmer(PorterStemmer):
-    """
-    All those porter stemmer implementations look hideous.
-    make at least the stem method nicer.
-    """
+if PYSTEMMER:
+    class Stemmer(object):
 
-    def stem(self, word):
-        word = word.lower()
-        return PorterStemmer.stem(self, word, 0, len(word) - 1)
+        def __init__(self):
+            self._stemmer = PyStemmer.Stemmer('english')
 
+        def stem(self, word):
+            return self._stemmer.stemWord(word.lower())
 
+        def stemWords(self, iter):
+            import itertools
+            return self._stemmer.stemWords(itertools.imap(lambda x: x.lower(), iter))
+else:
+    class Stemmer(PorterStemmer):
+        """
+        All those porter stemmer implementations look hideous.
+        make at least the stem method nicer.
+        """
+
+        def stem(self, word):
+            word = word.lower()
+            return PorterStemmer.stem(self, word, 0, len(word) - 1)
+
+
+
 class WordCollector(NodeVisitor):
     """
     A special visitor that collects words for the `IndexBuilder`.
@@ -196,11 +216,11 @@
         visitor = WordCollector(doctree)
         doctree.walk(visitor)
 
-        def add_term(word, prefix='', stem=self._stemmer.stem):
+        def add_term(word, stem=self._stemmer.stem):
             word = stem(word)
             if len(word) < 3 or word in stopwords or word.isdigit():
                 return
-            self._mapping.setdefault(prefix + word, set()).add(filename)
+            self._mapping.setdefault(word, set()).add(filename)
 
         for word in word_re.findall(title):
             add_term(word)
Index: builders/html.py
===================================================================
--- builders/html.py	(リビジョン 71045)
+++ builders/html.py	(作業コピー)
@@ -23,7 +23,7 @@
 
 from docutils import nodes
 from docutils.io import DocTreeInput, StringOutput
-from docutils.core import publish_parts
+from docutils.core import Publisher, publish_parts
 from docutils.utils import new_document
 from docutils.frontend import OptionParser
 from docutils.readers.doctree import Reader as DoctreeReader
@@ -181,14 +181,26 @@
         """Utility: Render a lone doctree node."""
         doc = new_document('<partial node>')
         doc.append(node)
-        return publish_parts(
-            doc,
-            source_class=DocTreeInput,
-            reader=DoctreeReader(),
-            writer=HTMLWriter(self),
-            settings_overrides={'output_encoding': 'unicode'}
-        )
 
+        # cache publisher object.
+        if 'publisher' not in self.__dict__:
+            self.publisher = Publisher(
+                    source_class = DocTreeInput,
+                    destination_class=StringOutput)
+            self.publisher.set_components('standalone',
+                    'restructuredtext', 'pseudoxml')
+
+        pub = self.publisher
+
+        pub.reader = DoctreeReader()
+        pub.writer = HTMLWriter(self)
+        pub.process_programmatic_settings(
+                None, {'output_encoding': 'unicode'}, None)
+        pub.set_source(doc, None)
+        pub.set_destination(None, None)
+        pub.publish()
+        return pub.writer.parts
+        
     def prepare_writing(self, docnames):
         from sphinx.search import IndexBuilder
docutils.patch (application/octet-stream, 1.9 KB)
Index: parsers/rst/states.py
===================================================================
--- parsers/rst/states.py	(リビジョン 71045)
+++ parsers/rst/states.py	(作業コピー)
@@ -207,6 +207,7 @@
     """
 
     nested_sm = NestedStateMachine
+    nested_sm_cache = []
 
     def __init__(self, state_machine, debug=0):
         self.nested_sm_kwargs = {'state_classes': state_classes,
@@ -255,21 +256,33 @@
         Create a new StateMachine rooted at `node` and run it over the input
         `block`.
         """
+        use_default = 0
         if state_machine_class is None:
             state_machine_class = self.nested_sm
+            use_default += 1
         if state_machine_kwargs is None:
             state_machine_kwargs = self.nested_sm_kwargs
+            use_default += 1
         block_length = len(block)
-        state_machine = state_machine_class(debug=self.debug,
+
+        if use_default == 2 and self.nested_sm_cache: #use default sm cache
+            state_machine = self.nested_sm_cache.pop()
+        else:
+            state_machine = state_machine_class(debug=self.debug,
                                             **state_machine_kwargs)
         state_machine.run(block, input_offset, memo=self.memo,
                           node=node, match_titles=match_titles)
-        state_machine.unlink()
+        if use_default == 2:
+            self.nested_sm_cache.append(state_machine)
+        else:
+            state_machine.unlink()
+
         new_offset = state_machine.abs_line_offset()
         # No `block.parent` implies disconnected -- lines aren't in sync:
         if block.parent and (len(block) - block_length) != 0:
             # Adjustment for block if modified in nested parse:
             self.state_machine.next_line(len(block) - block_length)
+
         return new_offset
 
     def nested_list_parse(self, block, input_offset, node, initial_state,
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.