test(fusion_accounting_reports): performance benchmarks with P95 targets

Made-with: Cursor
This commit is contained in:
gsinghpal
2026-04-19 16:29:15 -04:00
parent 3c7a1c8cea
commit 6a53da6002
3 changed files with 157 additions and 1 deletions

View File

@@ -1,6 +1,6 @@
{
'name': 'Fusion Accounting Reports',
'version': '19.0.1.0.36',
'version': '19.0.1.0.37',
'category': 'Accounting/Accounting',
'summary': 'AI-augmented financial reports (P&L, balance sheet, trial balance, GL).',
'description': """

View File

@@ -24,3 +24,4 @@ from . import test_period_picker
from . import test_migration_round_trip
from . import test_coexistence
from . import test_reports_tours
from . import test_performance_benchmarks

View File

@@ -0,0 +1,155 @@
"""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}")