You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Importing adcp.client / adcp.server costs ~520 MiB RSS on 9.x (3.4x 6.6): adcp.types._eager loads the full type surface #1694
Importing adcp.client, adcp.server.test_controller, adcp.decisioning or adcp.compat.versions loads the whole generated type surface via adcp.types._eager. Since 9.x that costs about 520 MiB of resident memory, 3.4 times what 6.6 cost. A seller agent that ran comfortably in a 512Mi container on 6.6 is OOM-killed at startup on 9.3.
Measurements
Each figure is peak RSS (ru_maxrss) of a fresh python -I -c "import <module>", measured on Python 3.12.6 with no other packages.
module
6.6.0
9.3.0
10.0.0b1
adcp
16 MiB
17 MiB
17 MiB
adcp.types._eager
153 MiB
521 MiB
504 MiB
adcp.client
200 MiB
580 MiB
565 MiB
adcp.server.test_controller
187 MiB
576 MiB
562 MiB
What adcp.types._eager loads:
6.6.0
9.3.0
10.0.0b1
Pydantic model classes defined
1,965
3,224
3,347
...of which have a built validator
418
351
355
adcp.* modules imported
716
1,128
1,175
Notes:
The model count grew by 1.6x, but memory grew by 3.4x, so the cost per class roughly doubled as well.
defer_build=True already keeps most validators unbuilt. What remains is defining the classes themselves: FieldInfo objects, core-schema dicts and typing objects.
tracemalloc attributes only about 174 MiB of the 9.3 import to the Python heap. The rest is outside it, probably in pydantic-core.
In a real seller (an MCP server on fastmcp built on adcp.server, which also uses ADCPClient for outbound governance), memory goes from 248 MiB on 6.6 to about 660 MiB on 9.3 inside a container.
Why an adopter can't avoid it
Importing flat names from their defining modules instead of adcp.types helps only until any SDK module that needs _eager is imported. adcp.client, adcp.server.test_controller, adcp.decisioning.state_machines and adcp.compat.versions each do. In a seller that uses governance (ADCPClient) or the comply controller, the full surface always loads.
In addition, importing any one module under adcp.types.domains.<domain> executes that domain's __init__, which imports all of its modules. As a result, one import loads about 911 of 1,088 domain modules, a floor of about 137 MiB.
Possible directions
Let adcp.client, adcp.server.test_controller, adcp.decisioning and adcp.compat import only the models they reference, rather than adcp.types._eager.
Make domain __init__ modules lazy with module-level __getattr__, as adcp.types already is, so importing one domain module doesn't load the whole domain.
Add a CI check on import RSS (for example, import adcp.client under a fixed budget), so growth is visible in the PR that causes it.
Reproduce
forvin 6.6.0 9.3.0 10.0.0b1;do
uv run -q --no-project --python 3.12 --with "adcp==$v" python -I -c \
"import resource, adcp.client; print('$v', resource.getrusage(resource.RUSAGE_SELF).ru_maxrss)"done# ru_maxrss is in KiB on Linux, bytes on macOS
Activity
fix(types): defer the import-time model rebuilds that cost adcp.clien…
fix(types): defer the import-time model rebuilds that cost adcp.clien…
fix(types): defer the import-time model rebuilds that cost adcp.clien…
fix(types): defer the import-time model rebuilds that cost adcp.clien…
fix(types): defer the import-time model rebuilds that cost adcp.clien…