Skip to content

Commit 9eafdf8

Browse files
author
Hadrien Huvelle
committed
Init profiler
1 parent 7473ce1 commit 9eafdf8

File tree

15 files changed

+864
-0
lines changed

15 files changed

+864
-0
lines changed

profiler/README.rst

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
========
2+
Profiler
3+
========
4+
5+
.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
6+
!! This file is generated by oca-gen-addon-readme !!
7+
!! changes will be overwritten. !!
8+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
9+
10+
This module provides a decorator to profile Python functions using yappi
11+
and store the results in the database for later analysis.
12+
13+
Yappi (Yet Another Python Profiler) is a tracing profiler that is thread-aware
14+
and provides more accurate timing information than cProfile, especially for
15+
multi-threaded applications like Odoo.
16+
17+
**Table of contents**
18+
19+
.. contents::
20+
:local:
21+
22+
Usage
23+
=====
24+
25+
To profile a function, simply use the ``@profiled()`` decorator::
26+
27+
from odoo.addons.profiler.tools import profiled
28+
29+
class MyModel(models.Model):
30+
_name = "my.model"
31+
32+
@profiled()
33+
def my_method(self):
34+
# Your code here
35+
pass
36+
37+
You can customize the sample rate (percentage of calls to profile)::
38+
39+
@profiled(sample_rate=0.01) # Profile 1% of calls
40+
def another_method(self):
41+
# Your code here
42+
pass
43+
44+
The profiling results are stored in the database and can be viewed in:
45+
Settings → Technical → Profiler → Profiler Results
46+
47+
Each profiling result includes:
48+
49+
* Interactive Call Graph visualization showing top functions by cumulative time
50+
* Raw statistics with detailed timing information
51+
* Download button to export .pstats files for external analysis
52+
53+
You can download the .pstats file from any profiling result and use external tools
54+
like gprof2dot to generate detailed call graphs::
55+
56+
gprof2dot -f pstats my_function.pstats | dot -Tpng -o output.png
57+
58+
Or use snakeviz for interactive visualization::
59+
60+
snakeviz my_function.pstats
61+
62+
Bug Tracker
63+
===========
64+
65+
Bugs are tracked on `GitHub Issues <https://github.com/OCA/server-tools/issues>`_.
66+
In case of trouble, please check there if your issue has already been reported.
67+
68+
Credits
69+
=======
70+
71+
Authors
72+
~~~~~~~
73+
74+
* Camptocamp
75+
76+
Contributors
77+
~~~~~~~~~~~~
78+
79+
* Camptocamp
80+
81+
Maintainers
82+
~~~~~~~~~~~
83+
84+
This module is maintained by the OCA.
85+
86+
.. image:: https://odoo-community.org/logo.png
87+
:alt: Odoo Community Association
88+
:target: https://odoo-community.org
89+
90+
OCA, or the Odoo Community Association, is a nonprofit organization whose
91+
mission is to support the collaborative development of Odoo features and
92+
promote its widespread use.
93+
94+
This module is part of the `OCA/server-tools <https://github.com/OCA/server-tools/tree/18.0/profiler>`_ project on GitHub.
95+
96+
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

profiler/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Copyright 2026 Camptocamp SA
2+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
3+
4+
from . import models
5+
from . import tools

profiler/__manifest__.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Copyright 2026 Camptocamp SA
2+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
3+
4+
{
5+
"name": "Profiler",
6+
"version": "18.0.1.0.0",
7+
"category": "Tools",
8+
"summary": "yappi profiler decorator with database storage",
9+
"author": "Camptocamp, Odoo Community Association (OCA)",
10+
"license": "AGPL-3",
11+
"website": "https://github.com/OCA/server-tools",
12+
"depends": ["base", "edi_exchange_template_oca"],
13+
"external_dependencies": {
14+
"python": ["yappi", "cairosvg"],
15+
},
16+
"data": [
17+
"security/ir.model.access.csv",
18+
"views/profiler_result_views.xml",
19+
"views/profiler_report_views.xml",
20+
],
21+
"installable": True,
22+
}

profiler/models/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Copyright 2026 Camptocamp SA
2+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
3+
4+
from . import profiler_result
5+
from . import profiler_report
6+
from . import edi_exchange_template_output
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Copyright 2026 Camptocamp SA
2+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
3+
4+
from odoo import models
5+
6+
from ..tools import profiled
7+
8+
9+
class EDIExchangeOutputTemplate(models.Model):
10+
_inherit = "edi.exchange.template.output"
11+
12+
@profiled(sample_rate=1.0)
13+
def exchange_generate(self, exchange_record, **kw):
14+
"""Generate output for given record using related QWeb template."""
15+
return super().exchange_generate(exchange_record, **kw)

profiler/models/profiler_report.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Copyright 2026 Camptocamp SA
2+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
3+
4+
from odoo import api, fields, models
5+
6+
7+
class ProfilerReport(models.TransientModel):
8+
_name = "profiler.report"
9+
_description = "Profiler Statistics Report"
10+
11+
name = fields.Char(string="Function Name")
12+
p95 = fields.Float(string="P95 Duration (s)", digits=(12, 6))
13+
p99 = fields.Float(string="P99 Duration (s)", digits=(12, 6))
14+
count = fields.Integer(string="Call Count")
15+
days_back = fields.Integer(string="Number of days", default=1)
16+
17+
@api.model
18+
def generate_report(self, days_back=1):
19+
"""Generate the profiler statistics report."""
20+
self.search([]).unlink()
21+
22+
query = """
23+
SELECT name,
24+
percentile_disc(0.95) WITHIN GROUP (ORDER BY duration) AS p95,
25+
percentile_disc(0.99) WITHIN GROUP (ORDER BY duration) AS p99,
26+
count(*) AS count
27+
FROM profiler_result
28+
WHERE create_date > now() - interval '%s day'
29+
GROUP BY name
30+
ORDER BY p99 DESC
31+
LIMIT 20
32+
"""
33+
34+
self.env.cr.execute(query, (days_back,))
35+
results = self.env.cr.fetchall()
36+
37+
report_records = []
38+
for row in results:
39+
report_records.append(
40+
{
41+
"name": row[0],
42+
"p95": row[1],
43+
"p99": row[2],
44+
"count": row[3],
45+
"days_back": days_back,
46+
}
47+
)
48+
49+
if report_records:
50+
self.create(report_records)
51+
52+
return {
53+
"type": "ir.actions.act_window",
54+
"name": "Profiler Statistics Report",
55+
"res_model": "profiler.report",
56+
"view_mode": "list",
57+
"target": "current",
58+
"context": {"create": False, "edit": False, "delete": False},
59+
}
60+
61+
def action_refresh(self):
62+
"""Refresh the report with current data."""
63+
days_back = self[0].days_back if self else 1
64+
return self.generate_report(days_back=days_back)

0 commit comments

Comments
 (0)