Skip to content

perf(http-client-python): precompute model class state to speed up list operations#10475

Draft
l0lawrence wants to merge 1 commit intomicrosoft:mainfrom
l0lawrence:perf-model-base-easy-wins
Draft

perf(http-client-python): precompute model class state to speed up list operations#10475
l0lawrence wants to merge 1 commit intomicrosoft:mainfrom
l0lawrence:perf-model-base-easy-wins

Conversation

@l0lawrence
Copy link
Copy Markdown
Member

Summary

Profiling azure-storage-blob's list_blobs against a 500-blob container on the in-progress TypeSpec migration (Azure/azure-sdk-for-python#45133) identified the generated _utils/model_base.py deserializer as the dominant hot path – roughly half the wall time was spent there, causing a ~-48% throughput regression vs. the current msrest-generated package. Download/upload were unaffected because they deserialize exactly one model per HTTP call regardless of payload size; list_blobs is the only operation where one response produces N model instances, so per-instance overhead is what matters.

This PR addresses the most mechanical of those hot spots in the shared model_base.py.jinja2 template so every TypeSpec-emitted Python SDK benefits.

Changes

  1. Precompute cls._defaults once in Model.__new__Model.__init__ no longer walks _attr_to_rest_field on every instance build; it just copies the pre-built mapping.
  2. Promote _RestField._rest_name from @property to plain attribute – set once in __new__; removes a descriptor lookup that was hit once per field per model (~73k calls for a 500-blob list).
  3. Replace the string-keyed _calculated set with a _calculated_done: bool flag – checked via cls.__dict__.get("_calculated_done", False) so subclasses re-run the per-class setup correctly without inheriting the flag.
  4. Narrow _deserialize_default's except Exception to except DeserializationError – the caller _deserialize_with_callable already wraps everything into DeserializationError, so this is semantically equivalent for the success-and-expected-failure paths while surfacing real coding bugs (AttributeError/TypeError) instead of masking them as deserialization failures.

No public API change; no generated-code change for consumers beyond the _utils/model_base.py body itself.

Validation

  • npm run build – clean.
  • npm run regenerate – unbranded fixtures regenerate successfully. Regenerated _utils/model_base.py contains the new code paths (_calculated_done, _defaults =, except DeserializationError).
  • Unit tests: tests/unit/test_model_base_serialization.py + test_model_base_xml_serialization.py156/157 pass. The one failure (test_null_serialization) is a pre-existing cross-package isinstance mismatch (azure.core.serialization._Null vs. corehttp.serialization._Null) in the unchanged _deserialize_with_callable dispatcher and is unrelated to this change – it reproduces on main when both azure.core and corehttp are importable simultaneously in the test env.

Follow-ups (not in this PR)

There are larger wins still on the table that I'd like to address in a second PR if this one is well-received:

  • Cache per-class XML metadata (xml_name, wrapped, attribute) so _init_from_xml stops recomputing it once per field per instance.
  • Cache the typing.get_args / annotation analysis in _deserialize that currently re-runs for every instance.
  • Flatten the 13-branch _deserialize_with_callable dispatcher into a precomputed callable per field.

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

…st operations

Profiling azure-storage-blob's list_blobs against a 500-blob container
showed the TypeSpec runtime deserializer as the dominant hot path:
  - Model.__init__ rebuilt a defaults dict per instance
  - _RestField._rest_name was a @Property called once per field per model
    (73,024 calls for 500 blobs)
  - Model._calculated used a string set + membership test per subclass
  - _deserialize_default swallowed all exceptions, masking real bugs

This change:
  * Precomputes cls._defaults once in Model.__new__
  * Promotes _rest_name from property to plain attribute set in __new__
  * Replaces the _calculated string set with a cls._calculated_done bool
    (checked via __dict__.get so subclasses don't inherit the flag)
  * Narrows except Exception to except DeserializationError in
    _deserialize_default (safe - _deserialize_with_callable already wraps)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 23, 2026

Open in StackBlitz

npm i https://pkg.pr.new/@typespec/http-client-python@10475

commit: 021e589

@microsoft-github-policy-service microsoft-github-policy-service Bot added the emitter:client:python Issue for the Python client emitter: @typespec/http-client-python label Apr 23, 2026
@github-actions
Copy link
Copy Markdown
Contributor

All changed packages have been documented.

  • @typespec/http-client-python
Show changes

@typespec/http-client-python - fix ✏️

Speed up model deserialization for list-style operations by precomputing per-class defaults and rest-name metadata once in __new__, turning _RestField._rest_name and Model._calculated_done into plain attribute reads instead of a @property and a string-keyed set lookup. Also narrows a broad except Exception in _deserialize_default to except DeserializationError so real coding bugs are no longer silently swallowed.

@l0lawrence l0lawrence marked this pull request as draft April 23, 2026 17:51
@azure-sdk
Copy link
Copy Markdown
Collaborator

You can try these changes here

🛝 Playground 🌐 Website 🛝 VSCode Extension

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

emitter:client:python Issue for the Python client emitter: @typespec/http-client-python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants