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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ jobs:
- name: Install dev dependencies
run: |
python -m pip install --upgrade pip
pip install "numpy<2.5.0"
pip install -e ".[dev]"

- name: ruff check
Expand Down Expand Up @@ -60,6 +61,7 @@ jobs:
- name: Install package
run: |
python -m pip install --upgrade pip
pip install "numpy<2.5.0"
pip install -e ".[dev]"

- name: Run tests
Expand Down
6 changes: 6 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 2024-05-18 - Fast row-wise Euclidean norm in pure NumPy
**Learning:** In performance-critical paths, computing the batch norm of a 2D array via `np.linalg.norm(arr, axis=1)` is relatively slow. Using `np.sqrt(np.einsum('ij,ij->i', arr, arr))` is significantly faster (~4x speedup on a laptop CPU for typical batch sizes). If `keepdims=True` behavior is needed, appending `[:, np.newaxis]` matches the original shape seamlessly.
**Action:** Always prefer `np.sqrt(np.einsum('ij,ij->i', arr, arr))` over `np.linalg.norm(arr, axis=1)` when computing row-wise vector norms in NumPy to eliminate dispatch overhead and improve execution speed.
## 2024-07-18 - Batching small writes with bytearray in ChecksumWriter

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add blank lines around heading.

As per static analysis hints, headings should be surrounded by blank lines. Without these blank lines, some Markdown parsers may fail to render this line as a heading, breaking the document structure.

🛠️ Proposed fix
+
 ## 2024-07-18 - Batching small writes with bytearray in ChecksumWriter
+
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
## 2024-07-18 - Batching small writes with bytearray in ChecksumWriter
## 2024-07-18 - Batching small writes with bytearray in ChecksumWriter
🧰 Tools
🪛 markdownlint-cli2 (0.23.0)

[warning] 4-4: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Above

(MD022, blanks-around-headings)


[warning] 4-4: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.jules/bolt.md at line 4, Add blank lines immediately before and after the
“2024-07-18 - Batching small writes with bytearray in ChecksumWriter” Markdown
heading in .jules/bolt.md, preserving the heading text and surrounding content.

Source: Linters/SAST tools

**Learning:** In `ChecksumWriter`, frequent small file writes combined with `zlib.crc32` updates can introduce significant overhead. Batching these small writes into a single `bytearray` buffer (e.g., 64KB chunks) reduces system call frequency and CRC32 update overhead, yielding a ~1.4x speedup. Large chunks should bypass the buffer to avoid unnecessary memory allocations.
**Action:** Use a bounded `bytearray` buffer for sequential file writers to aggregate small writes before flushing to disk and calculating checksums, while maintaining a fast-path for large writes.
## 2024-07-18 - CI Type Statement errors with NumPy 2.5
**Learning:** GitHub Actions CI `mypy` jobs might fail with "Type statement is only supported in Python 3.12 and greater" in `numpy/__init__.pyi`. This is caused by `numpy>=2.5.0` adopting new Python 3.12+ syntax for type aliases while the project is pinned to test against `python_version = "3.10"` in `pyproject.toml`.
**Action:** Pin `numpy<2.5.0` during the CI package installation step to restore type-checking compatibility without changing the project's supported target configurations.
27 changes: 24 additions & 3 deletions snapvec/_file_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@

import os
import struct
import typing

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

This import is redundant because IO and Callable are already imported from typing on line 35, and the union type annotation on line 67 can be simplified to bytes | bytearray using the | operator.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use native union syntax.

Since this file already utilizes the PEP 604 union operator | (as seen in the __exit__ method signature), using typing.Union here introduces mixed styling. Adopting the native union operator ensures consistency and allows you to remove the typing import.

♻️ Proposed fixes

Remove the unnecessary import:

-import typing

Use the native union syntax:

-    def write(self, data: typing.Union[bytes, bytearray]) -> int:
+    def write(self, data: bytes | bytearray) -> int:

Also applies to: 67-67

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@snapvec/_file_format.py` at line 31, Replace the remaining typing.Union
annotations in the relevant definitions with native PEP 604 union syntax using
|, consistent with the existing __exit__ signature, then remove the now-unused
typing import.

import zlib
from pathlib import Path
from types import TracebackType
Expand Down Expand Up @@ -60,21 +61,41 @@ def __init__(self, f: IO[bytes]) -> None:
self._f = f
self._crc = 0
self._finalised = False
self._buffer = bytearray()
self._buf_size = 65536

def write(self, data: bytes) -> int:
def write(self, data: typing.Union[bytes, bytearray]) -> int:

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

Since this file already uses the | operator for union types (e.g., str | Path and BaseException | None) and has from __future__ import annotations enabled, we should use bytes | bytearray instead of typing.Union[bytes, bytearray] to maintain consistency with the rest of the codebase.

Suggested change
def write(self, data: typing.Union[bytes, bytearray]) -> int:
def write(self, data: bytes | bytearray) -> int:

if self._finalised:
raise RuntimeError(
"ChecksumWriter.write called after finalise(); the "
"trailer has already been emitted."
)
self._crc = zlib.crc32(data, self._crc)
return self._f.write(data)

data_len = len(data)
if data_len >= self._buf_size:
if self._buffer:
self.flush()
self._crc = zlib.crc32(data, self._crc)
return self._f.write(data)

self._buffer.extend(data)
if len(self._buffer) >= self._buf_size:
self.flush()

return data_len

def flush(self) -> None:
if self._buffer:
self._crc = zlib.crc32(self._buffer, self._crc)
self._f.write(self._buffer)
self._buffer.clear()
Comment on lines +87 to +91

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Cascade flush() to the underlying file.

The introduction of buffering invalidates the contract described in the class docstring (lines 54-58), which advises callers to "use the underlying file directly" to flush. If a caller follows this advice and bypasses the wrapper, any data still in self._buffer will remain unflushed and unwritten, potentially causing data loss.

To maintain expected file-like behavior and ensure data integrity, cascade the wrapper's flush() operation to the underlying file. Additionally, consider updating the docstring to clarify that callers must now flush the wrapper itself.

🛠️ Proposed fix
     def flush(self) -> None:
         if self._buffer:
             self._crc = zlib.crc32(self._buffer, self._crc)
             self._f.write(self._buffer)
             self._buffer.clear()
+        self._f.flush()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def flush(self) -> None:
if self._buffer:
self._crc = zlib.crc32(self._buffer, self._crc)
self._f.write(self._buffer)
self._buffer.clear()
def flush(self) -> None:
if self._buffer:
self._crc = zlib.crc32(self._buffer, self._crc)
self._f.write(self._buffer)
self._buffer.clear()
self._f.flush()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@snapvec/_file_format.py` around lines 87 - 91, Update _file_format.py’s flush
method to always call the underlying file’s flush after draining self._buffer,
ensuring buffered data is written before the file is flushed. Revise the class
docstring guidance to direct callers to flush the wrapper rather than bypassing
it.


def finalise(self) -> None:
"""Write the trailer. Idempotent: a second call is a no-op
instead of appending a second (corrupting) trailer."""
if self._finalised:
return
self.flush()
self._f.write(_TRAILER_MAGIC)
self._f.write(struct.pack("<I", self._crc & 0xFFFFFFFF))
self._finalised = True
Expand Down
Loading