mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-10-30 19:45:44 +00:00
feat(benchmark): seed custom fields and time the list request with selection data
Also fixes the factory imports, which moved to paperless_testing.factories. Custom fields give the CustomFieldInstance counts real work to do, and the new documents_selection_data endpoint is the request the web UI makes on every list load. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a66803456e
commit
e9303f0c58
5 files changed
+66
-10
No files matched your search
@@ -34,8 +34,9 @@ manage.py benchmark list-scenarios
|
||||
creates two named users, `perf_target` (mixed owned/shared documents,
|
||||
realistic guardian permission grants) and `perf_admin` (superuser), plus a
|
||||
general user/group pool with realistic permission-row ratios.
|
||||
- **`run`** times the 3 built-in API endpoint benchmarks
|
||||
(`/api/documents/`, `/api/documents/?page_size=50`, `/api/tags/?page_size=100000`) for both
|
||||
- **`run`** times the 4 built-in API endpoint benchmarks
|
||||
(`/api/documents/`, `/api/documents/?page_size=50`, `/api/tags/?page_size=100000`,
|
||||
and the UI's list request with `include_selection_data=true`) for both
|
||||
`perf_target` and `perf_admin`, reporting min/median/max wall-clock and SQL
|
||||
query count. Requires `seed` to have already run — it reuses that data, it
|
||||
does not seed its own.
|
||||
|
||||
@@ -13,6 +13,8 @@ def _reset_table_names() -> list[str]:
|
||||
from guardian.models import UserObjectPermission
|
||||
|
||||
from documents.models import Correspondent
|
||||
from documents.models import CustomField
|
||||
from documents.models import CustomFieldInstance
|
||||
from documents.models import Document
|
||||
from documents.models import DocumentType
|
||||
from documents.models import StoragePath
|
||||
@@ -20,6 +22,8 @@ def _reset_table_names() -> list[str]:
|
||||
|
||||
return [
|
||||
Document.tags.through._meta.db_table,
|
||||
CustomFieldInstance._meta.db_table,
|
||||
CustomField._meta.db_table,
|
||||
Document._meta.db_table,
|
||||
Tag._meta.db_table,
|
||||
Correspondent._meta.db_table,
|
||||
|
||||
@@ -14,6 +14,13 @@ ENDPOINTS: tuple[tuple[str, str], ...] = (
|
||||
("documents_default", "/api/documents/"),
|
||||
("documents_page50", "/api/documents/?page_size=50"),
|
||||
("tags_all", "/api/tags/?page_size=100000"),
|
||||
(
|
||||
"documents_selection_data",
|
||||
(
|
||||
"/api/documents/?page=1&page_size=50&ordering=-created"
|
||||
"&truncate_content=true&include_selection_data=true"
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -135,12 +135,12 @@ class Command(BaseCommand):
|
||||
|
||||
self.stdout.write(f"# label={options['label']} repeat={options['repeat']}")
|
||||
self.stdout.write(
|
||||
f"{'user':7s} {'endpoint':20s} {'queries':>8s} "
|
||||
f"{'user':7s} {'endpoint':26s} {'queries':>8s} "
|
||||
f"{'min_ms':>9s} {'median_ms':>10s} {'max_ms':>9s}",
|
||||
)
|
||||
for r in results:
|
||||
self.stdout.write(
|
||||
f"{r.user_label:7s} {r.endpoint_name:20s} {r.query_count:8d} "
|
||||
f"{r.user_label:7s} {r.endpoint_name:26s} {r.query_count:8d} "
|
||||
f"{r.min_ms:9.1f} {r.median_ms:10.1f} {r.max_ms:9.1f}",
|
||||
)
|
||||
append_history(
|
||||
|
||||
@@ -43,6 +43,8 @@ SHARED_WITH_CHANGE_FRACTION = 0.5
|
||||
# realistic permission-join shape for arbitrary users, not only perf_target.
|
||||
USER_PERM_ROWS_PER_DOC = 1_414 / 12_000
|
||||
GROUP_PERM_ROWS_PER_DOC = 27_232 / 12_000
|
||||
# Share of documents that carry any custom field values.
|
||||
CUSTOM_FIELD_DOC_FRACTION = 0.4
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
@@ -55,6 +57,7 @@ class _TierCounts:
|
||||
other_users: int
|
||||
groups: int
|
||||
tags_per_doc: tuple[int, int]
|
||||
custom_fields: int = 0
|
||||
|
||||
|
||||
TIERS: dict[Tier, _TierCounts] = {
|
||||
@@ -67,6 +70,7 @@ TIERS: dict[Tier, _TierCounts] = {
|
||||
other_users=3,
|
||||
groups=2,
|
||||
tags_per_doc=(1, 3),
|
||||
custom_fields=3,
|
||||
),
|
||||
"medium": _TierCounts(
|
||||
documents=20_000,
|
||||
@@ -77,6 +81,7 @@ TIERS: dict[Tier, _TierCounts] = {
|
||||
other_users=10,
|
||||
groups=5,
|
||||
tags_per_doc=(2, 6),
|
||||
custom_fields=10,
|
||||
),
|
||||
"large": _TierCounts(
|
||||
documents=360_000,
|
||||
@@ -87,6 +92,7 @@ TIERS: dict[Tier, _TierCounts] = {
|
||||
other_users=25,
|
||||
groups=10,
|
||||
tags_per_doc=(3, 7),
|
||||
custom_fields=25,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -150,7 +156,7 @@ def _create_users_and_groups(
|
||||
) -> tuple[User, User, tuple[User, ...], tuple[Group, ...]]:
|
||||
from django.contrib.auth.models import Group
|
||||
|
||||
from documents.tests.factories import UserFactory
|
||||
from paperless_testing.factories import UserFactory
|
||||
|
||||
perf_target = UserFactory.create(username="perf_target")
|
||||
_grant_model_level_permissions(perf_target)
|
||||
@@ -171,10 +177,10 @@ def _create_lookup_tables(counts: _TierCounts):
|
||||
from documents.models import DocumentType
|
||||
from documents.models import StoragePath
|
||||
from documents.models import Tag
|
||||
from documents.tests.factories import CorrespondentFactory
|
||||
from documents.tests.factories import DocumentTypeFactory
|
||||
from documents.tests.factories import StoragePathFactory
|
||||
from documents.tests.factories import TagFactory
|
||||
from paperless_testing.factories import CorrespondentFactory
|
||||
from paperless_testing.factories import DocumentTypeFactory
|
||||
from paperless_testing.factories import StoragePathFactory
|
||||
from paperless_testing.factories import TagFactory
|
||||
|
||||
tags = tuple(Tag.objects.bulk_create(TagFactory.build_batch(counts.tags)))
|
||||
correspondents = tuple(
|
||||
@@ -230,8 +236,10 @@ def _seed_documents(
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from guardian.models import UserObjectPermission
|
||||
|
||||
from documents.models import CustomField
|
||||
from documents.models import CustomFieldInstance
|
||||
from documents.models import Document
|
||||
from documents.tests.factories import DocumentFactory
|
||||
from paperless_testing.factories import DocumentFactory
|
||||
|
||||
tag_ids = [t.pk for t in tags]
|
||||
correspondent_ids = [c.pk for c in correspondents]
|
||||
@@ -249,6 +257,21 @@ def _seed_documents(
|
||||
)
|
||||
through_model = Document.tags.through
|
||||
|
||||
# A subset of documents carries 1-3 custom field values, so the
|
||||
# CustomFieldInstance link table is populated for the counts that join it.
|
||||
custom_field_ids = [
|
||||
f.pk
|
||||
for f in CustomField.objects.bulk_create(
|
||||
[
|
||||
CustomField(
|
||||
name=f"bench field {i}",
|
||||
data_type=CustomField.FieldDataType.STRING,
|
||||
)
|
||||
for i in range(counts.custom_fields)
|
||||
],
|
||||
)
|
||||
]
|
||||
|
||||
document_count = 0
|
||||
owned_document_pks: list[int] = []
|
||||
remaining = counts.documents
|
||||
@@ -300,6 +323,27 @@ def _seed_documents(
|
||||
if through_rows:
|
||||
through_model.objects.bulk_create(through_rows, batch_size=CHUNK_SIZE)
|
||||
|
||||
if custom_field_ids:
|
||||
custom_field_rows = []
|
||||
for doc in created:
|
||||
if rng.random() >= CUSTOM_FIELD_DOC_FRACTION:
|
||||
continue
|
||||
for field_id in rng.sample(
|
||||
custom_field_ids,
|
||||
min(rng.randint(1, 3), len(custom_field_ids)),
|
||||
):
|
||||
custom_field_rows.append(
|
||||
CustomFieldInstance(
|
||||
document_id=doc.pk,
|
||||
field_id=field_id,
|
||||
value_text=f"value {doc.pk}",
|
||||
),
|
||||
)
|
||||
CustomFieldInstance.objects.bulk_create(
|
||||
custom_field_rows,
|
||||
batch_size=CHUNK_SIZE,
|
||||
)
|
||||
|
||||
perm_rows = []
|
||||
for doc, bucket in zip(created, owner_buckets, strict=True):
|
||||
if bucket == "unowned":
|
||||
|
||||
Reference in new issue
Block a user