Files
firmware/tools/gcode-bench/bench_throughput.py
T
Oleksandr ShyshatskyiandClaude Opus 5 b55461cd6b Cancelling an object leaves the SD reader with nothing to pace it
EXCLUDE_OBJECT installs the ExcludeObject move transform, and inside a
cancelled object _ignore_move updates Python state without ever calling
next_transform.move(). Nothing reaches the toolhead, so the lookahead
buffer that normally blocks gcode.run_script never fills, and
virtual_sdcard.work_handler runs on its own yields alone -- one
reactor.pause per 8192-byte read, about 330 lines apart. The eboard's
soft-PWM refresh misses its deadline and the MCU shuts down with
"Timer too close".

Measured on a Creator 5 Pro: 37 s from "Excluding object" to the
shutdown, with 1 Hz status queries answered in 1.30-1.78 s throughout
while the main MCU sat at mcu_awake=0.000 and gcodein=0.

EXCLUDE_OBJECT_DEFINE is not the cost. The line costs 115 ms through the
live klippy and nothing thereafter; three prints of the same file
without a cancel complete with bytes_retransmit=0. Removing it removes
the object there is to cancel, not the work.

tools/gcode-bench holds the harnesses, including the shlex fast path
that takes that DEFINE from 91.6 ms to 5.9 ms on the printer -- worth
having for large plates, but not this bug.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AVUMfbNuKUWWV32PBcydwb
2026-08-31 15:25:51 +03:00

43 lines
1.5 KiB
Python
Executable File

#!/usr/bin/env python3
"""Measure klipper's gcode dispatch throughput on the move lines of a real file.
Only the parse+dispatch half runs: the handlers are no-ops, so the number is a
floor on what klippy must spend per line before any kinematics happen.
"""
import argparse, os, sys, time
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from bench_define import build
def main():
p = argparse.ArgumentParser()
p.add_argument('--klipper', required=True)
p.add_argument('--gcode', required=True)
p.add_argument('--limit', type=int, default=20000)
p.add_argument('--repeat', type=int, default=3)
a = p.parse_args()
moves = []
with open(a.gcode, 'r', errors='replace') as f:
for line in f:
if line[:2] in ('G1', 'G0') and line[2:3] in (' ', '\n'):
moves.append(line.rstrip('\n'))
if len(moves) >= a.limit:
break
gc, eo = build(a.klipper)
for cmd in ('G0', 'G1', 'G92', 'M204', 'M106', 'M107'):
gc.register_command(cmd, lambda gcmd: None)
gc.gcode_handlers = gc.ready_gcode_handlers
best = None
for _ in range(a.repeat):
t0 = time.perf_counter()
gc._process_commands(moves, need_ack=False)
dt = time.perf_counter() - t0
best = dt if best is None else min(best, dt)
print('move lines: %d' % len(moves))
print('parse+dispatch: %.3f s total, %.3f ms/line, %.0f lines/s'
% (best, best * 1e3 / len(moves), len(moves) / best))
main()