0
0
Fork 0

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) <noreply@anthropic.com>
This commit is contained in:
Karstein Kvistad 2026-04-26 16:53:25 +02:00
parent 2e2ab00ae1
commit 2607063306
6 changed files with 673 additions and 173 deletions

View file

@ -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 ###")

View file

@ -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()", ()))

View file

@ -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=<uuid>, 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)()

View file

@ -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

View file

@ -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 ###")

394
tests/bench-results.json Normal file
View file

@ -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
}
]
}
]
}
}