[Twisted-web] caching patch updated

Andrea Arcangeli andrea at cpushare.com
Wed Feb 9 18:59:00 MST 2005


Hello everyone,

This is an updated version of the caching patch against current SVN.
This is mostly code from the Nevow-caching branch from dialtone with the
addition of a cacheTimeout logic on the whole rendering that I use for
the whole non-user dependent dynamic pages of my site. I also use
dialtone's stan cache for some fragment (not all unfortunately) and that
was a significant speedup but I still need the other more aggressive
caching in the Page class.

Please let me know if you see any bug in this code, thanks! To me it
looks useful enough to be merged in trunk but that's just my humble
opinion.

This code allows >200req per second to be served for dynamic but mostly
static data. It doesn't max out a 100mbit yet with <5k pages, but it
greatly exceeds the bandwidth of a 10mbit.

Index: Nevow/nevow/tags.py
===================================================================
--- Nevow/nevow/tags.py	(revision 1185)
+++ Nevow/nevow/tags.py	(working copy)
@@ -25,7 +25,7 @@
 """
 
 
-from nevow.stan import Proto, Tag, directive, raw, xml, CommentProto, invisible, slot, cdata
+from nevow.stan import Proto, Tag, directive, raw, xml, CommentProto, invisible, slot, cdata, cached
 
 
 comment = CommentProto()
@@ -62,7 +62,9 @@
 def inlineJS(s):
     return script(type="text/javascript", language="JavaScript")[xml('\n//<![CDATA[\n%s\n//]]>\n' % s)]
 
-__all__ = tags + ['invisible', 'comment', '_dir', '_del', '_object', '_map', 'drange', 'Tag', 'directive', 'xml', 'raw', 'slot', 'cdata', 'inlineJS'] + ['_%s' % x for x in range(100)]
+__all__ = tags + ['invisible', 'comment', '_dir', '_del', '_object',
+                  '_map', 'drange', 'Tag', 'directive', 'xml', 'raw',
+                  'slot', 'cached', 'cdata', 'inlineJS'] + ['_%s' % x for x in range(100)]
 
 
 ########################
Index: Nevow/nevow/__init__.py
===================================================================
--- Nevow/nevow/__init__.py	(revision 1185)
+++ Nevow/nevow/__init__.py	(working copy)
@@ -183,6 +183,7 @@
 nevow.flat.flatstan.RendererSerializer            nevow.inevow.IRenderer
 nevow.flat.flatstan.DirectiveSerializer           nevow.stan.directive
 nevow.flat.flatstan.SlotSerializer                nevow.stan.slot
+nevow.flat.flatstan.CachedSerializer              nevow.stan.cached 
 nevow.flat.flatstan.ContextSerializer             nevow.context.WovenContext
 nevow.flat.flatstan.DeferredSerializer            twisted.internet.defer.Deferred
 nevow.flat.flatstan.DeferredSerializer            twisted.internet.defer.DeferredList
Index: Nevow/nevow/flat/flatstan.py
===================================================================
--- Nevow/nevow/flat/flatstan.py	(revision 1185)
+++ Nevow/nevow/flat/flatstan.py	(working copy)
@@ -9,10 +9,14 @@
 from nevow import util
 from nevow.stan import Proto, Tag, xml, directive, Unset, invisible
 from nevow.inevow import IRenderer, IRendererFactory, IGettable, IData
-from nevow.flat import precompile, serialize
+from nevow.flat import precompile, serialize, iterflatten
 from nevow.accessors import convertToData
 from nevow.context import WovenContext
 
+from time import time as now
+from cStringIO import StringIO
+from twisted.internet import defer
+
 allowSingleton = ('img', 'br', 'hr', 'base', 'meta', 'link', 'param', 'area',
                   'input', 'col', 'basefont', 'isindex', 'frame')
 
@@ -226,6 +230,45 @@
         return serialize(original.default, context)
     return serialize(data, context)
 
+_CACHE = {}
+def CachedSerializer(original, context):
+    cached = _CACHE.get(original.name, None)
+    _now = now()
+    life = _now-original.lifetime
+    if cached and (cached[0] > life or not original.lifetime):
+        yield cached[1]
+        return
+    io = StringIO()
+    for child in iterflatten(original.children, context, io.write,
+                             lambda item: True):
+        if isinstance(child, tuple):
+            childDeferred, childReturner = child
+ 
+            d = defer.Deferred() ## A new deferred for the outer loop, whose result
+            ## we don't care about, because we don't want the outer loop to write
+            ## anything when this deferred fires -- only when the entire for loop
+            ## has completed and we have all the "children" flattened
+ 
+            def innerDeferredResultAvailable(result):
+                childReturner(result) ## Cause the inner iterflatten to continue
+                d.callback('') ## Cause the outer iterflatten to continue
+                return ''
+ 
+            childDeferred.addCallback(innerDeferredResultAvailable)
+ 
+            ## Make the outer loop wait on our new deferred.
+            ## We call the new deferred back with ''
+            ## Which will cause the outer loop to write '' to the request,
+            ## which doesn't matter. It will then call our "returner",
+            ## which is just the noop lambda below, because we don't care
+            ## about the return result of the new deferred, which is just
+            ## ''
+ 
+            yield d, lambda result: ''    
+    result = io.getvalue()
+    _CACHE[original.name] = (_now, result)
+    yield result
+
 def ContextSerializer(original, context):
     originalContext = original.clone(deep=False)
     originalContext.precompile = context and context.precompile or False
Index: Nevow/nevow/stan.py
===================================================================
--- Nevow/nevow/stan.py	(revision 1185)
+++ Nevow/nevow/stan.py	(working copy)
@@ -119,8 +119,33 @@
         """
         raise NotImplementedError, "Stan slot instances are not iterable."
 
+class cached(object):
+    """Marker for cached content
+    """
+    __slots__ = ['name', 'children', 'lifetime']
 
+    def __init__(self, name, lifetime=0):
+        self.name = name
+        self.children = []
+        self.lifetime = lifetime
 
+    def __repr__(self):
+        return "cached('%s','%s')" % self.name, self.lifetime
+
+    def __getitem__(self, children):
+        """cached content is what is being cached
+        """
+        if not isinstance(children, (list, tuple)):
+            children = [children]
+        self.children.extend(children)
+        return self
+
+    def __iter__(self):
+        """Prevent an infinite loop if someone tries to do
+            for x in cached('foo'):
+        """
+        raise NotImplementedError, "Stan slot instances are not iterable."
+
 class Tag(object):
     """Tag instances represent XML tags with a tag name, attributes,
     and children. Tag instances can be constructed using the Prototype
Index: Nevow/nevow/rend.py
===================================================================
--- Nevow/nevow/rend.py	(revision 1185)
+++ Nevow/nevow/rend.py	(working copy)
@@ -30,6 +30,7 @@
 from nevow import flat
 from nevow.util import log
 from nevow import util
+from nevow import url
 
 import formless
 from formless import iformless
@@ -374,6 +375,7 @@
             self.children = {}
         self.children[name] = child
     
+_CACHE = {}
 
 class Page(Fragment, ConfigurableFactory, ChildLookupMixin):
     """A page is the main Nevow resource and renders a document loaded
@@ -384,12 +386,48 @@
 
     buffered = False
 
+    cacheTimeout = None # 0 means cache forever, >0 sets the seconds of caching
+    __lastCacheRendering = 0 # this should not be touched by the parent class
+
     beforeRender = None
     afterRender = None
     addSlash = None
 
     flattenFactory = flat.flattenFactory
 
+    def hasCache(self, ctx):
+        if self.cacheTimeout is None:
+            return None
+
+        _now = now() # run gettimeofday only once
+        timeout = _now > self.__lastCacheRendering + self.cacheTimeout and \
+                  self.cacheTimeout > 0
+        c = self.lookupCache(ctx)
+        if timeout or c is None:
+            self.__lastCacheRendering = _now # stop other renders
+            from twisted.internet.defer import Deferred
+            d = Deferred()
+            self.storeCache(ctx, d)
+            # force only this rendering, others will wait the deferred
+            c = None
+        return c
+    def chainDeferredCache(self, ctx, d):
+        if self.cacheTimeout is None:
+            return d
+
+        from twisted.internet.defer import Deferred
+        c = self.lookupCache(ctx)
+        if isinstance(c, Deferred):
+            # we're the thread that went ahead to refresh the cache
+            d.chainDeferred(c)
+        return d
+    def cacheIDX(self, ctx):
+        return str(url.URL.fromContext(ctx))
+    def storeCache(self, ctx, c):
+        _CACHE[self.cacheIDX(ctx)] = c
+    def lookupCache(self, ctx):
+        return _CACHE.get(self.cacheIDX(ctx))
+
     def renderHTTP(self, ctx, doBefore=True):
         ## XXX request is really ctx now, change the name here
         request = inevow.IRequest(ctx)
@@ -412,11 +450,17 @@
             if self.afterRender is not None:
                 return util.maybeDeferred(self.afterRender,ctx)
 
-        if self.buffered:
+        c = self.hasCache(ctx)
+        if c:
+            return util.maybeDeferred(finishRequest).addCallback(lambda r: c)
+
+        if self.buffered or self.cacheTimeout is not None:
             io = StringIO()
             writer = io.write
             def finisher(result):
-                request.write(io.getvalue())
+                c = io.getvalue()
+                self.storeCache(ctx, c)
+                request.write(c)
                 return util.maybeDeferred(finishRequest).addCallback(lambda r: result)
         else:
             writer = request.write
@@ -426,7 +470,7 @@
         doc = self.docFactory.load()
         ctx =  WovenContext(ctx, tags.invisible[doc])
 
-        return self.flattenFactory(doc, ctx, writer, finisher)
+        return self.chainDeferredCache(ctx, self.flattenFactory(doc, ctx, writer, finisher))
 
     def rememberStuff(self, ctx):
         Fragment.rememberStuff(self, ctx)


This last bit avoids an error at runtime, clearly getStyleSheet should
be overriden then (not stylesheet).

Index: Nevow/nevow/vhost.py
===================================================================
--- Nevow/nevow/vhost.py	(revision 1185)
+++ Nevow/nevow/vhost.py	(working copy)
@@ -19,7 +19,7 @@
 """
 
     def getStyleSheet(self):
-        return self.stylesheet
+        return VirtualHostList.stylesheet
  
     def data_hostlist(self, context, data):
         return self.nvh.hosts.keys()



More information about the Twisted-web mailing list