[CalendarServer-changes] [6060] CalendarServer/trunk/contrib

source_changes at macosforge.org source_changes at macosforge.org
Wed Aug 11 14:21:19 PDT 2010


Revision: 6060
          http://trac.macosforge.org/projects/calendarserver/changeset/6060
Author:   exarkun at twistedmatrix.com
Date:     2010-08-11 14:21:19 -0700 (Wed, 11 Aug 2010)
Log Message:
-----------
Initial checkin of some exploratory work on performance measurement and benchmarking tools

Very early stuff, really just some experiments to see what's possible and
what's interesting.  Expect everything to change.

Added Paths:
-----------
    CalendarServer/trunk/contrib/performance/
    CalendarServer/trunk/contrib/performance/benchmark.sh
    CalendarServer/trunk/contrib/performance/httpauth.py
    CalendarServer/trunk/contrib/performance/io_measure.d
    CalendarServer/trunk/contrib/performance/mkcal.py

Added: CalendarServer/trunk/contrib/performance/benchmark.sh
===================================================================
--- CalendarServer/trunk/contrib/performance/benchmark.sh	                        (rev 0)
+++ CalendarServer/trunk/contrib/performance/benchmark.sh	2010-08-11 21:21:19 UTC (rev 6060)
@@ -0,0 +1,2 @@
+#!/bin/bash
+sudo PYTHONPATH=$PYTHONPATH:../../../vobject/:../../../../CalDAVClientLibrary/trunk/src:../../../Twisted/ python mkcal.py 5466 5465


Property changes on: CalendarServer/trunk/contrib/performance/benchmark.sh
___________________________________________________________________
Added: svn:executable
   + *

Added: CalendarServer/trunk/contrib/performance/httpauth.py
===================================================================
--- CalendarServer/trunk/contrib/performance/httpauth.py	                        (rev 0)
+++ CalendarServer/trunk/contrib/performance/httpauth.py	2010-08-11 21:21:19 UTC (rev 6060)
@@ -0,0 +1,86 @@
+import shlex
+
+from twisted.web.http_headers import Headers
+
+class BasicChallenge(object):
+    def __init__(self, realm):
+        self.realm = realm
+
+
+    def response(self, uri, keyring):
+        username, password = keyring.passwd.find_user_password(self.realm, uri)
+        credentials = ('%s:%s' % (username, password)).encode('base64').strip()
+        authorization = 'basic ' + credentials
+        return {'authorization': [authorization]}
+
+
+
+class AuthHandlerAgent(object):
+    def __init__(self, agent, authinfo):
+        self._agent = agent
+        self._authinfo = authinfo
+
+
+    def request(self, method, uri, headers=None, bodyProducer=None):
+        d = self._agent.request(method, uri, headers, bodyProducer)
+        d.addCallback(self._authenticate, method, uri, headers, bodyProducer)
+        return d
+
+
+    def _parse(self, authorization):
+        parts = shlex.split(authorization)
+        scheme = parts.pop(0)
+        args = dict([p.split('=', 1) for p in parts])
+        if scheme == 'basic':
+            return BasicChallenge(**args)
+        return None
+
+
+    def _authenticate(self, response, method, uri, headers, bodyProducer):
+        if response.code == 401:
+            # Look for a challenge
+            authorization = response.headers.getRawHeaders('www-authenticate')
+            if authorization is None:
+                raise Exception("401 response with no WWW-Authenticate header")
+
+            for auth in authorization:
+                challenge = self._parse(auth)
+                if challenge is None:
+                    continue
+
+                if headers is None:
+                    headers = Headers()
+                else:
+                    headers = Headers(dict(headers.getAllRawHeaders()))
+                for k, vs in challenge.response(uri, self._authinfo).iteritems():
+                    for v in vs:
+                        headers.addRawHeader(k, v)
+
+                return self._agent.request(method, uri, headers, bodyProducer)
+
+        return response
+
+
+if __name__ == '__main__':
+    from urllib2 import HTTPDigestAuthHandler
+    handler = HTTPDigestAuthHandler()
+    handler.add_password(
+        realm="Test Realm",
+        uri="http://localhost:8008/",
+        user="user01",
+        passwd="user01")
+
+    from twisted.web.client import Agent
+    from twisted.internet import reactor
+    from twisted.python.log import err
+    agent = AuthHandlerAgent(Agent(reactor), handler)
+    d = agent.request(
+        'DELETE', 'http://localhost:8008/calendars/users/user01/monkeys3/')
+    def deleted(response):
+        print response.code
+        print response.headers
+        reactor.stop()
+    d.addCallback(deleted)
+    d.addErrback(err)
+    d.addCallback(lambda ign: reactor.stop())
+    reactor.run()

Added: CalendarServer/trunk/contrib/performance/io_measure.d
===================================================================
--- CalendarServer/trunk/contrib/performance/io_measure.d	                        (rev 0)
+++ CalendarServer/trunk/contrib/performance/io_measure.d	2010-08-11 21:21:19 UTC (rev 6060)
@@ -0,0 +1,56 @@
+
+/*
+ * Report current timestamp (nanoseconds) of io and SQLite3 events.
+ */
+
+#pragma D option switchrate=10hz
+
+/*
+ * Low-level I/O stuff
+ */
+
+io:::start
+/args[0]->b_flags & B_READ/
+{
+        printf("%d", args[0]->b_bcount);
+}
+
+io:::start
+/!(args[0]->b_flags & B_READ)/
+{
+        printf("%d", args[0]->b_bcount);
+}
+
+/*
+ * SQLite3 stuff
+ */
+
+pid$target:_sqlite3.so:_pysqlite_query_execute:entry
+{
+        self->executing = 1;
+        self->sql = "";
+        printf("%d", timestamp);
+}
+
+pid$target:_sqlite3.so:_pysqlite_query_execute:return
+{
+        self->executing = 0;
+        printf("%d %s", timestamp, self->sql);
+}
+
+pid$target::PyString_AsString:return
+/self->executing/
+{
+        self->sql = copyinstr(arg1);
+        self->executing = 0;
+}
+
+pid$target:_sqlite3.so:pysqlite_cursor_iternext:entry
+{
+        printf("%d", timestamp);
+}
+
+pid$target:_sqlite3.so:pysqlite_cursor_iternext:return
+{
+        printf("%d", timestamp);
+}

Added: CalendarServer/trunk/contrib/performance/mkcal.py
===================================================================
--- CalendarServer/trunk/contrib/performance/mkcal.py	                        (rev 0)
+++ CalendarServer/trunk/contrib/performance/mkcal.py	2010-08-11 21:21:19 UTC (rev 6060)
@@ -0,0 +1,352 @@
+"""
+Make a new calendar using an existing user account on an already-running iCal
+server.
+"""
+
+import sys
+
+from subprocess import PIPE, Popen
+from signal import SIGINT
+
+from urllib2 import HTTPDigestAuthHandler
+from uuid import uuid1, uuid4
+from datetime import datetime, timedelta
+from time import time
+from StringIO import StringIO
+
+import vobject
+
+from client.account import CalDAVAccount
+from protocol.url import URL
+
+from zope.interface import implements
+
+from twisted.internet.protocol import ProcessProtocol, Protocol
+from twisted.internet.defer import (
+    Deferred, inlineCallbacks, returnValue, succeed, gatherResults)
+from twisted.internet import reactor
+from twisted.web.iweb import IBodyProducer
+from twisted.web.client import Agent
+from twisted.web.http_headers import Headers
+
+from httpauth import AuthHandlerAgent
+
+
+# XXX Represent these as vobjects?  Would make it easier to add more vevents.
+event = """\
+BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//Apple Inc.//iCal 4.0.3//EN
+BEGIN:VTIMEZONE
+TZID:America/New_York
+BEGIN:STANDARD
+DTSTART:20071104T020000
+RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:20070311T020000
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+END:VTIMEZONE
+%(VEVENTS)s\
+END:VCALENDAR
+"""
+
+vfreebusy = """\
+BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+VERSION:2.0
+METHOD:REQUEST
+PRODID:-//Apple Inc.//iCal 4.0.3//EN
+BEGIN:VFREEBUSY
+UID:81F582C8-4E7F-491C-85F4-E541864BE0FA
+DTEND:20100730T150000Z
+ATTENDEE:urn:uuid:user02
+DTSTART:20100730T140000Z
+X-CALENDARSERVER-MASK-UID:EC75A61B-08A3-44FD-BFBB-2457BBD0D490
+DTSTAMP:20100729T174751Z
+ORGANIZER:mailto:user01 at example.com
+SUMMARY:Availability for urn:uuid:user02
+END:VFREEBUSY
+END:VCALENDAR
+"""
+
+def formatDate(d):
+    return ''.join(filter(str.isalnum, d.isoformat()))
+
+def makeEvent(i):
+    s = """\
+BEGIN:VEVENT
+UID:%(UID)s
+DTSTART;TZID=America/New_York:%(START)s
+DTEND;TZID=America/New_York:%(END)s
+CREATED:20100729T193912Z
+DTSTAMP:20100729T195557Z
+SEQUENCE:%(SEQUENCE)s
+SUMMARY:STUFF IS THINGS
+TRANSP:OPAQUE
+END:VEVENT
+"""
+    base = datetime(2010, 7, 30, 11, 15, 00)
+    interval = timedelta(0, 5)
+    duration = timedelta(0, 3)
+    return event % {
+        'VEVENTS': s % {
+            'UID': uuid4(),
+            'START': formatDate(base + i * interval),
+            'END': formatDate(base + i * interval + duration),
+            'SEQUENCE': i,
+            },
+        }
+
+
+def makeEvents(n):
+    return [makeEvent(i) for i in range(n)]
+
+
+class _DiscardReader(Protocol):
+    def __init__(self, finished):
+        self.finished = finished
+
+
+    def dataReceived(self, bytes):
+        pass
+
+
+    def connectionLost(self, reason):
+        self.finished.callback(None)
+
+
+
+def readBody(response):
+    finished = Deferred()
+    response.deliverBody(_DiscardReader(finished))
+    return finished
+
+
+
+class StringProducer(object):
+    implements(IBodyProducer)
+
+    def __init__(self, body):
+        self._body = body
+        self.length = len(self._body)
+
+
+    def startProducing(self, consumer):
+        consumer.write(self._body)
+        return succeed(None)
+
+
+ at inlineCallbacks
+def measure(pids, events, samples):
+    # First set things up
+    account = CalDAVAccount(
+        "localhost:8008", user="user01", pswd="user01", root="/", principal="/")
+    account.session.deleteResource(
+        URL("/calendars/users/user01/monkeys3/"))
+    account.session.makeCalendar(
+        URL("/calendars/users/user01/monkeys3/"))
+
+    for i, cal in enumerate(makeEvents(events)):
+        account.session.writeData(
+            URL("/calendars/users/user01/monkeys3/foo-%d.ics" % (i,)),
+            cal,
+            "text/calendar")
+
+    # CalDAVClientLibrary can't seem to POST things.
+    authinfo = HTTPDigestAuthHandler()
+    authinfo.add_password(
+        realm="Test Realm",
+        uri="http://localhost:8008/",
+        user="user01",
+        passwd="user01")
+
+    agent = AuthHandlerAgent(Agent(reactor), authinfo)
+    method = 'POST'
+    uri = 'http://localhost:8008/calendars/__uids__/user01/outbox/'
+    headers = Headers({"content-type": ["text/calendar"]})
+    body = StringProducer(vfreebusy)
+
+    # Now sample it a bunch of times
+    data = []
+    with DTraceCollector(pids) as dtrace:
+        for i in range(samples):
+            before = time()
+            response = yield agent.request(
+                method, uri, headers, body)
+            yield readBody(response)
+            after = time()
+            data.append(after - before)
+    stats = {Duration('urlopen time'): data}
+    stats.update((yield dtrace))
+    returnValue(stats)
+
+
+class _Statistic(object):
+    def __init__(self, name):
+        self.name = name
+
+
+    def summarize(self, data):
+        print 'mean', self.name, mean(data)
+        print 'median', self.name, median(data)
+        print 'stddev', self.name, stddev(data)
+        print 'sum', self.name, sum(data)
+
+
+    def write(self, basename, data):
+        fObj = file(basename % (self.name,), 'w')
+        fObj.write('\n'.join(map(str, data)) + '\n')
+        fObj.close()
+
+
+
+class Duration(_Statistic):
+    pass
+
+
+
+class Bytes(_Statistic):
+    pass
+
+
+
+class DTraceCollector(object):
+    def __init__(self, pids):
+        self.pids = pids
+        self._read = []
+        self._write = []
+        self._execute = []
+        self._iternext = []
+
+
+    def stats(self):
+        return {
+            Bytes('read'): self._read,
+            Bytes('write'): self._write,
+            Duration('execute'): self._execute,
+            Duration('iternext'): self._iternext,
+            }
+
+
+    def _parse(self, dtrace):
+        file('dtrace.log', 'a').write(dtrace)
+
+
+        start = None
+        for L in dtrace.splitlines():
+            parts = L.split(None)
+            if len(parts) >= 4:
+                event = parts[2]
+                func, stage = event.split(':')
+                value = int(parts[3])
+                if stage == 'entry':
+                    start = value
+                elif stage == 'return':
+                    if start is None:
+                        print func, 'return without entry at', parts[3]
+                        continue
+                    end = int(parts[3])
+                    diff = end - start
+                    if func == '_pysqlite_query_execute':
+                        accum = self._execute
+                    elif func == 'pysqlite_cursor_iternext':
+                        accum = self._iternext
+                    else:
+                        continue
+                    if diff < 0:
+                        print 'Completely bogus dealie', func, start, end
+                    else:
+                        accum.append(diff)
+                    start = None
+                else:
+                    continue
+
+
+    def __enter__(self):
+        finished = []
+        self.dtraces = {}
+        for p in self.pids:
+            d = Deferred()
+            self.dtraces[p] = reactor.spawnProcess(
+                IOMeasureConsumer(d),
+                "/usr/sbin/dtrace",
+                ["/usr/sbin/dtrace", "-p", str(p), "-s", "io_measure.d"])
+            d.addCallback(self._cleanup, p)
+            d.addCallback(self._parse)
+            finished.append(d)
+        return gatherResults(finished).addCallback(lambda ign: self.stats())
+
+
+    def _cleanup(self, passthrough, pid):
+        del self.dtraces[pid]
+        return passthrough
+
+
+    def __exit__(self, type, value, traceback):
+        for proc in self.dtraces.itervalues():
+            proc.signalProcess(SIGINT)
+
+
+
+class IOMeasureConsumer(ProcessProtocol):
+    def __init__(self, done):
+        self.done = done
+
+
+    def connectionMade(self):
+        self.out = StringIO()
+
+
+    def errReceived(self, bytes):
+        print bytes
+
+    def outReceived(self, bytes):
+        self.out.write(bytes)
+
+
+    def processEnded(self, reason):
+        self.done.callback(self.out.getvalue())
+
+
+def mean(samples):
+    return sum(samples) / len(samples)
+
+
+def median(samples):
+    return sorted(samples)[len(samples) / 2]
+
+
+def stddev(samples):
+    m = mean(samples)
+    variance = sum([(datum - m) ** 2 for datum in samples]) / len(samples)
+    return variance ** 0.5
+
+ at inlineCallbacks
+def main():
+    # Figure out which pids we are benchmarking.
+    pids = map(int, sys.argv[1:])
+
+    for numEvents in [1, 100]: #, 1000]:#, 10000]:
+        print 'Testing', numEvents, 'events'
+        data = yield measure(pids, numEvents, 100)
+        for k, v in data.iteritems():
+            if v:
+                k.summarize(v)
+                k.write('vfreebusy.%%s.%d' % (numEvents,), v)
+
+
+if __name__ == '__main__':
+    from twisted.python.log import err
+    d = main()
+    d.addErrback(err)
+    d.addCallback(lambda ign: reactor.stop())
+    reactor.run()
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20100811/8906143a/attachment-0001.html>


More information about the calendarserver-changes mailing list