2 from random import randrange
11 __author__ = "Jan Medved"
12 __copyright__ = "Copyright(c) 2014, Cisco Systems, Inc."
13 __license__ = "New-style BSD"
14 __email__ = "jmedved@cisco.com"
17 class Counter(object):
18 def __init__(self, start=0):
19 self.lock = threading.Lock()
22 def increment(self, value=1):
33 def __init__(self, verbose=False):
34 self.verbose = verbose
37 self.start = time.time()
40 def __exit__(self, *args):
41 self.end = time.time()
42 self.secs = self.end - self.start
43 self.msecs = self.secs * 1000 # millisecs
45 print("elapsed time: %f ms" % self.msecs)
48 class ShardPerformanceTester(object):
50 The ShardPerformanceTester class facilitates performance testing of CDS shards. The test starts a number of
51 threads, where each thread issues a specified number of resource retrieval requests to URLs specified at the
52 beginning of a test. A ShardPerformanceTester object gets its connection parameters to the system-under-test
53 and its test parameters when it is instantiated. The set of URLs from where to retrieve resources is
54 specified when a test is started. By passing in the appropriate URLs, the test can be used to test data
55 retrieval performance of different shards or different resources at different granularities, etc.
58 headers = {"Accept": "application/json"}
60 def __init__(self, host, port, auth, threads, nrequests, plevel):
66 self.requests = nrequests
67 self.threads = threads
70 self.print_lock = threading.Lock()
71 self.cond = threading.Condition()
75 self.url_counters = []
78 def make_request(self, session, urls):
80 Makes a request for a resource at a random URL selected from a list of URLs passed as input parameter
81 :param session: Session to system under test
82 :param urls: List of resource URLs
83 :return: Status code from the resource request call
85 url_index = randrange(0, len(urls))
86 r_url = urls[url_index]
87 self.url_counters[url_index].increment()
90 r = session.get(r_url, headers=self.headers, stream=False)
93 r_url, headers=self.headers, stream=False, auth=("admin", "admin")
97 def worker(self, tid, urls):
99 Worker thread function. Connects to system-under-test and makes 'self.requests' requests for
100 resources to URLs randomly selected from 'urls'
101 :param tid: Worker thread ID
102 :param urls: List of resource URLs
107 s = requests.Session()
109 with self.print_lock:
110 print(" Thread %d: Performing %d requests" % (tid, self.requests))
113 for r in range(self.requests):
114 sts = self.make_request(s, urls)
120 ok_rate = res[200] / t.secs
121 total_rate = sum(res.values()) / t.secs
123 with self.print_lock:
124 print("Thread %d done:" % tid)
125 print(" Time: %.2f," % t.secs)
126 print(" Success rate: %.2f, Total rate: %.2f" % (ok_rate, total_rate))
127 print(" Per-thread stats: ")
129 self.threads_done += 1
130 self.total_rate += total_rate
135 self.cond.notifyAll()
137 def run_test(self, urls):
139 Runs the performance test. Starts 'self.threads' worker threads, waits for all of them to finish and
141 :param urls: List of urls from which to request resources
148 # Initialize url counters
149 del self.url_counters[:]
150 for i in range(len(urls)):
151 self.url_counters.append(Counter(0))
153 # Start all worker threads
154 for i in range(self.threads):
155 t = threading.Thread(target=self.worker, args=(i, urls))
159 # Wait for all threads to finish and measure the execution time
161 while self.threads_done < self.threads:
165 # Print summary results. Each worker prints its owns results too.
166 print("\nSummary Results:")
168 " Requests/sec (total_sum): %.2f"
169 % ((self.threads * self.requests) / t.secs)
172 " Requests/sec (measured): %.2f"
173 % ((self.threads * self.requests) / t.secs)
175 print(" Time: %.2f" % t.secs)
176 self.threads_done = 0
179 print(" Per URL Counts: ")
180 for i in range(len(urls)):
181 print("%d" % self.url_counters[i].value)
185 class TestUrlGenerator(object):
187 Base abstract class to generate test URLs for ShardPerformanceTester. First, an entire subtree representing
188 a shard or a set of resources is retrieved, then a set of URLS to access small data stanzas is created. This
189 class only defines the framework, the methods that create URL sets are defined in derived classes.
192 def __init__(self, host, port, auth):
195 :param host: Controller's IP address
196 :param port: Controller's RESTCONF port
197 :param auth: Indicates whether to use authentication with default user/password (admin/admin)
203 self.resource_string = ""
205 def url_generator(self, data):
207 Abstract URL generator. Must be overridden in a derived class
208 :param data: Bulk resource data (JSON) from which to generate the URLs
209 :return: List of generated Resources
212 "Abstract class '%s' should never be used standalone"
213 % (self.__class__.__name__)
219 Drives the generation of test URLs. First, it gets a 'bulk' resource (e.g. the entire inventory
220 or the entire topology) from the controller specified during int() and then invokes a resource-specific
221 URL generator to create a set of resource-specific URLs.
223 t_url = "http://" + self.host + ":" + self.port + "/" + self.resource_string
224 headers = {"Accept": "application/json"}
228 r = requests.get(t_url, headers=headers, stream=False)
231 t_url, headers=headers, stream=False, auth=("admin", "admin")
234 if r.status_code != 200:
236 "Failed to get HTTP response from '%s', code %d"
237 % ((t_url, r.status_code))
241 r_url = self.url_generator(json.loads(r.content))
244 "Failed to get json from '%s'. Please make sure you are connected to mininet."
251 class TopoUrlGenerator(TestUrlGenerator):
253 Class to generate test URLs from the topology shard.
254 :return: List of generated Resources
257 def __init__(self, host, port, auth):
258 TestUrlGenerator.__init__(self, host, port, auth)
259 self.resource_string = (
260 "restconf/operational/network-topology:network-topology/topology/flow:1"
263 def url_generator(self, topo_data):
266 nodes = topo_data["topology"][0]["node"]
268 tpoints = node["termination-point"]
269 for tpoint in tpoints:
275 + "/restconf/operational/network-topology:network-topology/topology/flow:1/node/"
277 + "/termination-point/"
280 url_list.append(t_url)
283 print("Error parsing topology json")
287 class InvUrlGenerator(TestUrlGenerator):
289 Class to generate test URLs from the inventory shard.
292 def __init__(self, host, port, auth):
293 TestUrlGenerator.__init__(self, host, port, auth)
294 self.resource_string = "restconf/operational/opendaylight-inventory:nodes"
296 def url_generator(self, inv_data):
299 nodes = inv_data["nodes"]["node"]
301 nconns = node["node-connector"]
308 + "/restconf/operational/opendaylight-inventory:nodes/node/"
312 + "/opendaylight-port-statistics:flow-capable-node-connector-statistics"
314 url_list.append(i_url)
317 print("Error parsing inventory json")
321 if __name__ == "__main__":
322 parser = argparse.ArgumentParser(
323 description="Flow programming performance test: First adds and then deletes flows "
324 "into the config tree, as specified by optional parameters."
330 help="Host where odl controller is running (default is 127.0.0.1)",
335 help="Port on which odl's RESTCONF is listening (default is 8181)",
342 help="Use the ODL default username/password 'admin'/'admin' to authenticate access to REST; "
343 "default: no authentication",
349 help="Number of request worker threads to start in each cycle; default=1. ",
355 help="Number of requests each worker thread will send to the controller; default=100.",
359 choices=["inv", "topo", "topo+inv", "all"],
361 help="Which resource to test: inventory, topology, or both; default both",
367 help="Print level: controls output verbosity. 0-lowest, 1-highest; default 0",
369 in_args = parser.parse_args()
374 # If required, get topology resource URLs
375 if in_args.resource != "inventory":
376 tg = TopoUrlGenerator(in_args.host, in_args.port, in_args.auth)
377 topo_urls += tg.generate()
378 if len(topo_urls) == 0:
379 print("Failed to generate topology URLs")
382 # If required, get inventory resource URLs
383 if in_args.resource != "topology":
384 ig = InvUrlGenerator(in_args.host, in_args.port, in_args.auth)
385 inv_urls += ig.generate()
386 if len(inv_urls) == 0:
387 print("Failed to generate inventory URLs")
390 if in_args.resource == "topo+inv" or in_args.resource == "all":
391 # To have balanced test results, the number of URLs for topology and inventory must be the same
392 if len(topo_urls) != len(inv_urls):
393 print("The number of topology and inventory URLs don't match")
396 st = ShardPerformanceTester(
405 if in_args.resource == "all" or in_args.resource == "topo":
406 print("===================================")
407 print("Testing topology shard performance:")
408 print("===================================")
409 st.run_test(topo_urls)
411 if in_args.resource == "all" or in_args.resource == "inv":
412 print("====================================")
413 print("Testing inventory shard performance:")
414 print("====================================")
415 st.run_test(inv_urls)
417 if in_args.resource == "topo+inv" or in_args.resource == "all":
418 print("===============================================")
419 print("Testing combined shards (topo+inv) performance:")
420 print("===============================================")
421 st.run_test(topo_urls + inv_urls)