Files
Odoo-Modules/fusion_accounting_reports/tests/test_performance_benchmarks.py
2026-04-19 16:29:15 -04:00

156 lines
5.8 KiB
Python

"""Performance benchmarks with P95 targets, tagged 'benchmark'.
These tests are not part of the default test run; they execute when invoked
explicitly with --test-tags 'post_install,benchmark' (or just 'benchmark').
Targets (Phase 2 ship):
compute_pnl <2000ms p95
compute_balance_sheet <2000ms p95
compute_trial_balance <1000ms p95
compute_gl <3000ms p95
drill_down <500ms p95
controller.run <2500ms p95
Hard assertions are set to ~5x the target so a flaky CI run doesn't break the
build. The PERF lines printed to stdout are the source of truth for tracking.
"""
import json
import statistics
import time
from datetime import date
from odoo.tests.common import HttpCase, TransactionCase, tagged, new_test_user
from odoo.addons.fusion_accounting_reports.services.date_periods import Period
def _percentile(samples, p):
if not samples:
return 0
if len(samples) == 1:
return samples[0]
sorted_s = sorted(samples)
idx = int(len(sorted_s) * p / 100)
return sorted_s[min(idx, len(sorted_s) - 1)]
@tagged('post_install', '-at_install', 'benchmark')
class TestEngineBenchmarks(TransactionCase):
def setUp(self):
super().setUp()
self.period = Period(
date(2026, 1, 1), date(2026, 12, 31), 'Bench 2026',
)
self.engine = self.env['fusion.report.engine']
def test_compute_pnl_p95(self):
timings = []
for _ in range(5):
start = time.perf_counter()
self.engine.compute_pnl(self.period, company_id=self.env.company.id)
timings.append((time.perf_counter() - start) * 1000)
p95 = _percentile(timings, 95)
median = statistics.median(timings)
msg = f"compute_pnl: median={median:.0f}ms p95={p95:.0f}ms"
print(f"\n PERF: {msg} (target <2000ms)")
self.assertLess(p95, 10000, f"way over budget: {msg}")
def test_compute_balance_sheet_p95(self):
timings = []
for _ in range(5):
start = time.perf_counter()
self.engine.compute_balance_sheet(
date(2026, 12, 31), company_id=self.env.company.id,
)
timings.append((time.perf_counter() - start) * 1000)
p95 = _percentile(timings, 95)
median = statistics.median(timings)
msg = f"compute_balance_sheet: median={median:.0f}ms p95={p95:.0f}ms"
print(f"\n PERF: {msg} (target <2000ms)")
self.assertLess(p95, 10000, f"way over budget: {msg}")
def test_compute_trial_balance_p95(self):
timings = []
for _ in range(5):
start = time.perf_counter()
self.engine.compute_trial_balance(
self.period, company_id=self.env.company.id,
)
timings.append((time.perf_counter() - start) * 1000)
p95 = _percentile(timings, 95)
median = statistics.median(timings)
msg = f"compute_trial_balance: median={median:.0f}ms p95={p95:.0f}ms"
print(f"\n PERF: {msg} (target <1000ms)")
self.assertLess(p95, 5000, f"way over budget: {msg}")
def test_compute_gl_p95(self):
timings = []
for _ in range(3): # GL is heavier; fewer iterations
start = time.perf_counter()
self.engine.compute_gl(self.period, company_id=self.env.company.id)
timings.append((time.perf_counter() - start) * 1000)
median = statistics.median(timings)
p95 = _percentile(timings, 95)
msg = f"compute_gl: median={median:.0f}ms p95={p95:.0f}ms (3 runs)"
print(f"\n PERF: {msg} (target <3000ms)")
self.assertLess(median, 15000, f"way over budget: {msg}")
def test_drill_down_p95(self):
line = self.env['account.move.line'].search([
('parent_state', '=', 'posted'),
], limit=1)
if not line:
self.skipTest("No posted journal lines available")
timings = []
for _ in range(10):
start = time.perf_counter()
self.engine.drill_down(
account_id=line.account_id.id,
period=self.period,
company_id=line.company_id.id,
)
timings.append((time.perf_counter() - start) * 1000)
p95 = _percentile(timings, 95)
median = statistics.median(timings)
msg = f"drill_down: median={median:.0f}ms p95={p95:.0f}ms"
print(f"\n PERF: {msg} (target <500ms)")
self.assertLess(p95, 2500, f"way over budget: {msg}")
@tagged('post_install', '-at_install', 'benchmark')
class TestControllerBenchmarks(HttpCase):
def test_run_endpoint_p95(self):
new_test_user(
self.env,
login='perf_user',
groups='base.group_user,account.group_account_invoice',
)
self.authenticate('perf_user', 'perf_user')
timings = []
for _ in range(5):
start = time.perf_counter()
response = self.url_open(
'/fusion/reports/run',
data=json.dumps({
'jsonrpc': '2.0',
'method': 'call',
'id': 1,
'params': {
'report_type': 'pnl',
'date_from': '2026-01-01',
'date_to': '2026-12-31',
'company_id': self.env.company.id,
},
}),
headers={'Content-Type': 'application/json'},
)
timings.append((time.perf_counter() - start) * 1000)
self.assertEqual(response.status_code, 200)
p95 = _percentile(timings, 95)
median = statistics.median(timings)
msg = f"controller.run: median={median:.0f}ms p95={p95:.0f}ms"
print(f"\n PERF: {msg} (target <2500ms)")
self.assertLess(p95, 12500, f"way over budget: {msg}")