From 3eef414735225420f543914d7807105b4c859f6e Mon Sep 17 00:00:00 2001 From: hugues de keyzer Date: Mon, 1 Jun 2026 14:30:02 +0200 Subject: [PATCH] [IMP] make odoo start up (much) faster * to find out static file paths, instead of loading the manifest of all modules found on the addons_path, load them only as they are needed. this solves 3 problems: 1. it makes odoo start up much faster, as only the manifest of the modules of which static files are accessed are loaded instead of traversing the whole addons_path, searching for modules and loading all the manifest files. 2. it avoids a race condition at startup in threaded mode, where multiple threads access Application.statics while it has not been computed yet, resulting in each thread computing it, further slowing down the startup. 3. if the same module is available multiple times on the addons_path, the first one found will be used for static files instead of the last one found, what does not match the loading of the other files. * remove the default argument value of _get_manifest_cache() and fix calls to include the value, to be able to re-use the cached values instead of re-computing them without the argument. --- odoo/addons/base/models/ir_asset.py | 6 ++-- odoo/http.py | 48 ++++++++++++++++++++++------- odoo/modules/module.py | 2 +- 3 files changed, 41 insertions(+), 15 deletions(-) diff --git a/odoo/addons/base/models/ir_asset.py b/odoo/addons/base/models/ir_asset.py index 157b1a28a8bbfb..ab85315bc938ea 100644 --- a/odoo/addons/base/models/ir_asset.py +++ b/odoo/addons/base/models/ir_asset.py @@ -202,7 +202,7 @@ def process_path(directive, target, path_def): # 2. Process all addons' manifests. for addon in self._topological_sort(tuple(addons)): - for command in odoo.modules.module._get_manifest_cached(addon)['assets'].get(bundle, ()): + for command in odoo.modules.module._get_manifest_cached(addon, None)['assets'].get(bundle, ()): directive, target, path_def = self._process_command(command) process_path(directive, target, path_def) @@ -257,7 +257,7 @@ def _topological_sort(self, addons_tuple): IrModule = self.env['ir.module.module'] def mapper(addon): - manif = odoo.modules.module._get_manifest_cached(addon) + manif = odoo.modules.module._get_manifest_cached(addon, None) from_terp = IrModule.get_values_from_terp(manif) from_terp['name'] = addon from_terp['depends'] = manif.get('depends', ['base']) @@ -302,7 +302,7 @@ def _get_paths(self, path_def, installed, extensions=None): path_url = fs2web(path_def) path_parts = [part for part in path_url.split('/') if part] addon = path_parts[0] - addon_manifest = odoo.modules.module._get_manifest_cached(addon) + addon_manifest = odoo.modules.module._get_manifest_cached(addon, None) safe_path = True if addon_manifest: diff --git a/odoo/http.py b/odoo/http.py index 79e6bd085f7b1d..ae6cba94dc3ad2 100644 --- a/odoo/http.py +++ b/odoo/http.py @@ -159,7 +159,7 @@ import odoo from .exceptions import UserError, AccessError, AccessDenied -from .modules.module import get_manifest +from .modules.module import get_manifest, get_module_path from .modules.registry import Registry from .service import security, model as service_model from .tools import (config, consteq, date_utils, file_path, parse_version, @@ -2117,6 +2117,41 @@ def _response(self, result=None, error=None): return self.request.make_json_response(response) +class ModuleStaticPathMap: + def __init__(self): + self._static_path_map = {} + + def get(self, module): + static_path = self._static_path_map.get(module) + if static_path: + return static_path + module_path = get_module_path(module) + if not module_path: + return None + # not passing module_path to get_manifest(), to take advantage of + # the cached values (in _get_manifest_cached()), as no code calls + # get_manifest() with a module_path. + manifest = get_manifest(module) + static_path = opj(module_path, "static") + if ( + manifest + and (manifest["installable"] or manifest["assets"]) + and os.path.isdir(static_path) + ): + self._static_path_map[module] = static_path + return static_path + return None + + def __getitem__(self, key): + static_path = self.get(key) + if static_path is not None: + return static_path + raise KeyError(key) + + def __contains__(self, value): + return self.get(value) is not None + + # ========================================================= # WSGI Entry Point # ========================================================= @@ -2131,16 +2166,7 @@ def statics(self): Map module names to their absolute ``static`` path on the file system. """ - mod2path = {} - for addons_path in odoo.addons.__path__: - for module in os.listdir(addons_path): - manifest = get_manifest(module) - static_path = opj(addons_path, module, 'static') - if (manifest - and (manifest['installable'] or manifest['assets']) - and os.path.isdir(static_path)): - mod2path[module] = static_path - return mod2path + return ModuleStaticPathMap() def get_static_file(self, url, host=''): """ diff --git a/odoo/modules/module.py b/odoo/modules/module.py index d1699178ab45c9..3017c3f4ee5150 100644 --- a/odoo/modules/module.py +++ b/odoo/modules/module.py @@ -446,7 +446,7 @@ def get_manifest(module, mod_path=None): return copy.deepcopy(_get_manifest_cached(module, mod_path)) @functools.lru_cache(maxsize=None) -def _get_manifest_cached(module, mod_path=None): +def _get_manifest_cached(module, mod_path): return load_manifest(module, mod_path) def load_information_from_description_file(module, mod_path=None):