|
| 1 | +# Copyright 2026 Camptocamp SA |
| 2 | +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) |
| 3 | + |
| 4 | +import logging |
| 5 | + |
| 6 | +from odoo import api, fields, models |
| 7 | +from odoo.exceptions import ValidationError |
| 8 | + |
| 9 | +from ..tools import dynamic_profile |
| 10 | + |
| 11 | +_logger = logging.getLogger(__name__) |
| 12 | + |
| 13 | + |
| 14 | +class ProfilerFunction(models.Model): |
| 15 | + _name = "profiler.function" |
| 16 | + _description = "Profiled Function" |
| 17 | + _order = "name" |
| 18 | + |
| 19 | + name = fields.Char(required=True) |
| 20 | + python_path = fields.Char( |
| 21 | + required=True, |
| 22 | + index=True, |
| 23 | + help="Use module.function or module.Class.method", |
| 24 | + ) |
| 25 | + sample_rate = fields.Float( |
| 26 | + default=0.05, |
| 27 | + help="Percentage of calls to profile (0.0 - 1.0)", |
| 28 | + ) |
| 29 | + active = fields.Boolean(default=True) |
| 30 | + |
| 31 | + @api.constrains("python_path") |
| 32 | + def _check_python_path(self): |
| 33 | + for record in self: |
| 34 | + if not record.python_path: |
| 35 | + continue |
| 36 | + try: |
| 37 | + dynamic_profile.validate_path(record.python_path) |
| 38 | + except Exception as exc: |
| 39 | + raise ValidationError( |
| 40 | + f"Invalid python path {record.python_path}: {exc}" |
| 41 | + ) from exc |
| 42 | + |
| 43 | + @api.model_create_multi |
| 44 | + def create(self, vals_list): |
| 45 | + records = super().create(vals_list) |
| 46 | + records._update_registry() |
| 47 | + return records |
| 48 | + |
| 49 | + def write(self, vals): |
| 50 | + res = super().write(vals) |
| 51 | + if {"python_path", "sample_rate", "active"} & set(vals): |
| 52 | + self._update_registry() |
| 53 | + return res |
| 54 | + |
| 55 | + def unlink(self): |
| 56 | + res = super().unlink() |
| 57 | + self._update_registry() |
| 58 | + return res |
| 59 | + |
| 60 | + def _update_registry(self): |
| 61 | + if self.env.registry.ready: |
| 62 | + self._unregister_hook() |
| 63 | + self._register_hook() |
| 64 | + self.env.registry.registry_invalidated = True |
| 65 | + else: |
| 66 | + _logger.info("Registry not ready, skipping profiler patch update") |
| 67 | + |
| 68 | + def _register_hook(self): |
| 69 | + res = super()._register_hook() |
| 70 | + active_records = self.search([("active", "=", True)]) |
| 71 | + dynamic_profile.patch_active_records(active_records) |
| 72 | + return res |
| 73 | + |
| 74 | + def _unregister_hook(self): |
| 75 | + res = super()._unregister_hook() |
| 76 | + for record in self.with_context(active_test=False).search([]): |
| 77 | + try: |
| 78 | + dynamic_profile.unpatch_path(record.python_path) |
| 79 | + except Exception as exc: |
| 80 | + _logger.warning( |
| 81 | + "Unable to remove profile patch for %s: %s", |
| 82 | + record.python_path, |
| 83 | + exc, |
| 84 | + ) |
| 85 | + return res |
0 commit comments