Skip to content
This repository was archived by the owner on Mar 26, 2026. It is now read-only.

Commit e843540

Browse files
authored
perf: reduce unnecessary copies, optimize Address comparison (#855)
Includes various other performance optimizations and minor whitespace fixes. Local testing cuts down Ads generation time to ~1m (and prevents OOM) and DialogflowCX from ~10 minutes to ~5 minutes.
1 parent c0175e2 commit e843540

10 files changed

Lines changed: 84 additions & 64 deletions

File tree

gapic/ads-templates/%namespace/%name/%version/%sub/__init__.py.j2

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,23 @@
88
them again.
99
-#}
1010
__all__ = (
11-
{% for subpackage in api.subpackages|dictsort %}
11+
{% filter sort_lines -%}
12+
{% for subpackage in api.subpackages -%}
1213
'{{ subpackage }}',
13-
{% endfor %}
14-
{% for service in api.services.values()|sort(attribute='client_name')
15-
if service.meta.address.subpackage == api.subpackage_view %}
14+
{% endfor -%}
15+
{% for service in api.services.values()
16+
if service.meta.address.subpackage == api.subpackage_view -%}
1617
'{{ service.client_name }}',
17-
{% endfor %}
18-
{% for proto in api.protos.values()|sort(attribute='name')
19-
if proto.meta.address.subpackage == api.subpackage_view %}
20-
{% for message in proto.messages.values() %}
18+
{% endfor -%}
19+
{% for proto in api.protos.values()
20+
if proto.meta.address.subpackage == api.subpackage_view -%}
21+
{% for message in proto.messages.values() -%}
2122
'{{ message.name }}',
22-
{% endfor %}
23-
{% for enum in proto.enums.values()|sort(attribute='name') %}
23+
{% endfor -%}
24+
{% for enum in proto.enums.values() -%}
2425
'{{ enum.name }}',
25-
{% endfor %}
26-
{% endfor %}
26+
{% endfor -%}
27+
{% endfor -%}
28+
{% endfilter -%}
2729
)
2830
{% endblock %}

gapic/ads-templates/%namespace/%name/%version/__init__.py.j2

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{% extends '_base.py.j2' %}
22
{% block content %}
33

4-
{% if opts.lazy_import %} {# lazy import #}
4+
{% if opts.lazy_import -%} {# lazy import #}
55
import importlib
66
import sys
77

gapic/ads-templates/%namespace/%name/__init__.py.j2

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{% extends '_base.py.j2' %}
22
{% block content %}
33

4-
{% if opts.lazy_import %} {# lazy import #}
4+
{% if opts.lazy_import -%} {# lazy import #}
55
import importlib
66
import sys
77

gapic/ads-templates/tests/unit/gapic/%name_%version/test_module_import.py.j2

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{% extends "_base.py.j2" %}
22
{% block content %}
33

4-
{% if opts.lazy_import %} {# lazy import #}
4+
{% if opts.lazy_import -%} {# lazy import #}
55
import pytest
66

77

gapic/generator/generator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,7 @@ def _get_filename(
378378
# Replace the %namespace variable.
379379
filename = filename.replace(
380380
"%namespace",
381-
os.path.sep.join([i.lower() for i in api_schema.naming.namespace]),
381+
os.path.sep.join(i.lower() for i in api_schema.naming.namespace),
382382
).lstrip(os.path.sep)
383383

384384
# Replace the %name, %version, and %sub variables.

gapic/schema/metadata.py

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -36,22 +36,30 @@
3636
from gapic.utils import cached_property
3737
from gapic.utils import RESERVED_NAMES
3838

39+
# This class is a minor hack to optimize Address's __eq__ method.
40+
3941

4042
@dataclasses.dataclass(frozen=True)
41-
class Address:
43+
class BaseAddress:
4244
name: str = ''
4345
module: str = ''
4446
module_path: Tuple[int, ...] = dataclasses.field(default_factory=tuple)
4547
package: Tuple[str, ...] = dataclasses.field(default_factory=tuple)
4648
parent: Tuple[str, ...] = dataclasses.field(default_factory=tuple)
49+
50+
51+
@dataclasses.dataclass(frozen=True)
52+
class Address(BaseAddress):
4753
api_naming: naming.Naming = dataclasses.field(
4854
default_factory=naming.NewNaming,
4955
)
5056
collisions: FrozenSet[str] = dataclasses.field(default_factory=frozenset)
5157

5258
def __eq__(self, other) -> bool:
53-
return all(getattr(self, i) == getattr(other, i)
54-
for i in ('name', 'module', 'module_path', 'package', 'parent'))
59+
# We don't want to use api_naming or collisions to determine equality,
60+
# so defer to the parent class's eq method.
61+
# This is an fairly important optimization for large APIs.
62+
return super().__eq__(other)
5563

5664
def __hash__(self):
5765
# Do NOT include collisions; they are not relevant.
@@ -94,7 +102,8 @@ def __str__(self) -> str:
94102
# Return the Python identifier.
95103
return '.'.join(self.parent + (self.name,))
96104

97-
def __repr__(self) -> str:
105+
@cached_property
106+
def __cached_string_repr(self):
98107
return "({})".format(
99108
", ".join(
100109
(
@@ -108,6 +117,9 @@ def __repr__(self) -> str:
108117
)
109118
)
110119

120+
def __repr__(self) -> str:
121+
return self.__cached_string_repr
122+
111123
@property
112124
def module_alias(self) -> str:
113125
"""Return an appropriate module alias if necessary.
@@ -118,16 +130,15 @@ def module_alias(self) -> str:
118130
while still providing names that are fundamentally readable
119131
to users (albeit looking auto-generated).
120132
"""
121-
if self.module in self.collisions | RESERVED_NAMES:
133+
# This is a minor optimization to prevent constructing a temporary set.
134+
if self.module in self.collisions or self.module in RESERVED_NAMES:
122135
return '_'.join(
123136
(
124137
''.join(
125-
[
126-
partial_name[0]
127-
for i in self.package
128-
for partial_name in i.split("_")
129-
if i != self.api_naming.version
130-
]
138+
partial_name[0]
139+
for i in self.package
140+
for partial_name in i.split("_")
141+
if i != self.api_naming.version
131142
),
132143
self.module,
133144
)
@@ -302,7 +313,11 @@ def with_context(self, *, collisions: FrozenSet[str]) -> 'Address':
302313
``Address`` object aliases module names to avoid naming collisions in
303314
the file being written.
304315
"""
305-
return dataclasses.replace(self, collisions=collisions)
316+
return (
317+
dataclasses.replace(self, collisions=collisions)
318+
if collisions and collisions != self.collisions
319+
else self
320+
)
306321

307322

308323
@dataclasses.dataclass(frozen=True)
@@ -340,7 +355,7 @@ def with_context(self, *, collisions: FrozenSet[str]) -> 'Metadata':
340355
return dataclasses.replace(
341356
self,
342357
address=self.address.with_context(collisions=collisions),
343-
)
358+
) if collisions and collisions != self.address.collisions else self
344359

345360

346361
@dataclasses.dataclass(frozen=True)

gapic/schema/wrappers.py

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -463,24 +463,24 @@ def with_context(self, *,
463463
visited_messages = visited_messages | {self}
464464
return dataclasses.replace(
465465
self,
466-
fields=collections.OrderedDict(
467-
(k, v.with_context(
466+
fields={
467+
k: v.with_context(
468468
collisions=collisions,
469469
visited_messages=visited_messages
470-
))
471-
for k, v in self.fields.items()
472-
) if not skip_fields else self.fields,
473-
nested_enums=collections.OrderedDict(
474-
(k, v.with_context(collisions=collisions))
470+
) for k, v in self.fields.items()
471+
} if not skip_fields else self.fields,
472+
nested_enums={
473+
k: v.with_context(collisions=collisions)
475474
for k, v in self.nested_enums.items()
476-
),
477-
nested_messages=collections.OrderedDict(
478-
(k, v.with_context(
475+
},
476+
nested_messages={
477+
k: v.with_context(
479478
collisions=collisions,
480479
skip_fields=skip_fields,
481480
visited_messages=visited_messages,
482-
))
483-
for k, v in self.nested_messages.items()),
481+
)
482+
for k, v in self.nested_messages.items()
483+
},
484484
meta=self.meta.with_context(collisions=collisions),
485485
)
486486

@@ -535,7 +535,7 @@ def with_context(self, *, collisions: FrozenSet[str]) -> 'EnumType':
535535
return dataclasses.replace(
536536
self,
537537
meta=self.meta.with_context(collisions=collisions),
538-
)
538+
) if collisions else self
539539

540540
@property
541541
def options_dict(self) -> Dict:
@@ -957,9 +957,11 @@ def with_context(self, *, collisions: FrozenSet[str]) -> 'Method':
957957
``Method`` object aliases module names to avoid naming collisions
958958
in the file being written.
959959
"""
960-
maybe_lro = self.lro.with_context(
961-
collisions=collisions
962-
) if self.lro else None
960+
maybe_lro = None
961+
if self.lro:
962+
maybe_lro = self.lro.with_context(
963+
collisions=collisions
964+
) if collisions else self.lro
963965

964966
return dataclasses.replace(
965967
self,
@@ -1195,13 +1197,12 @@ def with_context(self, *, collisions: FrozenSet[str]) -> 'Service':
11951197
"""
11961198
return dataclasses.replace(
11971199
self,
1198-
methods=collections.OrderedDict(
1199-
(k, v.with_context(
1200-
# A methodd's flattened fields create additional names
1200+
methods={
1201+
k: v.with_context(
1202+
# A method's flattened fields create additional names
12011203
# that may conflict with module imports.
12021204
collisions=collisions | frozenset(v.flattened_fields.keys()))
1203-
)
12041205
for k, v in self.methods.items()
1205-
),
1206+
},
12061207
meta=self.meta.with_context(collisions=collisions),
12071208
)

gapic/templates/%namespace/%name_%version/%sub/__init__.py.j2

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,21 +33,23 @@ from .types.{{ proto.module_name }} import {{ enum.name }}
3333
them again.
3434
-#}
3535
__all__ = (
36-
{% for subpackage in api.subpackages|dictsort %}
36+
{% filter sort_lines -%}
37+
{% for subpackage in api.subpackages -%}
3738
'{{ subpackage }}',
38-
{% endfor %}
39-
{% for service in api.services.values()|sort(attribute='client_name')
40-
if service.meta.address.subpackage == api.subpackage_view %}
39+
{% endfor -%}
40+
{% for service in api.services.values()
41+
if service.meta.address.subpackage == api.subpackage_view -%}
4142
'{{ service.client_name }}',
42-
{% endfor %}
43-
{% for proto in api.protos.values()|sort(attribute='name')
44-
if proto.meta.address.subpackage == api.subpackage_view %}
45-
{% for message in proto.messages.values()|sort(attribute='name') %}
43+
{% endfor -%}
44+
{% for proto in api.protos.values()
45+
if proto.meta.address.subpackage == api.subpackage_view -%}
46+
{% for message in proto.messages.values()|sort(attribute='name') -%}
4647
'{{ message.name }}',
47-
{% endfor %}
48-
{% for enum in proto.enums.values()|sort(attribute='name') %}
48+
{% endfor -%}
49+
{% for enum in proto.enums.values() -%}
4950
'{{ enum.name }}',
50-
{% endfor %}
51-
{% endfor %}
51+
{% endfor -%}
52+
{% endfor -%}
53+
{% endfilter %}
5254
)
5355
{% endblock %}

tests/unit/schema/test_metadata.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def test_address_str_with_context():
3333
package=('foo', 'bar'),
3434
module='baz',
3535
name='Bacon',
36-
).with_context(collisions={'baz'})
36+
).with_context(collisions=frozenset({'baz'}))
3737
assert str(addr) == 'fb_baz.Bacon'
3838

3939

tests/unit/schema/wrappers/test_message.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def test_message_ident():
5656

5757
def test_message_ident_collisions():
5858
message = make_message('Baz', package='foo.v1', module='bar').with_context(
59-
collisions={'bar'},
59+
collisions=frozenset({'bar'}),
6060
)
6161
assert str(message.ident) == 'fv_bar.Baz'
6262
assert message.ident.sphinx == 'foo.v1.bar.Baz'

0 commit comments

Comments
 (0)