Skip to content

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

Draft
l0lawrence wants to merge 1 commit into
microsoft: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 into
microsoft: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

l0lawrence added a commit to l0lawrence/azure-sdk-for-python that referenced this pull request Apr 24, 2026
Cherry-picks the four generator changes from microsoft/typespec#10475 so we can

benchmark the list_blobs perf improvements without waiting on a generator release.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@microsoft-github-policy-service microsoft-github-policy-service Bot added the stale Mark a PR that hasn't been recently updated and will be closed. label May 25, 2026
@microsoft-github-policy-service
Copy link
Copy Markdown
Contributor

Hi @@l0lawrence. Your PR has had no update for 30 days and it is marked as a stale PR. If it is not updated within 30 days, the PR will automatically be closed. If you want to refresh the PR, please remove the stale label.

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 stale Mark a PR that hasn't been recently updated and will be closed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants