Reduce dev startup memory by deferring unused integrations

Merged
AL
24d ago
+1600-197

Reflex dev startup imports optional data, plotting, image, database, and admin integrations even when the app does not use them. Spawned Granian workers also cannot reuse app modules preloaded by the supervisor, leaving an unnecessary copy resident between reloads.

Resolve optional serializers from already-loaded class identities, defer unused database/admin imports, and preload the app only for forked Granian workers. Preserve custom serializer overrides, exact-type/subclass precedence, runtime annotations, and cold direct-SQLModel relationship data.

This PR now also includes the backend-only follow-up:

  • State mutation tracking recognizes already-loaded model classes without eagerly importing SQLAlchemy. Model import order, cached classification, and explicit/wildcard access to MUTABLE_TYPES remain supported.
  • Move the pure write_file helper into the lightweight path-operations module, preserving its old compiler reexport. This keeps the compiler/state dependency graph out of the backend supervisor.

Latest backend-only measurements

Database-free blank app, Granian dev mode, macOS on Apple M5 Pro, Python 3.14.5, with optional dependencies installed. These are additional improvements over this PR's initial commit, measured again on both sides:

MetricInitial commit, remeasuredWith follow-upAdditional improvement
Startup process-tree RSS209.11 MiB163.95 MiB21.6% lower
Startup to ready456.1 ms336.5 ms26.2% faster
Python reload to ready524.8 ms398.4 ms24.1% faster
Post-reload process-tree RSS209.13 MiB164.02 MiB21.6% lower

Each side uses 5 fresh starts and 15 reloads; values are medians. The final source was verified unchanged after benchmarking. Startup ranged from 332.6–351.9 ms, reloads from 380.4–421.2 ms, and post-reload RSS from 163.91–164.28 MiB.

After the regression fixes in b2bf7009, a fresh 5-start/15-reload check measured 164.00 MiB startup RSS, 338.6 ms startup, 400.5 ms reload, and 164.03 MiB post-reload RSS. This retains the earlier savings; it is a separate-session sanity check, not an interleaved A/B benchmark.

After the second-audit fixes, an alternating same-session comparison against b2bf7009 (5 fresh starts and 15 reloads per revision, separate excluded warm-ups) measured 377.35 → 372.56 ms startup, 428.70 → 429.55 ms reload, 164.66 → 164.34 MiB startup RSS, and 164.59 → 164.41 MiB post-reload RSS. This small sample shows no noticeable regression; it does not establish a new speedup. Parent and spawned-worker import paths were verified for each revision. A separate final standalone run measured 163.89 MiB startup RSS.

For context, the earlier measurement of the first commit showed:

MetricOriginal frameworkInitial PR commit
Process-tree RSS375.27 MiB209.13 MiB
Startup to ready0.906 s0.446 s
Python reload to ready1.070 s0.533 s

The original baseline was c57b32cc8b0cbcb61ffe303b18a94f5cb3ca1a10; the follow-up baseline was cc9107032f71aa83b31427764d3cd2a0694899ec. The initial-commit values above come from separate benchmark passes, so small timing differences are expected.

Startup waits for /ping; reload timing starts at a Python source-file touch and waits for a replacement worker and a successful /ping. The compile-skip marker is removed before each fresh start. Telemetry and version checks are disabled on both sides.

RSS sums the supervisor, backend worker, and multiprocessing helper; it excludes Vite/browser memory and can double-count shared pages. Savings depend on installed and used integrations; database-using apps still pay their database import cost. These are fresh-process launches with warm filesystem caches. Linux/fork, other Python versions, large-app compile time, production performance, and long-duration leak freedom have not been established.

Validation

The latest fixes address all eight findings from the second adversarial audit:

  • Preserve SQLModel relationship data in cold state/event payloads, and preserve the historical serializer import, keyword argument, annotations, and pickle path.
  • A later compatibility import of reflex.model no longer silently replaces a custom SQLModel serializer or broad custom fallback. This deliberately removes the old import-order-dependent override; both cases have regression coverage.
  • Recognize actual optional-library classes by identity, so unrelated user packages named pandas do not trigger dependency imports.
  • Resolve defaults without importing libraries or rebuilding the registry. This removes the global serializer lock, destructive reordering, custom-hash lock inversion, and reentrant registration loss.
  • Finish Plotly's deferred JSON/engine initialization before fork only when Plotly Figure/Template classes are already loaded. Regression tests overlap fork with Template, JSON-helper, and optional orjson imports.
  • Count directly imported SQLModel database usage without loading the unused database integration.
  • Bound all serializer subprocess tests with timeouts.

Validation of the current fixes:

  • Final full unit suite: 8,360 passed, 19 skipped; 75.96% coverage on Python 3.14.5.
  • Focused serializer modules: 88 passed, 1 skipped (the explicit orjson case skips because that package is not installed in the Python 3.14 environment).
  • Two additional test-only broad-fallback cases subsequently passed; the expanded cold-process module has 15 passing cases and 1 optional-orjson skip on Python 3.14.
  • Final cold-process regression module: 16 passed on Python 3.12.13 with orjson installed; the JSON-helper and orjson fork cases also passed 8 fresh-process repeats each.
  • Plotly auto/json/orjson configurations and simulated unavailable-orjson fallback passed. No installed dependencies were changed for the fallback checks.
  • Full Playwright suite: 253 passed.
  • Docs suite on Python 3.12: 419 passed, 1 skipped, 1 expected failure.
  • Real docs backend: 8 WebSocket assertions across initial startup and 3 worker reloads.
  • Public counter, SQLite CRUD, and Excel/pandas/SQLModel data-visualization apps: all 8 dev/production runs passed, including browser interactions, reload/state survival, and a separate fresh Excel ingestion fixture.
  • 100 seeded base/current comparisons (6,700 mixed operations per revision) matched exactly. Missing optional dependencies, annotations, pickle, and repeated module reload checks passed; a real user app named pandas now compiles on both revisions.
  • Independent review also matched 624 optional/default/override inheritance combinations against the original base.
  • All configured pre-commit hooks passed: Ruff, codespell, stub generation, Pyright, ty, and Biome.
  • Regression failures were captured before implementing their fixes, including removing only the final warm-up call to reproduce the orjson hang.
  • News fragments included for both touched packages.

The full Playwright, docs, and public-app runs preceded the final narrowly scoped Plotly JSON-engine warm-up adjustment. The full unit suite, focused cross-environment fork tests, and startup benchmark were rerun after it.

Earlier broad validation on 46f9c7bf included large-state apps through 10,000 fields and normal/strict Granian plus Uvicorn reloads. Its selected state/event matrix reached 98 distinct passing cases and 10 skips across initial and isolated runs; failures in the initial combined batch also reproduced on base (including macOS select-all and fixture-isolation issues).

CI on preceding head b2bf7009 finished with 109 successes and 2 skips, including docs production checks. New-commit CI must run separately. Local full docs frontend/browser validation still requires Enterprise login; backend and docs-unit checks are not substitutes for it.

The fork tests cover the identified deferred-import races, not a blanket guarantee that arbitrary third-party code is safe to fork from a multithreaded process. Direct mutation of internal serializer registries is not a supported compatibility contract.

Changes to core features

  • Explained the change and its motivation.
  • Added regression tests and ran the unit suite locally.
  • Reviewed the patch adversarially and addressed compatibility findings.
  • Checked related open PRs; #6930 is related import work and also touches types.py.