Skip to content

Commit f6cd52d

Browse files
committed
perf(routing): cache lowercased routing strings to prevent redundant allocations
1 parent 6309a89 commit f6cd52d

7 files changed

Lines changed: 180 additions & 74 deletions

.jules/bolt.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
## 2024-04-12 - Python String Allocation in Tight Loops
2+
**Learning:** In the Python port's routing engine (`src/runtime.py`), frequent string operations inside tight nested loops (e.g., lowercasing strings and allocating arrays like `[module.name.lower(), module.source_hint.lower(), module.responsibility.lower()]` inside the `_score` method iteration) cause massive performance overhead due to Python's redundant string allocations.
3+
**Action:** Use `functools.cached_property` on domain objects (like `PortingModule.search_text`) to lazily precompute and cache concatenated/lowercased search strings. Separating them by null bytes (`\0`) prevents overlapping matches, resulting in significantly faster simple subset checks `if token in haystack` compared to looping over an array of dynamically allocated strings.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"session_id": "532325c073444fdbaf1c22efa19d7951",
3+
"messages": [
4+
"review MCP tool",
5+
"review MCP tool"
6+
],
7+
"input_tokens": 6,
8+
"output_tokens": 32
9+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"session_id": "83d95a0b8cd24e768b59f4a7c0f27c20",
3+
"messages": [
4+
"review MCP tool",
5+
"review MCP tool"
6+
],
7+
"input_tokens": 6,
8+
"output_tokens": 32
9+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"session_id": "a9685714a0964d8d82b2ea33e8d54b79",
3+
"messages": [
4+
"review MCP tool"
5+
],
6+
"input_tokens": 3,
7+
"output_tokens": 13
8+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"session_id": "ae9bc08fb15840e79f044165f37c84ff",
3+
"messages": [
4+
"review MCP tool",
5+
"review MCP tool"
6+
],
7+
"input_tokens": 6,
8+
"output_tokens": 32
9+
}

src/models.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
from dataclasses import dataclass, field
4+
from functools import cached_property
45

56

67
@dataclass(frozen=True)
@@ -16,7 +17,12 @@ class PortingModule:
1617
name: str
1718
responsibility: str
1819
source_hint: str
19-
status: str = 'planned'
20+
status: str = "planned"
21+
22+
@cached_property
23+
def search_text(self) -> str:
24+
# ⚡ Bolt: Cache lowercased concatenated strings to avoid redundant string allocations in routing loops
25+
return f"{self.name}\0{self.source_hint}\0{self.responsibility}".lower()
2026

2127

2228
@dataclass(frozen=True)
@@ -30,7 +36,7 @@ class UsageSummary:
3036
input_tokens: int = 0
3137
output_tokens: int = 0
3238

33-
def add_turn(self, prompt: str, output: str) -> 'UsageSummary':
39+
def add_turn(self, prompt: str, output: str) -> "UsageSummary":
3440
return UsageSummary(
3541
input_tokens=self.input_tokens + len(prompt.split()),
3642
output_tokens=self.output_tokens + len(output.split()),
@@ -44,6 +50,6 @@ class PortingBacklog:
4450

4551
def summary_lines(self) -> list[str]:
4652
return [
47-
f'- {module.name} [{module.status}] — {module.responsibility} (from {module.source_hint})'
53+
f"- {module.name} [{module.status}] — {module.responsibility} (from {module.source_hint})"
4854
for module in self.modules
4955
]

src/runtime.py

Lines changed: 133 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -38,64 +38,70 @@ class RuntimeSession:
3838

3939
def as_markdown(self) -> str:
4040
lines = [
41-
'# Runtime Session',
42-
'',
43-
f'Prompt: {self.prompt}',
44-
'',
45-
'## Context',
41+
"# Runtime Session",
42+
"",
43+
f"Prompt: {self.prompt}",
44+
"",
45+
"## Context",
4646
render_context(self.context),
47-
'',
48-
'## Setup',
49-
f'- Python: {self.setup.python_version} ({self.setup.implementation})',
50-
f'- Platform: {self.setup.platform_name}',
51-
f'- Test command: {self.setup.test_command}',
52-
'',
53-
'## Startup Steps',
54-
*(f'- {step}' for step in self.setup.startup_steps()),
55-
'',
56-
'## System Init',
47+
"",
48+
"## Setup",
49+
f"- Python: {self.setup.python_version} ({self.setup.implementation})",
50+
f"- Platform: {self.setup.platform_name}",
51+
f"- Test command: {self.setup.test_command}",
52+
"",
53+
"## Startup Steps",
54+
*(f"- {step}" for step in self.setup.startup_steps()),
55+
"",
56+
"## System Init",
5757
self.system_init_message,
58-
'',
59-
'## Routed Matches',
58+
"",
59+
"## Routed Matches",
6060
]
6161
if self.routed_matches:
6262
lines.extend(
63-
f'- [{match.kind}] {match.name} ({match.score}) — {match.source_hint}'
63+
f"- [{match.kind}] {match.name} ({match.score}) — {match.source_hint}"
6464
for match in self.routed_matches
6565
)
6666
else:
67-
lines.append('- none')
68-
lines.extend([
69-
'',
70-
'## Command Execution',
71-
*(self.command_execution_messages or ('none',)),
72-
'',
73-
'## Tool Execution',
74-
*(self.tool_execution_messages or ('none',)),
75-
'',
76-
'## Stream Events',
77-
*(f"- {event['type']}: {event}" for event in self.stream_events),
78-
'',
79-
'## Turn Result',
80-
self.turn_result.output,
81-
'',
82-
f'Persisted session path: {self.persisted_session_path}',
83-
'',
84-
self.history.as_markdown(),
85-
])
86-
return '\n'.join(lines)
67+
lines.append("- none")
68+
lines.extend(
69+
[
70+
"",
71+
"## Command Execution",
72+
*(self.command_execution_messages or ("none",)),
73+
"",
74+
"## Tool Execution",
75+
*(self.tool_execution_messages or ("none",)),
76+
"",
77+
"## Stream Events",
78+
*(f"- {event['type']}: {event}" for event in self.stream_events),
79+
"",
80+
"## Turn Result",
81+
self.turn_result.output,
82+
"",
83+
f"Persisted session path: {self.persisted_session_path}",
84+
"",
85+
self.history.as_markdown(),
86+
]
87+
)
88+
return "\n".join(lines)
8789

8890

8991
class PortRuntime:
9092
def route_prompt(self, prompt: str, limit: int = 5) -> list[RoutedMatch]:
91-
tokens = {token.lower() for token in prompt.replace('/', ' ').replace('-', ' ').split() if token}
93+
tokens = {
94+
token.lower()
95+
for token in prompt.replace("/", " ").replace("-", " ").split()
96+
if token
97+
}
9298
by_kind = {
93-
'command': self._collect_matches(tokens, PORTED_COMMANDS, 'command'),
94-
'tool': self._collect_matches(tokens, PORTED_TOOLS, 'tool'),
99+
"command": self._collect_matches(tokens, PORTED_COMMANDS, "command"),
100+
"tool": self._collect_matches(tokens, PORTED_TOOLS, "tool"),
95101
}
96102

97103
selected: list[RoutedMatch] = []
98-
for kind in ('command', 'tool'):
104+
for kind in ("command", "tool"):
99105
if by_kind[kind]:
100106
selected.append(by_kind[kind].pop(0))
101107

@@ -112,30 +118,59 @@ def bootstrap_session(self, prompt: str, limit: int = 5) -> RuntimeSession:
112118
setup = setup_report.setup
113119
history = HistoryLog()
114120
engine = QueryEnginePort.from_workspace()
115-
history.add('context', f'python_files={context.python_file_count}, archive_available={context.archive_available}')
116-
history.add('registry', f'commands={len(PORTED_COMMANDS)}, tools={len(PORTED_TOOLS)}')
121+
history.add(
122+
"context",
123+
f"python_files={context.python_file_count}, archive_available={context.archive_available}",
124+
)
125+
history.add(
126+
"registry", f"commands={len(PORTED_COMMANDS)}, tools={len(PORTED_TOOLS)}"
127+
)
117128
matches = self.route_prompt(prompt, limit=limit)
118129
registry = build_execution_registry()
119-
command_execs = tuple(registry.command(match.name).execute(prompt) for match in matches if match.kind == 'command' and registry.command(match.name))
120-
tool_execs = tuple(registry.tool(match.name).execute(prompt) for match in matches if match.kind == 'tool' and registry.tool(match.name))
130+
command_execs = tuple(
131+
registry.command(match.name).execute(prompt)
132+
for match in matches
133+
if match.kind == "command" and registry.command(match.name)
134+
)
135+
tool_execs = tuple(
136+
registry.tool(match.name).execute(prompt)
137+
for match in matches
138+
if match.kind == "tool" and registry.tool(match.name)
139+
)
121140
denials = tuple(self._infer_permission_denials(matches))
122-
stream_events = tuple(engine.stream_submit_message(
123-
prompt,
124-
matched_commands=tuple(match.name for match in matches if match.kind == 'command'),
125-
matched_tools=tuple(match.name for match in matches if match.kind == 'tool'),
126-
denied_tools=denials,
127-
))
141+
stream_events = tuple(
142+
engine.stream_submit_message(
143+
prompt,
144+
matched_commands=tuple(
145+
match.name for match in matches if match.kind == "command"
146+
),
147+
matched_tools=tuple(
148+
match.name for match in matches if match.kind == "tool"
149+
),
150+
denied_tools=denials,
151+
)
152+
)
128153
turn_result = engine.submit_message(
129154
prompt,
130-
matched_commands=tuple(match.name for match in matches if match.kind == 'command'),
131-
matched_tools=tuple(match.name for match in matches if match.kind == 'tool'),
155+
matched_commands=tuple(
156+
match.name for match in matches if match.kind == "command"
157+
),
158+
matched_tools=tuple(
159+
match.name for match in matches if match.kind == "tool"
160+
),
132161
denied_tools=denials,
133162
)
134163
persisted_session_path = engine.persist_session()
135-
history.add('routing', f'matches={len(matches)} for prompt={prompt!r}')
136-
history.add('execution', f'command_execs={len(command_execs)} tool_execs={len(tool_execs)}')
137-
history.add('turn', f'commands={len(turn_result.matched_commands)} tools={len(turn_result.matched_tools)} denials={len(turn_result.permission_denials)} stop={turn_result.stop_reason}')
138-
history.add('session_store', persisted_session_path)
164+
history.add("routing", f"matches={len(matches)} for prompt={prompt!r}")
165+
history.add(
166+
"execution",
167+
f"command_execs={len(command_execs)} tool_execs={len(tool_execs)}",
168+
)
169+
history.add(
170+
"turn",
171+
f"commands={len(turn_result.matched_commands)} tools={len(turn_result.matched_tools)} denials={len(turn_result.permission_denials)} stop={turn_result.stop_reason}",
172+
)
173+
history.add("session_store", persisted_session_path)
139174
return RuntimeSession(
140175
prompt=prompt,
141176
context=context,
@@ -151,42 +186,69 @@ def bootstrap_session(self, prompt: str, limit: int = 5) -> RuntimeSession:
151186
persisted_session_path=persisted_session_path,
152187
)
153188

154-
def run_turn_loop(self, prompt: str, limit: int = 5, max_turns: int = 3, structured_output: bool = False) -> list[TurnResult]:
189+
def run_turn_loop(
190+
self,
191+
prompt: str,
192+
limit: int = 5,
193+
max_turns: int = 3,
194+
structured_output: bool = False,
195+
) -> list[TurnResult]:
155196
engine = QueryEnginePort.from_workspace()
156-
engine.config = QueryEngineConfig(max_turns=max_turns, structured_output=structured_output)
197+
engine.config = QueryEngineConfig(
198+
max_turns=max_turns, structured_output=structured_output
199+
)
157200
matches = self.route_prompt(prompt, limit=limit)
158-
command_names = tuple(match.name for match in matches if match.kind == 'command')
159-
tool_names = tuple(match.name for match in matches if match.kind == 'tool')
201+
command_names = tuple(
202+
match.name for match in matches if match.kind == "command"
203+
)
204+
tool_names = tuple(match.name for match in matches if match.kind == "tool")
160205
results: list[TurnResult] = []
161206
for turn in range(max_turns):
162-
turn_prompt = prompt if turn == 0 else f'{prompt} [turn {turn + 1}]'
207+
turn_prompt = prompt if turn == 0 else f"{prompt} [turn {turn + 1}]"
163208
result = engine.submit_message(turn_prompt, command_names, tool_names, ())
164209
results.append(result)
165-
if result.stop_reason != 'completed':
210+
if result.stop_reason != "completed":
166211
break
167212
return results
168213

169-
def _infer_permission_denials(self, matches: list[RoutedMatch]) -> list[PermissionDenial]:
214+
def _infer_permission_denials(
215+
self, matches: list[RoutedMatch]
216+
) -> list[PermissionDenial]:
170217
denials: list[PermissionDenial] = []
171218
for match in matches:
172-
if match.kind == 'tool' and 'bash' in match.name.lower():
173-
denials.append(PermissionDenial(tool_name=match.name, reason='destructive shell execution remains gated in the Python port'))
219+
if match.kind == "tool" and "bash" in match.name.lower():
220+
denials.append(
221+
PermissionDenial(
222+
tool_name=match.name,
223+
reason="destructive shell execution remains gated in the Python port",
224+
)
225+
)
174226
return denials
175227

176-
def _collect_matches(self, tokens: set[str], modules: tuple[PortingModule, ...], kind: str) -> list[RoutedMatch]:
228+
def _collect_matches(
229+
self, tokens: set[str], modules: tuple[PortingModule, ...], kind: str
230+
) -> list[RoutedMatch]:
177231
matches: list[RoutedMatch] = []
178232
for module in modules:
179233
score = self._score(tokens, module)
180234
if score > 0:
181-
matches.append(RoutedMatch(kind=kind, name=module.name, source_hint=module.source_hint, score=score))
235+
matches.append(
236+
RoutedMatch(
237+
kind=kind,
238+
name=module.name,
239+
source_hint=module.source_hint,
240+
score=score,
241+
)
242+
)
182243
matches.sort(key=lambda item: (-item.score, item.name))
183244
return matches
184245

185246
@staticmethod
186247
def _score(tokens: set[str], module: PortingModule) -> int:
187-
haystacks = [module.name.lower(), module.source_hint.lower(), module.responsibility.lower()]
248+
# ⚡ Bolt: Use precomputed cached property to avoid redundant string allocations inside loops
249+
haystack = module.search_text
188250
score = 0
189251
for token in tokens:
190-
if any(token in haystack for haystack in haystacks):
252+
if token in haystack:
191253
score += 1
192254
return score

0 commit comments

Comments
 (0)