From a5101891d46833ff78daff14fc35676a1c36b140 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Wed, 11 Feb 2026 18:40:20 +0100 Subject: [PATCH 1/4] Add queue_job_profiler --- queue_job_profiler/README.rst | 96 ++++ queue_job_profiler/__init__.py | 2 + queue_job_profiler/__manifest__.py | 18 + queue_job_profiler/controllers/__init__.py | 1 + queue_job_profiler/controllers/main.py | 28 ++ queue_job_profiler/models/__init__.py | 2 + queue_job_profiler/models/queue_job.py | 43 ++ .../models/queue_job_function.py | 48 ++ queue_job_profiler/pyproject.toml | 3 + queue_job_profiler/readme/CONTRIBUTORS.md | 2 + queue_job_profiler/readme/DESCRIPTION.md | 7 + queue_job_profiler/readme/USAGE.md | 8 + .../static/description/index.html | 445 ++++++++++++++++++ queue_job_profiler/tests/__init__.py | 1 + queue_job_profiler/tests/test_job_profiler.py | 134 ++++++ .../views/queue_job_function_views.xml | 25 + queue_job_profiler/views/queue_job_views.xml | 25 + 17 files changed, 888 insertions(+) create mode 100644 queue_job_profiler/README.rst create mode 100644 queue_job_profiler/__init__.py create mode 100644 queue_job_profiler/__manifest__.py create mode 100644 queue_job_profiler/controllers/__init__.py create mode 100644 queue_job_profiler/controllers/main.py create mode 100644 queue_job_profiler/models/__init__.py create mode 100644 queue_job_profiler/models/queue_job.py create mode 100644 queue_job_profiler/models/queue_job_function.py create mode 100644 queue_job_profiler/pyproject.toml create mode 100644 queue_job_profiler/readme/CONTRIBUTORS.md create mode 100644 queue_job_profiler/readme/DESCRIPTION.md create mode 100644 queue_job_profiler/readme/USAGE.md create mode 100644 queue_job_profiler/static/description/index.html create mode 100644 queue_job_profiler/tests/__init__.py create mode 100644 queue_job_profiler/tests/test_job_profiler.py create mode 100644 queue_job_profiler/views/queue_job_function_views.xml create mode 100644 queue_job_profiler/views/queue_job_views.xml diff --git a/queue_job_profiler/README.rst b/queue_job_profiler/README.rst new file mode 100644 index 0000000000..9765639adc --- /dev/null +++ b/queue_job_profiler/README.rst @@ -0,0 +1,96 @@ +================== +Job Queue Profiler +================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:b6e605728ea05be0bf98dbe6e94c197bab21882e965286ef214f719e252b444e + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fqueue-lightgray.png?logo=github + :target: https://github.com/OCA/queue/tree/18.0/queue_job_profiler + :alt: OCA/queue +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/queue-18-0/queue-18-0-queue_job_profiler + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/queue&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This addon adds profiling controls to queue job functions and wraps +queue job execution in an Odoo profiler session when enabled. + +When profiling is enabled for a job function and the executing user +matches the configured profiling user, the queue job runner records SQL +and async stack traces via ``odoo.tools.profiler.Profiler`` and saves +the results into ``ir_profile``. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +1. Open the Queue Job Functions menu. +2. Open the job function you want to profile. +3. In the Profiler group, enable Profiling and set the Profiling user + and Profiling until. +4. Run the job with the selected user (the queue job runner usually + executes as the superuser). +5. Inspect the generated entries in ``ir_profile`` to review the + captured profiling data. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Camptocamp + +Contributors +------------ + +- `Camptocamp `__: + + - Simone Orsi + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/queue `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/queue_job_profiler/__init__.py b/queue_job_profiler/__init__.py new file mode 100644 index 0000000000..91c5580fed --- /dev/null +++ b/queue_job_profiler/__init__.py @@ -0,0 +1,2 @@ +from . import controllers +from . import models diff --git a/queue_job_profiler/__manifest__.py b/queue_job_profiler/__manifest__.py new file mode 100644 index 0000000000..4b27f4dd8f --- /dev/null +++ b/queue_job_profiler/__manifest__.py @@ -0,0 +1,18 @@ +# Copyright 2026 Camptocamp SA (https://www.camptocamp.com). +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl + +{ + "name": "Job Queue Profiler", + "version": "18.0.1.0.0", + "author": "Camptocamp, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/queue", + "license": "AGPL-3", + "category": "Generic Modules", + "depends": [ + "queue_job", + ], + "data": [ + "views/queue_job_function_views.xml", + "views/queue_job_views.xml", + ], +} diff --git a/queue_job_profiler/controllers/__init__.py b/queue_job_profiler/controllers/__init__.py new file mode 100644 index 0000000000..12a7e529b6 --- /dev/null +++ b/queue_job_profiler/controllers/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/queue_job_profiler/controllers/main.py b/queue_job_profiler/controllers/main.py new file mode 100644 index 0000000000..08c8097634 --- /dev/null +++ b/queue_job_profiler/controllers/main.py @@ -0,0 +1,28 @@ +# Copyright 2026 Camptocamp SA (https://www.camptocamp.com). +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo.tools.profiler import Profiler + +from odoo.addons.queue_job.controllers.main import RunJobController as BaseController + + +class RunJobController(BaseController): + def _try_perform_job(self, env, job): + if self._profiler_is_enabled(env, job): + return self._profiler_perform_job(env, job) + return super()._try_perform_job(env, job) + + def _profiler_is_enabled(self, env, job): + func_id = job.job_config.job_function_id + job_function = env["queue.job.function"].browse(func_id) + return job_function.is_profiling_enabled() + + def _profiler_perform_job(self, env, job): + with self._profiler_get(env, job): + result = super()._try_perform_job(env, job) + return result + + def _profiler_get(self, env, job): + func_id = job.job_config.job_function_id + job_function = env["queue.job.function"].browse(func_id) + return Profiler(description=job_function._profile_make_name(job)) diff --git a/queue_job_profiler/models/__init__.py b/queue_job_profiler/models/__init__.py new file mode 100644 index 0000000000..2cf4503488 --- /dev/null +++ b/queue_job_profiler/models/__init__.py @@ -0,0 +1,2 @@ +from . import queue_job_function +from . import queue_job diff --git a/queue_job_profiler/models/queue_job.py b/queue_job_profiler/models/queue_job.py new file mode 100644 index 0000000000..cc2ddc7e89 --- /dev/null +++ b/queue_job_profiler/models/queue_job.py @@ -0,0 +1,43 @@ +# Copyright 2019 Creu Blanca +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +from odoo import fields, models + + +class QueueJob(models.Model): + _inherit = "queue.job" + + job_is_profiled = fields.Boolean( + string="Profiled", + default=False, + help="Whether this job has been profiled or not.", + compute="_compute_job_is_profiled", + ) + + def _compute_job_is_profiled(self): + for job in self: + # don't care about perf as this is loaded only on the job form view + profile_name = job.job_function_id._profile_make_name(job) + job.job_is_profiled = bool(job._profiler_get_record(profile_name)) + + def _profiler_get_record(self, profile_name): + IrProfile = self.env["ir.profile"] + return IrProfile.search( + [("name", "=", profile_name)], + limit=1, + ) + + def action_view_profile(self): + self.ensure_one() + profile_name = self.job_function_id._profile_make_name(self) + profile = self._profiler_get_record(profile_name) + if not profile: + return {"type": "ir.actions.act_window_close"} + return { + "type": "ir.actions.act_window", + "name": "Profile", + "res_model": "ir.profile", + "view_mode": "form", + "res_id": profile.id, + "target": "current", + } diff --git a/queue_job_profiler/models/queue_job_function.py b/queue_job_profiler/models/queue_job_function.py new file mode 100644 index 0000000000..312f6a1ce2 --- /dev/null +++ b/queue_job_profiler/models/queue_job_function.py @@ -0,0 +1,48 @@ +# Copyright 2019 Creu Blanca +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +from odoo import api, exceptions, fields, models + + +class QueueJobFunction(models.Model): + _inherit = "queue.job.function" + + profiling_enabled = fields.Boolean( + string="Profiling enabled", + help="Indicates whether profiling is enabled for this job function.", + ) + profiling_user_id = fields.Many2one( + "res.users", + string="Profiling user", + help="The user allowed to perform profiling for this job function.", + ) + profiling_until = fields.Datetime( + string="Profiling until", + help="The date and time until which profiling is enabled " + "for this job function.", + ) + + def is_profiling_enabled(self): + self.ensure_one() + return ( + self.profiling_enabled + and self.profiling_user_id == self.env.user + and (self.profiling_until and self.profiling_until >= fields.Datetime.now()) + ) + + @api.constrains("profiling_enabled") + def _check_profiling_setup(self): + for record in self: + if record.profiling_enabled and ( + not record.profiling_user_id or not record.profiling_until + ): + raise exceptions.ValidationError( + self.env._( + "A profiling user and a profiling until date " + "must be set when profiling is enabled." + ) + ) + + def _profile_make_name(self, job): + self.ensure_one() + return f"queue.job {job.uuid} - {self.name}" diff --git a/queue_job_profiler/pyproject.toml b/queue_job_profiler/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/queue_job_profiler/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/queue_job_profiler/readme/CONTRIBUTORS.md b/queue_job_profiler/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..fc27d9f599 --- /dev/null +++ b/queue_job_profiler/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- [Camptocamp](https://camptocamp.com): + - Simone Orsi \<\> diff --git a/queue_job_profiler/readme/DESCRIPTION.md b/queue_job_profiler/readme/DESCRIPTION.md new file mode 100644 index 0000000000..8e8814426c --- /dev/null +++ b/queue_job_profiler/readme/DESCRIPTION.md @@ -0,0 +1,7 @@ +This addon adds profiling controls to queue job functions and wraps +queue job execution in an Odoo profiler session when enabled. + +When profiling is enabled for a job function and the executing user +matches the configured profiling user, the queue job runner records +SQL and async stack traces via `odoo.tools.profiler.Profiler` and saves +the results into `ir_profile`. \ No newline at end of file diff --git a/queue_job_profiler/readme/USAGE.md b/queue_job_profiler/readme/USAGE.md new file mode 100644 index 0000000000..868db0ccbe --- /dev/null +++ b/queue_job_profiler/readme/USAGE.md @@ -0,0 +1,8 @@ +1. Open the Queue Job Functions menu. +2. Open the job function you want to profile. +3. In the Profiler group, enable Profiling and set the Profiling user + and Profiling until. +4. Run the job with the selected user (the queue job runner usually executes + as the superuser). +5. Inspect the generated entries in `ir_profile` to review the captured + profiling data. \ No newline at end of file diff --git a/queue_job_profiler/static/description/index.html b/queue_job_profiler/static/description/index.html new file mode 100644 index 0000000000..9ab72a5454 --- /dev/null +++ b/queue_job_profiler/static/description/index.html @@ -0,0 +1,445 @@ + + + + + +Job Queue Profiler + + + +
+

Job Queue Profiler

+ + +

Beta License: AGPL-3 OCA/queue Translate me on Weblate Try me on Runboat

+

This addon adds profiling controls to queue job functions and wraps +queue job execution in an Odoo profiler session when enabled.

+

When profiling is enabled for a job function and the executing user +matches the configured profiling user, the queue job runner records SQL +and async stack traces via odoo.tools.profiler.Profiler and saves +the results into ir_profile.

+

Table of contents

+ +
+

Usage

+
    +
  1. Open the Queue Job Functions menu.
  2. +
  3. Open the job function you want to profile.
  4. +
  5. In the Profiler group, enable Profiling and set the Profiling user +and Profiling until.
  6. +
  7. Run the job with the selected user (the queue job runner usually +executes as the superuser).
  8. +
  9. Inspect the generated entries in ir_profile to review the +captured profiling data.
  10. +
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Camptocamp
  • +
+
+ +
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/queue project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/queue_job_profiler/tests/__init__.py b/queue_job_profiler/tests/__init__.py new file mode 100644 index 0000000000..f4cae97b5a --- /dev/null +++ b/queue_job_profiler/tests/__init__.py @@ -0,0 +1 @@ +from . import test_job_profiler diff --git a/queue_job_profiler/tests/test_job_profiler.py b/queue_job_profiler/tests/test_job_profiler.py new file mode 100644 index 0000000000..b820290dcb --- /dev/null +++ b/queue_job_profiler/tests/test_job_profiler.py @@ -0,0 +1,134 @@ +# copyright 2026 Camptocamp sa (https://www.camptocamp.com). +# license AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +from datetime import timedelta +from unittest.mock import patch + +from freezegun import freeze_time + +from odoo import exceptions, fields +from odoo.tests import common, new_test_user + +from odoo.addons.queue_job_profiler.controllers.main import RunJobController + + +class TestJobFunction(common.TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.user_func1 = cls.env["queue.job.function"].create( + {"model_id": cls.env["ir.model"]._get_id("res.users"), "method": "read"} + ) + cls.user_func2 = cls.env["queue.job.function"].create( + {"model_id": cls.env["ir.model"]._get_id("res.users"), "method": "read"} + ) + cls.user1 = new_test_user(cls.env, login="user1", groups="base.group_user") + cls.user2 = new_test_user(cls.env, login="user2", groups="base.group_user") + + def test_func_constraint(self): + with self.assertRaisesRegex( + exceptions.ValidationError, + "A profiling user and a profiling until date " + "must be set when profiling is enabled.", + ): + self.user_func1.profiling_enabled = True + + def _enable_profiling(self, job_function, user, delta=timedelta(hours=1)): + job_function.profiling_user_id = user + job_function.profiling_until = fields.Datetime.now() + delta + job_function.profiling_enabled = True + + def test_func_profiling_enabled(self): + # By default, profiling should be disabled for all users + for user in (self.env.user, self.user1, self.user2): + self.assertFalse(self.user_func1.with_user(user).is_profiling_enabled()) + # Enable it for user1 and check that it's only enabled for that user only + self._enable_profiling(self.user_func1, self.user1) + self.assertTrue(self.user_func1.with_user(self.user1).is_profiling_enabled()) + for user in (self.env.user, self.user2): + self.assertFalse(self.user_func1.with_user(user).is_profiling_enabled()) + # Check for another job function and user + self._enable_profiling(self.user_func2, self.user2) + self.assertTrue(self.user_func2.with_user(self.user2).is_profiling_enabled()) + for user in (self.env.user, self.user1): + self.assertFalse(self.user_func2.with_user(user).is_profiling_enabled()) + with freeze_time(fields.Datetime.now() + timedelta(days=1)): + # After the profiling_until date, profiling should be disabled for all users + for user in (self.env.user, self.user1, self.user2): + self.assertFalse(self.user_func1.with_user(user).is_profiling_enabled()) + self.assertFalse(self.user_func2.with_user(user).is_profiling_enabled()) + + def test_job_is_profiled(self): + job1 = self.env.user.with_delay().read(["id"]) + job1.store() + job_rec1 = job1.db_record() + job2 = self.env.user.with_delay().read(["id"]) + job2.store() + job_rec2 = job2.db_record() + jobs = job_rec1 | job_rec2 + self.assertEqual(jobs.mapped("job_is_profiled"), [False, False]) + profile_name1 = self.user_func1._profile_make_name(job_rec1) + profile_name2 = self.user_func2._profile_make_name(job_rec2) + prof1 = self.env["ir.profile"].create({"name": profile_name1}) + jobs.invalidate_recordset() + self.assertEqual(jobs.mapped("job_is_profiled"), [True, False]) + prof2 = self.env["ir.profile"].create({"name": profile_name2}) + jobs.invalidate_recordset() + self.assertEqual(jobs.mapped("job_is_profiled"), [True, True]) + self.assertEqual(job_rec1._profiler_get_record(profile_name1), prof1) + self.assertEqual(job_rec2._profiler_get_record(profile_name2), prof2) + + def test_job_action_view_profile(self): + job = self.env.user.with_delay().read(["id"]) + job.store() + job_rec = job.db_record() + self.assertEqual( + job_rec.action_view_profile(), {"type": "ir.actions.act_window_close"} + ) + profile_name = self.user_func1._profile_make_name(job_rec) + prof = self.env["ir.profile"].create({"name": profile_name}) + action = job_rec.action_view_profile() + self.assertEqual(action["type"], "ir.actions.act_window") + self.assertEqual(action["res_model"], "ir.profile") + self.assertEqual(action["res_id"], prof.id) + + def _run_controller(self, job, user=None): + controller = RunJobController() + env = self.env(user=user) if user else self.env + with ( + patch.object(self.env.cr, "commit"), + ): + controller._try_perform_job(env, job) + + def test_controller(self): + job = self.env.user.with_delay().read(["id"]) + self._enable_profiling(self.user_func1, self.user1) + # TODO @simahawk: I'd like to look for new `ir_profile` records + # but somehow looks very hard to find them + # since `Profiler` creates them in a separated db connection. + # All my attempts to mock the cr or de connection of `db_connect` + # failed poorly, so I prefer to not waste too much time aroudn this. + # I'm falling back to patching the controller method + # that should be called when profiling is enabled. + # The profiler init is tested in another method below. + # Not ideal, but better than no test :) + with patch.object(RunJobController, "_profiler_get") as mock_profiler_get: + self._run_controller(job) + mock_profiler_get.assert_not_called() + with patch.object(RunJobController, "_profiler_get") as mock_profiler_get: + self._run_controller(job, user=self.user1) + mock_profiler_get.assert_called_once() + with freeze_time(fields.Datetime.now() + timedelta(days=1)): + with patch.object(RunJobController, "_profiler_get") as mock_profiler_get: + self._run_controller(job) + mock_profiler_get.assert_not_called() + + def test_controller_profiler_get(self): + # as we mocked it before, make sure it works + controller = RunJobController() + job = self.env.user.with_delay().read(["id"]) + profiler = controller._profiler_get(self.env, job) + self.assertEqual( + profiler.description, f"queue.job {job.uuid} - {job.job_function_name}" + ) diff --git a/queue_job_profiler/views/queue_job_function_views.xml b/queue_job_profiler/views/queue_job_function_views.xml new file mode 100644 index 0000000000..57ecf10081 --- /dev/null +++ b/queue_job_profiler/views/queue_job_function_views.xml @@ -0,0 +1,25 @@ + + + + queue.job.function.form.profiler + queue.job.function + + + + + + + + + + + + diff --git a/queue_job_profiler/views/queue_job_views.xml b/queue_job_profiler/views/queue_job_views.xml new file mode 100644 index 0000000000..7e379a3795 --- /dev/null +++ b/queue_job_profiler/views/queue_job_views.xml @@ -0,0 +1,25 @@ + + + + queue.job.form.profiler + queue.job + + + + + + + + From 7818e8fb64234c21ec3f37cc297d6a25de30a7dc Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Thu, 12 Feb 2026 11:07:47 +0100 Subject: [PATCH 2/4] queue_job_profiler: make user note required --- queue_job_profiler/models/queue_job_function.py | 9 +++------ queue_job_profiler/tests/test_job_profiler.py | 3 +-- queue_job_profiler/views/queue_job_function_views.xml | 6 +----- 3 files changed, 5 insertions(+), 13 deletions(-) diff --git a/queue_job_profiler/models/queue_job_function.py b/queue_job_profiler/models/queue_job_function.py index 312f6a1ce2..6f1d20fcb7 100644 --- a/queue_job_profiler/models/queue_job_function.py +++ b/queue_job_profiler/models/queue_job_function.py @@ -26,20 +26,17 @@ def is_profiling_enabled(self): self.ensure_one() return ( self.profiling_enabled - and self.profiling_user_id == self.env.user and (self.profiling_until and self.profiling_until >= fields.Datetime.now()) + and (not self.profiling_user_id or self.profiling_user_id == self.env.user) ) @api.constrains("profiling_enabled") def _check_profiling_setup(self): for record in self: - if record.profiling_enabled and ( - not record.profiling_user_id or not record.profiling_until - ): + if record.profiling_enabled and not record.profiling_until: raise exceptions.ValidationError( self.env._( - "A profiling user and a profiling until date " - "must be set when profiling is enabled." + "A profiling until date must be set when profiling is enabled." ) ) diff --git a/queue_job_profiler/tests/test_job_profiler.py b/queue_job_profiler/tests/test_job_profiler.py index b820290dcb..1e4ce70be9 100644 --- a/queue_job_profiler/tests/test_job_profiler.py +++ b/queue_job_profiler/tests/test_job_profiler.py @@ -29,8 +29,7 @@ def setUpClass(cls): def test_func_constraint(self): with self.assertRaisesRegex( exceptions.ValidationError, - "A profiling user and a profiling until date " - "must be set when profiling is enabled.", + "A profiling until date must be set when profiling is enabled.", ): self.user_func1.profiling_enabled = True diff --git a/queue_job_profiler/views/queue_job_function_views.xml b/queue_job_profiler/views/queue_job_function_views.xml index 57ecf10081..c75bdc22eb 100644 --- a/queue_job_profiler/views/queue_job_function_views.xml +++ b/queue_job_profiler/views/queue_job_function_views.xml @@ -8,11 +8,7 @@ - + Date: Thu, 12 Feb 2026 15:35:59 +0100 Subject: [PATCH 3/4] queue_job_profiler: allow to select multiple users --- queue_job_profiler/README.rst | 11 +++++----- .../models/queue_job_function.py | 10 +++++---- queue_job_profiler/readme/DESCRIPTION.md | 3 ++- queue_job_profiler/readme/USAGE.md | 4 ++-- .../static/description/index.html | 11 +++++----- queue_job_profiler/tests/test_job_profiler.py | 22 ++++++++++++------- .../views/queue_job_function_views.xml | 5 ++++- 7 files changed, 40 insertions(+), 26 deletions(-) diff --git a/queue_job_profiler/README.rst b/queue_job_profiler/README.rst index 9765639adc..a2530f7ec9 100644 --- a/queue_job_profiler/README.rst +++ b/queue_job_profiler/README.rst @@ -32,9 +32,10 @@ This addon adds profiling controls to queue job functions and wraps queue job execution in an Odoo profiler session when enabled. When profiling is enabled for a job function and the executing user -matches the configured profiling user, the queue job runner records SQL -and async stack traces via ``odoo.tools.profiler.Profiler`` and saves -the results into ``ir_profile``. +matches one of the configured profiling users (or no users are set), the +queue job runner records SQL and async stack traces via +``odoo.tools.profiler.Profiler`` and saves the results into +``ir_profile``. **Table of contents** @@ -46,8 +47,8 @@ Usage 1. Open the Queue Job Functions menu. 2. Open the job function you want to profile. -3. In the Profiler group, enable Profiling and set the Profiling user - and Profiling until. +3. In the Profiler group, enable Profiling and set the Profiling users + (optional) and Profiling until. 4. Run the job with the selected user (the queue job runner usually executes as the superuser). 5. Inspect the generated entries in ``ir_profile`` to review the diff --git a/queue_job_profiler/models/queue_job_function.py b/queue_job_profiler/models/queue_job_function.py index 6f1d20fcb7..9118d05cd2 100644 --- a/queue_job_profiler/models/queue_job_function.py +++ b/queue_job_profiler/models/queue_job_function.py @@ -11,10 +11,10 @@ class QueueJobFunction(models.Model): string="Profiling enabled", help="Indicates whether profiling is enabled for this job function.", ) - profiling_user_id = fields.Many2one( + profiling_user_ids = fields.Many2many( "res.users", - string="Profiling user", - help="The user allowed to perform profiling for this job function.", + string="Profiling users", + help="The users allowed to perform profiling for this job function.", ) profiling_until = fields.Datetime( string="Profiling until", @@ -27,7 +27,9 @@ def is_profiling_enabled(self): return ( self.profiling_enabled and (self.profiling_until and self.profiling_until >= fields.Datetime.now()) - and (not self.profiling_user_id or self.profiling_user_id == self.env.user) + and ( + not self.profiling_user_ids or self.env.user in self.profiling_user_ids + ) ) @api.constrains("profiling_enabled") diff --git a/queue_job_profiler/readme/DESCRIPTION.md b/queue_job_profiler/readme/DESCRIPTION.md index 8e8814426c..06cb1376de 100644 --- a/queue_job_profiler/readme/DESCRIPTION.md +++ b/queue_job_profiler/readme/DESCRIPTION.md @@ -2,6 +2,7 @@ This addon adds profiling controls to queue job functions and wraps queue job execution in an Odoo profiler session when enabled. When profiling is enabled for a job function and the executing user -matches the configured profiling user, the queue job runner records +matches one of the configured profiling users (or no users are set), +the queue job runner records SQL and async stack traces via `odoo.tools.profiler.Profiler` and saves the results into `ir_profile`. \ No newline at end of file diff --git a/queue_job_profiler/readme/USAGE.md b/queue_job_profiler/readme/USAGE.md index 868db0ccbe..a58ee8eeff 100644 --- a/queue_job_profiler/readme/USAGE.md +++ b/queue_job_profiler/readme/USAGE.md @@ -1,7 +1,7 @@ 1. Open the Queue Job Functions menu. 2. Open the job function you want to profile. -3. In the Profiler group, enable Profiling and set the Profiling user - and Profiling until. +3. In the Profiler group, enable Profiling and set the Profiling users + (optional) and Profiling until. 4. Run the job with the selected user (the queue job runner usually executes as the superuser). 5. Inspect the generated entries in `ir_profile` to review the captured diff --git a/queue_job_profiler/static/description/index.html b/queue_job_profiler/static/description/index.html index 9ab72a5454..7a850cb7c3 100644 --- a/queue_job_profiler/static/description/index.html +++ b/queue_job_profiler/static/description/index.html @@ -373,9 +373,10 @@

Job Queue Profiler

This addon adds profiling controls to queue job functions and wraps queue job execution in an Odoo profiler session when enabled.

When profiling is enabled for a job function and the executing user -matches the configured profiling user, the queue job runner records SQL -and async stack traces via odoo.tools.profiler.Profiler and saves -the results into ir_profile.

+matches one of the configured profiling users (or no users are set), the +queue job runner records SQL and async stack traces via +odoo.tools.profiler.Profiler and saves the results into +ir_profile.

Table of contents

    @@ -394,8 +395,8 @@

    Usage

    1. Open the Queue Job Functions menu.
    2. Open the job function you want to profile.
    3. -
    4. In the Profiler group, enable Profiling and set the Profiling user -and Profiling until.
    5. +
    6. In the Profiler group, enable Profiling and set the Profiling users +(optional) and Profiling until.
    7. Run the job with the selected user (the queue job runner usually executes as the superuser).
    8. Inspect the generated entries in ir_profile to review the diff --git a/queue_job_profiler/tests/test_job_profiler.py b/queue_job_profiler/tests/test_job_profiler.py index 1e4ce70be9..908ff73c14 100644 --- a/queue_job_profiler/tests/test_job_profiler.py +++ b/queue_job_profiler/tests/test_job_profiler.py @@ -33,8 +33,8 @@ def test_func_constraint(self): ): self.user_func1.profiling_enabled = True - def _enable_profiling(self, job_function, user, delta=timedelta(hours=1)): - job_function.profiling_user_id = user + def _enable_profiling(self, job_function, users=None, delta=timedelta(hours=1)): + job_function.profiling_user_ids = users job_function.profiling_until = fields.Datetime.now() + delta job_function.profiling_enabled = True @@ -42,13 +42,19 @@ def test_func_profiling_enabled(self): # By default, profiling should be disabled for all users for user in (self.env.user, self.user1, self.user2): self.assertFalse(self.user_func1.with_user(user).is_profiling_enabled()) - # Enable it for user1 and check that it's only enabled for that user only - self._enable_profiling(self.user_func1, self.user1) + # Enable for all users (no profiling users set) + self._enable_profiling(self.user_func1) + for user in (self.env.user, self.user1, self.user2): + self.assertTrue(self.user_func1.with_user(user).is_profiling_enabled()) + # Enable it for user1 and user2 + self._enable_profiling(self.user_func1, users=self.user1 + self.user2) self.assertTrue(self.user_func1.with_user(self.user1).is_profiling_enabled()) - for user in (self.env.user, self.user2): - self.assertFalse(self.user_func1.with_user(user).is_profiling_enabled()) + self.assertTrue(self.user_func1.with_user(self.user2).is_profiling_enabled()) + self.assertFalse( + self.user_func1.with_user(self.env.user).is_profiling_enabled() + ) # Check for another job function and user - self._enable_profiling(self.user_func2, self.user2) + self._enable_profiling(self.user_func2, users=self.user2) self.assertTrue(self.user_func2.with_user(self.user2).is_profiling_enabled()) for user in (self.env.user, self.user1): self.assertFalse(self.user_func2.with_user(user).is_profiling_enabled()) @@ -102,7 +108,7 @@ def _run_controller(self, job, user=None): def test_controller(self): job = self.env.user.with_delay().read(["id"]) - self._enable_profiling(self.user_func1, self.user1) + self._enable_profiling(self.user_func1, [self.user1.id]) # TODO @simahawk: I'd like to look for new `ir_profile` records # but somehow looks very hard to find them # since `Profiler` creates them in a separated db connection. diff --git a/queue_job_profiler/views/queue_job_function_views.xml b/queue_job_profiler/views/queue_job_function_views.xml index c75bdc22eb..b351560088 100644 --- a/queue_job_profiler/views/queue_job_function_views.xml +++ b/queue_job_profiler/views/queue_job_function_views.xml @@ -8,7 +8,10 @@ - + Date: Thu, 12 Feb 2026 15:37:41 +0100 Subject: [PATCH 4/4] queue_job_profiler: use profile session name --- queue_job_profiler/controllers/main.py | 5 ++++- queue_job_profiler/tests/test_job_profiler.py | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/queue_job_profiler/controllers/main.py b/queue_job_profiler/controllers/main.py index 08c8097634..57d7576ccc 100644 --- a/queue_job_profiler/controllers/main.py +++ b/queue_job_profiler/controllers/main.py @@ -25,4 +25,7 @@ def _profiler_perform_job(self, env, job): def _profiler_get(self, env, job): func_id = job.job_config.job_function_id job_function = env["queue.job.function"].browse(func_id) - return Profiler(description=job_function._profile_make_name(job)) + return Profiler( + description=job_function._profile_make_name(job), + profile_session=f"{env.user.name} (uid={env.user.id})", + ) diff --git a/queue_job_profiler/tests/test_job_profiler.py b/queue_job_profiler/tests/test_job_profiler.py index 908ff73c14..6844ee1e2b 100644 --- a/queue_job_profiler/tests/test_job_profiler.py +++ b/queue_job_profiler/tests/test_job_profiler.py @@ -137,3 +137,6 @@ def test_controller_profiler_get(self): self.assertEqual( profiler.description, f"queue.job {job.uuid} - {job.job_function_name}" ) + self.assertEqual( + profiler.profile_session, f"{self.env.user.name} (uid={self.env.user.id})" + )