Initialize the savepoint semi-lazily (on-demand) as cycling a savepoint
generates 4~5 queries (depending whether the savepoint is explicitly
released on COMMIT or not):
SAVEPOINT
-- < do stuff>
-- commit
RELEASE SAVEPOINT -- or not
SAVEPOINT
-- close
ROLLBACK TO SAVEPOINT
RELEASE SAVEPOINT
With a lazy savepoint, this is just 0-1 queries (`SAVEPOINT` at the
first explicit query only, creating a cursor and then closing it
immediately is a no-op).
For reliability use a semi-lazy savepoint: always immediately emit a
`SAVEPOINT` on cursor creation, but don't automatically create one
after each `commit`. That limits the issues of overlapping (but
non-nested) savepoints.
Also fix the `generate` API to not use the request: when `website` was
converted to the new API, `cr`, `uid`, and `context` were dropped as
if it were a model... but it's not. So in order to recover an
execution environment, that was looked up on the session.
That, then, turns out to be an issue when `generate` is triggered from
an RPC call: the RPC layer creates its own cursor and environment
separate from the request's which may not have one at
all. Problematically during testing we're in `mono_db` mode, so the
request's cursor/env can be accessed and will be lazily initialized.
This then causes an issue with the `TestCursor`'s savepoints: rather
than be nested, the lifetimes of the request's and RPC's savepoints
only overlap[0]:
|-- rpc --|
|-- request --|
As a result, when the RPC's cursor is committed and released it
automatically released the request's, and the request's explicit
release then fails. This would break `/website:WithContext.test_search`.
By fixing the API of `generate`, it stops triggering the creation of a
request cursor, and therefore the overlap and resulting error.
[0] the laziness or eagerness of the savepointing in the test cursor
has no impact on this issue, as multiple requests have already been
issued on the RPC's test cursor before the request's is even
created
Part-of: odoo/odoo#76243
124 lines
4.4 KiB
Python
124 lines
4.4 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from odoo.tests.common import HttpCase
|
|
|
|
EXTRA_REQUEST = 4 - 1
|
|
""" During tests, the query on 'base_registry_signaling, base_cache_signaling'
|
|
won't be executed on hot state, but new queries related to the test cursor
|
|
will be added::
|
|
|
|
cr = Cursor() # SAVEPOINT
|
|
cr.commit() # RELEASE
|
|
cr.close()
|
|
cr = Cursor()
|
|
cr.execute(...) # SAVEPOINT
|
|
cr.commit() # RELEASE
|
|
cr.close()
|
|
"""
|
|
|
|
|
|
class UtilPerf(HttpCase):
|
|
def _get_url_hot_query(self, url):
|
|
url += ('?' not in url and '?' or '') + '&nocache'
|
|
|
|
# ensure worker is in hot state
|
|
self.url_open(url)
|
|
self.url_open(url)
|
|
|
|
sql_count = self.registry.test_cr.sql_log_count
|
|
self.url_open(url)
|
|
return self.registry.test_cr.sql_log_count - sql_count - EXTRA_REQUEST
|
|
|
|
|
|
class TestStandardPerformance(UtilPerf):
|
|
def test_10_perf_sql_img_controller(self):
|
|
self.authenticate('demo', 'demo')
|
|
url = '/web/image/res.users/2/image_256'
|
|
self.assertEqual(self._get_url_hot_query(url), 6)
|
|
|
|
def test_20_perf_sql_img_controller_bis(self):
|
|
url = '/web/image/website/1/favicon'
|
|
self.assertEqual(self._get_url_hot_query(url), 4)
|
|
self.authenticate('portal', 'portal')
|
|
self.assertEqual(self._get_url_hot_query(url), 4)
|
|
|
|
|
|
class TestWebsitePerformance(UtilPerf):
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
self.page, self.menu = self._create_page_with_menu('/sql_page')
|
|
|
|
def _create_page_with_menu(self, url):
|
|
name = url[1:]
|
|
website = self.env['website'].browse(1)
|
|
page = self.env['website.page'].create({
|
|
'url': url,
|
|
'name': name,
|
|
'type': 'qweb',
|
|
'arch': '<t name="%s" t-name="website.page_test_%s"> \
|
|
<t t-call="website.layout"> \
|
|
<div id="wrap"><div class="oe_structure"/></div> \
|
|
</t> \
|
|
</t>' % (name, name),
|
|
'key': 'website.page_test_%s' % name,
|
|
'is_published': True,
|
|
'website_id': website.id,
|
|
'track': False,
|
|
})
|
|
menu = self.env['website.menu'].create({
|
|
'name': name,
|
|
'url': url,
|
|
'page_id': page.id,
|
|
'website_id': website.id,
|
|
'parent_id': website.menu_id.id
|
|
})
|
|
return (page, menu)
|
|
|
|
def test_10_perf_sql_queries_page(self):
|
|
# standard untracked website.page
|
|
self.assertEqual(self._get_url_hot_query(self.page.url), 11)
|
|
self.menu.unlink()
|
|
self.assertEqual(self._get_url_hot_query(self.page.url), 13)
|
|
|
|
def test_15_perf_sql_queries_page(self):
|
|
# standard tracked website.page
|
|
self.page.track = True
|
|
self.assertEqual(self._get_url_hot_query(self.page.url), 19)
|
|
self.menu.unlink()
|
|
self.assertEqual(self._get_url_hot_query(self.page.url), 21)
|
|
|
|
def test_20_perf_sql_queries_homepage(self):
|
|
# homepage "/" has its own controller
|
|
self.assertEqual(self._get_url_hot_query('/'), 18)
|
|
|
|
def test_30_perf_sql_queries_page_no_layout(self):
|
|
# website.page with no call to layout templates
|
|
self.page.arch = '<div>I am a blank page</div>'
|
|
self.assertEqual(self._get_url_hot_query(self.page.url), 9)
|
|
|
|
def test_40_perf_sql_queries_page_multi_level_menu(self):
|
|
# menu structure should not impact SQL requests
|
|
_, menu_a = self._create_page_with_menu('/a')
|
|
_, menu_aa = self._create_page_with_menu('/aa')
|
|
_, menu_b = self._create_page_with_menu('/b')
|
|
_, menu_bb = self._create_page_with_menu('/bb')
|
|
_, menu_bbb = self._create_page_with_menu('/bbb')
|
|
_, menu_bbbb = self._create_page_with_menu('/bbbb')
|
|
_, menu_bbbbb = self._create_page_with_menu('/bbbbb')
|
|
self._create_page_with_menu('c')
|
|
menu_bbbbb.parent_id = menu_bbbb
|
|
menu_bbbb.parent_id = menu_bbb
|
|
menu_bbb.parent_id = menu_bb
|
|
menu_bb.parent_id = menu_b
|
|
menu_aa.parent_id = menu_a
|
|
|
|
self.assertEqual(self._get_url_hot_query(self.page.url), 11)
|
|
|
|
def test_50_perf_sql_web_assets(self):
|
|
# assets route /web/assets/..
|
|
self.url_open('/') # create assets attachments
|
|
assets_url = self.env['ir.attachment'].search([('url', '=like', '/web/assets/%/web.assets_common%.js')], limit=1).url
|
|
self.assertEqual(self._get_url_hot_query(assets_url), 2)
|