8000 refactor: reduce runtime overhead across core, Bridge and clients by wu-hongjun · Pull Request #9 · wu-hongjun/InstantLink · GitHub
[go: up one dir, main page]

Skip to content
Draft
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
23 changes: 13 additions & 10 deletions bridge/src/instantlink_bridge/camera/ftp.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,40 +81,43 @@ def prune_incoming_dir(
"""

moment = time.time() if now is None else now
protected = frozenset(path.resolve() for path in protected_paths)
protected = frozenset(str(path.resolve()) for path in protected_paths)
try:
candidates = [entry for entry in incoming_dir.resolve().iterdir() if entry.is_file()]
except OSError:
LOGGER.warning("ftp.incoming_prune_scan_failed dir=%s", incoming_dir, exc_info=True)
return []

stats: list[tuple[float, int, Path]] = []
for path in candidates:
# The parent is resolved above, so strings preserve exact path equality
# without repeatedly parsing/hashing Path components during retention.
stats: list[tuple[float, int, str]] = []
for candidate in candidates:
try:
info = path.stat()
info = candidate.stat()
except OSError:
continue
stats.append((info.st_mtime, info.st_size, path))
stats.append((info.st_mtime, info.st_size, str(candidate)))

stats.sort(key=lambda entry: entry[0]) # oldest first
total_bytes = sum(size for _, size, _ in stats)
newest = {path for _, _, path in stats[-keep_newest:]} if keep_newest > 0 else set()

removed: list[Path] = []

def drop(path: Path, size: int, reason: str) -> bool:
def drop(path: str, size: int, reason: str) -> bool:
nonlocal total_bytes
original = Path(path)
try:
path.unlink()
original.unlink()
except OSError:
LOGGER.warning("ftp.incoming_prune_unlink_failed path=%s", path, exc_info=True)
return False
total_bytes -= size
removed.append(path)
LOGGER.info("ftp.incoming_pruned file=%s size=%s reason=%s", path.name, size, reason)
removed.append(original)
LOGGER.info("ftp.incoming_pruned file=%s size=%s reason=%s", original.name, size, reason)
return True

survivors: list[tuple[float, int, Path]] = []
survivors: list[tuple[float, int, str]] = []
for mtime, size, path in stats:
age = moment - mtime
if path in protected or age < grace_s:
Expand Down
77 changes: 51 additions & 26 deletions bridge/src/instantlink_bridge/imaging/postprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -545,7 +545,44 @@ def render_adjustments_preview(
return apply_correction(apply_adjustments(source, profile), correction)


# Bound both axes: a row-only limit would still allocate large intermediates
# for panoramic sources. The float working set stays within one 512x256 tile.
_HUE_TILE_WIDTH = 512
_HUE_TILE_HEIGHT = 256


def _apply_hue(image: Image.Image, degrees: int) -> Image.Image:
"""Rotate pointwise HSV hue with a bounded working set for RGB images.

Tiles preserve the per-pixel arithmetic exactly: hue has no neighborhood
or frame geometry dependence. The source and RGB result remain full size;
only the NumPy intermediates are bounded. Small images avoid crop/paste.
"""
width, height = image.size
if (
image.mode != "RGB"
or width == 0
or height == 0
or (width <= _HUE_TILE_WIDTH and height <= _HUE_TILE_HEIGHT)
):
# Preserve the existing helper behavior outside its RGB input contract,
# including empty dimensions and unsupported channel layouts.
return _apply_hue_tile(image, degrees)

result = Image.new("RGB", image.size)
for top in range(0, height, _HUE_TILE_HEIGHT):
for left in range(0, width, _HUE_TILE_WIDTH):
box = (
left,
top,
min(left + _HUE_TILE_WIDTH, width),
min(top + _HUE_TILE_HEIGHT, height),
)
result.paste(_apply_hue_tile(image.crop(box), degrees), (left, top))
return result


def _apply_hue_tile(image: Image.Image, degrees: int) -> Image.Image:
"""Rotate the HSV hue channel by ``degrees`` using NumPy array math.

Converts RGB → HSV, shifts H by ``degrees / 360.0`` (wrapping [0, 1)),
Expand Down Expand Up @@ -599,31 +636,19 @@ def _apply_hue(image: Image.Image, degrees: int) -> Image.Image:
t = v * (1.0 - s * (1.0 - f))

out = np.empty_like(arr)
for channel, (v0, v1, v2, v3, v4, v5) in enumerate(
[
(v, q, p, p, t, v),
(t, v, v, q, p, p),
(p, p, t, v, v, q),
]
):
out[..., channel] = np.where(
i == 0,
v0,
np.where(
i == 1,
v1,
np.where(
i == 2,
v2,
np.where(
i == 3,
v3,
np.where(i == 4, v4, v5),
),
),
),
)

out_u8 = (np.clip(out, 0.0, 1.0) * 255.0 + 0.5).astype(np.uint8)
# Each channel has only four distinct values across the six hue sectors.
# Group identical choices to avoid two full-image selection temporaries
# per channel, preserving the original HSV values and rounding exactly.
out[..., 0] = np.where((i == 0) | (i == 5), v, np.where(i == 1, q, np.where(i == 4, t, p)))
out[..., 1] = np.where((i == 1) | (i == 2), v, np.where(i == 0, t, np.where(i == 3, q, p)))
out[..., 2] = np.where((i == 3) | (i == 4), v, np.where(i == 2, t, np.where(i == 5, q, p)))

# These are the same float32 operations in the same order as the original
# expression. Reuse our private output buffer instead of allocating an
# additional RGB float array for each step.
np.clip(out, 0.0, 1.0, out=out)
out *= 255.0
out += 0.5
out_u8 = out.astype(np.uint8)
result = Image.fromarray(out_u8)
return result
9 changes: 8 additions & 1 deletion bridge/src/instantlink_bridge/manager/diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,13 @@ def _read_redacted(source: SupportBundleSource) -> str | None:
return _redact_text(raw)


def _file_sha256(path: Path) -> str:
"""Hash a completed archive without allocating a copy of its contents."""

with path.open("rb") as archive:
return hashlib.file_digest(archive, "sha256").hexdigest()


def create_support_bundle(
*,
bundles_dir: Path,
Expand Down Expand Up @@ -278,7 +285,7 @@ def create_support_bundle(
contents.append(archive_name)

size_bytes = archive_path.stat().st_size
sha256 = hashlib.sha256(archive_path.read_bytes()).hexdigest()
sha256 = _file_sha256(archive_path)
return SupportBundleResult(
bundle_id=resolved_bundle_id,
archive_path=archive_path,
Expand Down
4 changes: 2 additions & 2 deletions bridge/src/instantlink_bridge/net/linux_snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import sys
import time
from collections.abc import Iterator
from ipaddress import IPv4Address, IPv4Interface
from ipaddress import IPv4Interface

_HEADER = struct.Struct("=IHHII")
_LINK = struct.Struct("=BBHiII")
Expand Down Expand Up @@ -174,7 +174,7 @@ def _parse_addresses(
local = attributes.get(2, attributes.get(1)) # IFA_LOCAL wins over point-to-point peer.
if prefix > 32 or local is None or len(local) != 4:
raise NetlinkSnapshotError("invalid IPv4 snapshot record")
addresses[links[index]].append(IPv4Interface(f"{IPv4Address(local)}/{prefix}"))
addresses[links[index]].append(IPv4Interface((local, prefix)))
return addresses


Expand Down
15 changes: 13 additions & 2 deletions bridge/src/instantlink_bridge/sync/outbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
import tempfile
import threading
import time
from dataclasses import asdict
from hashlib import sha256 as sha256_hasher
from pathlib import Path

Expand Down Expand Up @@ -204,7 +203,19 @@ def _write_index_locked(self, proposed: dict[str, OutboxItem]) -> None:
"""Publish a staged index while holding the mutation lock (or constructing)."""
payload = {
"version": INDEX_VERSION,
"items": [asdict(item) for item in proposed.values()],
# OutboxItem contains only immutable scalars; avoid the generic
# recursive dataclass traversal on every queue mutation.
"items": [
{
"item_id": item.item_id,
"file_name": item.file_name,
"size_bytes": item.size_bytes,
"sha256": item.sha256,
"received_at": item.received_at,
"source_remote_ip": item.source_remote_ip,
}
for item in proposed.values()
],
}
index_path = self._root / INDEX_FILE_NAME
tmp_path = self._root / f"{INDEX_FILE_NAME}.tmp"
Expand Down
Loading
0