Files
firmware/tools/gcode-bench/fastparse.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

119 lines
4.2 KiB
Python
Executable File

#!/usr/bin/env python3
"""Prototype + equivalence test for a shlex-free fast path in _get_extended_params.
shlex is only needed for shell-style quoting. When the raw parameters carry no
quote and no backslash, a plain split is identical -- and it does not cost
O(n^2) in the length of the longest token.
"""
import argparse, os, re, sys, time
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from bench_define import build
QUOTING = re.compile(r'''['"\\]''')
def fast_params(rawparams, error):
# shlex treats # and ; as comment introducers anywhere outside a quote.
for c in '#;':
i = rawparams.find(c)
if i >= 0:
rawparams = rawparams[:i]
try:
return {k.upper(): v for k, v in
(earg.split('=', 1) for earg in rawparams.split())}
except ValueError:
raise error("malformed")
def install(gc):
orig = gc._get_extended_params
def patched(gcmd):
rawparams = gcmd.get_raw_command_parameters()
if QUOTING.search(rawparams) is None:
eparams = fast_params(rawparams, gc.error)
gcmd._params.clear()
gcmd._params.update(eparams)
return gcmd
return orig(gcmd)
gc._get_extended_params = patched
return orig
def shlex_params(gc, rawparams):
import shlex
s = shlex.shlex(rawparams, posix=True)
s.whitespace_split = True
s.commenters = '#;'
try:
return {k.upper(): v for k, v in (e.split('=', 1) for e in s)}
except ValueError:
return 'ERROR'
def main():
p = argparse.ArgumentParser()
p.add_argument('--klipper', required=True)
p.add_argument('--gcode', nargs='*', default=[])
p.add_argument('--repeat', type=int, default=3)
a = p.parse_args()
gc, eo = build(a.klipper)
# ---- equivalence over every extended command in the given files ----
checked = skipped = 0
bad = []
for path in a.gcode:
with open(path, 'r', errors='replace') as f:
for line in f:
line = line.strip()
if not line or line[0] in ';#':
continue
cmd = line.split(' ', 1)[0].upper()
if gc.is_traditional_gcode(cmd) or not cmd.replace('_', 'A').isalnum():
continue
raw = line[len(cmd):]
if raw[:1].isspace():
raw = raw[1:]
if QUOTING.search(raw) is not None:
skipped += 1
continue
want = shlex_params(gc, raw)
try:
got = fast_params(raw, gc.error)
except Exception:
got = 'ERROR'
checked += 1
if got != want:
bad.append((line[:70], want, got))
print('equivalence: %d extended commands checked, %d quoted (fall back to shlex), %d mismatches'
% (checked, skipped, len(bad)))
for b in bad[:5]:
print(' MISMATCH %r\n shlex=%r\n fast =%r' % b)
# ---- synthetic edge cases ----
edges = ['NAME=A B=2', 'NAME=A ; trailing', 'NAME=A # hash', ' NAME=A B=2 ',
'NAME=A B=', 'CENTER=1,2 POLYGON=[[1,2],[3,4]]', '']
for e in edges:
want, got = shlex_params(gc, e), None
try:
got = fast_params(e, gc.error)
except Exception:
got = 'ERROR'
flag = 'ok ' if got == want else 'DIFF'
print(' %s %-34r shlex=%-38r fast=%r' % (flag, e, want, got))
# ---- timing on the real DEFINE ----
for path in a.gcode:
defines = [l.rstrip('\n') for l in open(path, errors='replace')
if l.startswith('EXCLUDE_OBJECT_DEFINE')]
if not defines:
continue
for label in ('shlex', 'fast'):
g, _ = build(a.klipper)
if label == 'fast':
install(g)
best = None
for _ in range(a.repeat):
t0 = time.perf_counter()
g._process_commands(list(defines), need_ack=False)
dt = time.perf_counter() - t0
best = dt if best is None else min(best, dt)
print('%s: %-5s %8.2f ms' % (os.path.basename(path), label, best * 1e3))
main()