From 26070633066e94d7fb876ef5f1c3cfd75f8f3fa8 Mon Sep 17 00:00:00 2001 From: Karstein Kvistad Date: Sun, 26 Apr 2026 16:53:25 +0200 Subject: [PATCH] fix: 4 broken-tool fixes (compile json long, connect_to_device LoginMode, create_folder SP21+ fallback, ensure_project_open cross-project switch) + bench results compile_project.py + get_compile_messages.py: - IronPython 2.7's json.dumps cannot serialize System.Int64-backed `long` values, which is what CODESYS's compile-message objects expose as line_number / position. Added _coerce_int + _coerce_str helpers and a shared _build_message_entry function. Three duplicated message-building blocks collapsed into single helper calls. - Defensive `try: json.dumps(...) except TypeError: json.dumps(default=str)` so a stray field that slips past the helpers doesn't kill the emit. connect_to_device.py: - SP21+ may expose the login enum as LoginMode rather than OnlineChangeOption. Extended the candidate sweep to probe both script_engine.LoginMode and script_engine.OnlineChangeOption AND online_app.LoginMode/OnlineChangeOption (some builds attach it to the app object). Added "OnlineChange" + "Login" + "Download" to the preferred-priority list. Added a 3-arg call shape variant for SPs that take (mode, secondary-mode, force-bool). create_folder.py: - parent_object.create_folder() is no longer exposed on every parent type in SP21+. Added two fallback factories tried in order: 1. parent.create_object(typeUuid='85d1215e-6520-4983-9a55-2d39d1f24cb4', name=...) 2. parent.add(script_engine.types.IecFolder, name=...) with detailed warnings when each path fails. Final TypeError now lists every factory tried so a future SP rotation surfaces clearly. ensure_project_open.py: - Uncommented the close-prior-project branch (was a TODO since the initial fork). Cross-project switches in a persistent CODESYS now do save() -> close() -> 500ms pump -> open(target). Without this, projects.open against a different already-primary project fails intermittently on file lock contention or pops a "project in use" modal that freezes the IDE thread. - save() is best-effort: if it raises (transient lock, save-as required) we still proceed with close + open rather than getting stuck in a half-switched state forever. tests/bench-results.json: - Captured timings from a clean run on MCPTest2 (PLCWinNT, 5 lib refs, ~12 POUs). 9 tools x 2 modes x iterations. Headers (mean ms): persistent vs headless -- open_project 7700 vs 40041 (~5x; first call cold) mirror_export 1547 vs 23723 (~15x) list_project_libraries 1565 vs 23322 (~15x) get_all_pou_code 1607 vs 23376 (~15x) save_project 2095 vs 23321 (~11x) create_pou (FB) 1540 vs 23903 (~16x) delete_object 1544 vs 27420 (~18x) bump_project_version 1540 vs 30678 (~20x) bump_project_version #2 1556 vs 37769 (~24x) - set_pou_code FAILED in both modes -- bench harness param-shape issue (multi-line code passed verbatim to triple-quoted-string interpolation doesn't survive the round-trip). Tool itself works fine through the MCP tool call path; bench needs to escape newlines / use the same prepareScriptWithHelpers shape the server uses. Filed for follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/scripts/compile_project.py | 149 ++++++----- src/scripts/connect_to_device.py | 76 ++++-- src/scripts/create_folder.py | 49 +++- src/scripts/ensure_project_open.py | 40 ++- src/scripts/get_compile_messages.py | 138 +++++----- tests/bench-results.json | 394 ++++++++++++++++++++++++++++ 6 files changed, 673 insertions(+), 173 deletions(-) create mode 100644 tests/bench-results.json diff --git a/src/scripts/compile_project.py b/src/scripts/compile_project.py index 3c9da2d..735a220 100644 --- a/src/scripts/compile_project.py +++ b/src/scripts/compile_project.py @@ -1,5 +1,72 @@ import sys, scriptengine as script_engine, os, traceback, json + +def _coerce_int(v): + """IronPython 2.7's json.dumps cannot serialize System.Int64-backed + `long` values, which is what CODESYS's compile-message objects expose + as line_number / position. Coerce to native int. Returns None on + failure so the message still serializes (just with line=null) instead + of taking down the whole emit.""" + if v is None: + return None + try: + return int(v) + except (TypeError, ValueError, OverflowError): + return None + + +def _coerce_str(v): + """Some message fields come back as System.Uri / System.IO.FileInfo / + similar CLR objects whose default __str__ json.dumps refuses. Force a + str() so the value is always serializable.""" + if v is None: + return None + try: + return str(v) + except Exception: + return None + + +def _build_message_entry(msg): + """Extract a JSON-serializable dict from a single compile-message object. + Centralised so both compile_project and get_compile_messages share the + same shape and the same coercion logic.""" + entry = {} + if hasattr(msg, 'severity'): + try: + sev = str(msg.severity).lower() + except Exception: + sev = 'unknown' + if 'error' in sev: + entry['severity'] = 'error' + elif 'warning' in sev: + entry['severity'] = 'warning' + elif 'info' in sev: + entry['severity'] = 'info' + else: + entry['severity'] = sev + else: + entry['severity'] = 'unknown' + text = None + for attr in ('text', 'message'): + if hasattr(msg, attr): + text = _coerce_str(getattr(msg, attr)) + if text is not None: + break + if text is None: + text = _coerce_str(msg) + entry['text'] = text + if hasattr(msg, 'object_name'): + entry['object'] = _coerce_str(msg.object_name) + elif hasattr(msg, 'source'): + entry['object'] = _coerce_str(msg.source) + if hasattr(msg, 'line_number'): + entry['line'] = _coerce_int(msg.line_number) + elif hasattr(msg, 'position'): + entry['line'] = _coerce_int(msg.position) + return entry + + try: print("DEBUG: compile_project script: Project='%s'" % PROJECT_FILE_PATH) primary_project = ensure_project_open(PROJECT_FILE_PATH) @@ -56,29 +123,7 @@ try: if msg_objects is not None: messages_found = True for msg in msg_objects: - entry = {} - if hasattr(msg, 'severity'): - sev = str(msg.severity).lower() - if 'error' in sev: - entry['severity'] = 'error' - elif 'warning' in sev: - entry['severity'] = 'warning' - elif 'info' in sev: - entry['severity'] = 'info' - else: - entry['severity'] = sev - else: - entry['severity'] = 'unknown' - entry['text'] = getattr(msg, 'text', getattr(msg, 'message', str(msg))) - if hasattr(msg, 'object_name'): - entry['object'] = msg.object_name - elif hasattr(msg, 'source'): - entry['object'] = str(msg.source) - if hasattr(msg, 'line_number'): - entry['line'] = msg.line_number - elif hasattr(msg, 'position'): - entry['line'] = msg.position - messages.append(entry) + messages.append(_build_message_entry(msg)) print("DEBUG: Got %d messages from app.get_message_objects()" % len(messages)) except Exception as e: print("DEBUG: app.get_message_objects() failed: %s" % e) @@ -92,29 +137,7 @@ try: if msg_objects is not None: messages_found = True for msg in msg_objects: - entry = {} - if hasattr(msg, 'severity'): - sev = str(msg.severity).lower() - if 'error' in sev: - entry['severity'] = 'error' - elif 'warning' in sev: - entry['severity'] = 'warning' - elif 'info' in sev: - entry['severity'] = 'info' - else: - entry['severity'] = sev - else: - entry['severity'] = 'unknown' - entry['text'] = getattr(msg, 'text', getattr(msg, 'message', str(msg))) - if hasattr(msg, 'object_name'): - entry['object'] = msg.object_name - elif hasattr(msg, 'source'): - entry['object'] = str(msg.source) - if hasattr(msg, 'line_number'): - entry['line'] = msg.line_number - elif hasattr(msg, 'position'): - entry['line'] = msg.position - messages.append(entry) + messages.append(_build_message_entry(msg)) print("DEBUG: Got %d messages from system.get_message_objects()" % len(messages)) except Exception as e: print("DEBUG: system.get_message_objects() failed: %s" % e) @@ -128,34 +151,20 @@ try: if msg_objects is not None: messages_found = True for msg in msg_objects: - entry = {} - if hasattr(msg, 'severity'): - sev = str(msg.severity).lower() - if 'error' in sev: - entry['severity'] = 'error' - elif 'warning' in sev: - entry['severity'] = 'warning' - elif 'info' in sev: - entry['severity'] = 'info' - else: - entry['severity'] = sev - else: - entry['severity'] = 'unknown' - entry['text'] = getattr(msg, 'text', getattr(msg, 'message', str(msg))) - if hasattr(msg, 'object_name'): - entry['object'] = msg.object_name - elif hasattr(msg, 'source'): - entry['object'] = str(msg.source) - if hasattr(msg, 'line_number'): - entry['line'] = msg.line_number - elif hasattr(msg, 'position'): - entry['line'] = msg.position - messages.append(entry) + messages.append(_build_message_entry(msg)) print("DEBUG: Got %d messages from system.get_messages()" % len(messages)) except Exception as e: print("DEBUG: system.get_messages() failed: %s" % e) - messages_json = json.dumps(messages) + # Defensive json.dumps: if a stray field still slips past the coercion + # helpers, retry with a default=str fallback so a single odd type + # doesn't kill the whole emit. The default param converts unknown + # objects via str() instead of raising TypeError. + try: + messages_json = json.dumps(messages) + except TypeError as je: + print("WARN: json.dumps raised %s -- retrying with default=str fallback" % je) + messages_json = json.dumps(messages, default=lambda o: str(o)) print("### COMPILE_MESSAGES_START ###") print(messages_json) print("### COMPILE_MESSAGES_END ###") diff --git a/src/scripts/connect_to_device.py b/src/scripts/connect_to_device.py index df1b037..bf67eb6 100644 --- a/src/scripts/connect_to_device.py +++ b/src/scripts/connect_to_device.py @@ -19,36 +19,74 @@ try: if not hasattr(online_app, 'login'): raise TypeError("Online application does not support login().") - # Discover OnlineChangeOption members defensively. Different SPs expose - # different names. Build candidate enum values in priority order. + # Discover login-mode enum members defensively. Different SPs expose the + # enum under different names and different module locations: + # - Pre-SP21: script_engine.OnlineChangeOption (TryOnlineChange / WithDownload / ...) + # - SP21+: script_engine.LoginMode (rebadged; some members renamed/removed) + # - Some builds attach it to the online_app object instead. + # Probe every known location and merge the discovered members. + enum_sources = [] + for src_name in ('LoginMode', 'OnlineChangeOption'): + if hasattr(script_engine, src_name): + try: + enum_sources.append((src_name, getattr(script_engine, src_name))) + except Exception: + pass + for src_name in ('LoginMode', 'OnlineChangeOption'): + if hasattr(online_app, src_name): + try: + enum_sources.append(('online_app.' + src_name, getattr(online_app, src_name))) + except Exception: + pass + if not enum_sources: + print("DEBUG: No login-mode enum found on script_engine or online_app -- relying on plain-bool fallbacks.") + + # Priority order: prefer "no-download / online change" semantics (least + # invasive), then download variants, then null/none. + preferred_order = ('TryOnlineChange', 'OnlineChangeOnly', 'Try', 'OnlineChange', + 'WithDownload', 'ForceDownload', 'Download', + 'None_', 'None') + enum_candidates = [] - if hasattr(script_engine, 'OnlineChangeOption'): - oc = script_engine.OnlineChangeOption - oc_members = sorted([m for m in dir(oc) if not m.startswith('_')]) - print("DEBUG: OnlineChangeOption members: %s" % oc_members) - # Priority order: prefer "try"-ish (no-download), then "download" variants - for preferred in ('Try', 'TryOnlineChange', 'OnlineChangeOnly', - 'WithDownload', 'ForceDownload', 'None_', 'None'): - if preferred in oc_members: + seen_keys = set() + for src_name, oc in enum_sources: + try: + members = sorted([m for m in dir(oc) if not m.startswith('_')]) + except Exception: + members = [] + print("DEBUG: %s members: %s" % (src_name, members)) + for preferred in preferred_order: + if preferred in members: + key = '%s.%s' % (src_name, preferred) + if key not in seen_keys: + try: + enum_candidates.append((key, getattr(oc, preferred))) + seen_keys.add(key) + except Exception: + pass + for m in members: + key = '%s.%s' % (src_name, m) + if key not in seen_keys: try: - enum_candidates.append((preferred, getattr(oc, preferred))) - except Exception: - pass - # Append all remaining members as fallbacks - for m in oc_members: - if m not in [n for n, _ in enum_candidates]: - try: - enum_candidates.append((m, getattr(oc, m))) + enum_candidates.append((key, getattr(oc, m))) + seen_keys.add(key) except Exception: pass # Build call-shape candidates for login(): a list of (description, args-tuple). call_shapes = [] for nm, val in enum_candidates: + # Two-arg shape (most common SP21+): (mode, force_download_bool). call_shapes.append(("login(%s, False)" % nm, (val, False))) call_shapes.append(("login(%s, True)" % nm, (val, True))) + # One-arg shape (older). call_shapes.append(("login(%s)" % nm, (val,))) - # Also try plain bools and no-arg as fall-backs (for very old SPs) + # Three-arg shape some builds use: (mode, secondary-mode, bool). Try with + # the strongest "do nothing" pair we can find at the front of candidates. + if enum_candidates: + first_nm, first_val = enum_candidates[0] + call_shapes.append(("login(%s, %s, False)" % (first_nm, first_nm), (first_val, first_val, False))) + # Also try plain bools and no-arg as fall-backs (for very old SPs). call_shapes.append(("login(False)", (False,))) call_shapes.append(("login(True)", (True,))) call_shapes.append(("login()", ())) diff --git a/src/scripts/create_folder.py b/src/scripts/create_folder.py index 0944a8b..ba6438c 100644 --- a/src/scripts/create_folder.py +++ b/src/scripts/create_folder.py @@ -46,12 +46,51 @@ try: parent_name = getattr(parent_object, 'get_name', lambda: str(parent_object))() print("DEBUG: Using parent object: %s" % parent_name) - # Create the folder - if not hasattr(parent_object, 'create_folder'): - raise TypeError("Parent object '%s' of type %s does not support create_folder." % (parent_name, type(parent_object).__name__)) + # Create the folder. The factory shape changed across SPs: + # - Older: parent.create_folder(name='X') -- legacy API + # - SP21+: parent.create_folder(name='X') -- still preferred + # OR parent.create_object(typeUuid=, name='X') + # OR parent.add(script_engine.types.IecFolder, name='X') + # Some Application objects in SP21+ don't expose create_folder at all; + # fall through to the alternate factories so the tool works against + # both old and new project shapes. + new_folder = None + if hasattr(parent_object, 'create_folder'): + try: + print("DEBUG: Calling parent.create_folder(name='%s')" % FOLDER_NAME) + new_folder = parent_object.create_folder(name=FOLDER_NAME) + except Exception as e: + print("WARN: parent.create_folder() raised: %s -- trying alternate factories." % e) + new_folder = None - print("DEBUG: Calling create_folder: Name='%s'" % FOLDER_NAME) - new_folder = parent_object.create_folder(name=FOLDER_NAME) + if new_folder is None and hasattr(parent_object, 'create_object'): + # CODESYS folder type UUID. The canonical "generic IEC folder" type + # ID has been stable across SP19-SP22; verified via the SP22 stub + # Stubs/scriptengine/types.pyi and the helpme-codesys.com docs for + # ScriptObject.create_object. If a future SP rotates this UUID, the + # types.IecFolder branch below picks up the canonical reference + # automatically. + FOLDER_TYPE_UUID = '85d1215e-6520-4983-9a55-2d39d1f24cb4' + try: + print("DEBUG: parent.create_folder() unavailable. Trying parent.create_object(typeUuid=%s, name='%s')" % (FOLDER_TYPE_UUID, FOLDER_NAME)) + new_folder = parent_object.create_object(typeUuid=FOLDER_TYPE_UUID, name=FOLDER_NAME) + except Exception as e: + print("WARN: parent.create_object(typeUuid=%s) raised: %s" % (FOLDER_TYPE_UUID, e)) + new_folder = None + + if new_folder is None and hasattr(script_engine, 'types') and hasattr(script_engine.types, 'IecFolder') and hasattr(parent_object, 'add'): + try: + print("DEBUG: Trying parent.add(script_engine.types.IecFolder, name='%s')" % FOLDER_NAME) + new_folder = parent_object.add(script_engine.types.IecFolder, name=FOLDER_NAME) + except Exception as e: + print("WARN: parent.add(types.IecFolder) raised: %s" % e) + new_folder = None + + if new_folder is None: + raise TypeError( + "Parent object '%s' of type %s does not support any known folder-creation factory: " + "tried create_folder, create_object(typeUuid=...), and add(script_engine.types.IecFolder)." % ( + parent_name, type(parent_object).__name__)) if new_folder: new_folder_name = getattr(new_folder, 'get_name', lambda: FOLDER_NAME)() diff --git a/src/scripts/ensure_project_open.py b/src/scripts/ensure_project_open.py index ea6d077..5f32a5d 100644 --- a/src/scripts/ensure_project_open.py +++ b/src/scripts/ensure_project_open.py @@ -60,15 +60,37 @@ def ensure_project_open(target_project_path): # traceback.print_exc() # Optional: Print stack trace primary_project = None # Force reopen by falling through else: - # A *different* project is primary - print("DEBUG: Primary project is '%s', not the target '%s'." % (current_project_path, normalized_target_path)) - # Consider closing the wrong project if causing issues, but for now, just open target - # try: - # print("DEBUG: Closing incorrect primary project '%s'..." % current_project_path) - # primary_project.close() # Be careful with unsaved changes - # except Exception as close_err: - # print("WARN: Failed to close incorrect primary project: %s" % close_err) - primary_project = None # Force open target project + # A *different* project is primary -- close it cleanly + # before opening the target. Without this, projects.open + # against a different already-primary project tends to + # fail (file lock contention) or pop a "project in use" + # modal that freezes the IDE thread, breaking every + # subsequent script call with 60s timeouts. + # + # Save first so unsaved changes aren't lost. If save + # raises (e.g. transient lock), fall through to close + # anyway -- losing in-flight edits is worse than getting + # stuck in a half-switched state forever. + print("DEBUG: Primary project is '%s', not the target '%s'. Closing it before opening target..." % ( + current_project_path, normalized_target_path)) + try: + if hasattr(primary_project, 'save'): + try: + primary_project.save() + print("DEBUG: Saved prior primary before close.") + except Exception as save_err: + print("WARN: Failed to save prior primary (%s) -- continuing with close anyway." % save_err) + primary_project.close() + print("DEBUG: Closed prior primary '%s'." % current_project_path) + # Pump CODESYS so the close transition completes + # before we ask it to open something else. + try: + script_engine.system.delay(500) + except Exception: + pass + except Exception as close_err: + print("WARN: Failed to close prior primary project: %s -- attempting open anyway." % close_err) + primary_project = None # Force open target project except Exception as path_err: # Failed even to get the path of the supposed primary project diff --git a/src/scripts/get_compile_messages.py b/src/scripts/get_compile_messages.py index 69fd67e..119d098 100644 --- a/src/scripts/get_compile_messages.py +++ b/src/scripts/get_compile_messages.py @@ -1,5 +1,65 @@ import sys, scriptengine as script_engine, os, traceback, json + +def _coerce_int(v): + """IronPython 2.7's json.dumps cannot serialize System.Int64-backed + `long` values, which is what CODESYS's compile-message objects expose + as line_number / position. Coerce to native int.""" + if v is None: + return None + try: + return int(v) + except (TypeError, ValueError, OverflowError): + return None + + +def _coerce_str(v): + """Force str() on CLR-typed fields (System.Uri etc.).""" + if v is None: + return None + try: + return str(v) + except Exception: + return None + + +def _build_message_entry(msg): + entry = {} + if hasattr(msg, 'severity'): + try: + sev = str(msg.severity).lower() + except Exception: + sev = 'unknown' + if 'error' in sev: + entry['severity'] = 'error' + elif 'warning' in sev: + entry['severity'] = 'warning' + elif 'info' in sev: + entry['severity'] = 'info' + else: + entry['severity'] = sev + else: + entry['severity'] = 'unknown' + text = None + for attr in ('text', 'message'): + if hasattr(msg, attr): + text = _coerce_str(getattr(msg, attr)) + if text is not None: + break + if text is None: + text = _coerce_str(msg) + entry['text'] = text + if hasattr(msg, 'object_name'): + entry['object'] = _coerce_str(msg.object_name) + elif hasattr(msg, 'source'): + entry['object'] = _coerce_str(msg.source) + if hasattr(msg, 'line_number'): + entry['line'] = _coerce_int(msg.line_number) + elif hasattr(msg, 'position'): + entry['line'] = _coerce_int(msg.position) + return entry + + try: print("DEBUG: get_compile_messages script: Project='%s'" % PROJECT_FILE_PATH) primary_project = ensure_project_open(PROJECT_FILE_PATH) @@ -41,29 +101,7 @@ try: if msg_objects is not None: messages_found = True for msg in msg_objects: - entry = {} - if hasattr(msg, 'severity'): - sev = str(msg.severity).lower() - if 'error' in sev: - entry['severity'] = 'error' - elif 'warning' in sev: - entry['severity'] = 'warning' - elif 'info' in sev: - entry['severity'] = 'info' - else: - entry['severity'] = sev - else: - entry['severity'] = 'unknown' - entry['text'] = getattr(msg, 'text', getattr(msg, 'message', str(msg))) - if hasattr(msg, 'object_name'): - entry['object'] = msg.object_name - elif hasattr(msg, 'source'): - entry['object'] = str(msg.source) - if hasattr(msg, 'line_number'): - entry['line'] = msg.line_number - elif hasattr(msg, 'position'): - entry['line'] = msg.position - messages.append(entry) + messages.append(_build_message_entry(msg)) print("DEBUG: Got %d messages from app.get_message_objects()" % len(messages)) except Exception as e: print("DEBUG: app.get_message_objects() failed: %s" % e) @@ -77,29 +115,7 @@ try: if msg_objects is not None: messages_found = True for msg in msg_objects: - entry = {} - if hasattr(msg, 'severity'): - sev = str(msg.severity).lower() - if 'error' in sev: - entry['severity'] = 'error' - elif 'warning' in sev: - entry['severity'] = 'warning' - elif 'info' in sev: - entry['severity'] = 'info' - else: - entry['severity'] = sev - else: - entry['severity'] = 'unknown' - entry['text'] = getattr(msg, 'text', getattr(msg, 'message', str(msg))) - if hasattr(msg, 'object_name'): - entry['object'] = msg.object_name - elif hasattr(msg, 'source'): - entry['object'] = str(msg.source) - if hasattr(msg, 'line_number'): - entry['line'] = msg.line_number - elif hasattr(msg, 'position'): - entry['line'] = msg.position - messages.append(entry) + messages.append(_build_message_entry(msg)) print("DEBUG: Got %d messages from system.get_message_objects()" % len(messages)) except Exception as e: print("DEBUG: system.get_message_objects() failed: %s" % e) @@ -113,34 +129,16 @@ try: if msg_objects is not None: messages_found = True for msg in msg_objects: - entry = {} - if hasattr(msg, 'severity'): - sev = str(msg.severity).lower() - if 'error' in sev: - entry['severity'] = 'error' - elif 'warning' in sev: - entry['severity'] = 'warning' - elif 'info' in sev: - entry['severity'] = 'info' - else: - entry['severity'] = sev - else: - entry['severity'] = 'unknown' - entry['text'] = getattr(msg, 'text', getattr(msg, 'message', str(msg))) - if hasattr(msg, 'object_name'): - entry['object'] = msg.object_name - elif hasattr(msg, 'source'): - entry['object'] = str(msg.source) - if hasattr(msg, 'line_number'): - entry['line'] = msg.line_number - elif hasattr(msg, 'position'): - entry['line'] = msg.position - messages.append(entry) + messages.append(_build_message_entry(msg)) print("DEBUG: Got %d messages from system.get_messages()" % len(messages)) except Exception as e: print("DEBUG: system.get_messages() failed: %s" % e) - messages_json = json.dumps(messages) + try: + messages_json = json.dumps(messages) + except TypeError as je: + print("WARN: json.dumps raised %s -- retrying with default=str fallback" % je) + messages_json = json.dumps(messages, default=lambda o: str(o)) print("### COMPILE_MESSAGES_START ###") print(messages_json) print("### COMPILE_MESSAGES_END ###") diff --git a/tests/bench-results.json b/tests/bench-results.json new file mode 100644 index 0000000..13f05c6 --- /dev/null +++ b/tests/bench-results.json @@ -0,0 +1,394 @@ +{ + "timestamp": "2026-04-26T14:52:18.324Z", + "config": { + "project": "\\\\files\\karstein.kvistad\\Documents\\Claude\\PLC\\MCPTest2\\MCPTest2.project", + "codesys": "C:\\Program Files\\CODESYS 3.5.22.10\\CODESYS\\Common\\CODESYS.exe", + "profile": "CODESYS V3.5 SP22 Patch 1", + "iterations": 2 + }, + "workDir": "C:\\Users\\KARSTE~1.KVI\\AppData\\Local\\Temp\\codesys-mcp-bench-HBzAUg", + "results": { + "headless": [ + { + "id": "open_project", + "kind": "read", + "n": 2, + "min": 22012.8208, + "max": 58068.5602, + "mean": 40040.6905, + "okCount": 2, + "runs": [ + { + "ms": 58068.5602, + "ok": true, + "outputBytes": 1131, + "errorBytes": 0 + }, + { + "ms": 22012.8208, + "ok": true, + "outputBytes": 1131, + "errorBytes": 0 + } + ] + }, + { + "id": "mirror_export", + "kind": "read", + "n": 2, + "min": 23526.9205, + "max": 23919.1101, + "mean": 23723.0153, + "okCount": 2, + "runs": [ + { + "ms": 23919.1101, + "ok": true, + "outputBytes": 1516, + "errorBytes": 0 + }, + { + "ms": 23526.9205, + "ok": true, + "outputBytes": 1516, + "errorBytes": 0 + } + ] + }, + { + "id": "list_project_libraries", + "kind": "read", + "n": 2, + "min": 23288.2306, + "max": 23356.7295, + "mean": 23322.48005, + "okCount": 2, + "runs": [ + { + "ms": 23356.7295, + "ok": true, + "outputBytes": 3981, + "errorBytes": 0 + }, + { + "ms": 23288.2306, + "ok": true, + "outputBytes": 3979, + "errorBytes": 0 + } + ] + }, + { + "id": "get_all_pou_code", + "kind": "read", + "n": 2, + "min": 23359.3891, + "max": 23392.8083, + "mean": 23376.098700000002, + "okCount": 2, + "runs": [ + { + "ms": 23392.8083, + "ok": true, + "outputBytes": 10895, + "errorBytes": 0 + }, + { + "ms": 23359.3891, + "ok": true, + "outputBytes": 10895, + "errorBytes": 0 + } + ] + }, + { + "id": "save_project", + "kind": "read", + "n": 2, + "min": 23089.9258, + "max": 23551.2732, + "mean": 23320.5995, + "okCount": 2, + "runs": [ + { + "ms": 23551.2732, + "ok": true, + "outputBytes": 1312, + "errorBytes": 0 + }, + { + "ms": 23089.9258, + "ok": true, + "outputBytes": 1312, + "errorBytes": 0 + } + ] + }, + { + "id": "create_pou (FB)", + "kind": "write", + "n": 1, + "min": 23902.8924, + "max": 23902.8924, + "mean": 23902.8924, + "okCount": 1, + "runs": [ + { + "ms": 23902.8924, + "ok": true, + "outputBytes": 3475, + "errorBytes": 0 + } + ] + }, + { + "id": "set_pou_code (decl+impl)", + "kind": "write", + "n": 1, + "allFailed": true + }, + { + "id": "delete_object (FB_Bench)", + "kind": "write", + "n": 1, + "min": 27419.9285, + "max": 27419.9285, + "mean": 27419.9285, + "okCount": 1, + "runs": [ + { + "ms": 27419.9285, + "ok": true, + "outputBytes": 2872, + "errorBytes": 0 + } + ] + }, + { + "id": "bump_project_version (build)", + "kind": "write", + "n": 1, + "min": 30678.0223, + "max": 30678.0223, + "mean": 30678.0223, + "okCount": 1, + "runs": [ + { + "ms": 30678.0223, + "ok": true, + "outputBytes": 1746, + "errorBytes": 0 + } + ] + }, + { + "id": "bump_project_version (build #2)", + "kind": "write", + "n": 1, + "min": 37769.477, + "max": 37769.477, + "mean": 37769.477, + "okCount": 1, + "runs": [ + { + "ms": 37769.477, + "ok": true, + "outputBytes": 1746, + "errorBytes": 0 + } + ] + } + ], + "persistent": [ + { + "id": "open_project", + "kind": "read", + "n": 2, + "min": 740.7234, + "max": 14660.0126, + "mean": 7700.368, + "okCount": 2, + "runs": [ + { + "ms": 14660.0126, + "ok": true, + "outputBytes": 1119, + "errorBytes": 0 + }, + { + "ms": 740.7234, + "ok": true, + "outputBytes": 770, + "errorBytes": 0 + } + ] + }, + { + "id": "mirror_export", + "kind": "read", + "n": 2, + "min": 1545.8997, + "max": 1548.8912, + "mean": 1547.39545, + "okCount": 2, + "runs": [ + { + "ms": 1548.8912, + "ok": true, + "outputBytes": 1155, + "errorBytes": 0 + }, + { + "ms": 1545.8997, + "ok": true, + "outputBytes": 1155, + "errorBytes": 0 + } + ] + }, + { + "id": "list_project_libraries", + "kind": "read", + "n": 2, + "min": 1545.7068, + "max": 1583.9429, + "mean": 1564.82485, + "okCount": 2, + "runs": [ + { + "ms": 1545.7068, + "ok": true, + "outputBytes": 3612, + "errorBytes": 0 + }, + { + "ms": 1583.9429, + "ok": true, + "outputBytes": 3612, + "errorBytes": 0 + } + ] + }, + { + "id": "get_all_pou_code", + "kind": "read", + "n": 2, + "min": 1570.3402, + "max": 1642.7852, + "mean": 1606.5627, + "okCount": 2, + "runs": [ + { + "ms": 1570.3402, + "ok": true, + "outputBytes": 10530, + "errorBytes": 0 + }, + { + "ms": 1642.7852, + "ok": true, + "outputBytes": 10530, + "errorBytes": 0 + } + ] + }, + { + "id": "save_project", + "kind": "read", + "n": 2, + "min": 1538.7077, + "max": 2651.2456, + "mean": 2094.97665, + "okCount": 2, + "runs": [ + { + "ms": 2651.2456, + "ok": true, + "outputBytes": 949, + "errorBytes": 0 + }, + { + "ms": 1538.7077, + "ok": true, + "outputBytes": 949, + "errorBytes": 0 + } + ] + }, + { + "id": "create_pou (FB)", + "kind": "write", + "n": 1, + "min": 1539.7142, + "max": 1539.7142, + "mean": 1539.7142, + "okCount": 1, + "runs": [ + { + "ms": 1539.7142, + "ok": true, + "outputBytes": 3088, + "errorBytes": 0 + } + ] + }, + { + "id": "set_pou_code (decl+impl)", + "kind": "write", + "n": 1, + "allFailed": true + }, + { + "id": "delete_object (FB_Bench)", + "kind": "write", + "n": 1, + "min": 1543.7014, + "max": 1543.7014, + "mean": 1543.7014, + "okCount": 1, + "runs": [ + { + "ms": 1543.7014, + "ok": true, + "outputBytes": 2484, + "errorBytes": 0 + } + ] + }, + { + "id": "bump_project_version (build)", + "kind": "write", + "n": 1, + "min": 1540.0761, + "max": 1540.0761, + "mean": 1540.0761, + "okCount": 1, + "runs": [ + { + "ms": 1540.0761, + "ok": true, + "outputBytes": 1379, + "errorBytes": 0 + } + ] + }, + { + "id": "bump_project_version (build #2)", + "kind": "write", + "n": 1, + "min": 1555.9909, + "max": 1555.9909, + "mean": 1555.9909, + "okCount": 1, + "runs": [ + { + "ms": 1555.9909, + "ok": true, + "outputBytes": 1379, + "errorBytes": 0 + } + ] + } + ] + } +} \ No newline at end of file