Skip to content

Commit

Permalink
Performance testing scripts
Browse files Browse the repository at this point in the history
  • Loading branch information
Antti Ajanki authored and jvah committed May 24, 2013
1 parent 2095f77 commit 4c70bd0
Show file tree
Hide file tree
Showing 6 changed files with 240 additions and 1 deletion.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,17 @@ Possible levels:
- `verbose`: log event and subscriber creation and deletion
- `silly`: log submitted message content

Testing
-------

### Unit tests

`npm tests`

### Performance testing

See [tests/performance/README.md](tests/performance/README.md).

License
-------

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"scripts":
{
"start": "sudo node server.js",
"test": "mocha --compilers coffee:coffee-script --reporter spec tests/*"
"test": "mocha --compilers coffee:coffee-script --reporter spec tests/*.coffee"
},
"engines":
{
Expand Down
33 changes: 33 additions & 0 deletions tests/performance/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
Performance testing for pushd
=============================

Simple performance testing setup using HTTP push service:

1. Configuration:
- Enable http push server in settings.coffee
- Configure the number and frequency of generated messages by editing settings() method pushgenerator.py
1. Start the pushd server: `node server.js`
1. Start test performance listener: `node statsserver.js`
1. Start traffic generator: `python pushgenerator.py`

Traffic generator
-----------------

*pushgenerator.py* sends push notifications at a configurable rate.

The frequency of notifications and the number of subscribers can be
configured by editing the function settings() in pushgenerator.py.

pushd process is expected be running on localhost on port 5000
(configurable by PUSHD_SERVER variable in pushgenerator.py).

By default, pushgenerator.py creates random HTTP POST subscribers as
the receivers of the notifications. The HTTP push service must be
enabled in pushd's settings.coffee.

Statistics collector
--------------------

*statsserver.js* is a node.js server that can receive HTTP POST
notifications generated by pushgenerator.py and display some
statistics of the received notifications.
138 changes: 138 additions & 0 deletions tests/performance/pushgenerator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
#!/usr/bin/python

import time
import json
import urllib
import urllib2
import base64
import random
from multiprocessing import Process

PUSHD_SERVER = 'http://admin:admin@localhost:5000'
PUSHD_SERVER_WITHOUT_AUTH = 'http://localhost:5000'
PUSHD_AUTHORIZATION = 'Basic %s' % base64.encodestring('admin:admin')
TOKEN_HTTP = 'http://localhost:5001/log'

class RepeatingMessage:
def __init__(self, event, messagesPerMinute):
self.event = event
self.messagesPerMinute = messagesPerMinute
self.pushCount = 0

def push(self):
print 'Pushing message to ' + self.event
self.pushCount += 1
msg = self.generate_message()
urllib.urlopen(PUSHD_SERVER + '/event/' + self.event, msg).read()

def generate_message(self):
return 'title=performance test&msg=%s' % self.generate_body()

def generate_body(self):
t = time.time()
readable = time.strftime('%Y-%m-%d %I:%M:%S', time.localtime(t))
message = {'timestamp': t,
'readable_timestamp': readable,
'event': self.event}
return json.dumps(message)


class Subscriber:
def __init__(self, token, proto):
self.token = token
self.proto = proto
self.subscriberId = None
self.registerSubscriber()

def registerSubscriber(self):
print 'Registering subscriber %s' % self.token
data = 'proto=%s&token=%s&lang=fi&badge=0' % (self.proto, self.token)
response = urllib.urlopen(PUSHD_SERVER + '/subscribers', data).read()
parsedResponse = json.loads(response)
if 'id' not in parsedResponse:
raise RuntimeError('No id in the reponse')
self.subscriberId = parsedResponse['id']

def subscribe(self, event):
print 'User (token %s) subscribing to %s' % (self.token, event)
url = PUSHD_SERVER + '/subscriber/%s/subscriptions/%s' % \
(self.subscriberId, event)
data = 'ignore_message=0'
urllib.urlopen(url, data).read()

def unregister(self):
print 'Unregistering user %s' % self.token
url = PUSHD_SERVER_WITHOUT_AUTH + '/subscriber/%s' % self.subscriberId
request = urllib2.Request(url, data='')
request.add_header('Authorization', PUSHD_AUTHORIZATION)
request.get_method = lambda: 'DELETE'
opener = urllib2.build_opener(urllib2.HTTPHandler)
opener.open(request).read()


def pusherProcessMain(repeatingMessage):
try:
while True:
repeatingMessage.push()
time.sleep(60./repeatingMessage.messagesPerMinute)
except KeyboardInterrupt:
pass

print '%d messages pushed to %s' % \
(repeatingMessage.pushCount, repeatingMessage.event)

def generateRandomHTTPSubscribers(event, count):
subscribers = []
print 'Creating %d subscribers for %s' % (count, event)
for i in xrange(count):
subscriber = Subscriber(randomHTTPToken(), 'http')
subscriber.subscribe(event)
subscribers.append(subscriber)
return subscribers

def randomHTTPToken():
r = ''.join([random.choice('0123456789ABCDEF') for x in xrange(10)])
return TOKEN_HTTP + '/' + r

def startPushProcesses(targets):
print 'Starting %d push processes' % len(targets)
processes = []
for message in targets:
p = Process(target=pusherProcessMain, args=(message,))
p.daemon = True
p.start()
processes.append(p)
print 'All processes started'
return processes

def settings():
# events and notification frequencies
push_targets = [RepeatingMessage('performancetest1', 2),
RepeatingMessage('performancetest2', 10)]
subscribers = [generateRandomHTTPSubscribers(push_targets[0].event, 10),
generateRandomHTTPSubscribers(push_targets[1].event, 5)]
return push_targets, subscribers

def main():
push_targets, subscribers = settings()

processes = startPushProcesses(push_targets)

try:
while True:
time.sleep(100)
except KeyboardInterrupt:
print 'Quiting...'

for p in processes:
p.terminate()
p.join()

print 'All processes joined'

for subscribersForMessage in subscribers:
for subscriber in subscribersForMessage:
subscriber.unregister()

if __name__ == '__main__':
main()
54 changes: 54 additions & 0 deletions tests/performance/pushlistener.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
express = require 'express'

class TimeStatistics
constructor: ->
@count = 0
@sum = 0
@min = Infinity
@max = 0

update: (sample) ->
@count += 1
@sum += sample
@min = Math.min(sample, @min)
@max = Math.max(sample, @max)

toString: ->
avg = @sum/@count
"#{@count} messages received, avg: #{avg.toFixed(1)} ms (min: #{@min.toFixed(1)}, max: #{@max.toFixed(1)})"

timesPerEvent = {}

app = express()
app.use(express.bodyParser())

app.post /^\/log\/(\w+)$/, (req, res) ->
#console.log 'Received message'
#console.log req.body

receivedTime = Date.now()/1000.0

if not req.body.message?.default?
console.log 'No default message!'
res.send 400

body = JSON.parse req.body.message.default
if not body?.timestamp?
console.log 'No timestamp in the body!'
res.send 400

event = req.body.event

sentTime = body.timestamp
diff = (receivedTime-sentTime)*1000
if not timesPerEvent[event]?
timesPerEvent[event] = new TimeStatistics()
timesPerEvent[event].update(diff)

console.log "#{event} " + timesPerEvent[event].toString()

res.send 200

port = 5001
console.log "Listening on port #{port}"
app.listen port
3 changes: 3 additions & 0 deletions tests/performance/statsserver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env node
require('coffee-script');
require('./pushlistener');

0 comments on commit 4c70bd0

Please sign in to comment.