Load testing an ADF 11g application
We all know it’s important to see how your application behaves under lots of stress/load. There a multiple tools to measure the performance of the application. But you also need a tool that generates load in a way it’s like many users are ‘clicking’ in the application. The grinder is such a tool.
Its a Java based load testing framework which uses a generic approach. This means you can test anything you want that has a Java Api. The scripts are written in Jython. This a scripting language based on python. The grinder also comes with a console in which you can see the numbers, start en stop the scripts. Grinder is also able to record and play scripts over https . Regarding recording the scripts; this is very easy. You just start start the grinder proxy and add this to your browser and start the recording. If you need to connect trough a proxy from you work place you can easily add this to the startup parameters off the grinder proxy
A recorded script looks something like this:
# The Grinder 3.2
# HTTP script recorded by TCPProxy at 10-mrt-2010 11:16:49
from net.grinder.script import Test
from net.grinder.script.Grinder import grinder
from net.grinder.plugin.http import HTTPPluginControl, HTTPRequest
from HTTPClient import NVPair
connectionDefaults = HTTPPluginControl.getConnectionDefaults()
httpUtilities = HTTPPluginControl.getHTTPUtilities()
# To use a proxy server, uncomment the next line and set the host and port.
# connectionDefaults.setProxyServer("localhost", 8001)
# These definitions at the top level of the file are evaluated once,
# when the worker process is started.
connectionDefaults.defaultHeaders =
( NVPair('Accept-Language', 'nl,en-us;q=0.7,en;q=0.3'),
NVPair('Accept-Charset', 'ISO-8859-1,utf-8;q=0.7,*;q=0.7'),
NVPair('Accept-Encoding', 'gzip,deflate'),
NVPair('User-Agent', 'Mozilla/5.0 (Windows; U; Windows NT 5.1; nl; rv:1.9.1.8) Gecko/20100202 Firefox/3.5.8 (.NET CLR 3.5.30729)'), )
headers0=
( NVPair('Accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'), )
headers1=
( NVPair('Accept', 'image/png,image/*;q=0.8,*/*;q=0.5'),
NVPair('Referer', 'http://www.google.nl/'), )
headers2=
( NVPair('Accept', '*/*'),
NVPair('Referer', 'http://www.google.nl/'), )
headers4=
( NVPair('Accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'),
NVPair('Referer', 'http://www.google.nl/'), )
url0 = 'http://www.google.com:80'
url1 = 'http://www.google.nl:80'
url2 = 'http://clients1.google.nl:80'
# Create an HTTPRequest for each request, then replace the
# reference to the HTTPRequest with an instrumented version.
# You can access the unadorned instance using request101.__target__.
request101 = HTTPRequest(url=url0, headers=headers0)
request101 = Test(101, 'GET /').wrap(request101)
request201 = HTTPRequest(url=url1, headers=headers0)
request201 = Test(201, 'GET /').wrap(request201)
request202 = HTTPRequest(url=url1, headers=headers1)
request202 = Test(202, 'GET logo.gif').wrap(request202)
request203 = HTTPRequest(url=url1, headers=headers2)
request203 = Test(203, 'GET DWAGft_LXWU.js').wrap(request203)
request204 = HTTPRequest(url=url1, headers=headers2)
request204 = Test(204, 'GET 4a954c7ba824f253.js').wrap(request204)
request601 = HTTPRequest(url=url2, headers=headers2)
request601 = Test(601, 'GET search').wrap(request601)
request1401 = HTTPRequest(url=url1, headers=headers4)
request1401 = Test(1401, 'GET search').wrap(request1401)
request1402 = HTTPRequest(url=url1, headers=headers1)
request1402 = Test(1402, 'GET translate.gif').wrap(request1402)
request1701 = Test(1701, 'GET csi').wrap(request1701)
class TestRunner:
"""A TestRunner instance is created for each worker thread."""
# A method for each recorded page.
def page1(self):
"""GET / (request 101)."""
# Expecting 302 'Found'
result = request101.GET('/')
return result
def page2(self):
"""GET / (requests 201-204)."""
result = request201.GET('/')
request202.GET('/intl/nl_nl/images/logo.gif')
grinder.sleep(47)
request203.GET('/extern_js.js')
grinder.sleep(31)
request204.GET('/extern_chrome/4a954c7ba824f253.js')
return result
def page6(self):
"""GET search (request 601)."""
self.token_hl =
'nl'
self.token_q =
'z'
self.token_cp =
'1'
result = request601.GET('/complete/search' +
'?hl=' +
self.token_hl +
'&q=' +
self.token_q +
'&cp=' +
self.token_cp)
return result
** CUT **
def page14(self):
"""GET search (requests 1401-1402)."""
self.token_source =
'hp'
self.token_btnG =
'Google+zoeken'
self.token_meta =
''
self.token_aq =
'f'
self.token_oq =
'zoekString'
self.token_fp =
'4a954c7ba824f253'
result = request1401.GET('/search' +
'?hl=' +
self.token_hl +
'&source=' +
self.token_source +
'&q=' +
self.token_q +
'&btnG=' +
self.token_btnG +
'&meta=' +
self.token_meta +
'&aq=' +
self.token_aq +
'&oq=' +
self.token_oq +
'&fp=' +
self.token_fp)
grinder.sleep(360)
request1402.GET('/options/icons/translate.gif')
return result
** CUT **
def __call__(self):
"""This method is called for every run performed by the worker thread."""
self.page1() # GET / (request 101)
self.page2() # GET / (requests 201-204)
self.page3() # GET generate_204 (request 301)
self.page4() # GET nav_logo7.png (request 401)
self.page5() # GET csi (requests 501-502)
grinder.sleep(2000)
self.page6() # GET search (request 601)
grinder.sleep(78)
self.page7() # GET search (request 701)
grinder.sleep(156)
self.page8() # GET search (request 801)
grinder.sleep(438)
self.page9() # GET search (request 901)
grinder.sleep(141)
self.page10() # GET search (request 1001)
grinder.sleep(140)
self.page11() # GET search (request 1101)
grinder.sleep(125)
self.page12() # GET search (request 1201)
grinder.sleep(141)
self.page13() # GET search (request 1301)
grinder.sleep(2110)
self.page14() # GET search (requests 1401-1402)
grinder.sleep(31)
self.page15() # GET / (request 1501)
self.page16() # GET generate_204 (request 1601)
self.page17() # GET csi (request 1701)
def instrumentMethod(test, method_name, c=TestRunner):
"""Instrument a method with the given Test."""
unadorned = getattr(c, method_name)
import new
method = new.instancemethod(test.wrap(unadorned), None, c)
setattr(c, method_name, method)
# Replace each method with an instrumented version.
# You can call the unadorned method using self.page1.__target__().
instrumentMethod(Test(100, 'Page 1'), 'page1')
instrumentMethod(Test(200, 'Page 2'), 'page2')
instrumentMethod(Test(300, 'Page 3'), 'page3')
instrumentMethod(Test(400, 'Page 4'), 'page4')
instrumentMethod(Test(500, 'Page 5'), 'page5')
instrumentMethod(Test(600, 'Page 6'), 'page6')
instrumentMethod(Test(700, 'Page 7'), 'page7')
instrumentMethod(Test(800, 'Page 8'), 'page8')
instrumentMethod(Test(900, 'Page 9'), 'page9')
instrumentMethod(Test(1000, 'Page 10'), 'page10')
instrumentMethod(Test(1100, 'Page 11'), 'page11')
instrumentMethod(Test(1200, 'Page 12'), 'page12')
instrumentMethod(Test(1300, 'Page 13'), 'page13')
instrumentMethod(Test(1400, 'Page 14'), 'page14')
instrumentMethod(Test(1500, 'Page 15'), 'page15')
instrumentMethod(Test(1600, 'Page 16'), 'page16')
instrumentMethod(Test(1700, 'Page 17'), 'page17')
This recorded script goes to google and executes a searchString. Grinder records every request get or post (pictures, css, etc).
You can edit the script to random pick search strings on different requests or to print the result to check if the output is as expected.
Now how can we use this method to test an ADF application? At first it all looks the same. Just record the script. But when you run the script to verify it you’ll see you get errors in your console output regarding a variable called “ViewState”, when you look at the source of an .jspx file you see a hidden field looking like:
<input value="!idwh5stuw">
This field is used to do state saving. This variable must be send with every post.
A recorded ADF post could look like this (depending on your application):
def page8(self):
result = request801.POST('/mwp2/faces/jobboard.jspx' +
'?_adf.ctrl-state=' +
self.token__adfctrlstate +
'&sc=' +
self.token_sc,
( NVPair('page:basePage:multCol:zoekbalkSubview:zkCrit:0:f2:trefwoorden',’trefwoord’),
NVPair('org.apache.myfaces.trinidad.faces.FORM', 'ZoekForm'),
NVPair('javax.faces.ViewState', ‘!idwh5stuw’),
NVPair('oracle.adf.view.rich.RENDER', 'page:basePage:multCol:zoekbalkSubview:zkCrit'),
NVPair('event', 'page:basePage:multCol:zoekbalkSubview:zkCrit:0:f2:zoekenLink'),
NVPair('event.page:basePage:multCol:zoekbalkSubview:zkCrit:0:f2:zoekenLink', '<m xmlns="http://oracle.com/richClient/comm"><k v="type"><s>action</s></k></m>'),
NVPair('oracle.adf.view.rich.PROCESS', 'page:basePage:multCol:zoekbalkSubview:zkCrit'), ),
( NVPair('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8'), ))
return result
If you repeat this the value used when recording will not be present at the time you execute this script.
To solve this problem you need to extract the viewstate value from the previous response, because this is the viewstate needed for the post. To accomplish this we need to decode the result and read the viewstate from the result. To decode the result we only need to add the following option: connectionDefaults.useContentEncoding = 1. Now you can read the output from result.text as plain xml/html instead of encoded characters. We now need a function to extract the viewstate from the result.
def getViewState(responseString):
search = '<input type="hidden" value="([^"]+)">'
p = re.compile(search)
value = p.search(responseString)
if value:
return value.group(1)
else:
raise Exception('ViewState not found.')
We can put this is a variable and use this in the response. See the comments between the code for details.
# import the file containing the function getViewState.
from myCommonFunctions import *
#First put the result from a previous request (pageX) in a variable.
self.responseString = result.text
#Use the responsestring to get the viewstate
viewstate = getViewState(self.responseString)
#Now we can use this
def page8(self):
result = request801.POST('/mwp2/faces/jobboard.jspx' +
'?_adf.ctrl-state=' +
self.token__adfctrlstate +
'&sc=' +
self.token_sc,
( NVPair('page:basePage:multCol:zoekbalkSubview:zkCrit:0:f2:trefwoorden',’trefwoord’),
NVPair('org.apache.myfaces.trinidad.faces.FORM', 'ZoekForm'),
NVPair('javax.faces.ViewState', viewstate),
NVPair('oracle.adf.view.rich.RENDER', 'page:basePage:multCol:zoekbalkSubview:zkCrit'),
NVPair('event', 'page:basePage:multCol:zoekbalkSubview:zkCrit:0:f2:zoekenLink'),
NVPair('event.page:basePage:multCol:zoekbalkSubview:zkCrit:0:f2:zoekenLink', '<m xmlns="http://oracle.com/richClient/comm"><k v="type"><s>action</s></k></m>'),
NVPair('oracle.adf.view.rich.PROCESS', 'page:basePage:multCol:zoekbalkSubview:zkCrit'), ),
( NVPair('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8'), ))
#put the result in self.responseString for use in further requests.
self.responseString = result.text
return result
When we run above grinder script we’re not getting any errors. To be sure we can add the following statement to print the result to a file:
writeToFile(result.text, ‘output’)
#uses following function
def writeToFile(text, name):
pdoctypeHtml = re.compile('<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">')
pdoctypeHtmlOther = re.compile('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">')
values = re.findall(pdoctypeHtml, text)
valuesOther = re.findall(pdoctypeHtmlOther, text)
if values:
filename = grinder.getFilenameFactory().createFilename(name + "", "-%d.html" % grinder.runNumber)
elif valuesOther:
filename = grinder.getFilenameFactory().createFilename(name + "", "-%d.html" % grinder.runNumber)
else:
filename = grinder.getFilenameFactory().createFilename(name + "", "-%d.xml" % grinder.runNumber)
file = open(filename, "w")
print >> file, text
file.close()
When we check the response we’re seeing as an result the whole page. This seems okay but wait a minute.. We did a POST (a PPR, Partial Page Request) so we’re only expecting a small part as a response. To accomplish this we need to add a header parameter. Something grinder didn’t found when recording the script.
Grinders uses request numbers like : request801.POST(…) these are defined above the first testcase (page 1). It looks like this:
request801 = HTTPRequest(url=url0, headers=headers7)
request801 = Test(801, 'POST searchpage.jspx').wrap(request801)
This uses a header (number 7 in this case) which looks like:
headers7=
( NVPair('Accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'),
NVPair('Referer', targetUrl + '/mwp2/faces/pagina.jspx?sc=300&_adf.ctrl-state=19aqhempov_4'),
NVPair('Cache-Control', 'no-cache'),)
We only need to add the following header:
NVPair('Adf-Rich-Message', 'true')
And presto we’re getting the ppr response which you can recognize bij the <?Adf-Rich-Response-Type ?> tag in the response.
Something else you need to keep in mind is that on a get ADF redirects the request to add the _adf.ctrl-state to the url. Grinder only sets the first request in a testcase in the result variable. In case of a get this is the redirect so this will not containing the viewstate needed for future posts. Simple solution is putting the second request in the result variable. (see last example page 1) You don’t have to worry about parameters on the url Grinder puts these in variables out of the box. Also HTTPS, sessionId’s and cookies are supported out of the box. You’ll see the test cases with the requests to .jspx pages is big. This is because Grinder needs to get all the javascripts, css, picture’s etc. Grinder just mimics your browser behavior.
def page1(self):
"""GET searchpage.jspx (requests 101-130)."""
# Expecting 302 'Moved Temporarily'
result = request101.GET('/app/faces/ searchpage.jspx')
self.token_sc =
httpUtilities.valueFromLocationURI('sc') # '300'
self.token__adfctrlstate =
httpUtilities.valueFromLocationURI('_adf.ctrl-state') # 'pelho4xet_33'
result = request102.GET('/mwp2/faces/ searchpage.jspx' +
'?sc=' +
self.token_sc +
'&_adf.ctrl-state=' +
self.token__adfctrlstate)
request103.GET('/mwp2/adf/styles/cache/YGN-desktop-6p2opl-ltr-gecko-1.9.1.6.css')
request104.GET('/mwp2/afr/partition/gecko/default/opt/boot-11-r1.js')
request105.GET('/mwp2/adf/jsLibs/Common1_2_11_1.js')
request106.GET('/mwp2/afr/partition/gecko/default/opt/core-11-r1.js')
self.token_loc =
'nl'
request107.GET('/mwp2/adf/jsLibs/resources/LocaleElements_nl1_2_11_1.js' +
'?loc=' +
self.token_loc)
self.token_skinId =
'YGN.desktop'
request108.GET('/mwp2/afr/AdfTranslations-11-r1nl.js' +
'?loc=' +
self.token_loc +
'&skinId=' +
self.token_skinId)
request109.GET('/mwp2/tf/jobboard/js/jobboard.js')
request110.GET('/mwp2/components/autoSuggest.js')
request111.GET('/mwp2/tf/jobboard/js/zoekCriteria.js')
request112.GET('/mwp2/themas/YGN/css/extern.css')
request113.GET('/mwp2/js/adf.patches.js')
request114.GET('/mwp2/js/adf.functions.js')
request115.GET('/system/css/yacht_main.css')
request116.GET('/mwp2/js/adf.webtrends.js')
request117.GET('/mwp2/js/bevestiging.js')
request118.GET('/mwp2/js/mwp.optioneelblok.js')
request119.GET('/system/wt/webtrends.js')
request120.GET('/mwp2/js/xregexp-min.js')
request121.GET('/mwp2/js/xregexp-unicode.js')
request122.GET('/mwp2/afr/partition/gecko/default/opt/input-11-r1.js')
request123.GET('/mwp2/afr/partition/gecko/default/opt/detail-11-r1.js')
request124.GET('/mwp2/afr/partition/gecko/default/opt/box-11-r1.js')
request125.GET('/mwp2/afr/partition/gecko/default/opt/region-11-r1.js')
request126.GET('/mwp2/afr/partition/gecko/default/opt/uncommon-11-r1.js')
request127.GET('/mwp2/afr/partition/gecko/default/opt/header-11-r1.js')
request128.GET('/mwp2/afr/partition/gecko/default/opt/selectonelistbox-11-r1.js')
request129.GET('/mwp2/afr/partition/gecko/default/opt/selectonechoice-11-r1.js')
request130.GET('/mwp2/afr/partition/gecko/default/opt/form-11-r1.js')
writeToFile(result.text, searchpage _response_init')
self.responseString = result.text
return result

Comments are closed.