django-nativemojo 0.1.15__py3-none-any.whl → 0.1.17__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/METADATA +3 -2
- django_nativemojo-0.1.17.dist-info/RECORD +302 -0
- mojo/__init__.py +1 -1
- mojo/apps/account/management/commands/serializer_admin.py +121 -1
- mojo/apps/account/migrations/0006_add_device_tracking_models.py +72 -0
- mojo/apps/account/migrations/0007_delete_userdevicelocation.py +16 -0
- mojo/apps/account/migrations/0008_userdevicelocation.py +33 -0
- mojo/apps/account/migrations/0009_geolocatedip_subnet.py +18 -0
- mojo/apps/account/migrations/0010_group_avatar.py +20 -0
- mojo/apps/account/migrations/0011_user_org_registereddevice_pushconfig_and_more.py +118 -0
- mojo/apps/account/migrations/0012_remove_pushconfig_apns_key_file_and_more.py +21 -0
- mojo/apps/account/migrations/0013_pushconfig_test_mode_alter_pushconfig_apns_enabled_and_more.py +28 -0
- mojo/apps/account/migrations/0014_notificationdelivery_data_payload_and_more.py +48 -0
- mojo/apps/account/models/__init__.py +2 -0
- mojo/apps/account/models/device.py +279 -0
- mojo/apps/account/models/group.py +294 -8
- mojo/apps/account/models/member.py +14 -1
- mojo/apps/account/models/push/__init__.py +4 -0
- mojo/apps/account/models/push/config.py +112 -0
- mojo/apps/account/models/push/delivery.py +93 -0
- mojo/apps/account/models/push/device.py +66 -0
- mojo/apps/account/models/push/template.py +99 -0
- mojo/apps/account/models/user.py +190 -17
- mojo/apps/account/rest/__init__.py +2 -0
- mojo/apps/account/rest/device.py +39 -0
- mojo/apps/account/rest/group.py +8 -0
- mojo/apps/account/rest/push.py +187 -0
- mojo/apps/account/rest/user.py +95 -5
- mojo/apps/account/services/__init__.py +1 -0
- mojo/apps/account/services/push.py +363 -0
- mojo/apps/aws/migrations/0001_initial.py +206 -0
- mojo/apps/aws/migrations/0002_emaildomain_can_recv_emaildomain_can_send_and_more.py +28 -0
- mojo/apps/aws/migrations/0003_mailbox_is_domain_default_mailbox_is_system_default_and_more.py +31 -0
- mojo/apps/aws/migrations/0004_s3bucket.py +39 -0
- mojo/apps/aws/migrations/0005_alter_emaildomain_region_delete_s3bucket.py +21 -0
- mojo/apps/aws/models/__init__.py +19 -0
- mojo/apps/aws/models/email_attachment.py +99 -0
- mojo/apps/aws/models/email_domain.py +218 -0
- mojo/apps/aws/models/email_template.py +132 -0
- mojo/apps/aws/models/incoming_email.py +197 -0
- mojo/apps/aws/models/mailbox.py +288 -0
- mojo/apps/aws/models/sent_message.py +175 -0
- mojo/apps/aws/rest/__init__.py +6 -0
- mojo/apps/aws/rest/email.py +33 -0
- mojo/apps/aws/rest/email_ops.py +183 -0
- mojo/apps/aws/rest/messages.py +32 -0
- mojo/apps/aws/rest/send.py +101 -0
- mojo/apps/aws/rest/sns.py +403 -0
- mojo/apps/aws/rest/templates.py +19 -0
- mojo/apps/aws/services/__init__.py +32 -0
- mojo/apps/aws/services/email.py +390 -0
- mojo/apps/aws/services/email_ops.py +548 -0
- mojo/apps/docit/__init__.py +6 -0
- mojo/apps/docit/markdown_plugins/syntax_highlight.py +25 -0
- mojo/apps/docit/markdown_plugins/toc.py +12 -0
- mojo/apps/docit/migrations/0001_initial.py +113 -0
- mojo/apps/docit/migrations/0002_alter_book_modified_by_alter_page_modified_by.py +26 -0
- mojo/apps/docit/migrations/0003_alter_book_group.py +20 -0
- mojo/apps/docit/models/__init__.py +17 -0
- mojo/apps/docit/models/asset.py +231 -0
- mojo/apps/docit/models/book.py +227 -0
- mojo/apps/docit/models/page.py +319 -0
- mojo/apps/docit/models/page_revision.py +203 -0
- mojo/apps/docit/rest/__init__.py +10 -0
- mojo/apps/docit/rest/asset.py +17 -0
- mojo/apps/docit/rest/book.py +22 -0
- mojo/apps/docit/rest/page.py +22 -0
- mojo/apps/docit/rest/page_revision.py +17 -0
- mojo/apps/docit/services/__init__.py +11 -0
- mojo/apps/docit/services/docit.py +315 -0
- mojo/apps/docit/services/markdown.py +44 -0
- mojo/apps/fileman/backends/s3.py +209 -0
- mojo/apps/fileman/models/file.py +45 -9
- mojo/apps/fileman/models/manager.py +269 -3
- mojo/apps/incident/migrations/0007_event_uid.py +18 -0
- mojo/apps/incident/migrations/0008_ticket_ticketnote.py +55 -0
- mojo/apps/incident/migrations/0009_incident_status.py +18 -0
- mojo/apps/incident/migrations/0010_event_country_code.py +18 -0
- mojo/apps/incident/migrations/0011_incident_country_code.py +18 -0
- mojo/apps/incident/migrations/0012_alter_incident_status.py +18 -0
- mojo/apps/incident/models/__init__.py +1 -0
- mojo/apps/incident/models/event.py +35 -0
- mojo/apps/incident/models/incident.py +2 -0
- mojo/apps/incident/models/ticket.py +62 -0
- mojo/apps/incident/reporter.py +21 -3
- mojo/apps/incident/rest/__init__.py +1 -0
- mojo/apps/incident/rest/ticket.py +43 -0
- mojo/apps/jobs/__init__.py +489 -0
- mojo/apps/jobs/adapters.py +24 -0
- mojo/apps/jobs/cli.py +616 -0
- mojo/apps/jobs/daemon.py +370 -0
- mojo/apps/jobs/examples/sample_jobs.py +376 -0
- mojo/apps/jobs/examples/webhook_examples.py +203 -0
- mojo/apps/jobs/handlers/__init__.py +5 -0
- mojo/apps/jobs/handlers/webhook.py +317 -0
- mojo/apps/jobs/job_engine.py +734 -0
- mojo/apps/jobs/keys.py +203 -0
- mojo/apps/jobs/local_queue.py +363 -0
- mojo/apps/jobs/management/__init__.py +3 -0
- mojo/apps/jobs/management/commands/__init__.py +3 -0
- mojo/apps/jobs/manager.py +1327 -0
- mojo/apps/jobs/migrations/0001_initial.py +97 -0
- mojo/apps/jobs/migrations/0002_alter_job_max_retries_joblog.py +39 -0
- mojo/apps/jobs/models/__init__.py +6 -0
- mojo/apps/jobs/models/job.py +441 -0
- mojo/apps/jobs/rest/__init__.py +2 -0
- mojo/apps/jobs/rest/control.py +466 -0
- mojo/apps/jobs/rest/jobs.py +421 -0
- mojo/apps/jobs/scheduler.py +571 -0
- mojo/apps/jobs/services/__init__.py +6 -0
- mojo/apps/jobs/services/job_actions.py +465 -0
- mojo/apps/jobs/settings.py +209 -0
- mojo/apps/logit/models/log.py +3 -0
- mojo/apps/metrics/__init__.py +8 -1
- mojo/apps/metrics/redis_metrics.py +198 -0
- mojo/apps/metrics/rest/__init__.py +3 -0
- mojo/apps/metrics/rest/categories.py +266 -0
- mojo/apps/metrics/rest/helpers.py +48 -0
- mojo/apps/metrics/rest/permissions.py +99 -0
- mojo/apps/metrics/rest/values.py +277 -0
- mojo/apps/metrics/utils.py +17 -0
- mojo/decorators/http.py +40 -1
- mojo/helpers/aws/__init__.py +11 -7
- mojo/helpers/aws/inbound_email.py +309 -0
- mojo/helpers/aws/kms.py +413 -0
- mojo/helpers/aws/ses_domain.py +959 -0
- mojo/helpers/crypto/__init__.py +1 -1
- mojo/helpers/crypto/utils.py +15 -0
- mojo/helpers/location/__init__.py +2 -0
- mojo/helpers/location/countries.py +262 -0
- mojo/helpers/location/geolocation.py +196 -0
- mojo/helpers/logit.py +37 -0
- mojo/helpers/redis/__init__.py +2 -0
- mojo/helpers/redis/adapter.py +606 -0
- mojo/helpers/redis/client.py +48 -0
- mojo/helpers/redis/pool.py +225 -0
- mojo/helpers/request.py +8 -0
- mojo/helpers/response.py +8 -0
- mojo/middleware/auth.py +1 -1
- mojo/middleware/cors.py +40 -0
- mojo/middleware/logging.py +131 -12
- mojo/middleware/mojo.py +5 -0
- mojo/models/rest.py +271 -57
- mojo/models/secrets.py +86 -0
- mojo/serializers/__init__.py +16 -10
- mojo/serializers/core/__init__.py +90 -0
- mojo/serializers/core/cache/__init__.py +121 -0
- mojo/serializers/core/cache/backends.py +518 -0
- mojo/serializers/core/cache/base.py +102 -0
- mojo/serializers/core/cache/disabled.py +181 -0
- mojo/serializers/core/cache/memory.py +287 -0
- mojo/serializers/core/cache/redis.py +533 -0
- mojo/serializers/core/cache/utils.py +454 -0
- mojo/serializers/{manager.py → core/manager.py} +53 -4
- mojo/serializers/core/serializer.py +475 -0
- mojo/serializers/{advanced/formats → formats}/csv.py +116 -139
- mojo/serializers/suggested_improvements.md +388 -0
- testit/client.py +1 -1
- testit/helpers.py +14 -0
- testit/runner.py +23 -6
- django_nativemojo-0.1.15.dist-info/RECORD +0 -234
- mojo/apps/notify/README.md +0 -91
- mojo/apps/notify/README_NOTIFICATIONS.md +0 -566
- mojo/apps/notify/admin.py +0 -52
- mojo/apps/notify/handlers/example_handlers.py +0 -516
- mojo/apps/notify/handlers/ses/__init__.py +0 -25
- mojo/apps/notify/handlers/ses/complaint.py +0 -25
- mojo/apps/notify/handlers/ses/message.py +0 -86
- mojo/apps/notify/management/commands/__init__.py +0 -1
- mojo/apps/notify/management/commands/process_notifications.py +0 -370
- mojo/apps/notify/mod +0 -0
- mojo/apps/notify/models/__init__.py +0 -12
- mojo/apps/notify/models/account.py +0 -128
- mojo/apps/notify/models/attachment.py +0 -24
- mojo/apps/notify/models/bounce.py +0 -68
- mojo/apps/notify/models/complaint.py +0 -40
- mojo/apps/notify/models/inbox.py +0 -113
- mojo/apps/notify/models/inbox_message.py +0 -173
- mojo/apps/notify/models/outbox.py +0 -129
- mojo/apps/notify/models/outbox_message.py +0 -288
- mojo/apps/notify/models/template.py +0 -30
- mojo/apps/notify/providers/aws.py +0 -73
- mojo/apps/notify/rest/ses.py +0 -0
- mojo/apps/notify/utils/__init__.py +0 -2
- mojo/apps/notify/utils/notifications.py +0 -404
- mojo/apps/notify/utils/parsing.py +0 -202
- mojo/apps/notify/utils/render.py +0 -144
- mojo/apps/tasks/README.md +0 -118
- mojo/apps/tasks/__init__.py +0 -44
- mojo/apps/tasks/manager.py +0 -644
- mojo/apps/tasks/rest/__init__.py +0 -2
- mojo/apps/tasks/rest/hooks.py +0 -0
- mojo/apps/tasks/rest/tasks.py +0 -76
- mojo/apps/tasks/runner.py +0 -439
- mojo/apps/tasks/task.py +0 -99
- mojo/apps/tasks/tq_handlers.py +0 -132
- mojo/helpers/crypto/__pycache__/hash.cpython-310.pyc +0 -0
- mojo/helpers/crypto/__pycache__/sign.cpython-310.pyc +0 -0
- mojo/helpers/crypto/__pycache__/utils.cpython-310.pyc +0 -0
- mojo/helpers/redis.py +0 -10
- mojo/models/meta.py +0 -262
- mojo/serializers/advanced/README.md +0 -363
- mojo/serializers/advanced/__init__.py +0 -247
- mojo/serializers/advanced/formats/__init__.py +0 -28
- mojo/serializers/advanced/formats/excel.py +0 -516
- mojo/serializers/advanced/formats/json.py +0 -239
- mojo/serializers/advanced/formats/response.py +0 -485
- mojo/serializers/advanced/serializer.py +0 -568
- mojo/serializers/optimized.py +0 -618
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/LICENSE +0 -0
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/NOTICE +0 -0
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/WHEEL +0 -0
- /mojo/apps/{notify → aws/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/handlers → docit/markdown_plugins}/__init__.py +0 -0
- /mojo/apps/{notify/management → docit/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/providers → jobs/examples}/__init__.py +0 -0
- /mojo/apps/{notify/rest → jobs/migrations}/__init__.py +0 -0
- /mojo/{serializers → rest}/openapi.py +0 -0
- /mojo/serializers/{settings_example.py → examples/settings.py} +0 -0
- /mojo/{apps/notify/handlers/ses/bounce.py → serializers/formats/__init__.py} +0 -0
- /mojo/serializers/{advanced/formats → formats}/localizers.py +0 -0
@@ -0,0 +1,388 @@
|
|
1
|
+
# Django-MOJO Serializer: Suggested Performance Improvements
|
2
|
+
|
3
|
+
This document outlines a concrete plan to further optimize serialization and database fetching for the Django-MOJO serializers, with an emphasis on the `OptimizedGraphSerializer`. The goal is to reduce CPU overhead, minimize ORM round-trips, and leverage database features where possible, all while maintaining full backward compatibility.
|
4
|
+
|
5
|
+
-------------------------------------------------------------------------------
|
6
|
+
|
7
|
+
## Goals
|
8
|
+
|
9
|
+
- Reduce per-object overhead during serialization
|
10
|
+
- Eliminate repeated graph resolution and _meta lookups in hot paths
|
11
|
+
- Optimize QuerySet fetching patterns (select_related, prefetch_related, only)
|
12
|
+
- Introduce a `values()`-based fast path for "simple graphs" that need only model fields
|
13
|
+
- Preserve functional correctness, optional nested graphs, and extras
|
14
|
+
- Keep improved behaviors behind feature flags where needed
|
15
|
+
|
16
|
+
-------------------------------------------------------------------------------
|
17
|
+
|
18
|
+
## Summary of Observed Bottlenecks
|
19
|
+
|
20
|
+
- Repeated graph config parsing per object
|
21
|
+
- Frequent `_meta.get_field` calls inside tight loops
|
22
|
+
- Query optimizations applied ad hoc (per call) without caching the plan
|
23
|
+
- Attribute access and Python-side transformations dominating CPU time
|
24
|
+
- Lost opportunities to use `.values()` fast path when no nested graphs/extras are needed
|
25
|
+
- Repeated serialization of the same related objects across a single request (now improved with request cache)
|
26
|
+
|
27
|
+
-------------------------------------------------------------------------------
|
28
|
+
|
29
|
+
## Proposed Improvements
|
30
|
+
|
31
|
+
### 1) Graph Compilation with LRU Cache
|
32
|
+
|
33
|
+
Compile a model’s graph once per `(model_class, graph_name)` and cache the result. The compilation should produce:
|
34
|
+
|
35
|
+
- Resolved fields list (fallback to all model fields if not specified)
|
36
|
+
- Extras normalized to a list of (method_name, alias) tuples
|
37
|
+
- Related graphs map
|
38
|
+
- Pre-resolved FK/OneToOne field names set (for fast PK extraction)
|
39
|
+
- Prebuilt attribute getters for nested dot paths in extras (via `operator.attrgetter`)
|
40
|
+
- A reusable “query plan” (see #2)
|
41
|
+
|
42
|
+
Benefits:
|
43
|
+
- Eliminates repeated RestMeta/graph lookups
|
44
|
+
- Eliminates per-field _meta calls in hot loops
|
45
|
+
- Provides a single source of truth for fetching and serialization
|
46
|
+
|
47
|
+
Implementation notes:
|
48
|
+
- Use `functools.lru_cache(maxsize=512)` for `compile_graph(model_class, graph_name)`
|
49
|
+
- Normalize cases where graph exists but has no `fields` (serialize all model fields)
|
50
|
+
|
51
|
+
Configuration:
|
52
|
+
- MOJO_OPTIMIZED_COMPILE_CACHE_MAXSIZE (default: 512)
|
53
|
+
|
54
|
+
|
55
|
+
### 2) Query Plan Caching and Application
|
56
|
+
|
57
|
+
Attach a query plan to the compiled graph that includes:
|
58
|
+
- `select_related` fields (FK and O2O discovered in fields and related graphs)
|
59
|
+
- `prefetch_related` fields (M2M and reverse relations)
|
60
|
+
- `only()` field list for the main model (minimize columns fetched)
|
61
|
+
- Optional `Prefetch()` for related sets with `.only()` tuning
|
62
|
+
|
63
|
+
Apply the query plan once on the collection queryset before evaluation.
|
64
|
+
|
65
|
+
Benefits:
|
66
|
+
- Minimizes N+1 queries
|
67
|
+
- Reduces column payload
|
68
|
+
- Centralizes fetching strategy for consistent performance
|
69
|
+
|
70
|
+
Configuration:
|
71
|
+
- MOJO_OPTIMIZED_APPLY_QUERY_PLAN = True
|
72
|
+
- MOJO_OPTIMIZED_ONLY_FIELDS = True
|
73
|
+
|
74
|
+
|
75
|
+
### 3) `.values()` Fast Path for Simple Graphs
|
76
|
+
|
77
|
+
When a graph has:
|
78
|
+
- No extras
|
79
|
+
- No related graphs
|
80
|
+
- Only plain model fields (no callables)
|
81
|
+
|
82
|
+
Then use `QuerySet.values(*fields)` and return rows directly, with minimal post-processing (datetime/decimal handling as needed).
|
83
|
+
|
84
|
+
Benefits:
|
85
|
+
- Skips model instance creation and attribute access
|
86
|
+
- Significantly faster for large lists
|
87
|
+
|
88
|
+
Notes:
|
89
|
+
- If you later add extras or nested graphs, the fast path must be bypassed automatically
|
90
|
+
|
91
|
+
Configuration:
|
92
|
+
- MOJO_OPTIMIZED_VALUES_FASTPATH = True
|
93
|
+
|
94
|
+
|
95
|
+
### 4) Share a Single Request Cache Across Nested Serialization
|
96
|
+
|
97
|
+
Ensure nested serializers share the same request-scoped cache dict, so repeated related objects are serialized once per request.
|
98
|
+
|
99
|
+
Benefits:
|
100
|
+
- Prevents re-serializing the same related object multiple times within a request
|
101
|
+
- Big wins for graphs with repeated authors/users/tags/etc.
|
102
|
+
|
103
|
+
|
104
|
+
### 5) Hot Loop Optimizations
|
105
|
+
|
106
|
+
- Inline FK check via the compiled plan’s `fk_fields` set
|
107
|
+
- Bind local variables outside loops (e.g., `svf = self._serialize_value_fast`, `data = {}; fields = plan['fields']`) to reduce attribute lookups
|
108
|
+
- Avoid `_meta.get_field` in loops (rely on the compiled plan)
|
109
|
+
- Return early when values are None/unset as appropriate
|
110
|
+
|
111
|
+
Benefits:
|
112
|
+
- Small per-iteration savings amplify at 1k+ objects
|
113
|
+
|
114
|
+
|
115
|
+
### 6) Extras Handling and DB Annotations
|
116
|
+
|
117
|
+
For simple extras (counts, derived numbers) that can be expressed via annotations:
|
118
|
+
- Prefer `.annotate()` and include them in `values()` (for the fast path) or in `.only()` plan for instance path
|
119
|
+
- For method-based extras, keep current behavior but consider caching per object in the request cache if deterministic
|
120
|
+
|
121
|
+
Examples:
|
122
|
+
- `post_count` => `.annotate(post_count=Count('posts'))`
|
123
|
+
- `full_name` for `User` => annotate `Concat(F('first_name'), Value(' '), F('last_name'))` when appropriate
|
124
|
+
|
125
|
+
Configuration:
|
126
|
+
- MOJO_OPTIMIZED_ALLOW_ANNOTATIONS = True
|
127
|
+
|
128
|
+
|
129
|
+
### 7) Iterator vs List Strategy
|
130
|
+
|
131
|
+
- Default: use `list(qs)` which is fastest in typical scenarios when memory allows
|
132
|
+
- For very large datasets, provide an option to stream via `iterator(chunk_size=...)`
|
133
|
+
- When using `.values()` fast path, the memory footprint is smaller; streaming remains available
|
134
|
+
|
135
|
+
Configuration:
|
136
|
+
- MOJO_OPTIMIZED_LIST_EVALUATION = True
|
137
|
+
- MOJO_OPTIMIZED_ITERATOR_CHUNK_SIZE = 2000
|
138
|
+
|
139
|
+
|
140
|
+
### 8) JSON Serialization Path
|
141
|
+
|
142
|
+
- Keep `ujson` (orjson if available in future) for hot paths
|
143
|
+
- Avoid pretty-printing in normal responses
|
144
|
+
- `ensure_ascii=False`
|
145
|
+
- Provide a per-response toggle for tooling/debugging
|
146
|
+
|
147
|
+
Configuration:
|
148
|
+
- MOJO_JSON_USE_UJSON = True
|
149
|
+
- MOJO_JSON_PRETTY_DEBUG = False
|
150
|
+
|
151
|
+
|
152
|
+
### 9) Logging and Metrics
|
153
|
+
|
154
|
+
- Keep logging at WARNING or above in hot paths
|
155
|
+
- Add lightweight counters (per process) for:
|
156
|
+
- How many serializations used values fast path
|
157
|
+
- Cache hit/miss for request-scoped cache (approximate)
|
158
|
+
- Expose a `get_performance_info()` snapshot on serializer instances (already present; keep it cheap)
|
159
|
+
|
160
|
+
Configuration:
|
161
|
+
- MOJO_OPTIMIZED_COLLECT_METRICS = True
|
162
|
+
|
163
|
+
|
164
|
+
### 10) Feature Flags and Backwards Compatibility
|
165
|
+
|
166
|
+
- All new behavior guarded by settings flags (enabled by default where safe)
|
167
|
+
- Maintain previous public API
|
168
|
+
- Fallback paths for missing `RestMeta` or graphs:
|
169
|
+
- If graph not found => fallback to `default`
|
170
|
+
- If no RestMeta/default => serialize all fields (current behavior, keep)
|
171
|
+
|
172
|
+
-------------------------------------------------------------------------------
|
173
|
+
|
174
|
+
## Design Sketches (Pseudo-code)
|
175
|
+
|
176
|
+
Graph compilation:
|
177
|
+
|
178
|
+
@lru_cache(maxsize=512)
|
179
|
+
def compile_graph(model_class, graph_name):
|
180
|
+
graphs = getattr(getattr(model_class, 'RestMeta', None), 'GRAPHS', {}) or {}
|
181
|
+
gc = graphs.get(graph_name) or graphs.get('default') or {}
|
182
|
+
fields = gc.get('fields') or [f.name for f in model_class._meta.fields]
|
183
|
+
extras = normalize_extras(gc.get('extra', [])) # list of (method, alias)
|
184
|
+
related_graphs = gc.get('graphs', {})
|
185
|
+
fk_fields = detect_fk_fields(model_class, fields)
|
186
|
+
extra_getters = build_attrgetters_for_dotted_paths(extras)
|
187
|
+
plan = build_query_plan(model_class, fields, related_graphs)
|
188
|
+
return {
|
189
|
+
'fields': fields,
|
190
|
+
'extras': extras,
|
191
|
+
'related_graphs': related_graphs,
|
192
|
+
'fk_fields': fk_fields,
|
193
|
+
'extra_getters': extra_getters,
|
194
|
+
'plan': plan,
|
195
|
+
}
|
196
|
+
|
197
|
+
Values fast path:
|
198
|
+
|
199
|
+
def can_use_values_fastpath(compiled):
|
200
|
+
return not compiled['extras'] and not compiled['related_graphs']
|
201
|
+
|
202
|
+
def serialize_queryset_fast_values(qs, compiled):
|
203
|
+
qs = apply_only(qs, compiled['plan'])
|
204
|
+
rows = list(qs.values(*compiled['fields']))
|
205
|
+
return postprocess_rows(rows) # datetime/decimal fixups
|
206
|
+
|
207
|
+
Applying query plan:
|
208
|
+
|
209
|
+
def apply_query_plan(qs, compiled):
|
210
|
+
plan = compiled['plan']
|
211
|
+
if plan['select_related']: qs = qs.select_related(*plan['select_related'])
|
212
|
+
if plan['prefetch_related']: qs = qs.prefetch_related(*plan['prefetch_related'])
|
213
|
+
if plan['only_fields']: qs = qs.only(*plan['only_fields'])
|
214
|
+
return qs
|
215
|
+
|
216
|
+
Instance hot loop:
|
217
|
+
|
218
|
+
def serialize_instance_with_compiled(obj, compiled, request_cache):
|
219
|
+
data = {}
|
220
|
+
svf = self._serialize_value_fast
|
221
|
+
for fname in compiled['fields']:
|
222
|
+
val = getattr(obj, fname, None)
|
223
|
+
if fname in compiled['fk_fields'] and hasattr(val, 'pk'):
|
224
|
+
data[fname] = val.pk
|
225
|
+
else:
|
226
|
+
if callable(val):
|
227
|
+
try: val = val()
|
228
|
+
except: val = None
|
229
|
+
data[fname] = svf(val)
|
230
|
+
for (method_name, alias) in compiled['extras']:
|
231
|
+
val = resolve_extra_value(obj, method_name, compiled['extra_getters'])
|
232
|
+
data[alias] = svf(val)
|
233
|
+
for related_name, sub_graph in compiled['related_graphs'].items():
|
234
|
+
data[related_name] = serialize_related(obj, related_name, sub_graph, request_cache)
|
235
|
+
return data
|
236
|
+
|
237
|
+
-------------------------------------------------------------------------------
|
238
|
+
|
239
|
+
## Should We Patch the Existing Serializer or Add a New One?
|
240
|
+
|
241
|
+
Recommendation: Patch the existing `OptimizedGraphSerializer` incrementally behind feature flags.
|
242
|
+
|
243
|
+
Pros:
|
244
|
+
- Avoids registry complexity and code duplication
|
245
|
+
- Keeps a single optimized path maintained and tested
|
246
|
+
- We can toggle new behaviors in production via settings
|
247
|
+
- Existing API and behavior preserved (with improved performance)
|
248
|
+
|
249
|
+
When to consider a new serializer:
|
250
|
+
- If you want an experimental “v2” without any risk to current behavior
|
251
|
+
- If dramatic API changes (not planned here) are desired
|
252
|
+
|
253
|
+
Compromise approach:
|
254
|
+
- Keep changes in `OptimizedGraphSerializer`
|
255
|
+
- Add a “strict fast mode” flag (e.g., `simple_mode=True`) for safe fallbacks during rollout
|
256
|
+
- If needed, register an alias “optimized_v2” pointing to the same class but with different default flags, selectable via settings/manager
|
257
|
+
|
258
|
+
-------------------------------------------------------------------------------
|
259
|
+
|
260
|
+
## Rollout Strategy
|
261
|
+
|
262
|
+
1) Phase 1 (compile + plan):
|
263
|
+
- Introduce compile_graph() with LRU cache
|
264
|
+
- Attach and apply query plan (select_related/prefetch/only)
|
265
|
+
- Keep behavior identical (no values fast path yet)
|
266
|
+
- Flag: MOJO_OPTIMIZED_APPLY_QUERY_PLAN = True (default True)
|
267
|
+
|
268
|
+
2) Phase 2 (values fast path):
|
269
|
+
- Implement can_use_values_fastpath() and serialize_queryset_fast_values()
|
270
|
+
- Enable for limited models/graphs (opt-in list)
|
271
|
+
- Flag: MOJO_OPTIMIZED_VALUES_FASTPATH = True (default True), with allowlist MOJO_VALUES_FASTPATH_ALLOW = []
|
272
|
+
|
273
|
+
3) Phase 3 (extras annotations):
|
274
|
+
- Add optional annotation hooks
|
275
|
+
- Maintain correctness: fallback to Python extras if annotation not configured
|
276
|
+
- Flag: MOJO_OPTIMIZED_ALLOW_ANNOTATIONS = False (default off)
|
277
|
+
|
278
|
+
4) Phase 4 (observability):
|
279
|
+
- Add counters for fast path usage and request cache stats
|
280
|
+
- Light HTTP headers or log lines during debug windows
|
281
|
+
|
282
|
+
-------------------------------------------------------------------------------
|
283
|
+
|
284
|
+
## Testing Plan
|
285
|
+
|
286
|
+
- Unit tests:
|
287
|
+
- Graph compilation correctness (fields, extras, fk_fields, plan)
|
288
|
+
- Fallback behaviors when no RestMeta or empty graph fields
|
289
|
+
- Request-cache sharing across nested graphs
|
290
|
+
- Values fast path returns same data shape as instance path for simple graphs
|
291
|
+
|
292
|
+
- Integration tests:
|
293
|
+
- Paginated collections with and without nested graphs
|
294
|
+
- Sorting, only(), select_related and prefetch coverage
|
295
|
+
- Large datasets (e.g. 10k rows) for memory/time regression testing
|
296
|
+
|
297
|
+
- Performance tests:
|
298
|
+
- Compare simple vs advanced vs optimized across:
|
299
|
+
- simple fields-only graphs
|
300
|
+
- graphs with extras
|
301
|
+
- graphs with nested related graphs
|
302
|
+
- Vary dataset sizes (1k, 10k, 100k)
|
303
|
+
|
304
|
+
-------------------------------------------------------------------------------
|
305
|
+
|
306
|
+
## Metrics and Diagnostics
|
307
|
+
|
308
|
+
- Counters (in-memory):
|
309
|
+
- optimized.values_fastpath_used (count)
|
310
|
+
- optimized.request_cache_hits/misses (approximate)
|
311
|
+
- Optional headers (debug only):
|
312
|
+
- X-Serializer-FastPath: values|instance
|
313
|
+
- X-Request-Cache: hits=N;misses=M
|
314
|
+
- Expose a `get_performance_info()` summary (already exists; keep cheap)
|
315
|
+
|
316
|
+
-------------------------------------------------------------------------------
|
317
|
+
|
318
|
+
## Risks and Mitigations
|
319
|
+
|
320
|
+
- values() fast path may lose custom Python-only transformation:
|
321
|
+
- Only enable when no extras or nested graphs are present
|
322
|
+
- Optionally support DB annotations if needed
|
323
|
+
- only() could hide columns needed by extras:
|
324
|
+
- Start with fields-only; allow expanding via settings/annotations
|
325
|
+
- Prefetch can over-fetch if misconfigured:
|
326
|
+
- Cache plan per graph; keep minimal defaults, expand conservatively
|
327
|
+
- Memory pressure:
|
328
|
+
- Provide iterator chunk option; defaults to list-based for speed
|
329
|
+
|
330
|
+
-------------------------------------------------------------------------------
|
331
|
+
|
332
|
+
## Configuration (Proposed)
|
333
|
+
|
334
|
+
- MOJO_OPTIMIZED_COMPILE_CACHE_MAXSIZE = 512
|
335
|
+
- MOJO_OPTIMIZED_APPLY_QUERY_PLAN = True
|
336
|
+
- MOJO_OPTIMIZED_ONLY_FIELDS = True
|
337
|
+
- MOJO_OPTIMIZED_VALUES_FASTPATH = True
|
338
|
+
- MOJO_VALUES_FASTPATH_ALLOW = [] (optional allowlist of model.graph pairs)
|
339
|
+
- MOJO_OPTIMIZED_ALLOW_ANNOTATIONS = False
|
340
|
+
- MOJO_OPTIMIZED_LIST_EVALUATION = True
|
341
|
+
- MOJO_OPTIMIZED_ITERATOR_CHUNK_SIZE = 2000
|
342
|
+
- MOJO_OPTIMIZED_COLLECT_METRICS = True
|
343
|
+
- MOJO_JSON_USE_UJSON = True
|
344
|
+
- MOJO_JSON_PRETTY_DEBUG = False
|
345
|
+
|
346
|
+
-------------------------------------------------------------------------------
|
347
|
+
|
348
|
+
## Acceptance Criteria
|
349
|
+
|
350
|
+
- For simple fields-only graphs, optimized serializer outperforms advanced by at least 30%
|
351
|
+
- For graphs with common repeated relationships, request-scoped caching yields 2x+ speedups vs simple
|
352
|
+
- Functional parity with existing behavior (fallbacks still work)
|
353
|
+
- No regressions in correctness verified by tests
|
354
|
+
- Feature flags allow safe rollback
|
355
|
+
|
356
|
+
-------------------------------------------------------------------------------
|
357
|
+
|
358
|
+
## Proposed Timeline
|
359
|
+
|
360
|
+
- Week 1:
|
361
|
+
- Implement compile_graph() + query plan
|
362
|
+
- Unit tests and initial benchmarks
|
363
|
+
- Week 2:
|
364
|
+
- Implement values() fast path + allowlist
|
365
|
+
- Add request cache sharing guards + metrics
|
366
|
+
- Benchmarks across datasets
|
367
|
+
- Week 3:
|
368
|
+
- Optional DB annotations for extras
|
369
|
+
- Documentation and rollout
|
370
|
+
- Enable fast path by default for safe graphs
|
371
|
+
|
372
|
+
-------------------------------------------------------------------------------
|
373
|
+
|
374
|
+
## Action Items
|
375
|
+
|
376
|
+
- [ ] Add compile_graph() (LRU-cached) and query plan builder
|
377
|
+
- [ ] Wire query plan in serialize() for QuerySets
|
378
|
+
- [ ] Implement values fast path detection + execution
|
379
|
+
- [ ] Ensure related nested serializations share the request cache
|
380
|
+
- [ ] Add feature flags and minimal metrics
|
381
|
+
- [ ] Expand unit tests and integration tests
|
382
|
+
- [ ] Document configuration and rollout steps
|
383
|
+
|
384
|
+
-------------------------------------------------------------------------------
|
385
|
+
|
386
|
+
## Conclusion
|
387
|
+
|
388
|
+
By moving to a compiled-graph model, caching the fetch plan, and using a `values()` fast path for simple graphs, we can significantly reduce CPU and DB overhead while preserving the current API and functionality. The improvements are incremental, reversible via feature flags, and deliver measurable performance gains across common workloads.
|
testit/client.py
CHANGED
@@ -61,7 +61,7 @@ class RestClient:
|
|
61
61
|
response = requests.request(method, url, headers=headers, **kwargs)
|
62
62
|
if self.logger:
|
63
63
|
self.logger.info("REQUEST", f"{method}:{url}", headers)
|
64
|
-
self.logger.info(kwargs.get("json", ""))
|
64
|
+
self.logger.info("params:",kwargs.get("params", ""), "json:", kwargs.get("json", ""))
|
65
65
|
try:
|
66
66
|
data = objict.fromdict(response.json()) if response.content else None
|
67
67
|
response_data = objict(response=data, status_code=response.status_code)
|
testit/helpers.py
CHANGED
@@ -206,10 +206,24 @@ def get_mock_user():
|
|
206
206
|
Returns:
|
207
207
|
objict: A mock user object with basic attributes.
|
208
208
|
"""
|
209
|
+
from mojo.helpers import crypto
|
209
210
|
user = objict()
|
210
211
|
user.id = 1
|
211
212
|
user.username = "mockuser"
|
212
213
|
user.email = "mockuser@example.com"
|
213
214
|
user.is_authenticated = True
|
215
|
+
user.password = crypto.random_string(16)
|
214
216
|
user.has_permission = lambda perm: True
|
215
217
|
return user
|
218
|
+
|
219
|
+
def get_admin_user():
|
220
|
+
"""
|
221
|
+
Creates a mock admin user object.
|
222
|
+
|
223
|
+
Returns:
|
224
|
+
objict: A mock admin user object with basic attributes.
|
225
|
+
"""
|
226
|
+
user = get_mock_user()
|
227
|
+
user.is_superuser = True
|
228
|
+
user.is_staff = True
|
229
|
+
return user
|
testit/runner.py
CHANGED
@@ -45,8 +45,8 @@ def setup_parser():
|
|
45
45
|
help="Run only this app/module")
|
46
46
|
parser.add_argument("--method", type=str, default=None,
|
47
47
|
help="Run only a specific test method")
|
48
|
-
parser.add_argument("-t", "--test",
|
49
|
-
help="Specify
|
48
|
+
parser.add_argument("-t", "--test", action="append", dest="test_modules",
|
49
|
+
help="Specify test modules or specific tests (can be used multiple times)")
|
50
50
|
parser.add_argument("-q", "--quick", action="store_true",
|
51
51
|
help="Run only tests flagged as critical/quick")
|
52
52
|
parser.add_argument("-x", "--extra", type=str, default=None,
|
@@ -65,6 +65,8 @@ def setup_parser():
|
|
65
65
|
help="Do not run Mojo app tests")
|
66
66
|
parser.add_argument("--onlymojo", action="store_true",
|
67
67
|
help="Only run Mojo app tests")
|
68
|
+
parser.add_argument("--ignore", action="append", dest="ignore_modules",
|
69
|
+
help="Ignore specific test modules (can be used multiple times)")
|
68
70
|
return parser.parse_args()
|
69
71
|
|
70
72
|
|
@@ -144,7 +146,8 @@ def run_module_setup(opts, module, test_name, module_name):
|
|
144
146
|
|
145
147
|
|
146
148
|
def run_module_tests(opts, module, test_name, module_name):
|
147
|
-
|
149
|
+
if not getattr(opts, 'client', None):
|
150
|
+
opts.client = testit.client.RestClient(opts.host, logger=opts.logger)
|
148
151
|
test_key = f"{module_name}.{test_name}"
|
149
152
|
logit.color_print(f"\nRUNNING TEST: {test_key}", logit.ConsoleLogger.BLUE)
|
150
153
|
started = time.time()
|
@@ -225,19 +228,33 @@ def main(opts):
|
|
225
228
|
sys.path.insert(0, parent_test_root)
|
226
229
|
parent_test_modules = sorted([d for d in os.listdir(parent_test_root) if os.path.isdir(os.path.join(parent_test_root, d))])
|
227
230
|
|
228
|
-
|
231
|
+
# Handle multiple --test arguments
|
232
|
+
if opts.test_modules:
|
233
|
+
for test_spec in opts.test_modules:
|
234
|
+
if '.' in test_spec:
|
235
|
+
module_name, test_name = test_spec.split('.', 1)
|
236
|
+
run_module_tests_by_name(opts, module_name, test_name)
|
237
|
+
else:
|
238
|
+
run_tests_for_module(opts, test_spec, TEST_ROOT, parent_test_root)
|
239
|
+
elif opts.module and opts.test:
|
229
240
|
run_module_tests_by_name(opts, opts.module, opts.test)
|
230
241
|
elif opts.module:
|
231
242
|
run_tests_for_module(opts, opts.module, TEST_ROOT, parent_test_root)
|
232
243
|
else:
|
233
244
|
test_root = os.path.join(paths.APPS_ROOT, "tests")
|
234
245
|
test_modules = sorted([d for d in os.listdir(test_root) if os.path.isdir(os.path.join(test_root, d))])
|
246
|
+
|
247
|
+
# Filter out ignored modules
|
248
|
+
ignored_modules = opts.ignore_modules or []
|
249
|
+
|
235
250
|
if parent_test_modules and not opts.nomojo:
|
236
251
|
for module_name in parent_test_modules:
|
237
|
-
|
252
|
+
if module_name not in ignored_modules:
|
253
|
+
run_tests_for_module(opts, module_name, parent_test_root)
|
238
254
|
if not opts.onlymojo:
|
239
255
|
for module_name in test_modules:
|
240
|
-
|
256
|
+
if module_name not in ignored_modules:
|
257
|
+
run_tests_for_module(opts, module_name, test_root)
|
241
258
|
|
242
259
|
# Summary Output
|
243
260
|
print("\n" + "=" * 80)
|