Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@
## 2024-03-16 - Batch JSON File I/O Operations
**Learning:** When using JSON file-backed repositories, iterating over items sequentially and calling `save_item` or `record_attempt` inside a loop leads to N+1 file read/write operations. This creates a significant performance bottleneck, especially when importing progress snapshots with numerous items and attempts.
**Action:** Prefer batch processing methods (e.g., `save_items`, `record_attempts`) so file-backed adapters can load storage once, update it in memory, and write it back in a single pass.

## 2025-03-01 - Avoid full deserialization during batch updates

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The date "2025-03-01" appears to be in the future. Please update this to reflect the actual date the learning was made or a past date to maintain accuracy in the learning log.

**Learning:** Fully deserializing and re-serializing all JSON records during a batch update (like `save_items`) causes unnecessary O(N) CPU and memory overhead, slowing down updates when the dataset is large.
**Action:** Update the raw JSON dictionary entries in-place by matching string-casted IDs, preventing full repository deserialization and speeding up partial batch updates.
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,25 @@ def save_items(self, items: list[LearningItem]) -> None:

storage = self._load_storage()
raw_items = storage.get("items", [])
existing_items = []
if isinstance(raw_items, list):
existing_items = [
_item_from_dict(entry) for entry in raw_items if isinstance(entry, dict)
]
by_id = {existing.id: existing for existing in existing_items}
for item in items:
by_id[item.id] = item
storage["items"] = [_item_to_dict(entry) for entry in by_id.values()]
if not isinstance(raw_items, list):
raw_items = []

# Performance optimization:
# Instead of fully deserializing all items (O(N) CPU overhead),
# we convert the new items to dicts and update the raw JSON list in-place.
# This speeds up partial batch updates significantly when the repository is large.
items_to_save = {str(item.id): _item_to_dict(item) for item in items}

for i, entry in enumerate(raw_items):
if isinstance(entry, dict):
entry_id = str(entry.get("id"))
if entry_id in items_to_save:
raw_items[i] = items_to_save.pop(entry_id)
Comment on lines +60 to +62

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current logic for entry_id = str(entry.get("id")) could lead to an issue if an item in raw_items is a dictionary but lacks an "id" key. In such a case, entry.get("id") would return None, and str(None) would result in the string "None". If items_to_save happens to contain an item with the actual ID "None", this would lead to an incorrect match and potential data corruption or unexpected behavior. It's safer to explicitly check if entry_id_raw is not None before casting it to a string and using it as a key.

Suggested change
entry_id = str(entry.get("id"))
if entry_id in items_to_save:
raw_items[i] = items_to_save.pop(entry_id)
entry_id_raw = entry.get("id")
if entry_id_raw is not None:
entry_id = str(entry_id_raw)
if entry_id in items_to_save:
raw_items[i] = items_to_save.pop(entry_id)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Update every matching ID during batch save

Removing the entry from items_to_save on first match means only the first duplicate record for an ID is updated, while later duplicates are left stale in raw_items. This is a behavior regression from the previous implementation, which rebuilt items through an ID-keyed map and therefore collapsed duplicates deterministically; with this change, repositories initialized with duplicate seed IDs (or legacy duplicated data) can return conflicting versions of the same item from list_items(), causing inconsistent scheduling/progress updates.

Useful? React with πŸ‘Β / πŸ‘Ž.


for new_item in items_to_save.values():
raw_items.append(new_item)

storage["items"] = raw_items
self._save_storage(storage)

def list_attempts(self) -> list[Attempt]:
Expand Down
Loading