[MERGE] forward port of branch 7.0 up to revid 4067 chs@openerp.com-20131114142639...
[odoo/odoo.git] / openerpcommand / benchmarks.py
1 """
2 Define a base class for client-side benchmarking.
3 """
4 import hashlib
5 import multiprocessing
6 import sys
7 import time
8
9 from .client import Client
10
11 class Bench(Client):
12     """
13     Base class for concurrent benchmarks. The measure_once() method must be
14     overriden.
15
16     Each sub-benchmark will be run in its own process then a report is done
17     with all the results (shared with the main process using a
18     `multiprocessing.Array`).
19     """
20
21     def __init__(self, subparsers=None):
22         super(Bench, self).__init__(subparsers)
23         self.parser.add_argument('-n', '--samples', metavar='INT',
24             default=100, help='number of measurements to take')
25             # TODO if -n <int>s is given (instead of -n <int>), run the
26             # benchmark for <int> seconds and return the number of iterations.
27         self.parser.add_argument('-o', '--output', metavar='PATH',
28             required=True, help='path to save the generated report')
29         self.parser.add_argument('--append', action='store_true',
30             default=False, help='append the report to an existing file')
31         self.parser.add_argument('-j', '--jobs', metavar='JOBS',
32             default=1, help='number of concurrent workers')
33         self.parser.add_argument('--seed', metavar='SEED',
34             default=0, help='a value to ensure different runs can create unique data')
35         self.worker = -1
36
37     def work(self, iarr=None):
38         if iarr:
39             # If an array is given, it means we are a worker process...
40             self.work_slave(iarr)
41         else:
42             # ... else we are the main process and we will spawn workers,
43             # passing them an array.
44             self.work_master()
45
46     def work_master(self):
47         N = int(self.args.samples)
48         self.arrs = [(i, multiprocessing.Array('f', range(N)))
49             for i in xrange(int(self.args.jobs))]
50         ps = [multiprocessing.Process(target=self.run, args=(arr,))
51             for arr in self.arrs]
52         [p.start() for p in ps]
53         [p.join() for p in ps]
54
55         self.report_html()
56
57     def work_slave(self, iarr):
58         j, arr = iarr
59         self.worker = j
60         N = int(self.args.samples)
61         total_t0 = time.time()
62         for i in xrange(N):
63             t0 = time.time()
64             self.measure_once(i)
65             t1 = time.time()
66             arr[i] = t1 - t0
67             print >> sys.stdout, '\r%s' % ('|' * (i * 60 / N)),
68             print >> sys.stdout, '%s %s%%' % \
69                 (' ' * (60 - (i * 60 / N)), int(float(i+1)/N*100)),
70             sys.stdout.flush()
71         total_t1 = time.time()
72         print '\nDone in %ss.' % (total_t1 - total_t0)
73
74     def report_html(self):
75         series = []
76         for arr in self.arrs:
77             serie = """{
78                 data: %s,
79                 points: { show: true }
80             }""" % ([[x, i] for i, x in enumerate(arr)],)
81             series.append(serie)
82         chart_id = hashlib.md5(" ".join(sys.argv)).hexdigest()
83         HEADER = """<!doctype html>
84 <title>Benchmarks</title>
85 <meta charset=utf-8>
86 <script type="text/javascript" src="js/jquery.min.js"></script>
87 <script type="text/javascript" src="js/jquery.flot.js"></script>
88 """
89
90         CONTENT = """<h1>%s</h1>
91 %s
92 <div id='chart_%s' style='width:400px;height:300px;'>...</div>
93 <script type="text/javascript">
94 $.plot($("#chart_%s"), [%s],
95   {yaxis: { ticks: false }});
96 </script>""" % (self.bench_name, ' '.join(sys.argv), chart_id, chart_id,
97         ','.join(series))
98         if self.args.append:
99             with open(self.args.output, 'a') as f:
100                 f.write(CONTENT,)
101         else:
102             with open(self.args.output, 'w') as f:
103                 f.write(HEADER + CONTENT,)
104
105     def measure_once(self, i):
106         """
107         The `measure_once` method is called --jobs times. A `i` argument is
108         supplied to allow to create unique values for each execution (e.g. to
109         supply fresh identifiers to a `create` method.
110         """
111         pass
112
113 class BenchRead(Bench):
114     """Read a record repeatedly."""
115
116     command_name = 'bench-read'
117     bench_name = 'res.users.read(1)'
118
119     def __init__(self, subparsers=None):
120         super(BenchRead, self).__init__(subparsers)
121         self.parser.add_argument('-m', '--model', metavar='MODEL',
122             required=True, help='the model')
123         self.parser.add_argument('-i', '--id', metavar='RECORDID',
124             required=True, help='the record id')
125
126     def measure_once(self, i):
127         self.execute(self.args.model, 'read', [self.args.id], [])
128
129 class BenchFieldsViewGet(Bench):
130     """Read a record's fields and view architecture repeatedly."""
131
132     command_name = 'bench-view'
133     bench_name = 'res.users.fields_view_get(1)'
134
135     def __init__(self, subparsers=None):
136         super(BenchFieldsViewGet, self).__init__(subparsers)
137         self.parser.add_argument('-m', '--model', metavar='MODEL',
138             required=True, help='the model')
139         self.parser.add_argument('-i', '--id', metavar='RECORDID',
140             required=True, help='the record id')
141
142     def measure_once(self, i):
143         self.execute(self.args.model, 'fields_view_get', self.args.id)
144
145 class BenchDummy(Bench):
146     """Dummy (call test.limits.model.consume_nothing())."""
147
148     command_name = 'bench-dummy'
149     bench_name = 'test.limits.model.consume_nothing()'
150
151     def __init__(self, subparsers=None):
152         super(BenchDummy, self).__init__(subparsers)
153         self.parser.add_argument('-a', '--args', metavar='ARGS',
154             default='', help='some arguments to serialize')
155
156     def measure_once(self, i):
157         self.execute('test.limits.model', 'consume_nothing')
158
159 class BenchLogin(Bench):
160     """Login (update res_users.date)."""
161
162     command_name = 'bench-login'
163     bench_name = 'res.users.login(1)'
164
165     def measure_once(self, i):
166         self.common_proxy.login(self.database, self.user, self.password)