Skip to content

Commit eab126c

Browse files
widgetiiclaude
andauthored
Windowed ACK: 88 KB/s write (96% line efficiency) (#36)
Send W blocks (16 × 16KB = 256KB) back-to-back without waiting for ACKs, then read all W ACKs. Pipelines round-trips, approaching line speed. Falls back to single-block retry on failure. Benchmark (hi3516ev300, 921600 baud, 16MB): Per-packet ACK: 30 KB/s, 9.0 min (33%) Streaming: 77 KB/s, 3.5 min (84%) Windowed (W=16): 88 KB/s, 3.1 min (96%) Theoretical max: 90 KB/s, 3.0 min Co-authored-by: Dmitry Ilyin <widgetii@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2482aa2 commit eab126c

File tree

1 file changed

+88
-26
lines changed

1 file changed

+88
-26
lines changed

src/defib/agent/client.py

Lines changed: 88 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ def unstable(self) -> list[SectorResult]:
9292
WRITE_CHUNK_SIZE = 512
9393
# Max bytes per WRITE block.
9494
WRITE_MAX_TRANSFER = 16 * 1024
95+
# Number of blocks sent before reading ACKs (windowed ACK).
96+
# Higher = less round-trip overhead, but more data at risk on failure.
97+
WRITE_WINDOW = 16 # 16 × 16KB = 256KB in flight
9598

9699
# Optimal baud rate from hi3516ev300 + FT232R benchmarks.
97100
# 921600 is the highest rate verified for both READ and WRITE with
@@ -268,11 +271,13 @@ async def write_memory(
268271
on_progress: Callable[[int, int], None] | None = None,
269272
fast: bool = True,
270273
) -> bool:
271-
"""Write data to device RAM in chunked transfers.
274+
"""Write data to device RAM using windowed ACK.
272275
273-
If fast=True and data > 4KB, switches to DEFAULT_FAST_BAUD.
274-
Splits into WRITE_MAX_TRANSFER-sized blocks to avoid PL011 FIFO
275-
overflow on uncached DDR. Each block uses WRITE_CHUNK_SIZE packets.
276+
Sends W blocks (CMD_WRITE + DATA stream) back-to-back without
277+
waiting for ACKs, then reads all W ACKs. This pipelines the
278+
round-trips, approaching line speed.
279+
280+
Falls back to single-block mode on failure.
276281
"""
277282
self._clear_rx_buffers()
278283

@@ -281,34 +286,91 @@ async def write_memory(
281286

282287
total = len(data)
283288
offset = 0
284-
max_retries = 5
289+
window = min(WRITE_WINDOW, (total + WRITE_MAX_TRANSFER - 1) // WRITE_MAX_TRANSFER)
285290

286291
while offset < total:
287-
block_size = min(WRITE_MAX_TRANSFER, total - offset)
288-
block = data[offset:offset + block_size]
292+
# Send a window of blocks without waiting for ACKs
293+
blocks_sent = 0
294+
window_start = offset
289295

290-
ok = False
291-
for attempt in range(max_retries):
292-
ok = await self._write_block(addr + offset, block)
293-
if ok:
296+
for _ in range(window):
297+
if offset >= total:
298+
break
299+
block_size = min(WRITE_MAX_TRANSFER, total - offset)
300+
block = data[offset:offset + block_size]
301+
crc = zlib.crc32(block) & 0xFFFFFFFF
302+
303+
# Send CMD_WRITE + all DATA packets (no wait)
304+
payload = struct.pack("<III", addr + offset, block_size, crc)
305+
await send_packet(self._transport, CMD_WRITE, payload)
306+
307+
blk_off = 0
308+
seq = 0
309+
while blk_off < block_size:
310+
chunk = min(WRITE_CHUNK_SIZE, block_size - blk_off)
311+
pkt = struct.pack("<H", seq) + block[blk_off:blk_off + chunk]
312+
await send_packet(self._transport, RSP_DATA, pkt)
313+
blk_off += chunk
314+
seq += 1
315+
316+
offset += block_size
317+
blocks_sent += 1
318+
319+
# Now read all ACKs: initial ACK + CRC ACK per block
320+
all_ok = True
321+
for i in range(blocks_sent):
322+
try:
323+
# Initial ACK
324+
cmd, resp = await recv_response(self._transport, timeout=10.0)
325+
if cmd != RSP_ACK or resp[0] != ACK_OK:
326+
all_ok = False
327+
break
328+
329+
# CRC ACK
330+
crc_timeout = max(60.0, WRITE_MAX_TRANSFER / 50000)
331+
cmd, resp = await recv_response(self._transport, timeout=crc_timeout)
332+
if cmd != RSP_ACK or resp[0] != ACK_OK:
333+
all_ok = False
334+
break
335+
except Exception:
336+
all_ok = False
294337
break
295-
logger.warning(
296-
"WRITE retry %d at offset %d/%d",
297-
attempt + 1, offset, total,
298-
)
299-
import asyncio
300-
await asyncio.sleep(0.1)
301338

302-
if not ok:
303-
logger.error(
304-
"WRITE failed at offset %d/%d after %d retries",
305-
offset, total, max_retries,
306-
)
307-
return False
339+
if on_progress:
340+
on_progress(window_start + (i + 1) * WRITE_MAX_TRANSFER, total)
308341

309-
offset += block_size
310-
if on_progress:
311-
on_progress(offset, total)
342+
if not all_ok:
343+
# Fall back to single-block retry for the failed window
344+
logger.warning("Window failed at offset %d, retrying single-block", window_start)
345+
offset = window_start
346+
window = 1 # Degrade to single-block mode
347+
self._clear_rx_buffers()
348+
349+
# Drain any stale ACKs from partially-completed window
350+
import asyncio
351+
await asyncio.sleep(0.5)
352+
port = getattr(self._transport, '_port', None)
353+
if port is not None:
354+
port.reset_input_buffer()
355+
from defib.agent.protocol import _port_buffers
356+
_port_buffers[id(port)] = bytearray()
357+
358+
# Retry this window as single blocks
359+
for _ in range(blocks_sent):
360+
if offset >= total:
361+
break
362+
block_size = min(WRITE_MAX_TRANSFER, total - offset)
363+
block = data[offset:offset + block_size]
364+
ok = await self._write_block(addr + offset, block)
365+
if not ok:
366+
logger.error("Single-block retry failed at %d/%d", offset, total)
367+
return False
368+
offset += block_size
369+
if on_progress:
370+
on_progress(offset, total)
371+
372+
# Restore window for next iteration
373+
window = min(WRITE_WINDOW, (total - offset + WRITE_MAX_TRANSFER - 1) // WRITE_MAX_TRANSFER)
312374

313375
return True
314376

0 commit comments

Comments
 (0)