Files
odoo_source/addons/web/tests/test_assets.py
T
Xavier-Do 64bbef5bc1 [IMP] base, web: generate assets outside rendering
The generation inside the rendering has some drawbacks:

- `commit_assetsbundle` is needed for reports rendering because the
template rendering may generate some assets that will be accessed by
another transaction before the transaction is committed. But this
solution is not ideal since the transaction is committed in the middle
of the request

- when the first rendered page is a 404, the assets are not committed
and the page is broken.

- when starting, deleting an attachment can create a concurrent update
error and the request is retried. This will occur once per attachment
and for all worker trying to access the same resource. The whole
transaction is rollbacked, even the previously created assets bundle.

- The cold page load is a slower since there is more work to do.

- Implementing a readonly request is difficult because it could be
transformed to read write and re-executed if the assets bundle does not
exist.

Generating assets when needed solves those issues. The concurrency
when deleting an assets could still occur but only once per bundle, and
in a smaller transaction. This could be solved with a lock now that we
have more control on the transaction. The commit_assetsbundle can be
removed and 404 page should have a correct layout. The cold page load
could be a little faster because the assets bundle can be generated in
parallel requests instead of sequentially when rendering the page.

Part-of: odoo/odoo#131353
2023-10-27 11:34:52 +00:00

117 lines
4.9 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
import time
import odoo
import odoo.tests
from odoo.tests.common import HttpCase
from odoo.modules.module import get_manifest
from odoo.tools import mute_logger
from unittest.mock import patch
_logger = logging.getLogger(__name__)
class TestAssetsGenerateTimeCommon(odoo.tests.TransactionCase):
def generate_bundles(self, unlink=True):
if unlink:
self.env['ir.attachment'].search([('url', '=like', '/web/assets/%')]).unlink() # delete existing attachement
installed_module_names = self.env['ir.module.module'].search([('state', '=', 'installed')]).mapped('name')
bundles = {
key
for module in installed_module_names
for key in get_manifest(module).get('assets', [])
}
for bundle_name in bundles:
with mute_logger('odoo.addons.base.models.assetsbundle'):
for assets_type in 'css', 'js':
try:
start_t = time.time()
css = assets_type == 'css'
js = assets_type == 'js'
bundle = self.env['ir.qweb']._get_asset_bundle(bundle_name, css=css, js=js)
if assets_type == 'css' and bundle.stylesheets:
bundle.css()
if assets_type == 'js' and bundle.javascripts:
bundle.js()
yield (f'{bundle_name}.{assets_type}', time.time() - start_t)
except ValueError:
_logger.info('Error detected while generating bundle %r %s', bundle_name, assets_type)
@odoo.tests.tagged('post_install', '-at_install', 'assets_bundle')
class TestLogsAssetsGenerateTime(TestAssetsGenerateTimeCommon):
def test_logs_assets_generate_time(self):
"""
The purpose of this test is to monitor the time of assets bundle generation.
This is not meant to test the generation failure, hence the try/except and the mute logger.
"""
for bundle, duration in list(self.generate_bundles()):
_logger.info('Bundle %r generated in %.2fs', bundle, duration)
def test_logs_assets_check_time(self):
"""
The purpose of this test is to monitor the time of assets bundle generation.
This is not meant to test the generation failure, hence the try/except and the mute logger.
"""
start = time.time()
for bundle, duration in self.generate_bundles(False):
_logger.info('Bundle %r checked in %.2fs', bundle, duration)
duration = time.time() - start
_logger.info('All bundle checked in %.2fs', duration)
@odoo.tests.tagged('post_install', '-at_install', '-standard', 'test_assets')
class TestPregenerateTime(HttpCase):
def test_logs_pregenerate_time(self):
self.env['ir.qweb']._pregenerate_assets_bundles()
start = time.time()
self.env.registry.clear_cache()
self.env.cache.invalidate()
with self.profile(collectors=['sql', odoo.tools.profiler.PeriodicCollector(interval=0.01)], disable_gc=True):
self.env['ir.qweb']._pregenerate_assets_bundles()
duration = time.time() - start
_logger.info('All bundle checked in %.2fs', duration)
@odoo.tests.tagged('post_install', '-at_install', '-standard', 'assets_bundle')
class TestAssetsGenerateTime(TestAssetsGenerateTimeCommon):
"""
This test is meant to be run nightly to ensure bundle generation does not exceed
a low threshold
"""
def test_assets_generate_time(self):
thresholds = {
'web.qunit_suite_tests.js': 3.6,
'project.webclient.js': 2.5,
'point_of_sale.pos_assets_backend.js': 2.5,
'web.assets_backend.js': 2.5,
}
for bundle, duration in self.generate_bundles():
threshold = thresholds.get(bundle, 2)
self.assertLess(duration, threshold, "Bundle %r took more than %s sec" % (bundle, threshold))
@odoo.tests.tagged('post_install', '-at_install')
class TestLoad(HttpCase):
def test_assets_already_exists(self):
self.authenticate('admin', 'admin')
# TODO xdo adapt this test. url open won't generate attachment anymore even if not pregenerated
_save_attachment = odoo.addons.base.models.assetsbundle.AssetsBundle.save_attachment
def save_attachment(bundle, extension, content):
attachment = _save_attachment(bundle, extension, content)
message = f"Trying to save an attachement for {bundle.name} when it should already exist: {attachment.url}"
_logger.error(message)
return attachment
with patch('odoo.addons.base.models.assetsbundle.AssetsBundle.save_attachment', save_attachment):
self.url_open('/web').raise_for_status()
self.url_open('/').raise_for_status()