Files
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

62 lines
2.2 KiB
Python
Executable File

#!/usr/bin/env python3
"""Time klipper's real handling of EXCLUDE_OBJECT lines taken from a gcode file."""
import argparse, cProfile, io, os, pstats, sys, time
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from bench_define import build
def timeit(fn, repeat):
best = None
for _ in range(repeat):
t0 = time.perf_counter(); fn(); dt = time.perf_counter() - t0
best = dt if best is None else min(best, dt)
return best
def main():
p = argparse.ArgumentParser()
p.add_argument('--klipper', required=True)
p.add_argument('--gcode', required=True)
p.add_argument('--repeat', type=int, default=5)
p.add_argument('--profile', action='store_true')
a = p.parse_args()
defines, starts = [], []
with open(a.gcode, 'r', errors='replace') as f:
for line in f:
if line.startswith('EXCLUDE_OBJECT_DEFINE'):
defines.append(line.rstrip('\n'))
elif line.startswith(('EXCLUDE_OBJECT_START', 'EXCLUDE_OBJECT_END')):
starts.append(line.rstrip('\n'))
print('%s: %d DEFINE (%d bytes), %d START/END (%d bytes)'
% (os.path.basename(a.gcode), len(defines), sum(map(len, defines)),
len(starts), sum(map(len, starts))))
for d in defines:
pts = d.count('],[') + 1 if 'POLYGON' in d else 0
print(' DEFINE %d bytes, %d polygon points' % (len(d), pts))
def run_defines():
gc, eo = build(a.klipper)
gc._process_commands(list(defines), need_ack=False)
def run_starts():
gc, eo = build(a.klipper)
gc._process_commands(list(starts), need_ack=False)
def run_setup():
build(a.klipper)
setup = timeit(run_setup, a.repeat)
td = timeit(run_defines, a.repeat) - setup
ts = timeit(run_starts, a.repeat) - setup
print('DEFINE block : %8.1f ms' % (td * 1e3))
print('all START/END : %8.1f ms (%.3f ms each)'
% (ts * 1e3, ts * 1e3 / max(len(starts), 1)))
if a.profile:
gc, eo = build(a.klipper)
pr = cProfile.Profile(); pr.enable()
gc._process_commands(list(defines), need_ack=False)
pr.disable()
s = io.StringIO()
pstats.Stats(pr, stream=s).sort_stats('tottime').print_stats(10)
print(s.getvalue())
main()