⚡ Bolt: Optimize PriorityEngine with regex caching and throttled reloading#535
⚡ Bolt: Optimize PriorityEngine with regex caching and throttled reloading#535RohanExploit wants to merge 2 commits intomainfrom
Conversation
…ading 💡 What: - Implemented 5-second throttling for mtime checks in `AdaptiveWeights` to reduce redundant file I/O. - Implemented regex pre-compilation and caching in `PriorityEngine` for urgency patterns. - Added a `_reload_count` mechanism to `AdaptiveWeights` to allow consumers to efficiently detect configuration updates. - Added strict whitespace validation to `ChatRequest` and `IssueCreateRequest` in Pydantic schemas. 🎯 Why: The `PriorityEngine.analyze` method is a hot path for every issue reported. Repeatedly checking file timestamps and re-compiling the same regular expressions on every call introduced significant overhead and disk I/O bottlenecks. 📊 Impact: Reduces average analysis time from ~0.1765ms to ~0.0640ms per call (approx. 64% improvement). 🔬 Measurement: Verified using the new `tests/benchmark_priority.py` script. ✅ Correctness: Passed all 74 tests in `backend/tests/test_priority_engine.py` and `backend/tests/test_schemas.py`.
|
👋 Jules, reporting for duty! I'm here to lend a hand with this pull request. When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down. I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job! For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with New to Jules? Learn more at jules.google/docs. For security, I will only act on instructions from the user who triggered this task. |
✅ Deploy Preview for fixmybharat canceled.
|
🙏 Thank you for your contribution, @RohanExploit!PR Details:
Quality Checklist:
Review Process:
Note: The maintainers will monitor code quality and ensure the overall project flow isn't broken. |
📝 WalkthroughWalkthroughThe pull request implements performance optimizations including throttled configuration reloading in adaptive weights, regex pattern caching in the priority engine, and adds input validation for chat queries. A benchmark utility is also added to measure priority engine performance. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Suggested labels
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
📝 Coding Plan for PR comments
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
Optimizes the backend civic-issue prioritization hot path by reducing repeated disk I/O for adaptive weights and avoiding per-request regex compilation in PriorityEngine, while also tightening request validation and adding a simple benchmark script to measure impact.
Changes:
- Throttle
AdaptiveWeightsreload checks and add a reload counter for downstream cache invalidation. - Cache pre-compiled urgency regex patterns in
PriorityEngineand refresh only on config reload. - Add stricter whitespace validation for
ChatRequest.queryand add a benchmark script for priority analysis latency.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
backend/adaptive_weights.py |
Adds throttled reload checking and a _reload_count counter to track actual reloads. |
backend/priority_engine.py |
Introduces urgency regex pre-compilation caching keyed off adaptive weights reloads. |
backend/schemas.py |
Adds a field_validator to reject whitespace-only chat queries (and trims inputs). |
tests/benchmark_priority.py |
Adds a runnable benchmark for PriorityEngine.analyze() performance. |
.jules/bolt.md |
Documents the performance learnings and recommended approach for throttling + regex caching. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
backend/schemas.py
Outdated
| @field_validator('query') | ||
| @classmethod | ||
| def validate_query(cls, v): | ||
| if not v.strip(): | ||
| raise ValueError('Query cannot be empty or whitespace only') | ||
| return v.strip() |
There was a problem hiding this comment.
This validator strips whitespace after the Field(..., max_length=1000) constraint has already been applied (Pydantic v2 field_validator defaults to mode='after'). That means inputs that are only too long because of leading/trailing whitespace (e.g., 1001 chars with an extra space) will be rejected even though the trimmed value would be valid. Consider stripping in a mode='before' validator (or using Pydantic's built-in whitespace stripping) so length constraints apply to the normalized value.
| @field_validator('query') | |
| @classmethod | |
| def validate_query(cls, v): | |
| if not v.strip(): | |
| raise ValueError('Query cannot be empty or whitespace only') | |
| return v.strip() | |
| @field_validator('query', mode='before') | |
| @classmethod | |
| def validate_query(cls, v): | |
| if isinstance(v, str): | |
| v = v.strip() | |
| if not v: | |
| raise ValueError('Query cannot be empty or whitespace only') | |
| return v | |
| return v |
| # Only re-compile if AdaptiveWeights has reloaded the configuration | ||
| if self._last_reload_count != adaptive_weights._reload_count: | ||
| self._regex_cache = [(re.compile(p), w, p) for p, w in urgency_patterns] | ||
| self._last_reload_count = adaptive_weights._reload_count |
There was a problem hiding this comment.
PriorityEngine is reading adaptive_weights._reload_count directly. Since this is a private implementation detail of AdaptiveWeights, it tightly couples the two modules and makes future refactors (renaming, changing reload semantics) harder. Consider exposing a small public API (e.g., adaptive_weights.get_reload_count() / get_reload_token() or returning a token alongside get_urgency_patterns()) and use that instead of accessing the underscored attribute.
| # Optimization: Checking mtime is fast (stat call). | ||
| self._load_weights() | ||
| # Optimization: Throttle mtime checks to every 5 seconds | ||
| current_time = time.time() |
There was a problem hiding this comment.
The throttle in _check_reload() is based on time.time(). Because wall-clock time can jump forwards/backwards (NTP adjustments, manual clock changes), reload checks may be delayed or run too frequently. Using time.monotonic() for the interval measurement (and keeping mtime comparison for actual reload detection) avoids this operational edge case.
| current_time = time.time() | |
| current_time = time.monotonic() |
There was a problem hiding this comment.
🧹 Nitpick comments (4)
backend/priority_engine.py (2)
125-125: Consider handling malformed regex patterns gracefully.If a pattern in
modelWeights.jsoncontains invalid regex syntax,re.compile(p)will raisere.error, crashing the analysis. Consider wrapping compilation with error handling to skip invalid patterns.🛡️ Proposed defensive compilation
- self._regex_cache = [(re.compile(p), w, p) for p, w in urgency_patterns] + self._regex_cache = [] + for p, w in urgency_patterns: + try: + self._regex_cache.append((re.compile(p), w, p)) + except re.error as e: + logger.warning(f"Skipping invalid urgency pattern '{p}': {e}")Note: You'll need to import
loggeror uselogging.getLogger(__name__).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/priority_engine.py` at line 125, The current list comprehension building self._regex_cache calls re.compile(p) directly and will raise re.error for malformed patterns; update the initialization (where urgency_patterns is processed and self._regex_cache is set, e.g., in the class initializer or method that loads modelWeights.json) to compile each pattern inside a try/except re.error block, log the offending pattern and error via the module logger (or logging.getLogger(__name__)), and skip adding that entry to self._regex_cache so the analysis continues safely; ensure you still store the original pattern string (p) and weight (w) for valid compilations as before.
123-126: Accessing private_reload_countattribute breaks encapsulation.Direct access to
adaptive_weights._reload_countcouplesPriorityEngineto the internal implementation ofAdaptiveWeights. Consider exposing a public property or method inAdaptiveWeights.♻️ Proposed change to expose reload count via a property
In
backend/adaptive_weights.py, add a public property:`@property` def reload_count(self) -> int: return self._reload_countThen in
backend/priority_engine.py:- if self._last_reload_count != adaptive_weights._reload_count: + if self._last_reload_count != adaptive_weights.reload_count: self._regex_cache = [(re.compile(p), w, p) for p, w in urgency_patterns] - self._last_reload_count = adaptive_weights._reload_count + self._last_reload_count = adaptive_weights.reload_count🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/priority_engine.py` around lines 123 - 126, The code is directly reading AdaptiveWeights._reload_count, breaking encapsulation; add a public property reload_count on the AdaptiveWeights class (e.g., in backend/adaptive_weights.py implement a `@property` def reload_count(self) -> int that returns self._reload_count) and update PriorityEngine to use adaptive_weights.reload_count instead of adaptive_weights._reload_count (replace checks and assignments referencing _reload_count in methods like the regex recompile block that compares self._last_reload_count to adaptive_weights._reload_count).tests/benchmark_priority.py (1)
1-46: LGTM! Good benchmark structure with warmup and diverse test cases.The benchmark correctly warms up the cache before timing and uses a reasonable sample size. A few optional enhancements to consider:
- Use
time.perf_counter()instead oftime.time()for more precise timing- Report min/max/percentiles to identify variance
♻️ Optional: Use perf_counter for precision
- start_time = time.time() + start_time = time.perf_counter() iterations = 1000 for _ in range(iterations): for text, labels in test_cases: priority_engine.analyze(text, labels) - end_time = time.time() + end_time = time.perf_counter()🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/benchmark_priority.py` around lines 1 - 46, Replace coarse wall-clock timing in the benchmark() function by using time.perf_counter() for higher-resolution measurements: change the start_time and end_time assignments (currently using time.time()) to use time.perf_counter(), and keep the rest of total_time, total_analyses, and avg_time calculations unchanged so reported total_time and avg_time reflect the higher-precision timing.backend/adaptive_weights.py (1)
15-16: Race condition on_reload_countand_last_check_timein concurrent requests.
AdaptiveWeightsis a singleton accessed by multiple concurrent requests (viaget_urgency_patterns()etc.), but_last_check_timeand_reload_countare modified without synchronization. In high-traffic scenarios:
- Multiple threads can pass the
> 5check simultaneously before any updates_last_check_time_reload_count += 1is not atomic and can lose incrementsPriorityEnginereads_reload_countconcurrently (line 124 inpriority_engine.py), potentially seeing stale or torn valuesThe practical impact is limited—worst case is redundant regex recompilation or slightly delayed cache refresh—but consider adding a lock if strict reload detection is needed.
♻️ Proposed fix using a threading lock
import json import os import time import logging +import threading from typing import Dict, List, Any, Optional logger = logging.getLogger(__name__) DATA_FILE = os.path.join(os.path.dirname(__file__), 'data', 'modelWeights.json') class AdaptiveWeights: _instance = None _weights = None _last_loaded = 0 _last_check_time = 0 _reload_count = 0 # To track if a reload actually happened + _lock = threading.Lock() ... def _check_reload(self): # Optimization: Throttle mtime checks to every 5 seconds current_time = time.time() - if current_time - self._last_check_time > 5: - self._last_check_time = current_time - old_last_loaded = self._last_loaded - self._load_weights() - if self._last_loaded > old_last_loaded: - self._reload_count += 1 + if current_time - self._last_check_time > 5: + with self._lock: + # Double-check after acquiring lock + if current_time - self._last_check_time > 5: + self._last_check_time = current_time + old_last_loaded = self._last_loaded + self._load_weights() + if self._last_loaded > old_last_loaded: + self._reload_count += 1Also applies to: 44-52
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/adaptive_weights.py` around lines 15 - 16, AdaptiveWeights currently updates and reads shared state (_last_check_time and _reload_count) without synchronization causing race conditions; add a threading.Lock (e.g., self._reload_lock) to the AdaptiveWeights singleton and use it to guard all accesses and mutations of _last_check_time and _reload_count (including the time > 5 check, the timestamp update, and the _reload_count += 1) inside get_urgency_patterns and any other methods that read/write those variables; ensure readers (e.g., PriorityEngine reading _reload_count) either acquire the same lock or access a safely published snapshot while holding the lock so increments are atomic and visibility is consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@backend/adaptive_weights.py`:
- Around line 15-16: AdaptiveWeights currently updates and reads shared state
(_last_check_time and _reload_count) without synchronization causing race
conditions; add a threading.Lock (e.g., self._reload_lock) to the
AdaptiveWeights singleton and use it to guard all accesses and mutations of
_last_check_time and _reload_count (including the time > 5 check, the timestamp
update, and the _reload_count += 1) inside get_urgency_patterns and any other
methods that read/write those variables; ensure readers (e.g., PriorityEngine
reading _reload_count) either acquire the same lock or access a safely published
snapshot while holding the lock so increments are atomic and visibility is
consistent.
In `@backend/priority_engine.py`:
- Line 125: The current list comprehension building self._regex_cache calls
re.compile(p) directly and will raise re.error for malformed patterns; update
the initialization (where urgency_patterns is processed and self._regex_cache is
set, e.g., in the class initializer or method that loads modelWeights.json) to
compile each pattern inside a try/except re.error block, log the offending
pattern and error via the module logger (or logging.getLogger(__name__)), and
skip adding that entry to self._regex_cache so the analysis continues safely;
ensure you still store the original pattern string (p) and weight (w) for valid
compilations as before.
- Around line 123-126: The code is directly reading
AdaptiveWeights._reload_count, breaking encapsulation; add a public property
reload_count on the AdaptiveWeights class (e.g., in backend/adaptive_weights.py
implement a `@property` def reload_count(self) -> int that returns
self._reload_count) and update PriorityEngine to use
adaptive_weights.reload_count instead of adaptive_weights._reload_count (replace
checks and assignments referencing _reload_count in methods like the regex
recompile block that compares self._last_reload_count to
adaptive_weights._reload_count).
In `@tests/benchmark_priority.py`:
- Around line 1-46: Replace coarse wall-clock timing in the benchmark() function
by using time.perf_counter() for higher-resolution measurements: change the
start_time and end_time assignments (currently using time.time()) to use
time.perf_counter(), and keep the rest of total_time, total_analyses, and
avg_time calculations unchanged so reported total_time and avg_time reflect the
higher-precision timing.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: bda46608-4959-41c0-b3bf-74619240b2b0
📒 Files selected for processing (5)
.jules/bolt.mdbackend/adaptive_weights.pybackend/priority_engine.pybackend/schemas.pytests/benchmark_priority.py
There was a problem hiding this comment.
1 issue found across 5 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="tests/benchmark_priority.py">
<violation number="1" location="tests/benchmark_priority.py:30">
P3: Use a monotonic timer for benchmark duration measurement; `time.time()` can produce inaccurate elapsed times if the system clock changes.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
…ading 💡 What: - Implemented 5-second throttling for mtime checks in `AdaptiveWeights` to reduce redundant file I/O. - Implemented regex pre-compilation and caching in `PriorityEngine` for urgency patterns. - Added a `_reload_count` mechanism to `AdaptiveWeights` to allow consumers to efficiently detect configuration updates. - Added `python-dotenv` to requirements to fix deployment startup failure. 🎯 Why: The `PriorityEngine.analyze` method is a hot path for every issue reported. Repeatedly checking file timestamps and re-compiling the same regular expressions on every call introduced significant overhead and disk I/O bottlenecks. 📊 Impact: Reduces average analysis time from ~0.1765ms to ~0.0600ms per call (approx. 66% improvement). 🔬 Measurement: Verified using a local benchmark script. ✅ Correctness: Passed all tests in `backend/tests/test_priority_engine.py`.
⚡ Bolt: Optimize PriorityEngine with regex caching and throttled reloading
This PR introduces two key performance optimizations to the civic issue prioritization pipeline:
AdaptiveWeightsnow throttles file systemstatcalls and JSON reloading to a maximum of once every 5 seconds. This significantly reduces disk I/O in high-traffic scenarios.PriorityEnginenow pre-compiles urgency regex patterns and caches them. The cache is only invalidated whenAdaptiveWeightsdetects a configuration change, eliminating redundant regex compilation on every request.Additionally, this PR:
_reload_countmonotonic counter toAdaptiveWeightsto facilitate efficient cache invalidation in dependent services.tests/benchmark_priority.pyto measure and verify the performance gains.ChatRequestandIssueCreateRequestto reject empty or whitespace-only inputs.Performance Impact:
Average analysis time per issue dropped from 0.1765 ms to 0.0640 ms, a 64% reduction in latency for the prioritization hot path.
PR created automatically by Jules for task 11986845432225652175 started by @RohanExploit
Summary by cubic
Speeds up the prioritization hot path by throttling config checks and caching pre-compiled urgency regexes, cutting average analysis time to ~0.060 ms (~66%). Also adds a benchmark and fixes startup by loading env vars.
Refactors
AdaptiveWeightsmtime/JSON checks to every 5s; increment_reload_counton real reloads.PriorityEngine; refresh only when_reload_countchanges.tests/benchmark_priority.pyto track per-analysis latency.Dependencies
python-dotenvtobackend/requirements.txtandbackend/requirements-render.txtfor env loading.Written for commit 4b91621. Summary will update on new commits.
Summary by CodeRabbit
Performance Improvements
Bug Fixes
Documentation