Skip to content

Commit 1f80d1b

Browse files
committed
[bug-2022889] Add MWU result caching to performance api
1 parent 1a337d4 commit 1f80d1b

4 files changed

Lines changed: 466 additions & 1 deletion

File tree

tests/webapp/api/test_perfcompare_api.py

Lines changed: 343 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55
from django.urls import reverse
66

77
from treeherder.model.models import Job
8-
from treeherder.perf.models import PerformanceDatum, PerformanceDatumReplicate
8+
from treeherder.perf.models import (
9+
PerfCompareMwuCache,
10+
PerformanceDatum,
11+
PerformanceDatumReplicate,
12+
)
913
from treeherder.webapp.api import perfcompare_utils
1014

1115
pytestmark = pytest.mark.perf
@@ -1396,3 +1400,341 @@ def test_perfcompare_results_with_silverman_kde_enabled(
13961400

13971401
# Verify warnings is a list (may be empty or contain warnings)
13981402
assert isinstance(result_with_kde["silverman_warnings"], list)
1403+
1404+
1405+
def test_mwu_cache_hit_returns_cached_response(
1406+
client,
1407+
create_signature,
1408+
create_perf_datum,
1409+
test_perf_signature,
1410+
test_repository,
1411+
try_repository,
1412+
eleven_jobs_stored,
1413+
test_perfcomp_push,
1414+
test_perfcomp_push_2,
1415+
test_linux_platform,
1416+
test_option_collection,
1417+
):
1418+
# Given: base and new signatures with MWU-compatible perf data
1419+
perf_jobs = Job.objects.filter(pk__in=range(1, 11)).order_by("push__time").all()
1420+
1421+
test_perfcomp_push.time = FOUR_DAYS_AGO
1422+
test_perfcomp_push.repository = try_repository
1423+
test_perfcomp_push.save()
1424+
test_perfcomp_push_2.time = datetime.datetime.now()
1425+
test_perfcomp_push_2.save()
1426+
1427+
suite = "a11yr"
1428+
test = "dhtml.html"
1429+
extra_options = "e10s fission stylo webrender"
1430+
measurement_unit = "ms"
1431+
1432+
base_sig = create_signature(
1433+
signature_hash=(20 * "c1"),
1434+
extra_options=extra_options,
1435+
platform=test_linux_platform,
1436+
measurement_unit=measurement_unit,
1437+
suite=suite,
1438+
test=test,
1439+
test_perf_signature=test_perf_signature,
1440+
repository=try_repository,
1441+
application="firefox",
1442+
)
1443+
base_perf_data_values = [32.4, 33.1, 31.8]
1444+
1445+
for i, value in enumerate(base_perf_data_values):
1446+
job = perf_jobs[i]
1447+
job.push = test_perfcomp_push
1448+
job.save()
1449+
perf_datum = PerformanceDatum.objects.create(
1450+
value=value,
1451+
push_timestamp=job.push.time,
1452+
job=job,
1453+
push=job.push,
1454+
repository=try_repository,
1455+
signature=base_sig,
1456+
)
1457+
perf_datum.push.time = job.push.time
1458+
perf_datum.push.save()
1459+
1460+
new_sig = create_signature(
1461+
signature_hash=(20 * "c2"),
1462+
extra_options=extra_options,
1463+
platform=test_linux_platform,
1464+
measurement_unit=measurement_unit,
1465+
suite=suite,
1466+
test=test,
1467+
test_perf_signature=test_perf_signature,
1468+
repository=test_repository,
1469+
application="firefox",
1470+
)
1471+
new_perf_data_values = [40.2, 41.5, 39.8]
1472+
1473+
for i, value in enumerate(new_perf_data_values):
1474+
job = perf_jobs[i + 3]
1475+
job.push = test_perfcomp_push_2
1476+
job.save()
1477+
perf_datum = PerformanceDatum.objects.create(
1478+
value=value,
1479+
push_timestamp=job.push.time,
1480+
job=job,
1481+
push=job.push,
1482+
repository=test_repository,
1483+
signature=new_sig,
1484+
)
1485+
perf_datum.push.time = job.push.time
1486+
perf_datum.push.save()
1487+
1488+
assert PerfCompareMwuCache.objects.count() == 0
1489+
1490+
query_params = (
1491+
f"?base_repository={try_repository.name}&new_repository={test_repository.name}"
1492+
f"&new_revision={test_perfcomp_push_2.revision}"
1493+
f"&framework={test_perf_signature.framework_id}"
1494+
f"&interval=604800&no_subtests=true&test_version=mann-whitney-u"
1495+
)
1496+
1497+
# When: the same MWU comparison is requested twice
1498+
first_response = client.get(reverse("perfcompare-results") + query_params)
1499+
assert first_response.status_code == 200
1500+
1501+
second_response = client.get(reverse("perfcompare-results") + query_params)
1502+
assert second_response.status_code == 200
1503+
1504+
# Then: both responses are identical and a single cache row was created
1505+
assert first_response.json() == second_response.json()
1506+
assert PerfCompareMwuCache.objects.count() == 1
1507+
1508+
1509+
def test_mwu_cache_different_params_produce_different_cache_keys(
1510+
client,
1511+
create_signature,
1512+
create_perf_datum,
1513+
test_perf_signature,
1514+
test_repository,
1515+
try_repository,
1516+
eleven_jobs_stored,
1517+
test_perfcomp_push,
1518+
test_perfcomp_push_2,
1519+
test_linux_platform,
1520+
test_option_collection,
1521+
):
1522+
# Given: base and new signatures with enough data for MWU analysis
1523+
perf_jobs = Job.objects.filter(pk__in=range(1, 11)).order_by("push__time").all()
1524+
1525+
test_perfcomp_push.time = FOUR_DAYS_AGO
1526+
test_perfcomp_push.repository = try_repository
1527+
test_perfcomp_push.save()
1528+
test_perfcomp_push_2.time = datetime.datetime.now()
1529+
test_perfcomp_push_2.save()
1530+
1531+
suite = "a11yr"
1532+
test = "dhtml.html"
1533+
extra_options = "e10s fission stylo webrender"
1534+
measurement_unit = "ms"
1535+
1536+
base_sig = create_signature(
1537+
signature_hash=(20 * "d1"),
1538+
extra_options=extra_options,
1539+
platform=test_linux_platform,
1540+
measurement_unit=measurement_unit,
1541+
suite=suite,
1542+
test=test,
1543+
test_perf_signature=test_perf_signature,
1544+
repository=try_repository,
1545+
)
1546+
base_perf_data_values = [100.0, 105.0, 102.0]
1547+
1548+
for i, value in enumerate(base_perf_data_values):
1549+
job = perf_jobs[i]
1550+
job.push = test_perfcomp_push
1551+
job.save()
1552+
perf_datum = PerformanceDatum.objects.create(
1553+
value=value,
1554+
push_timestamp=job.push.time,
1555+
job=job,
1556+
push=job.push,
1557+
repository=try_repository,
1558+
signature=base_sig,
1559+
)
1560+
perf_datum.push.time = job.push.time
1561+
perf_datum.push.save()
1562+
1563+
new_sig = create_signature(
1564+
signature_hash=(20 * "d2"),
1565+
extra_options=extra_options,
1566+
platform=test_linux_platform,
1567+
measurement_unit=measurement_unit,
1568+
suite=suite,
1569+
test=test,
1570+
test_perf_signature=test_perf_signature,
1571+
repository=test_repository,
1572+
)
1573+
new_perf_data_values = [110.0, 115.0, 112.0]
1574+
1575+
for i, value in enumerate(new_perf_data_values):
1576+
job = perf_jobs[i + 3]
1577+
job.push = test_perfcomp_push_2
1578+
job.save()
1579+
perf_datum = PerformanceDatum.objects.create(
1580+
value=value,
1581+
push_timestamp=job.push.time,
1582+
job=job,
1583+
push=job.push,
1584+
repository=test_repository,
1585+
signature=new_sig,
1586+
)
1587+
perf_datum.push.time = job.push.time
1588+
perf_datum.push.save()
1589+
1590+
base_query = (
1591+
f"?base_repository={try_repository.name}&new_repository={test_repository.name}"
1592+
f"&new_revision={test_perfcomp_push_2.revision}"
1593+
f"&framework={test_perf_signature.framework_id}"
1594+
f"&interval=604800&no_subtests=true&test_version=mann-whitney-u"
1595+
)
1596+
1597+
# Given: no cache entries exist yet
1598+
assert PerfCompareMwuCache.objects.count() == 0
1599+
1600+
# When: two MWU requests are made with different enable_silverman_kde parameters
1601+
query_without_kde = base_query
1602+
first_response = client.get(reverse("perfcompare-results") + query_without_kde)
1603+
assert first_response.status_code == 200
1604+
1605+
query_with_kde = base_query + "&enable_silverman_kde=true&replicates=true"
1606+
second_response = client.get(reverse("perfcompare-results") + query_with_kde)
1607+
assert second_response.status_code == 200
1608+
1609+
# Then: two distinct cache entries exist (different hash keys)
1610+
assert PerfCompareMwuCache.objects.count() == 2
1611+
cache_keys = set(PerfCompareMwuCache.objects.values_list("hash_key", flat=True))
1612+
assert len(cache_keys) == 2
1613+
1614+
# Then: the responses differ in silverman_kde content
1615+
first_result = first_response.json()[0]
1616+
second_result = second_response.json()[0]
1617+
assert first_result["silverman_kde"] is None
1618+
assert second_result["silverman_kde"] is not None
1619+
1620+
1621+
def test_mwu_cache_recalculates_after_data_change(
1622+
client,
1623+
create_signature,
1624+
create_perf_datum,
1625+
test_perf_signature,
1626+
test_repository,
1627+
try_repository,
1628+
eleven_jobs_stored,
1629+
test_perfcomp_push,
1630+
test_perfcomp_push_2,
1631+
test_linux_platform,
1632+
test_option_collection,
1633+
):
1634+
# Given: base and new signatures with MWU-compatible perf data
1635+
perf_jobs = Job.objects.filter(pk__in=range(1, 11)).order_by("push__time").all()
1636+
1637+
test_perfcomp_push.time = FOUR_DAYS_AGO
1638+
test_perfcomp_push.repository = try_repository
1639+
test_perfcomp_push.save()
1640+
test_perfcomp_push_2.time = datetime.datetime.now()
1641+
test_perfcomp_push_2.save()
1642+
1643+
suite = "a11yr"
1644+
test = "dhtml.html"
1645+
extra_options = "e10s fission stylo webrender"
1646+
measurement_unit = "ms"
1647+
1648+
base_sig = create_signature(
1649+
signature_hash=(20 * "e1"),
1650+
extra_options=extra_options,
1651+
platform=test_linux_platform,
1652+
measurement_unit=measurement_unit,
1653+
suite=suite,
1654+
test=test,
1655+
test_perf_signature=test_perf_signature,
1656+
repository=try_repository,
1657+
application="firefox",
1658+
)
1659+
base_perf_data_values = [32.4, 33.1]
1660+
1661+
for i, value in enumerate(base_perf_data_values):
1662+
job = perf_jobs[i]
1663+
job.push = test_perfcomp_push
1664+
job.save()
1665+
perf_datum = PerformanceDatum.objects.create(
1666+
value=value,
1667+
push_timestamp=job.push.time,
1668+
job=job,
1669+
push=job.push,
1670+
repository=try_repository,
1671+
signature=base_sig,
1672+
)
1673+
perf_datum.push.time = job.push.time
1674+
perf_datum.push.save()
1675+
1676+
new_sig = create_signature(
1677+
signature_hash=(20 * "e2"),
1678+
extra_options=extra_options,
1679+
platform=test_linux_platform,
1680+
measurement_unit=measurement_unit,
1681+
suite=suite,
1682+
test=test,
1683+
test_perf_signature=test_perf_signature,
1684+
repository=test_repository,
1685+
application="firefox",
1686+
)
1687+
new_perf_data_values = [40.2, 41.5]
1688+
1689+
for i, value in enumerate(new_perf_data_values):
1690+
job = perf_jobs[i + 3]
1691+
job.push = test_perfcomp_push_2
1692+
job.save()
1693+
perf_datum = PerformanceDatum.objects.create(
1694+
value=value,
1695+
push_timestamp=job.push.time,
1696+
job=job,
1697+
push=job.push,
1698+
repository=test_repository,
1699+
signature=new_sig,
1700+
)
1701+
perf_datum.push.time = job.push.time
1702+
perf_datum.push.save()
1703+
1704+
query_params = (
1705+
f"?base_repository={try_repository.name}&new_repository={test_repository.name}"
1706+
f"&new_revision={test_perfcomp_push_2.revision}"
1707+
f"&framework={test_perf_signature.framework_id}"
1708+
f"&interval=604800&no_subtests=true&test_version=mann-whitney-u"
1709+
)
1710+
1711+
# When: first MWU query is made, caching the result
1712+
first_response = client.get(reverse("perfcompare-results") + query_params)
1713+
assert first_response.status_code == 200
1714+
assert PerfCompareMwuCache.objects.count() == 1
1715+
_first_cache_key = PerfCompareMwuCache.objects.first().hash_key
1716+
1717+
# Given: a new data point is added for the base signature (simulating a retrigger)
1718+
job = perf_jobs[2]
1719+
job.push = test_perfcomp_push
1720+
job.save()
1721+
perf_datum = PerformanceDatum.objects.create(
1722+
value=31.8,
1723+
push_timestamp=job.push.time,
1724+
job=job,
1725+
push=job.push,
1726+
repository=try_repository,
1727+
signature=base_sig,
1728+
)
1729+
perf_datum.push.time = job.push.time
1730+
perf_datum.push.save()
1731+
1732+
# When: the same MWU query is made again after data changed
1733+
second_response = client.get(reverse("perfcompare-results") + query_params)
1734+
assert second_response.status_code == 200
1735+
1736+
# Then: a second cache entry exists with a different hash key
1737+
# because the total data point count changed
1738+
assert PerfCompareMwuCache.objects.count() == 2
1739+
cache_keys = list(PerfCompareMwuCache.objects.values_list("hash_key", flat=True))
1740+
assert cache_keys[0] != cache_keys[1]
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Generated by Django 6.0.3 on 2026-08-03 20:23
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("perf", "0077_add_infra_status"),
10+
]
11+
12+
operations = [
13+
migrations.CreateModel(
14+
name="PerfCompareMwuCache",
15+
fields=[
16+
(
17+
"id",
18+
models.AutoField(
19+
auto_created=True,
20+
primary_key=True,
21+
serialize=False,
22+
verbose_name="ID",
23+
),
24+
),
25+
(
26+
"hash_key",
27+
models.CharField(db_index=True, max_length=64, unique=True),
28+
),
29+
("results", models.JSONField()),
30+
("created", models.DateTimeField(auto_now_add=True)),
31+
],
32+
options={
33+
"db_table": "perf_compare_mwu_cache",
34+
},
35+
),
36+
]

0 commit comments

Comments
 (0)