I successfully load balanced the static server with pythondirector. However it has a few problems: 1) it cannot handle ssl 2) even if it can handle ssl I still need to share the session cookies and I'm thinking that using sql or atop to do that isn't necessary: the session doesn't need to survive a reboot, a reboot would be a visible disruption of the services anyway, so I'm thinking to write a little session daemon that only stores the cookies using pb to do that. In theory shared memory would be much more efficient in smp, but at least this socket model has the advantage of scaling in the cluster too 3) the load balancer misses an API with the real webserver to pass up the client IP address, that's annoying, especially the logs gets screwed Other than that the speed of the load balancer is excellent, and I get exactly the double number of pages rendered per second (after fixing a small bug in pythondirector that confused ab2). Perhaps I'm going with wrong priorities though, the major offender is compy, compy must be dropped from Nevow ASAP :). Leaving it as a compatibility API may be ok, but internally compy can't invoke getComponents anymore if we do care about writing remotely optimal code. We should try with the new zope.interfaces API first and see if it underperforms so horribly as getInterfaces does. Secondly I'm looking into caching the html and to render some fragment only once every 10 seconds in the background (so the downloads will never have to wait for a rendering of some mostly static fragment anymore). So overall there's an huge room for improvements. What do other people think?
Andrea Arcangeli wrote:
3) the load balancer misses an API with the real webserver to pass up the client IP address, that's annoying, especially the logs gets screwed
We need that for reverse proxying, too. My current idea is to replace the /vhost/http/hostname[:port]/path style with /vhost;host=hostname[;port=port][;scheme=https][;client=1.2.3.4]/path to allow passing arbitrary variables. That way it's easy to extend in the future.
On Sat, Jan 29, 2005 at 07:33:47PM +0200, Tommi Virtanen wrote:
Andrea Arcangeli wrote:
3) the load balancer misses an API with the real webserver to pass up the client IP address, that's annoying, especially the logs gets screwed
We need that for reverse proxying, too.
My current idea is to replace the /vhost/http/hostname[:port]/path style with /vhost;host=hostname[;port=port][;scheme=https][;client=1.2.3.4]/path to allow passing arbitrary variables. That way it's easy to extend in the future.
Isn't that going to waste more resources and isn't it much more complex than the approach I've taken to simply pass a variable to appserver.NevowSite(proxyPeer = 1). Perhaps proxyPeer isn't the right name for the variable but you get the idea. Now my patch is too dirty to even think at applying it, but it still shows the place to touch and I didn't need to play with vhost complexity, I keep the trigger as a parameter to NevowSite (the literal parameter isn't implemented yet, and that's why it clearly cannot be applied). In theory banana could pass down the whole object returned by getPeer() from the load balancer to the http server, but I was scared to hurt performance by using banana, and I'm only dealing with .host and .port which I'm sure will go fast.
On Sat, 29 Jan 2005 15:55:50 +0100, Andrea Arcangeli <andrea@cpushare.com> wrote:
I successfully load balanced the static server with pythondirector.
However it has a few problems:
1) it cannot handle ssl 2) even if it can handle ssl I still need to share the session cookies and I'm thinking that using sql or atop to do that isn't necessary: the session doesn't need to survive a reboot, a reboot would be a visible disruption of the services anyway, so I'm thinking to write a little session daemon that only stores the cookies using pb to do that. In theory shared memory would be much more efficient in smp, but at least this socket model has the advantage of scaling in the cluster too 3) the load balancer misses an API with the real webserver to pass up the client IP address, that's annoying, especially the logs gets screwed
Other than that the speed of the load balancer is excellent, and I get exactly the double number of pages rendered per second (after fixing a small bug in pythondirector that confused ab2).
It's probably also a good idea to write a balancer that works on unix sockets. And this means also writing a good path to which it should dispatch. Tell me more about the session daemon. Anyway we are designing an ISessionManager interface to let you write whatever sessionFactory you need, a database or a SessionDaemon or a file or something else. Probably you can help with it by coming in #twisted.web and commenting it with one of us (Donovan, Matt, Tv, me and others).
Perhaps I'm going with wrong priorities though, the major offender is compy, compy must be dropped from Nevow ASAP :). Leaving it as a
compy is not going away :). Writing a compy2 speedup in Pyrex will help and will probably also be faster than zope.interface since it will be a lot smaller.
compatibility API may be ok, but internally compy can't invoke getComponents anymore if we do care about writing remotely optimal code. We should try with the new zope.interfaces API first and see if it underperforms so horribly as getInterfaces does.
zope.interface is twice as fast without the compatibility stuff in twisted and I think it is the same for Nevow.
Secondly I'm looking into caching the html and to render some fragment only once every 10 seconds in the background (so the downloads will never have to wait for a rendering of some mostly static fragment anymore).
I wrote this VERY simple stuff for caching a page: Index: rend.py =================================================================== --- rend.py (revision 1105) +++ 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 @@ -376,6 +377,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 @@ -417,7 +419,8 @@ io = StringIO() writer = io.write def finisher(result): - request.write(io.getvalue()) + c = _CACHE[url.fromContext(ctx)] = io.getvalue() + request.write(c) finishRequest() return result else: @@ -425,12 +428,17 @@ def finisher(result): finishRequest() return result + c = _CACHE.get(url.fromContext(ctx), None) + if c is None: + doc = self.docFactory.load() + ctx = WovenContext(ctx, tags.invisible[doc]) + + return self.flattenFactory(doc, ctx, writer, finisher) + else: + request.write(c) + finishRequest() + return c - doc = self.docFactory.load() - ctx = WovenContext(ctx, tags.invisible[doc]) - - return self.flattenFactory(doc, ctx, writer, finisher) - def rememberStuff(self, ctx): Fragment.rememberStuff(self, ctx) ctx.remember(self, inevow.IResource) This works and I've tested it. Rendering speed went from 6-7 requests/sec to 26 req/sec on my poor ibook with the database on the same computer and ab too. This patch is simple, probably too simple (in fact it would be better to cache the flattening result, this would be a lot more fine grained) since it only works in buffered mode (patching this patch to work in non buffered mode is not hard at all though)
So overall there's an huge room for improvements. What do other people think?
I also think that the optimizations branch is worth of some experimentation. I got twice the rendering speed in dynamic pages thanks to its adapters caching. I'd give it a try. Overall I think nevow can, and will, speedup at least by a factor of 5.
On Jan 29, 2005, at 7:26 PM, Valentino Volonghi aka Dialtone wrote:
Perhaps I'm going with wrong priorities though, the major offender is compy, compy must be dropped from Nevow ASAP :). Leaving it as a
compy is not going away :).
dp also stated this at one point on IRC. I didn't have time to discuss it at that point, but I think it ought to be discussed. Why does nevow need/want its own components API, that is either 1) is a fork that is nearly identical to the old twisted API (when running on Tw 1.3), or 2) uses the old twisted API compatibility layer (when running on Tw 2.0). Twisted has successfully disposed of its components API, for several good reasons. What purpose does it serve to continue the life of that code in Nevow? As far as I'm concerned, the appropriate course of action is to convert everything to use the zope interface API.
Writing a compy2 speedup in Pyrex will help and will probably also be faster than zope.interface since it will be a lot smaller.
Now not only are we keeping the old code, but heavily rewriting it? I have my doubts about those speed benefits, too... James
On Sat, Jan 29, 2005 at 08:24:49PM -0500, James Y Knight wrote:
On Jan 29, 2005, at 7:26 PM, Valentino Volonghi aka Dialtone wrote:
Perhaps I'm going with wrong priorities though, the major offender is compy, compy must be dropped from Nevow ASAP :). Leaving it as a
compy is not going away :).
dp also stated this at one point on IRC. I didn't have time to discuss it at that point, but I think it ought to be discussed. Why does nevow need/want its own components API, that is either 1) is a fork that is nearly identical to the old twisted API (when running on Tw 1.3), or 2) uses the old twisted API compatibility layer (when running on Tw 2.0). Twisted has successfully disposed of its components API, for several good reasons. What purpose does it serve to continue the life of that code in Nevow?
I definitely agree with you, there's no point in duplicating this code except to risk having more bugs and risk not merging optimizations from the other duplicate forks. Let's not fall in love with compy if that means duplicating efforts.
As far as I'm concerned, the appropriate course of action is to convert everything to use the zope interface API.
Yep. And if it's still *that* slow, then we should rewrite *zope.interfaces* (not compy) with pyrex. what's the point of writing compy with pyrex and leaving zope.interfaces dogslow? We should do that once for all if we do it. Leaving compy as a pure "API-forwarder", so old code doesn't break may even be ok of course, but it should not try to duplicate anything that is already available in zope.interfaces. But still I'm sceptical about the computational complexity of the compy stuff, so I believe that should be evaluated before going in C.
Writing a compy2 speedup in Pyrex will help and will probably also be faster than zope.interface since it will be a lot smaller.
Now not only are we keeping the old code, but heavily rewriting it? I have my doubts about those speed benefits, too...
Agreed.
On Sun, Jan 30, 2005 at 12:36:03PM +0100, Andrea Arcangeli wrote: [...]
Yep. And if it's still *that* slow, then we should rewrite *zope.interfaces* (not compy) with pyrex. what's the point of writing compy with pyrex and leaving zope.interfaces dogslow? We should do that once for all if we do it.
zope.interface already has an (optional) C module with optimisations in it. -Andrew.
On Sun, Jan 30, 2005 at 11:18:52PM +1100, Andrew Bennetts wrote:
zope.interface already has an (optional) C module with optimisations in it.
Yet another reason for definitely switching to zope.interfaces. Still I'd like an evaluation of the computational complexity of zope.interfaces to be sure the C implementation isn't a workaround. I'm not going to use interfaces in my code at all, unless somebody confirms interfaces run always in O(1) like if we could do ctx.request instead of IRequest(ctx). ctx.request is definitely O(1), and IRequest(ctx) must be avalable in O(1) too, otherwise it's a mistake to use interfaces at all. ctx.request is blazing fast, and IRequest(ctx) must not be slower than ctx.request. And I'm glad with basic twisted there are no interfaces at all. Like going from protocol to factory has to be done with protocol.factory, not IFactory(protocol). I really prefer to stay with pointers and not with the interfaces as long as I can in my code, unless somebody guarantees that zope.interfaces is O(1). (my http server is an exception, I use interfaces there since I was partly forced by inevow)
On Sun, Jan 30, 2005 at 12:26:33AM +0000, Valentino Volonghi wrote:
It's probably also a good idea to write a balancer that works on unix sockets. And this means also writing a good path to which it should dispatch.
I already wrote something that works for me, but I'm running into troubles with ssl. For various reasons I can't use this dirty hack unless it covers ssl too, and before I can truly load balance the ssl I'll need to share the session first. Here the hack just in case somebody can find it useful (works perfectly with http). Just make sure to leave the 8080/8081 etc.. closed by the firewall or it'd be trivial to fake the client IP address in the logs. Only the load balancer port must be open in the firewall. You're warned ;) I'm not proposing this hack for merging, it doesn't even have an API to pass to appserver.NevowSite, but this might be useful to get an hint on how to make it work. --- ./Nevow/nevow/appserver.py.~1~ 2005-01-29 02:12:44.000000000 +0100 +++ ./Nevow/nevow/appserver.py 2005-01-29 17:11:16.000000000 +0100 @@ -222,7 +222,8 @@ class NevowSite(server.Site): def __init__(self, *args, **kwargs): server.Site.__init__(self, *args, **kwargs) self.context = context.SiteContext() - + self.proxyPeer = True + def remember(self, obj, inter=None): """Remember the given object for the given interfaces (or all interfaces obj implements) in the site's context. --- ./Twisted/twisted/web/http.py.~1~ 2005-01-14 20:44:45.000000000 +0100 +++ ./Twisted/twisted/web/http.py 2005-01-29 17:43:56.000000000 +0100 @@ -526,7 +526,10 @@ class Request: # cache the client and server information, we'll need this later to be # serialized and sent with the request so CGIs will work remotely - self.client = self.channel.transport.getPeer() + if not self.channel.proxyPeer: + self.client = self.channel.transport.getPeer() + else: + self.client = self.channel.proxyPeer self.host = self.channel.transport.getHost() # Argument processing @@ -909,6 +912,7 @@ class HTTPChannel(basic.LineReceiver, po __header = '' __first_line = 1 __content = None + proxyPeer = None # set in instances or subclasses requestFactory = Request @@ -921,11 +925,18 @@ class HTTPChannel(basic.LineReceiver, po def connectionMade(self): self.setTimeout(self.timeOut) - + + def handleProxyPeer(self, line): + self.proxyPeer = self.transport.getPeer() + self.proxyPeer.host, self.proxyPeer.port = line.split() + def lineReceived(self, line): self.resetTimeout() if self.__first_line: + if self.factory.proxyPeer and not self.proxyPeer: + self.handleProxyPeer(line) + return # if this connection is not persistent, drop any data which # the client (illegally) sent after the last request. if not self.persistent: @@ -1086,6 +1097,7 @@ class HTTPFactory(protocol.ServerFactory logPath = os.path.abspath(logPath) self.logPath = logPath self.timeOut = timeout + self.proxyPeer = False def buildProtocol(self, addr): p = protocol.ServerFactory.buildProtocol(self, addr) Index: pythondirector/pydirector/pdnetworktwisted.py =================================================================== RCS file: /cvsroot/pythondirector/pythondirector/pydirector/pdnetworktwisted.py,v retrieving revision 1.11 diff -u -p -r1.11 pdnetworktwisted.py --- pythondirector/pydirector/pdnetworktwisted.py 14 Dec 2004 13:31:39 -0000 1.11 +++ pythondirector/pydirector/pdnetworktwisted.py 29 Jan 2005 16:49:56 -0000 @@ -58,7 +58,7 @@ class Sender(Protocol): """ if self.receiver is not None: if reason.type is twisted.internet.error.ConnectionDone: - return + pass elif reason.type is twisted.internet.error.ConnectionLost: pass else: @@ -78,7 +78,8 @@ class Sender(Protocol): we've connected to the destination server. tell the other end it's ok to send any buffered data from the client. """ - #print "client connection",self.factory + peer = self.receiver.transport.getPeer() + self.transport.write('%s %s\r\n' % (peer.host, peer.port)) if self.receiver.receiverOk: self.receiver.setSender(self) else:
Tell me more about the session daemon. Anyway we are designing an ISessionManager interface to let you write whatever sessionFactory you need, a database or a SessionDaemon or a file or something else. Probably you can help with it by coming in #twisted.web and commenting it with one of us (Donovan, Matt, Tv, me and others).
You're right. I'm having an hard time to use #irc because I'm doing this in my spare time, often at weird times, I can't work on this during the day or I would go bankrupt ;).
Perhaps I'm going with wrong priorities though, the major offender is compy, compy must be dropped from Nevow ASAP :). Leaving it as a
compy is not going away :). Writing a compy2 speedup in Pyrex will help and will probably also be faster than zope.interface since it will be a lot smaller.
I diagree, see other email for the details on the reasoning of my disagreement ;).
zope.interface is twice as fast without the compatibility stuff in twisted and I think it is the same for Nevow.
So let's use zope.interfaces. I don't care if we pass through twisted, especially if raw zope.interfaces is faster and twisted depends on it anyway, we should probably avoid passing through twisted. But like twisted is giving it up to use its own implementation, we should give it up to use our implementation. The twice as fast will translate in thousand times faster. This thing gets called thousand of times per page or similar. I get 100000 calls of the deprecated API in a trivial workload, so much that removing the deprecation warning one liner that I posted some day ago, is already a double digit percent boost ;). So I believe it worth a try, and eliminating duplicated code sure cannot make things worse in the long run ;).
Secondly I'm looking into caching the html and to render some fragment only once every 10 seconds in the background (so the downloads will never have to wait for a rendering of some mostly static fragment anymore).
I wrote this VERY simple stuff for caching a page:
Index: rend.py =================================================================== --- rend.py (revision 1105) +++ 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 @@ -376,6 +377,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 @@ -417,7 +419,8 @@ io = StringIO() writer = io.write def finisher(result): - request.write(io.getvalue()) + c = _CACHE[url.fromContext(ctx)] = io.getvalue() + request.write(c) finishRequest() return result else: @@ -425,12 +428,17 @@ def finisher(result): finishRequest() return result + c = _CACHE.get(url.fromContext(ctx), None) + if c is None: + doc = self.docFactory.load() + ctx = WovenContext(ctx, tags.invisible[doc]) + + return self.flattenFactory(doc, ctx, writer, finisher) + else: + request.write(c) + finishRequest() + return c
- doc = self.docFactory.load() - ctx = WovenContext(ctx, tags.invisible[doc]) - - return self.flattenFactory(doc, ctx, writer, finisher) - def rememberStuff(self, ctx): Fragment.rememberStuff(self, ctx) ctx.remember(self, inevow.IResource)
This works and I've tested it.
Rendering speed went from 6-7 requests/sec to 26 req/sec on my poor ibook with the database on the same computer and ab too.
This is great, I'll play with this code very soon. This is a much more significant optimization than the load balancer, with the load balancer I could only double the number of pages per second.
This patch is simple, probably too simple (in fact it would be better to cache the flattening result, this would be a lot more fine grained) since it only works in buffered mode (patching this patch to work in non buffered mode is not hard at all though)
No problem, it's still a good start ;).
So overall there's an huge room for improvements. What do other people think?
I also think that the optimizations branch is worth of some experimentation. I got twice the rendering speed in dynamic pages thanks to its adapters caching. I'd give it a try.
Overall I think nevow can, and will, speedup at least by a factor of 5.
Sounds great, thanks!
On Sun, 30 Jan 2005 12:46:35 +0100, Andrea Arcangeli <andrea@cpushare.com> wrote:
So I believe it worth a try, and eliminating duplicated code sure cannot make things worse in the long run ;).
This is the biggest try though :). Formless will need a lot of work.
Index: rend.py =================================================================== --- rend.py (revision 1105) +++ 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 @@ -376,6 +377,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 @@ -417,7 +419,8 @@ io = StringIO() writer = io.write def finisher(result): - request.write(io.getvalue()) + c = _CACHE[url.fromContext(ctx)] = io.getvalue() + request.write(c) finishRequest() return result else: @@ -425,12 +428,17 @@ def finisher(result): finishRequest() return result + c = _CACHE.get(url.fromContext(ctx), None) + if c is None: + doc = self.docFactory.load() + ctx = WovenContext(ctx, tags.invisible[doc]) + + return self.flattenFactory(doc, ctx, writer, finisher) + else: + request.write(c) + finishRequest() + return c
- doc = self.docFactory.load() - ctx = WovenContext(ctx, tags.invisible[doc]) - - return self.flattenFactory(doc, ctx, writer, finisher) - def rememberStuff(self, ctx): Fragment.rememberStuff(self, ctx) ctx.remember(self, inevow.IResource)
This works and I've tested it.
Rendering speed went from 6-7 requests/sec to 26 req/sec on my poor ibook with the database on the same computer and ab too.
This is great, I'll play with this code very soon. This is a much more significant optimization than the load balancer, with the load balancer I could only double the number of pages per second.
Actually I wonder how did you manage to use this patch. I notice it is wrong in 2 lines when the code calls url.fromContext(ctx) which should be url.URL.fromContext(ctx).path
On Sun, Jan 30, 2005 at 11:02:14PM +0000, Valentino Volonghi wrote:
This is the biggest try though :). Formless will need a lot of work.
And formless will be the one that will get the most benefit too ;) But we probably don't need to convert formless to give it a try: we can try to do the other parts first. The page I'm benchmarking (the one that now runs at 227 hits/sec ;) has zero formless, it's only containing renderings and still getInterfaces gets at the very top, so much that commenting out the warning line is a very measurable boost.
Actually I wonder how did you manage to use this patch. I notice it is wrong in 2 lines when the code calls url.fromContext(ctx) which should be url.URL.fromContext(ctx).path
See the other emails.
On Sun, Jan 30, 2005 at 12:26:33AM +0000, Valentino Volonghi wrote:
This works and I've tested it.
Even if you tested it, I doubt you really benchmarked it ;). Likely it was disabled for you (perhaps you forgot setup.py install?) but I fixed you great hack and here we go: Index: nevow/rend.py =================================================================== --- nevow/rend.py (revision 1134) +++ 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 @@ -415,7 +417,8 @@ io = StringIO() writer = io.write def finisher(result): - request.write(io.getvalue()) + c = _CACHE[str(url.URL.fromContext(ctx))] = io.getvalue() + request.write(c) finishRequest() return result else: @@ -423,12 +426,17 @@ def finisher(result): finishRequest() return result + c = _CACHE.get(str(url.URL.fromContext(ctx))) + if c is None: + doc = self.docFactory.load() + ctx = WovenContext(ctx, tags.invisible[doc]) + + return self.flattenFactory(doc, ctx, writer, finisher) + else: + request.write(c) + finishRequest() + return c - doc = self.docFactory.load() - ctx = WovenContext(ctx, tags.invisible[doc]) - - return self.flattenFactory(doc, ctx, writer, finisher) - def rememberStuff(self, ctx): Fragment.rememberStuff(self, ctx) ctx.remember(self, inevow.IResource) Index: nevow/vhost.py =================================================================== --- nevow/vhost.py (revision 1134) +++ 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() I get 224!! pages per second from the homepage with this. This is exactly what I need. I had to set buffered = True in the pages where I enabled this of course. This is ApacheBench, Version 2.0.40-dev <$Revision: 1.121.2.10 $> apache-2.0 Copyright (c) 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ Copyright (c) 1998-2002 The Apache Software Foundation, http://www.apache.org/ Benchmarking opteron (be patient).....done Server Software: TwistedWeb/aa Server Hostname: opteron Server Port: 8080 Document Path: / Document Length: 10606 bytes Concurrency Level: 2 Time taken for tests: 0.439832 seconds Complete requests: 100 Failed requests: 0 Write errors: 0 Total transferred: 1072500 bytes HTML transferred: 1060600 bytes Requests per second: 227.36 [#/sec] (mean) Time per request: 8.797 [ms] (mean) Time per request: 4.398 [ms] (mean, across all concurrent requests) Transfer rate: 2380.45 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 0 0 0.0 0 0 Processing: 8 8 0.3 8 9 Waiting: 8 8 0.1 8 9 Total: 8 8 0.3 8 9 Percentage of the requests served within a certain time (ms) 50% 8 66% 8 75% 8 80% 8 90% 8 95% 9 98% 9 99% 9 100% 9 (longest request) Transfer rate was 2.3Mbyte/sec. This is very great! Something is buggy still, since I get the same page duplicated twice, but the performance are already excellent, actually much faster than what I need. And a lot more than 5 times faster, it goes from 7 req per second to 227 req per second, so it's 32 times faster. The next bit to stack on top of the above is a timeout parameter, after the timeout the page has to be re-rendered in the _background_, it's enough to attach a deferred to it, that will overwrite the cache after the rendering is complete. I'm also afraid I should use the flattener on the url before transforming it to a string? Or not? You're right that removing compy isn't that an high priority compared to enabling the caching right. Still I'd like to see compy removed in favour of zope.interfaces, on the same lines of twisted. Not everything will be cached, the very dynamic stuff cannot be cached, and compy will help there. But all http I can cache it, only the ssl is completely dynamic. This is as fast as using 32 UP systems with the http load balancer, so you're very right that this was the first angle of attack to use ;). Many thanks!
On Sun, Jan 30, 2005 at 01:56:50PM +0100, Andrea Arcangeli wrote:
but I fixed you great hack and here we go:
Ok I already made it good enough for merging IMHO! Please don't keep this in a branch that risks to get obsolete. This is a major useful feature IMHO. Index: nevow/rend.py =================================================================== --- nevow/rend.py (revision 1134) +++ 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,29 @@ 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 refreshCache(self): + assert self.cacheTimeout is not None + _now = now() # run gettimeofday only once + timeout = _now > self.__lastCacheRendering + self.cacheTimeout and self.cacheTimeout > 0 + if timeout: + self.__lastCacheRendering = _now + return timeout + 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): ## XXX request is really ctx now, change the name here request = inevow.IRequest(ctx) @@ -411,24 +430,27 @@ if self.afterRender is not None: self.afterRender(ctx) - if self.buffered: + if self.buffered or self.cacheTimeout is not None: io = StringIO() writer = io.write def finisher(result): - request.write(io.getvalue()) - finishRequest() - return result + c = io.getvalue() + self.storeCache(ctx, c) + return c else: writer = request.write def finisher(result): finishRequest() return result + c = self.lookupCache(ctx) + if c is None or self.refreshCache(): + doc = self.docFactory.load() + ctx = WovenContext(ctx, tags.invisible[doc]) - doc = self.docFactory.load() - ctx = WovenContext(ctx, tags.invisible[doc]) + return self.flattenFactory(doc, ctx, writer, finisher) + else: + return c - return self.flattenFactory(doc, ctx, writer, finisher) - def rememberStuff(self, ctx): Fragment.rememberStuff(self, ctx) ctx.remember(self, inevow.IResource) Index: nevow/vhost.py =================================================================== --- nevow/vhost.py (revision 1134) +++ 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() Only one thing I'm not sure about: I'm unsure about the meaning of the result passed to the finisher. Does it matter at all? Is it always '' right? It has to be a null string, I can't see how it can't be a null string. Otherwise we'd need to cache it too and change the patch a bit. In my limited testing result is always '' so I didn't bother to cache it. You know, at >200 req per second with quite a ton of dynamic stuff inside, I'm very relaxed now. 220 req per second means the homepage could sustain a load of 19 million hits per day and 570million hits per month. It will be less than that, since the completely dynamic part will still suck much cpu power, but having the basic web going fast is a great bonus already, and clearly there will be more traffic on the outside pages than in the inside pages. The timeout I'm using is 10 sec, that means once every 10 sec it will execute a synchronous rendering. But that's ok, if the load goes up too much moving it to 60 sec will fix it. I believe this caching scheme should stay in place and be merged, since it's the most efficient caching possible, very suitable for pages that changes not very frequently or that are completely static. Other caching with more finer granularity can happen on top of this, but this is the highest prio one IMHO. This for example fits perfectly in the "/" page of my site and other high traffic mostly static html pages (it's not completely static and it changes once every 10 sec, so it's still possible to edit the xml files or to rebuild the class with stan loader). Setting cacheTimeout <= 0 will cache the page forever, that ok for loaders.stan unless you use rebuild. Here you see below the only change I had to make to my site to enable the caching in a production ready usage. Thanks a lot Valentino^wdialtone! ;) --- cpushare/web/redirect.py 29 Jan 2005 02:05:56 -0000 1.8 +++ cpushare/web/redirect.py 30 Jan 2005 14:06:10 -0000 @@ -21,6 +21,7 @@ class download_class(basepage_class): class redirect_http_to_https(root_basepage_class): addSlash = True + cacheTimeout = 10 docFactory = loaders.xmlfile('root_page.xml', XMLDIR) child_css = static.File('styles/cpushare.css')
On Sun, Jan 30, 2005 at 03:19:00PM +0100, Andrea Arcangeli wrote:
+ c = self.lookupCache(ctx) + if c is None or self.refreshCache(): + doc = self.docFactory.load() + ctx = WovenContext(ctx, tags.invisible[doc])
In the above place I realized there was a subtle and not really important race condition where the following renderings could get old stale data from the cache while the flattening was running. My object is to call gettimeofday only one (since it's costly, especially on x86 w/o vsyscalls like x86-64) and secondly I want to run a single flattening, so moving the timestamp into the finisher wouldn't have fixed it either (since that would have invoked many unnecessary flattening, until the first one would have completed). That race would have been a very minor problem for my usage, but I fixed this optimally in this further update. This however use chained deferreds, so that I store the deferred in the cache and all following renderers now stop waiting for the single flattening to complete. So now cache is usable only with twisted, but I think this is perfectly ok, since without deferred the optimal implementation isn't doable (and if the twisted thread isn't persistent the cache will be destroyed anyway by execve ;). The thread that invokes the flattening (i.e. the one calling chainDeferredCache) could return 'd' too, not necessairly 'c', but I thought returning c would be more robust there too and less likely to break in the long run, because it exercises the code that only makes a difference under the race condition window. (all other guys will have to wait for 'c' not 'd') Performance isn't an issue there. So this is more complex, but more correct, and it works fine too so far. Performance is unchanged, only the race condition window is closed by making caching dependent on twisted. Should still run w/o twisted as long as you don't try to add caching to it. So I'm keeping it applyed and I'll start optimizing all possible pages with this feature. It should be good enough for merging. Feel free to change the variable names if you don't like my coding style (I tried not to follow the kernel coding style even if I like it more ;) Thanks. Index: nevow/rend.py =================================================================== --- nevow/rend.py (revision 1134) +++ 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,47 @@ 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): + d.chainDeferred(c) + return c + 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): ## XXX request is really ctx now, change the name here request = inevow.IRequest(ctx) @@ -411,23 +448,27 @@ if self.afterRender is not None: self.afterRender(ctx) - if self.buffered: + if self.buffered or self.cacheTimeout is not None: io = StringIO() writer = io.write def finisher(result): - request.write(io.getvalue()) - finishRequest() - return result + c = io.getvalue() + self.storeCache(ctx, c) + return c else: writer = request.write def finisher(result): finishRequest() return result + c = self.hasCache(ctx) + if c: + return c + 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) As usual this unrelated fix is queued. Index: nevow/vhost.py =================================================================== --- nevow/vhost.py (revision 1134) +++ 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()
I did some benchmarking with the last patch applied, and there's not really a significant benefit anymore from the load balancer with this patch. Without caching (at 6/7 req per second) load balancing was a net 100% improvement, now it's down to a 23.5% improvement in terms of req per second. ab2 -n 1000 -c 100 localhost:8080/ | grep 'Requests Requests per second: 224.73 [#/sec] (mean) ab2 -n 1000 -c 100 localhost:8081/ | grep 'Requests Requests per second: 225.18 [#/sec] (mean) ab2 -n 1000 -c 100 localhost:8079/ | grep 'Requests Requests per second: 278.22 [#/sec] (mean) The load balancer was on port 79 and it balanced the load across 8080 and 8081 on a two-way through loopback interface. Probably on a 4-way it would make more difference since the load balancer itself takes some cpu. The nice thing is that the above is still a completely dynamic page, but it changes now at most once every 10 seconds, which is exactly what I need. Overall this is a much superior approach and much simpler to setup if compared to the reverse cache proxying IMHO, and it should perform better too since it avoids the two context switches between proxy -> server -> proxy. So the next improvement on top of this would be replace compy with zope.interfaces for the very dynamic ssl part that cannot be cached at all. Thanks! ;)
On Jan 30, 2005, at 9:19 AM, Andrea Arcangeli wrote:
On Sun, Jan 30, 2005 at 01:56:50PM +0100, Andrea Arcangeli wrote:
but I fixed you great hack and here we go:
Ok I already made it good enough for merging IMHO! Please don't keep this in a branch that risks to get obsolete. This is a major useful feature IMHO.
Yes, it is. One thing that I think would make it more useful, if it can be pulled off, is to allow caching at any level. That is, something like the following stan: html[body[cached(timeout=10, keys=(IFoo, IBar))[semi_constant_header_stuff], very_dynamic_content]] That is, a function which will render its contents to a string the first time it's called, and store/return it for the next 10 seconds, using the same mechanism as the posted patch. The cache would be keyed off certain interfaces, and only those will get passed on to the functions being rendered inside, thus ensuring the safety of the cache. I think there are a lot of pages that are mostly "semi-static", but have some very dynamic content in them, so something like could be *very* useful. Anyhow, this is just an outline of how I think it might work, but I haven't tried to implement it yet, so I don't know if it will work out the way I'd like it to. :) James
On Tue, 1 Feb 2005 00:32:25 -0500, James Y Knight <foom@fuhm.net> wrote:
Yes, it is. One thing that I think would make it more useful, if it can be pulled off, is to allow caching at any level. That is, something like the following stan: html[body[cached(timeout=10, keys=(IFoo, IBar))[semi_constant_header_stuff], very_dynamic_content]]
That is, a function which will render its contents to a string the first time it's called, and store/return it for the next 10 seconds, using the same mechanism as the posted patch. The cache would be keyed off certain interfaces, and only those will get passed on to the functions being rendered inside, thus ensuring the safety of the cache.
I think there are a lot of pages that are mostly "semi-static", but have some very dynamic content in them, so something like could be *very* useful.
Anyhow, this is just an outline of how I think it might work, but I haven't tried to implement it yet, so I don't know if it will work out the way I'd like it to. :)
Here I am. Below there's a more fine grained implementation. You can use it like this: cached(name="foo", lifetime=10)[t.p['hi boy']] the name should be enough to give a cache for everyone since you can do: cached(name=IAvatar(ctx).uid, lifetime=60)[t.p['Hi ', IAvatar(ctx).username]] If you don't pass the lifetime parameter it won't be cached. I must admit I haven't tested it yet since I don't know how to write unittests for this stuff... ;P. Let me know what do you think about. I also think that having another nevow tag for this cached would be ok. Index: nevow/tags.py =================================================================== --- nevow/tags.py (revision 1136) +++ nevow/tags.py (working copy) @@ -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/flat/flatstan.py =================================================================== --- nevow/flat/flatstan.py (revision 1136) +++ nevow/flat/flatstan.py (working copy) @@ -226,6 +226,31 @@ return serialize(original.default, context) return serialize(data, context) +_CACHE = {} +from time import time as now +def CachedSerializer(original, context): + if context.precompile: + original.children = precompile(original.children, context) + return original + + cached = _CACHE.get(original.name, None) + if cached and cached[0] > now()-original.lifetime: + return cached[1] + toSerialize = serialize(original.children, context) + tmp = [] + while 1: + try: + d = toSerialize.next() + tmp.append(d) + except StopIteration: + c = ''.join(tmp) + break + except AttributeError: + c = toSerialize + break + _CACHE[original.name] = (now(), c) + return c + def ContextSerializer(original, context): originalContext = original.clone(deep=False) originalContext.precompile = context and context.precompile or False Index: nevow/__init__.py =================================================================== --- nevow/__init__.py (revision 1136) +++ nevow/__init__.py (working copy) @@ -182,6 +182,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/stan.py =================================================================== --- nevow/stan.py (revision 1136) +++ 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
On Tue, 01 Feb 2005 12:58:49 GMT, Valentino Volonghi aka Dialtone <dialtone@divmod.com> wrote: The first version of the patch didn't actually work. But I wrote a new version, also thanks to fzZzy and this time it works although it has a flaw since in weever caching the content slot (which is filled with a Fragment) results in 2 big red Nones and the rendered fragment. As I said in the first mail you can use caching with: t.cached(name=some_sensible_name, lifetime=MAX_LIFE)[cached_content] This patch provides, probably, the finest granularity in caching the rendering. Anyway the patch is below: Index: nevow/tags.py =================================================================== --- nevow/tags.py (revision 1136) +++ 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/flat/flatstan.py =================================================================== --- nevow/flat/flatstan.py (revision 1136) +++ nevow/flat/flatstan.py (working copy) @@ -9,7 +9,7 @@ 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 @@ -226,6 +226,56 @@ return serialize(original.default, context) return serialize(data, context) +_CACHE = {} +from time import time as now +from cStringIO import StringIO +from twisted.internet import defer +def CachedSerializer(original, context): + cached = _CACHE.get(original.name, None) + life = now()-original.lifetime + if cached and cached[0] > life: +## print "="*20 +## print cached[0] +## print life +## print "="*20 + yield cached[1] + return +## if cached: +## print "="*20 +## print cached[0] +## print life +## print "="*20 + 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: None + 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/__init__.py =================================================================== --- nevow/__init__.py (revision 1136) +++ nevow/__init__.py (working copy) @@ -182,6 +182,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/stan.py =================================================================== --- nevow/stan.py (revision 1136) +++ 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
On Tue, 2005-02-01 at 16:05 +0000, Valentino Volonghi aka Dialtone wrote:
On Tue, 01 Feb 2005 12:58:49 GMT, Valentino Volonghi aka Dialtone <dialtone@divmod.com> wrote:
The first version of the patch didn't actually work. But I wrote a new version, also thanks to fzZzy and this time it works although it has a flaw since in weever caching the content slot (which is filled with a Fragment) results in 2 big red Nones and the rendered fragment.
As I said in the first mail you can use caching with:
t.cached(name=some_sensible_name, lifetime=MAX_LIFE)[cached_content]
This patch provides, probably, the finest granularity in caching the rendering.
Anyway the patch is below:
I think it would be much better if the _CACHE module-scope dict was replaced with an object remembered in the context. There are a couple of reasons for this: * We can have persistence to the file system when necessary. * We can remember a cache manager on a resource to allow drop-in components (and their child resources) to manage their own caching. This also allows some root resource class to have multiple instances, where the interface names used as the cache keys will likely be the same, to be deployed under a single site. * The cache manager API can be extended in the future to allow manual clearing of cache items, i.e. some public web UI can cache parts of the page indefintely and an admin UI (that shares the same cache manager) can clear cached data as objects are modified. There are also a couple of features that I can see stan.cached "growing" later on. I've mentioned some of these on IRC. * Cache scope, i.e. application vs session. As I've said on IRC, I can see a real use case for session-scoped caching, i.e. I get my cached version; you get yours. * It might be nice to allow timeouts to be defined as "every hour", "every fifteen minutes", "at 12am". Yeah, I'm talking cron-like ;-). Hmm, one last idea is cache groups. I think this is especially applicable to the above idea of having an API to clear cache objects. Say some part of the page includes content from two objects: a Foo with id 3 and a Bar with id 8. The fragment could be cached against the key ((Foo,3),(Bar,8)). If some user then changed the Foo,3 object it would clear (Foo,3) cached objects; if Bar,8 was changed it would clear (Bar,8) cached objects. Either one would remove the ((Foo,3),(Bar,8)) cached content. Obviously, it would be up to the application to choose its keys carefull but, basically, if the equivalent of "(Foo,3) in ((Foo,3),(Bar,8))" succeeds then the object would be cleared. I don't think we have to add all these features right now as long as the initial API takes these sorts of use cases into consideration. Cheers, Matt
Index: nevow/tags.py =================================================================== --- nevow/tags.py (revision 1136) +++ 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/flat/flatstan.py =================================================================== --- nevow/flat/flatstan.py (revision 1136) +++ nevow/flat/flatstan.py (working copy) @@ -9,7 +9,7 @@ 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
@@ -226,6 +226,56 @@ return serialize(original.default, context) return serialize(data, context)
+_CACHE = {} +from time import time as now +from cStringIO import StringIO +from twisted.internet import defer +def CachedSerializer(original, context): + cached = _CACHE.get(original.name, None) + life = now()-original.lifetime + if cached and cached[0] > life: +## print "="*20 +## print cached[0] +## print life +## print "="*20 + yield cached[1] + return +## if cached: +## print "="*20 +## print cached[0] +## print life +## print "="*20 + 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: None + 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/__init__.py =================================================================== --- nevow/__init__.py (revision 1136) +++ nevow/__init__.py (working copy) @@ -182,6 +182,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/stan.py =================================================================== --- nevow/stan.py (revision 1136) +++ 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
_______________________________________________ Twisted-web mailing list Twisted-web@twistedmatrix.com http://twistedmatrix.com/cgi-bin/mailman/listinfo/twisted-web
On Feb 1, 2005, at 12:15 PM, Matt Goodall wrote:
I think it would be much better if the _CACHE module-scope dict was replaced with an object remembered in the context. There are a couple of reasons for this:
I was actually thinking you could just store it in the 'cached' class itself.
* Cache scope, i.e. application vs session. As I've said on IRC, I can see a real use case for session-scoped caching, i.e. I get my cached version; you get yours.
This would be solved by keying off of which bits of context you allow through. If you let ISession through to the cached rendering context, it would have to be equal to use that cached representation. If you don't let it through, then there's no way you can mess up and let people get eachothers' session data. I think that also does what you were saying about cache groups. James
On Tue, Feb 01, 2005 at 04:05:40PM +0000, Valentino Volonghi wrote:
Anyway the patch is below:
Looks a great start. I'll give it a spin overnight to see what happens.
+_CACHE = {}
Shouldn't this be stored in the respective classes?
+def CachedSerializer(original, context): + cached = _CACHE.get(original.name, None) + life = now()-original.lifetime
Can we execute only one single gettimeofday? gettimeofday is one of the biggest kernel costs of twisted in general (modulo poll). I will deploy initially on x86 (on x86-64 with vsyscalls gettimeofday is zerocost). Could you also keep it similar to my patch where a timeout <= 0 means "cache forever"?
+ if cached and cached[0] > life: +## print "="*20 +## print cached[0] +## print life +## print "="*20 + yield cached[1] + return
Why yield if you return immediatly? Why not return cached[1]?
+ _CACHE[original.name] = (now(), result)
what is contained in original.name? How to identify exactly which object is being cached? (just to understand how should I use this exactly)
+ yield result
here again, why not return result? Do I understand correctly this more finegriend cache doesn't obsolete the other cache? The other cache is probably the fastest we can get, and it pretty much solves my problem for the high traffic part. However I will definitely need this finegrined cache as well in the longer run, even if it's lower priority. One of my first objectives would be to workaround the dogslow rendering of the annotate.Choice renderer. But I'll get great benefit in the header fragment and in other fragments too. As for the session cache, that's a much lower prio to me, the real obvious caching we can do is for _global_ stuff that is the same for the whole site, always. Thanks and keep up the great work! ;) PS. Still I would like to see compy removed, since not everything will be cached. There are parts where I will not cache anything.
On Tue, 1 Feb 2005 20:09:57 +0100, Andrea Arcangeli <andrea@cpushare.com> wrote:
Anyway the patch is below:
Looks a great start.
I'll give it a spin overnight to see what happens.
+_CACHE = {}
Shouldn't this be stored in the respective classes?
There are MANY ideas on where to put this _CACHE. jamwt volunteered for writing a memcache like http://www.danga.com/memcached/ Which will probably be one of the backends. There are also other problems to solve about this cache, but it is working with this patch (that is now committed in the caching branch) and people can test it or provide different behaviours.
+def CachedSerializer(original, context): + cached = _CACHE.get(original.name, None) + life = now()-original.lifetime
Can we execute only one single gettimeofday? gettimeofday is one of the biggest kernel costs of twisted in general (modulo poll). I will deploy initially on x86 (on x86-64 with vsyscalls gettimeofday is zerocost).
Could you also keep it similar to my patch where a timeout <= 0 means "cache forever"?
Yep, this is just a first attempt. Further work will be done on the caching branch.
+ if cached and cached[0] > life: +## print "="*20 +## print cached[0] +## print life +## print "="*20 + yield cached[1] + return
Why yield if you return immediatly? Why not return cached[1]?
Try it yourself :) In python you cannot have a return statement with arguments when inside a generator. CachedSerializer is in fact a generator (because of the yield keyword inside the func body) and can't have a return statement with arguments.
+ _CACHE[original.name] = (now(), result)
what is contained in original.name? How to identify exactly which object is being cached? (just to understand how should I use this exactly)
original name is the first argument of the tag instance. t.cached(name="foobar") this will create an empty cached tag with name foobar, you can also do: t.cached(name=(IFoo, IBar)) as was suggested if you need. No check is done on the type of name but it must be hashable.
Do I understand correctly this more finegriend cache doesn't obsolete the other cache?
I think it does and will surely do if someone will write the flatsax stuff to use it with xhtml templates. with stan you can do: docFatory = loaders.stan(t.cached(name="MainPage", lifetime=10)[t.html[....]]) Which will do the same thing as the first ancient patch. I also get similar performances with the new patch: 26 req/sec and it shouldn't be any slower.
The other cache is probably the fastest we can get, and it pretty much solves my problem for the high traffic part.
I still think 250 req/sec are too much. Are you sure that is not the redirect page in guard?
PS. Still I would like to see compy removed, since not everything will be cached. There are parts where I will not cache anything.
I've talked to dp and he said that compy will be there only to not depend on twisted but it will definately directly use zope.interface if present.
On Tue, Feb 01, 2005 at 08:02:27PM +0000, Valentino Volonghi wrote:
On Tue, 1 Feb 2005 20:09:57 +0100, Andrea Arcangeli <andrea@cpushare.com> wrote:
Anyway the patch is below:
Looks a great start.
I'll give it a spin overnight to see what happens.
+_CACHE = {}
Shouldn't this be stored in the respective classes?
There are MANY ideas on where to put this _CACHE. jamwt volunteered for writing a memcache like http://www.danga.com/memcached/ Which will probably be one of the backends.
Is that a separate task? One of the benefits of having cache inside the python VM that runs the webserver is no context switches and no inter process communication. So I'm not very excited about caching outside nevow ;)
In python you cannot have a return statement with arguments when inside a generator. CachedSerializer is in fact a generator (because of the yield keyword inside the func body) and can't have a return statement with arguments.
Didn't know about that...
+ _CACHE[original.name] = (now(), result)
what is contained in original.name? How to identify exactly which object is being cached? (just to understand how should I use this exactly)
original name is the first argument of the tag instance.
t.cached(name="foobar")
this will create an empty cached tag with name foobar, you can also do:
t.cached(name=(IFoo, IBar))
as was suggested if you need. No check is done on the type of name but it must be hashable.
Ok, so it's up to me to avoid collisions, it's not like the more lowlevel cache where the url was picked automatically.
Do I understand correctly this more finegriend cache doesn't obsolete the other cache?
I think it does and will surely do if someone will write the flatsax stuff to use it with xhtml templates.
with stan you can do:
docFatory = loaders.stan(t.cached(name="MainPage", lifetime=10)[t.html[....]])
Which will do the same thing as the first ancient patch. I also get similar performances with the new patch: 26 req/sec and it shouldn't be any slower.
All my pages are using xml (except for the forms that come from formless), so I'd need the cache for xml templates too. But I really liked the caching using URL, I don't want having to write name="something" by hand.
The other cache is probably the fastest we can get, and it pretty much solves my problem for the high traffic part.
I still think 250 req/sec are too much. Are you sure that is not the redirect page in guard?
There's no guard (I know the issue with the guard redirect), I get 200 req/sec just fine (over loopback, it goes down to 180req/sec with ethernet in the middle). I cannot easily evalute if your same approach for xml is going to work at the same speed of the httprendering, but for sure I don't want having to write name="xxx" by hand. So for now I stick with the cache in the httprendering that guarantees me no cpu bottleneck until 200req/sec. The httprender cache is so easy to use and so efficient and gets automatically right the whole http site, that it doesn't worth for me to even think at messing things up and convert stuff to the new method, even if only because I'd need to choose the index of the hash by hand (and even assuming it has the same performance). So I'd still suggest to apply that patch to the trunk. Not everyone will want to use the more finegrined caches, for dynamic but mostly static data, the httprender cache is just ideal. For the SSL site where I cannot use the httprender cache at all, I'll need the xml loader caching or the fragments, since I only used xml fragments. But I suspect this stan cache could already solve all the forms rendering if I do something like tags.cached(..)[webform.renderForms()], I'm going to try it in a few minutes ;). If I can optimize the forms with this cache it'll be great.
I've talked to dp and he said that compy will be there only to not depend on twisted but it will definately directly use zope.interface if present.
Ok great news! Thanks ;)
On Wed, Feb 02, 2005 at 03:48:40AM +0100, Andrea Arcangeli wrote:
jamwt volunteered for writing a memcache like http://www.danga.com/memcached/ Which will probably be one of the backends.
Is that a separate task? One of the benefits of having cache inside the python VM that runs the webserver is no context switches and no inter process communication. So I'm not very excited about caching outside nevow ;)
I think the aim should be to make this caching feature sufficiently useful on its own, while being sufficiently extensible to allow people to do what they want with it. It'd be nice to have the option of using memcached, or an in-process cache, or... well, you get the idea. I liked Matt's idea of storing something in the context, like an ICacheManager. Of course, I should disclose I liked the idea because I'd planned on the same thing for Payago. :)
Ok, so it's up to me to avoid collisions, it's not like the more lowlevel cache where the url was picked automatically.
Yes, and if you don't trust yourself to avoid collisions, having the cache manager remembered in the context will let you implement a URL picker :) Of course, I think caching is one of those problems that will always be very site-specific, and any implementation that involves modifying the Page class is going to be either too general-purpose to be useful, or too invasive to be stable all the time, for all projects. Your httprender patch is useful, but probably better placed in a subclass. My two cents (adjusted for inflation). -- Alex Levy WWW: http://mesozoic.geecs.org/ "Never let your sense of morals prevent you from doing what is right." -- Salvor Hardin, Isaac Asimov's _Foundation_
On Tue, Feb 01, 2005 at 10:21:22PM -0500, Alex Levy wrote:
I think the aim should be to make this caching feature sufficiently useful on its own, while being sufficiently extensible to allow people to do what they want with it. It'd be nice to have the option of using memcached, or an in-process cache, or... well, you get the idea.
I liked Matt's idea of storing something in the context, like an ICacheManager. Of course, I should disclose I liked the idea because I'd planned on the same thing for Payago. :)
Sure, I'm not against options.
Of course, I think caching is one of those problems that will always be very site-specific, and any implementation that involves modifying the Page class is going to be either too general-purpose to be useful, or too invasive to be stable all the time, for all projects. Your httprender patch is useful,
That's why it can be enabled on a page-by-page basis. And I get 100% caching of the whole http site with the pure cache in rend.Page and it has taken me minutes to enable it and now it runs at 200req/sec instead of 7req/sec. If I change it, I will get no benefit, the API will be more complex, and I only risk to run slower. All forms I have are under ssl, for the ssl part I could pratically cache nothing with that cache and I need the more finegrined one (that I already successfully enabled on all forms but one that is so dynamic that I can't cache it even with the stan cache, but luckily it's the one for the account registration, so it's sure not high traffic). If you could give me a way to use the finegrined cache to cache a whole xml page, then I could benchmark the difference and evaluate if perhaps I should take the pain of using the more complex less ideal API for the http part just to reduce the nevow complexity and to avoid changing rend.Page. I never use loader.stan for the http part (and that's the part I can easily benchmark since it has no guard and no ssl).
but probably better placed in a subclass.
That would probably duplicate code, so I don't think it's ideal, even if it would save you a check on self.cacheTimeout per rendering if you don't use it.
On Wed, Feb 02, 2005 at 03:48:40AM +0100, Andrea Arcangeli wrote:
rendering if I do something like tags.cached(..)[webform.renderForms()], I'm going to try it in a few minutes ;). If I can optimize the forms with this cache it'll be great.
Works great indeed ;). In the first form where I tested it, the rendering time improved from 263msec, to 145msec. In some other form with huge RequiredChoice this should be even more dramatic. If we do the same thing for the xml rendering of the headers I'll like be able to take the time down to 40msec or less. Still this is nothing if compared to the httprendering cache (httprender turns the rendering down to 5msec). So I'm most certainly going to use both httprendering for the high traffic dynamic (but temporary static) data, and the stan cache for the forms and other bits that allows partial caching but that have always dynamic data inside. Here below a very small improvement over your code. This way by default if you don't pass the lifetime parameter it means "cache forever", and that's exactly how I'm going to use it. This conforms with the httprender API that also cache forever with a value == 0. Plus it microoptimizes away one gettimeofday for each rendering. If one is skilled enough to use rebuild (I'm not yet ;), he can sure do a _CACHE = {} too, so I expect most usages will not pass the lifetime and they will use a "forever" cache. BTW, if we keep using the name _CACHE for all caches it'll be easier to find all bits we have to flush to create an API just for rebuild or manhole, to flush all caches. --- ./nevow/flat/flatstan.py.~1~ 2005-02-02 03:51:27.000000000 +0100 +++ ./nevow/flat/flatstan.py 2005-02-02 04:06:18.000000000 +0100 @@ -233,8 +233,9 @@ def SlotSerializer(original, context): _CACHE = {} def CachedSerializer(original, context): cached = _CACHE.get(original.name, None) - life = now()-original.lifetime - if cached and cached[0] > life: + _now = now() + life = _now-original.lifetime + if cached and (cached[0] > life or not original.lifetime): yield cached[1] return io = StringIO() @@ -265,7 +266,7 @@ def CachedSerializer(original, context): yield d, lambda result: '' result = io.getvalue() - _CACHE[original.name] = (now(), result) + _CACHE[original.name] = (_now, result) yield result def ContextSerializer(original, context):
On Wed, 2 Feb 2005 03:48:40 +0100, Andrea Arcangeli <andrea@cpushare.com> wrote:
Is that a separate task? One of the benefits of having cache inside the python VM that runs the webserver is no context switches and no inter process communication. So I'm not very excited about caching outside nevow ;)
The benefits are that it scales to multiple computers, which means a better way to distribute the load. But, as I said, it's just one of the possible backends.
Ok, so it's up to me to avoid collisions, it's not like the more lowlevel cache where the url was picked automatically.
In weever I did a test, and I simply put this in the base class where I fill the content slot (each page is a main page which contains a content slot that is filled with the content Fragment): t.cached(name=str(self.__class__)+IA(ctx).get('uid', ''), lifetime=10)[...] Doing this in the main baseclass gave me caching on ALL pages with a per/user cache. Anyway this is just an implementation problem. Nobody stops you from doing: def myHttpCache(ctx, lifetime): return t.cached(name=str(url.URL.fromContext(ctx)), lifetime=lifetime) Which will give you the same result.
All my pages are using xml (except for the forms that come from formless), so I'd need the cache for xml templates too.
But I really liked the caching using URL, I don't want having to write name="something" by hand.
When I say that it won't work with xml templates I mean that you need a way to create a cached tag from the xml. This is all.
On Wed, Feb 02, 2005 at 07:56:14AM +0000, Valentino Volonghi wrote:
The benefits are that it scales to multiple computers, which means a better way to distribute the load. But, as I said, it's just one of the possible backends.
For most common sites, I doubt the gain of scaling the caching could ever be significant. There's so much memory on each system that local caching w/o context switches and w/o interprocess communication makes more sense IMHO. Especially if you can get to the data in almost constant time with hashes. I get a 4/5msec total time for each page with local caching in the httprender, it would probably get at visible slowdown if I were to access the cache remotely through sockets. Anyway I'm not against options, infact I'd like to keep the option myself to use the so handy and trivial to use httprender cache ;), I'm just prioritizing on what's is more important first ;)
In weever I did a test, and I simply put this in the base class where I fill the content slot (each page is a main page which contains a content slot that is filled with the content Fragment):
t.cached(name=str(self.__class__)+IA(ctx).get('uid', ''), lifetime=10)[...]
Isn't that going to leak memory badly with tons of users seldom accessing the site? At least for my usage I can't do the above or it would risk to run my app out of memory. lifetime isn't a timer, so it'll never be freed. To get automatic garbage collection one should store it in the session, or at least attach a timer that fires indipendently if the rendering is enabled or not.
Doing this in the main baseclass gave me caching on ALL pages with a per/user cache. Anyway this is just an implementation problem. Nobody stops you from doing:
def myHttpCache(ctx, lifetime): return t.cached(name=str(url.URL.fromContext(ctx)), lifetime=lifetime)
Which will give you the same result.
I'm using this only for the forms right now: def renderCachedForms(ctx, lifetime=0, *args, **kwargs): return tags.cached(lifetime=lifetime, name='formless-'+str(url.URL.fromContext(ctx)))[webform.renderForms(*args, **kwargs)] It's very handy to use by just repalcing webform.renderForms() with renderCachedForms(ctx).
When I say that it won't work with xml templates I mean that you need a way to create a cached tag from the xml. This is all.
I still doubt it'll be as fast as httprender cache, but I can try. It has taken me minutes to enable httprender caching in the whole http site, so I tend to consider that API more handy, and peformance should be a bit higher too. So I don't see much point to use the stan cache in order to cache whole documents when the other lowlevel cache can be used in the rend.Page.
On Wed, 2 Feb 2005 19:12:29 +0100, Andrea Arcangeli <andrea@cpushare.com> wrote:
t.cached(name=str(self.__class__)+IA(ctx).get('uid', ''), lifetime=10)[...]
Isn't that going to leak memory badly with tons of users seldom accessing the site? At least for my usage I can't do the above or it would risk to run my app out of memory. lifetime isn't a timer, so it'll never be freed.
Since you are the one that is setting the cache this is actually not a problem at all. You can do isLogged = IA(ctx).get('uid', None) t.cached(name=str(self.__class__)+('1','0')[isLogged == False], lifetime=10)[..] This won't leak anything except for logged/not logged users which is what you want. Now it should be clear what are the advantages of being able to set the name yourself :).
To get automatic garbage collection one should store it in the session, or at least attach a timer that fires indipendently if the rendering is enabled or not.
Just running the same thing that expires the sessions is enough. It's 5 lines of code or something like that, of course this should be left to the cache manager implementation.
I'm using this only for the forms right now:
def renderCachedForms(ctx, lifetime=0, *args, **kwargs): return tags.cached(lifetime=lifetime, name='formless-'+str(url.URL.fromContext(ctx)))[webform.renderForms(*args, **kwargs)]
It's very handy to use by just repalcing webform.renderForms() with renderCachedForms(ctx).
I think you can cache the result of self.docFactory.load() in the same way you put t.cached() in loader.stan()
I still doubt it'll be as fast as httprender cache, but I can try.
from my tests it shouldn't be that slower. But you do over 200 req/sec so it can be different.
It has taken me minutes to enable httprender caching in the whole http site, so I tend to consider that API more handy, and peformance should be a bit higher too. So I don't see much point to use the stan cache in order to cache whole documents when the other lowlevel cache can be used in the rend.Page.
With this it should take a lot less.
On Wed, Feb 02, 2005 at 06:35:30PM +0000, Valentino Volonghi wrote:
I think you can cache the result of self.docFactory.load() in the same way you put t.cached() in loader.stan()
I'd certainly need to cache the xml fragments. I've code like this: class xx_class(rend.Fragment): docFactory = loaders.xmlfile('xxx.xml', XMLDIR, ignoreDocType = True) I'd need to cache the output of the above. It's not exactly clear to me how to do that with the stan cache, could you make an example (or if you already did it in weever let me know and I'll have a look). If I can use the stan cache for the above, then I can use it for the rend.Page too, and I can benchmark if there's any difference. thanks!
On Wed, 2 Feb 2005 20:03:47 +0100, Andrea Arcangeli <andrea@cpushare.com> wrote:
I'd certainly need to cache the xml fragments.
I've code like this:
class xx_class(rend.Fragment): docFactory = loaders.xmlfile('xxx.xml', XMLDIR, ignoreDocType = True)
I'd need to cache the output of the above.
It's not exactly clear to me how to do that with the stan cache, could you make an example (or if you already did it in weever let me know and I'll have a look).
http://svn.berlios.de/viewcvs/weever/branches/new-db/src/web/main.py?rev=167&view=markup Here it is. Search for the render_content method inside the MasterPage class. It is like this: def render_content(self, ctx, data): ct = t.cached(name=str(self.__class__), lifetime=10)[self.content(self.args, data[FIRST_POST])] ctx.tag.fillSlots('content', ct) return ctx.tag I already know that using self.__class__ as a name is not enough, but that was just a test and I already removed it until it is merged in nevow trunk.
On Sun, 30 Jan 2005 13:56:50 +0100, Andrea Arcangeli <andrea@cpushare.com> wrote:
On Sun, Jan 30, 2005 at 12:26:33AM +0000, Valentino Volonghi wrote:
This works and I've tested it.
Even if you tested it, I doubt you really benchmarked it ;). Likely it was disabled for you (perhaps you forgot setup.py install?)
Yeah, right... That was an 'old' version. And I forgot to fix it.
Transfer rate was 2.3Mbyte/sec. This is very great!
Something is buggy still, since I get the same page duplicated twice, but the performance are already excellent, actually much faster than what I need. And a lot more than 5 times faster, it goes from 7 req per second to 227 req per second, so it's 32 times faster.
Multiplier depends on the page size.
The next bit to stack on top of the above is a timeout parameter, after the timeout the page has to be re-rendered in the _background_, it's enough to attach a deferred to it, that will overwrite the cache after the rendering is complete.
I'm also afraid I should use the flattener on the url before transforming it to a string? Or not?
It's better to use: url.URL.fromContext(ctx).path
This is as fast as using 32 UP systems with the http load balancer, so you're very right that this was the first angle of attack to use ;).
Ehe :). Not bad for a 1 UP system.
On Sun, Jan 30, 2005 at 11:09:34PM +0000, Valentino Volonghi wrote:
Yeah, right... That was an 'old' version. And I forgot to fix it.
Never mind, that was good enough hacking starting point for me ;)
It's better to use:
url.URL.fromContext(ctx).path
Wouldn't that screwup caching across different vhost? It would fail caching on different nevow sites on different ports too. So I believe the __str__ method is more correct index for the hash.
Ehe :). Not bad for a 1 UP system.
Indeed ;)
participants (7)
-
Alex Levy
-
Andrea Arcangeli
-
Andrew Bennetts
-
James Y Knight
-
Matt Goodall
-
Tommi Virtanen
-
Valentino Volonghi aka Dialtone