Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/instrumentserver/client/proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,12 @@ def _getProxyParameters(self) -> None:
bluePrint=pbp,
setpoints_instrument=self,
)
else:
# the Server can change a unit in place (a Type's unit edit
# propagates to its Instances), so keep it current
param = self.parameters[pn]
if isinstance(param, Parameter):
param.unit = p.unit

delKeys = []
for pn in self.parameters.keys():
Expand Down
19 changes: 7 additions & 12 deletions src/instrumentserver/gui/parameter_manager/logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -408,30 +408,25 @@ def rank_lock_targets(
follower: str,
candidates: Iterable[str],
claims: Mapping[str, Claim],
arm_rel: Optional[str] = None,
) -> List[str]:
"""The arm strip's Target candidates in the mock's completer order.

``arm_rel`` is the Follower's path relative to its Instance (the part
behind the Claiming Type's Instance path), or ``None`` when the
Follower is claimed by no Type; an explicit ``arm_rel`` argument
overrides it, which the Types tab's Type Lock re-target (plan task
5.5) uses to rank for a Type's entry path — there is no claimed
Follower and so nothing to exclude. Rank 0: the candidate's own
Follower is claimed by no Type. Rank 0: the candidate's own
relative path equals ``arm_rel`` (the same leaf on a sibling Instance,
the mock's first pick). Rank 1: ``.<arm_rel>`` occurs in the candidate
(a submodule on the way). Rank 2: everything else. Equal ranks order
alphabetically; the Follower itself is never a candidate. Cycles are
not filtered here: the Server refuses them and the arm strip shows its
error text.
"""
if arm_rel is None:
follower_claim = claims.get(follower)
arm_rel = (
follower[len(follower_claim.instance) + 1 :]
if follower_claim is not None
else None
)
follower_claim = claims.get(follower)
arm_rel = (
follower[len(follower_claim.instance) + 1 :]
if follower_claim is not None
else None
)

def own_rel(candidate: str) -> Optional[str]:
claim = claims.get(candidate)
Expand Down
237 changes: 194 additions & 43 deletions src/instrumentserver/gui/parameter_manager/panels.py

Large diffs are not rendered by default.

223 changes: 163 additions & 60 deletions src/instrumentserver/gui/parameter_manager/widget.py

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions src/instrumentserver/params.py
Original file line number Diff line number Diff line change
Expand Up @@ -1654,6 +1654,9 @@ def set_type_parameter_unit(self, type_name: str, path: str, unit: str) -> None:
propagated.add(full)
if self.has_param(full):
cast(Parameter, self.parameter(full)).unit = unit
global_path = f"_globals.{type_name}.{path}"
if self.has_param(global_path):
cast(Parameter, self.parameter(global_path)).unit = unit
for name in affected:
self._broadcast_type_update(name)

Expand Down Expand Up @@ -2042,6 +2045,21 @@ def _ensure_global_target(self, type_name: str, path: str) -> str:
self._broadcast_parameter_creation(global_path, entry.default, entry.unit)
return global_path

def ensure_global_targets(self) -> List[str]:
"""Create the Globals parameter ``_globals.<type>.<path>`` of every
Type entry that has none yet, with the entry's default value and
unit, and return the created paths; the GUI calls it so every
entry has a global value to edit. Raises like
:meth:`_ensure_global_target`."""
created: List[str] = []
for type_name, definition in self._types.items():
for path in definition.parameters:
global_path = f"_globals.{type_name}.{path}"
if not self.has_param(global_path):
self._ensure_global_target(type_name, path)
created.append(global_path)
return created

# ------------------------------------------------------------------
# Type Locks (plan decision D17, task 3.2)
#
Expand Down
4 changes: 3 additions & 1 deletion src/instrumentserver/server/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,9 @@ def startServer(self) -> bool:
pool.submit(self._handleRouterMessage, identity, message)

# handle wakeup events (one or more workers finished)
if self._wakeup_r in socks and (socks[self._wakeup_r] & zmq.POLLIN):
# the poller reports a plain socket by its file descriptor,
# not by the socket object
if socks.get(self._wakeup_r.fileno(), 0) & zmq.POLLIN:
# Drain the wakeup pipe so it doesn't stay "always readable"
try:
# Read whatever is there; content doesn't matter
Expand Down
22 changes: 22 additions & 0 deletions test/pytest/test_basic_functionality.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import time

from qcodes.math_utils.field_vector import FieldVector


Expand Down Expand Up @@ -71,3 +73,23 @@ def test_sending_and_receiving_arbitrary_objects(cli, start_server):
new_field_vector = FieldVector(101, 102, 103)
magnet.set_field(new_field_vector)
assert magnet.get_field().is_equal(new_field_vector)


def test_the_server_goes_idle_after_answering_requests(cli, start_server):
"""Regression: the Server's request loop is woken when a worker has
finished a request, and must drain that wakeup. Left undrained, every
poll returns at once and an idle Server spins a whole core. After a
few requests the process may use only a fraction of an idle second's
CPU time; the Server runs in this process, on its own thread."""
for _ in range(3):
cli.list_instruments()
time.sleep(0.5) # the last response goes out and the loop settles

start = time.process_time()
time.sleep(1.0)
used = time.process_time() - start

assert used < 0.5, (
f"the process used {used:.2f} s of CPU time in an idle second: "
"the Server's request loop does not go idle"
)
Loading
Loading