Skip to content

fix: skip schema lookup when value is not a str#401

Open
piste-jp wants to merge 1 commit into
capnproto:masterfrom
piste-jp:fix/perf
Open

fix: skip schema lookup when value is not a str#401
piste-jp wants to merge 1 commit into
capnproto:masterfrom
piste-jp:fix/perf

Conversation

@piste-jp
Copy link
Copy Markdown

Summary

_DynamicStructBuilder.from_dict performs a self.schema.fields.get(key) lookup for every key in the input dict, regardless of value type. This lookup was introduced in #351 to support base64-decoding of str values destined for Data fields, but it runs unconditionally.

Moving the lookup inside the isinstance(val, str) branch makes it skipped for the common case (bool, int, dict, list, bytes, etc.) while preserving the decode behavior bit-for-bit for str values.

Why this matters

In real-time systems that construct many messages per second via SomeStruct.new_message(**kwargs), this lookup lands on the hot path:

For our use case, even a kwargs dict as small as {'valid': False, 'logMonoTime': <int>} pays for two schema lookups per new_message() call. Neither value is a str, so the decode branch never fires — but the lookup runs anyway.

Measurement

Measured with py-spy on aarch64, 30s sampling at 100Hz, on two processes that call new_message heavily:

new_message self time v2.1.0 v2.2.0 (current) v2.2.0 + this patch
Process A (state estimator, 100Hz publisher) 1.4% 18.7% 1.0%
Process B (websocket bridge, ~20 topics) 0.0% 42.4% 0.6%

Total CPU time for Process B over the 30s window:

total CPU time
v2.1.0 2.33s
v2.2.0 4.25s (+82%)
v2.2.0 + patch 1.66s (-61% vs v2.2.0)

Restoring the v2.1.0 hot-path behavior brought the regressed publish frequency (100Hz → 98.4Hz) back to the expected rate, and dropped per-process CPU usage to the level seen before #351.

Visual comparison (py-spy "Left Heavy" view, process B)

v2.2.0 (current):
v2.2.0

new_message (the wide purple block) dominates the publisher's
main loop, taking roughly half of the total CPU time.

v2.2.0 + this patch:
v2.2.0 + patch

new_message is no longer visible as a hot frame. The publisher
loop is back to spending its time on actual work (writer, send,
send_frame, compress_sync).

Compatibility

No behavior change for str values: the field.proto.slot.type.which() == "data" check and base64.b64decode(val) call are unchanged, just nested one level deeper. The existing test_blob_to_dict_base64.py still passes.

Diff

 def from_dict(self, dict d):
     for key, val in d.iteritems():
         if key != 'which':
-            field = self.schema.fields.get(key)
             if isinstance(val, str):
+                field = self.schema.fields.get(key)
                 dtype = field.proto.slot.type.which()
                 if dtype == "data":
                     # decode bytes from utf-8 base64 encoding
                     val = base64.b64decode(val)
             try:
                 self._set(key, val)

Since 8467490 (capnproto#351, "added binary support in dictionaries via base64
encoding"), `_DynamicStructBuilder.from_dict` does a
`self.schema.fields.get(key)` lookup for every key in the input dict.
The lookup was added so that `str` values destined for a `Data` field
can be base64-decoded, but the lookup itself runs unconditionally, even
when the value is `bool`, `int`, `dict`, `list`, `bytes`, etc.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant