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

92 lines
3.3 KiB
Python
Executable File

#!/usr/bin/env python3
"""Drive klipper's real EXCLUDE_OBJECT_DEFINE path with stub printer objects.
Answers two questions: how long does a define block take to process, and
which function inside it spends the time.
"""
import argparse, cProfile, io, os, pstats, sys, time
def load_klippy(root):
sys.path.insert(0, os.path.join(root, 'klippy'))
sys.path.insert(0, root)
import gcode as gcode_mod
sys.modules.setdefault('klippy', type(sys)('klippy'))
from extras import exclude_object
return gcode_mod, exclude_object
class StubMutex:
def __enter__(self): return self
def __exit__(self, *a): return False
class StubReactor:
def mutex(self): return StubMutex()
class StubGCodeMove:
def set_move_transform(self, t, force=False): return None
class StubPrinter:
config_error = RuntimeError
def __init__(self): self.objects = {}
def get_start_args(self): return {}
def register_event_handler(self, *a): pass
def get_reactor(self): return StubReactor()
def lookup_object(self, name, default=None): return self.objects.get(name, default)
def load_object(self, config, name): return self.objects.setdefault(name, StubGCodeMove())
def send_event(self, *a): pass
def invoke_shutdown(self, msg): raise RuntimeError(msg)
class StubConfig:
def __init__(self, printer): self._printer = printer
def get_printer(self): return self._printer
def build(root, quiet=True):
gcode_mod, exclude_object = load_klippy(root)
printer = StubPrinter()
gc = gcode_mod.GCodeDispatch(printer)
printer.objects['gcode'] = gc
if not quiet:
gc.register_output_handler(lambda m: sys.stderr.write(m + '\n'))
else:
gc.register_output_handler(lambda m: None)
gc.is_printer_ready = True
gc.gcode_handlers = gc.ready_gcode_handlers
eo = exclude_object.ExcludeObject(StubConfig(printer))
return gc, eo
def run(gc, lines):
t0 = time.perf_counter()
gc._process_commands(lines, need_ack=False)
return time.perf_counter() - t0
def main():
p = argparse.ArgumentParser()
p.add_argument('--klipper', required=True, help='klipper source root')
p.add_argument('--objects', type=int, default=25)
p.add_argument('--points', type=int, default=64)
p.add_argument('--repeat', type=int, default=3)
p.add_argument('--profile', action='store_true')
a = p.parse_args()
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from genobjects import block
lines = block(a.objects, a.points)
nbytes = sum(len(l) for l in lines)
best = None
for _ in range(a.repeat):
gc, eo = build(a.klipper)
dt = run(gc, lines)
best = dt if best is None else min(best, dt)
print('objects=%d points=%d bytes=%d lines=%d'
% (a.objects, a.points, nbytes, len(lines)))
print('block wall time: %.3f s (%.2f ms/object, %.1f us/byte)'
% (best, best * 1000.0 / a.objects, best * 1e6 / nbytes))
print('defined objects: %d' % len(eo.objects))
if a.profile:
gc, eo = build(a.klipper)
pr = cProfile.Profile()
pr.enable(); run(gc, lines); pr.disable()
s = io.StringIO()
pstats.Stats(pr, stream=s).sort_stats('tottime').print_stats(14)
print(s.getvalue())
if __name__ == "__main__":
main()