Vo Minh Thu (OpenERP) has proposed merging lp:~openerp-dev/openobject-server/trunk-simple-table-stats-vmt into lp:openobject-server.
Requested reviews: OpenERP Core Team (openerp) For more details, see: https://code.launchpad.net/~openerp-dev/openobject-server/trunk-simple-table-stats-vmt/+merge/122685 -- https://code.launchpad.net/~openerp-dev/openobject-server/trunk-simple-table-stats-vmt/+merge/122685 Your team OpenERP R&D Team is subscribed to branch lp:~openerp-dev/openobject-server/trunk-simple-table-stats-vmt.
=== modified file 'openerp/modules/registry.py' --- openerp/modules/registry.py 2012-08-13 15:05:01 +0000 +++ openerp/modules/registry.py 2012-09-14 14:47:22 +0000 @@ -182,6 +182,8 @@ try: registry.do_parent_store(cr) registry.get('ir.actions.report.xml').register_all(cr) + if openerp.tools.config['query_count_file']: + openerp.sql_db.write_query_count(openerp.tools.config['query_count_file'], False) cr.commit() finally: cr.close() === modified file 'openerp/sql_db.py' --- openerp/sql_db.py 2012-02-06 20:31:51 +0000 +++ openerp/sql_db.py 2012-09-14 14:47:22 +0000 @@ -35,13 +35,21 @@ __all__ = ['db_connect', 'close_db'] +from datetime import datetime as mdt +from datetime import timedelta from functools import wraps +from inspect import currentframe import logging import psycopg2.extensions from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT, ISOLATION_LEVEL_READ_COMMITTED, ISOLATION_LEVEL_REPEATABLE_READ from psycopg2.pool import PoolError from psycopg2.psycopg1 import cursor as psycopg1cursor -from threading import currentThread +import re +import threading + +import tools +from tools import config +from tools.func import frame_codeinfo psycopg2.extensions.register_type(psycopg2.extensions.UNICODE) @@ -65,19 +73,114 @@ psycopg2.extensions.register_type(psycopg2.extensions.new_type(typeoid, name, lambda x, cr: x)) psycopg2.extensions.register_type(psycopg2.extensions.new_type((700, 701, 1700,), 'float', undecimalize)) - -import tools -from tools.func import frame_codeinfo -from datetime import datetime as mdt -from datetime import timedelta -import threading -from inspect import currentframe - -import re -re_from = re.compile('.* from "?([a-zA-Z_0-9]+)"? .*$'); -re_into = re.compile('.* into "?([a-zA-Z_0-9]+)"? .*$'); - -sql_counter = 0 +re_query = re.compile('.*(update|from|into) "?([a-zA-Z_0-9]+)"?.*'); + +re_alter_query = re.compile('\s*alter\s+table\s+"?([a-zA-Z_0-9]+)"?.*', re.MULTILINE); +re_select_query = re.compile('\s*select\s+.*\s+from\s+"?([a-zA-Z_0-9]+)"?.*', re.MULTILINE); +re_delete_query = re.compile('\s*delete\s+from\s+"?([a-zA-Z_0-9]+)"?.*', re.MULTILINE); +re_insert_query = re.compile('\s*insert\s+into\s+"?([a-zA-Z_0-9]+)"?.*', re.MULTILINE); +re_select_no_table_query = re.compile('\s*select\s+.*', re.MULTILINE); +re_update_query = re.compile('\s*update\s+.*\s+set\s+"?([a-zA-Z_0-9]+)"?.*', re.MULTILINE); +re_create_sequence_query = re.compile('\s*create\s+sequence.*"?.*', re.MULTILINE); +re_drop_sequence_query = re.compile('\s*drop\s+sequence.*', re.MULTILINE); +re_comment_query = re.compile('\s*comment.*', re.MULTILINE); + +query_count_lock = threading.RLock() +query_count = 0 +query_counts = {} + +query_types = ('alter', 'select', 'delete', 'insert', 'update', 'create_seq', 'drop_seq', 'comment', 'other') + +def query_type(query): + q = query.lower() + types = [ + (re_alter_query, 'alter', 1), + (re_select_query, 'select', 1), + (re_delete_query, 'delete', 1), + (re_insert_query, 'insert', 1), + (re_select_no_table_query, 'select', '<no_table>'), + (re_update_query, 'update', 1), + (re_create_sequence_query, 'create_seq', '<no_table>'), + (re_drop_sequence_query, 'drop_seq', '<no_table>'), + (re_comment_query, 'comment', '<no_table>'), + ] + for r, typ, table in types: + m = r.match(q) + if m: + return typ, table if isinstance(table, str) else m.group(table) + return 'other', 'zzz_unknown' + +def count_query(query, delay): + """ + Collect a few statistics about database usage. They can be later written + to file using the `write_query_count` function. + + :param string query: the query being observed. + :param int delay: the time used to run the query against the database. + :returns: nothing + """ + global query_count_lock + global query_count + global query_counts + with query_count_lock: + m = query_type(query) + query_count += 1 + typ, table = m + d = dict.fromkeys(query_types, (0, 0)) + table_counts = query_counts.setdefault(table, d) + calls, elapsed_time = table_counts[typ] + table_counts[typ] = (calls + 1, elapsed_time + delay) + +def add_query_total(): + """ + Add a virtual table called `zzz_total` to sum all table counts, and add a + virtual query type `total` to each table. + """ + with query_count_lock: + total = query_counts['zzz_total'] = dict.fromkeys(query_types, (0, 0)) + for table, counts in query_counts.iteritems(): + counts['total'] = (0, 0) + for x in query_types: + count, delay = counts[x] + total_count, total_delay = counts['total'] + counts['total'] = (total_count + count, total_delay + delay) + total_count, total_delay = total[x] + total[x] = (total_count + count, total_delay + delay) + counts = query_counts['zzz_total'] + counts['total'] = (0, 0) + for x in query_types: + count, delay = counts[x] + total_count, total_delay = counts['total'] + counts['total'] = (total_count + count, total_delay + delay) + +def write_query_count(file_name, header): + """ + Write a few statistics about database table usage as CSV. The statistics + are collected using the `count_query` function. + + Assuming a `here.txt` filename: a possible command to display the results + is: + > cat here.txt | column -s, -t | sort -n -k 2 + + :param string file_name: file name to write the data. + :param bool header: specify whether a header is written before the data. + :returns: nothing + """ + with query_count_lock: + add_query_total() + with open(file_name, 'w') as w: + if header: + w.write("Table,Elapsed time (FROM),Elapsed time (FROM),Calls (FROM),Elapsed time (INTO),Elapsed time (INTO),Calls (INTO)\n") + for table, counts in query_counts.iteritems(): + w.write('%s,' % table) + for x in query_types + ('total',): + count, delay = counts[x] + delay_ = timedelta(microseconds=delay) + w.write('%s,' % count) + w.write('%s,' % delay) + w.write(('%s' if x == 'total' else '%s,') % delay_) + w.write("\n") + class Cursor(object): """Represents an open transaction to the PostgreSQL DB backend, @@ -141,26 +244,18 @@ """ IN_MAX = 1000 # decent limit on size of IN queries - guideline = Oracle limit - def check(f): - @wraps(f) - def wrapper(self, *args, **kwargs): - if self.__closed: - msg = 'Unable to use a closed cursor.' - if self.__closer: - msg += ' It was closed at %s, line %s' % self.__closer - raise psycopg2.OperationalError(msg) - return f(self, *args, **kwargs) - return wrapper + def check(self): + if self.__closed: + msg = 'Unable to use a closed cursor.' + if self.__closer: + msg += ' It was closed at %s, line %s' % self.__closer + raise psycopg2.OperationalError(msg) def __init__(self, pool, dbname, serialized=True): - self.sql_from_log = {} - self.sql_into_log = {} - # default log level determined at cursor creation, could be # overridden later for debugging purposes - self.sql_log = _logger.isEnabledFor(logging.DEBUG) + self.sql_log = True - self.sql_log_count = 0 self.__closed = True # avoid the call of close() (by __del__) if an exception # is raised by any of the following initialisations self._pool = pool @@ -197,14 +292,15 @@ _logger.warning(msg) self._close(True) - @check + def execute(self, query, params=None, log_exceptions=None): + self.check() if '%d' in query or '%f' in query: _logger.warning(query) _logger.warning("SQL queries cannot contain %d or %f anymore. " "Use only %s") - if self.sql_log: + if self.sql_log or config['query_count_file']: now = mdt.now() try: @@ -219,22 +315,12 @@ _logger.exception("bad query: %s", self._obj.query or query) raise - if self.sql_log: + if self.sql_log or config['query_count_file']: + _logger.debug("query: %s", self._obj.query) delay = mdt.now() - now delay = delay.seconds * 1E6 + delay.microseconds + count_query(query, delay) - _logger.debug("query: %s", self._obj.query) - self.sql_log_count+=1 - res_from = re_from.match(query.lower()) - if res_from: - self.sql_from_log.setdefault(res_from.group(1), [0, 0]) - self.sql_from_log[res_from.group(1)][0] += 1 - self.sql_from_log[res_from.group(1)][1] += delay - res_into = re_into.match(query.lower()) - if res_into: - self.sql_into_log.setdefault(res_into.group(1), [0, 0]) - self.sql_into_log[res_into.group(1)][0] += 1 - self.sql_into_log[res_into.group(1)][1] += delay return res @@ -243,36 +329,8 @@ safe for IN conditions, after uniquifying them.""" return tools.misc.split_every(self.IN_MAX, set(ids)) - def print_log(self): - global sql_counter - sql_counter += self.sql_log_count - if not self.sql_log: - return - def process(type): - sqllogs = {'from':self.sql_from_log, 'into':self.sql_into_log} - sum = 0 - if sqllogs[type]: - sqllogitems = sqllogs[type].items() - sqllogitems.sort(key=lambda k: k[1][1]) - _logger.debug("SQL LOG %s:", type) - sqllogitems.sort(lambda x,y: cmp(x[1][0], y[1][0])) - for r in sqllogitems: - delay = timedelta(microseconds=r[1][1]) - _logger.debug("table: %s: %s/%s", - r[0], delay, r[1][0]) - sum+= r[1][1] - sqllogs[type].clear() - sum = timedelta(microseconds=sum) - _logger.debug("SUM %s:%s/%d [%d]", - type, sum, self.sql_log_count, sql_counter) - sqllogs[type].clear() - process('from') - process('into') - self.sql_log_count = 0 - self.sql_log = False - - @check def close(self): + self.check() return self._close(False) def _close(self, leak=False): @@ -281,7 +339,6 @@ if self.sql_log: self.__closer = frame_codeinfo(currentframe(),3) - self.print_log() self._obj.close() @@ -299,13 +356,13 @@ if leak: self._cnx.leaked = True else: - chosen_template = tools.config['db_template'] + chosen_template = config['db_template'] templates_list = tuple(set(['template0', 'template1', 'postgres', chosen_template])) keep_in_pool = self.dbname not in templates_list self._pool.give_back(self._cnx, keep_in_pool=keep_in_pool) - @check def autocommit(self, on): + self.check() if on: isolation_level = ISOLATION_LEVEL_AUTOCOMMIT else: @@ -324,20 +381,20 @@ else ISOLATION_LEVEL_READ_COMMITTED self._cnx.set_isolation_level(isolation_level) - @check def commit(self): """ Perform an SQL `COMMIT` """ + self.check() return self._cnx.commit() - @check def rollback(self): """ Perform an SQL `ROLLBACK` """ + self.check() return self._cnx.rollback() - @check def __getattr__(self, name): + self.check() return getattr(self._obj, name) """ Set the mode of postgres operations for all cursors @@ -480,7 +537,7 @@ def dsn(db_name): _dsn = '' for p in ('host', 'port', 'user', 'password'): - cfg = tools.config['db_' + p] + cfg = config['db_' + p] if cfg: _dsn += '%s=%s ' % (p, cfg) @@ -499,8 +556,8 @@ def db_connect(db_name): global _Pool if _Pool is None: - _Pool = ConnectionPool(int(tools.config['db_maxconn'])) - currentThread().dbname = db_name + _Pool = ConnectionPool(int(config['db_maxconn'])) + threading.currentThread().dbname = db_name return Connection(_Pool, db_name) def close_db(db_name): @@ -508,7 +565,7 @@ """ You might want to call openerp.modules.registry.RegistryManager.delete(db_name) along this function.""" if _Pool: _Pool.close_all(dsn(db_name)) - ct = currentThread() + ct = threading.currentThread() if hasattr(ct, 'dbname'): delattr(ct, 'dbname') === modified file 'openerp/tools/config.py' --- openerp/tools/config.py 2012-09-12 11:19:10 +0000 +++ openerp/tools/config.py 2012-09-14 14:47:22 +0000 @@ -74,7 +74,8 @@ # Not exposed in the configuration file. self.blacklist_for_save = set( ['publisher_warranty_url', 'load_language', 'root_path', - 'init', 'save', 'config', 'update', 'stop_after_init']) + 'init', 'save', 'config', 'update', 'stop_after_init', + 'query_count_file']) # dictionary mapping option destination (keys in self.options) to MyOptions. self.casts = {} @@ -191,6 +192,9 @@ group.add_option('--log-level', dest='log_level', type='choice', choices=levels, my_default='info', help='specify the level of the logging. Accepted values: ' + str(levels) + ' (deprecated option).') + group.add_option("--query-count-file", dest="query_count_file", my_default=False, + help="Specify if some SQL query statistics are saved in a given file.") + parser.add_option_group(group) # SMTP Group @@ -375,7 +379,8 @@ 'netrpc', 'xmlrpc', 'syslog', 'without_demo', 'timezone', 'xmlrpcs_interface', 'xmlrpcs_port', 'xmlrpcs', 'static_http_enable', 'static_http_document_root', 'static_http_url_prefix', - 'secure_cert_file', 'secure_pkey_file', 'dbfilter', 'log_handler', 'log_level' + 'secure_cert_file', 'secure_pkey_file', 'dbfilter', 'log_handler', 'log_level', + 'query_count_file' ] for arg in keys:
_______________________________________________ Mailing list: https://launchpad.net/~openerp-dev-gtk Post to : openerp-dev-gtk@lists.launchpad.net Unsubscribe : https://launchpad.net/~openerp-dev-gtk More help : https://help.launchpad.net/ListHelp