From 607f330314a98eef5a0521f0a3e8a14671d324af Mon Sep 17 00:00:00 2001 From: Simon Heybrock Date: Fri, 13 Nov 2026 05:33:45 +0000 Subject: [PATCH 1/2] perf: build the LoKI workflow once per process LokiAtLarmorWorkflow() takes about 0.7 s of pure-Python time, holding the interpreter lock, and the LoKI bindings built it once per request (detector, masks, beam centre, I(Q)). Copying a built workflow takes about 6 ms. The bindings now build it once, lazily, and give each request its own copy, so parameters and providers set by one request do not reach the shared workflow or another request. Measured on the LoKI tutorial files, 24 cores (wall time, peak memory): chain (detector, masks, centre, I(Q)), warm: 7.0-7.5 s -> 4.0-4.1 s 4 x I(Q), serial: 15.0-15.3 s -> 12.2 s (8.2-8.4 GB -> 8.0-8.5 GB) 4 x I(Q), workspace threads: 19.1-20.7 s -> 13.6-14.2 s (~26 GB, unchanged) 4 x I(Q), one stage: 7.4 s -> 6.7-6.8 s (~14 GB, unchanged) Co-Authored-By: Claude Fable 5.2 --- .../src/ess/instruments/loki/bindings.py | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/packages/essinstruments/src/ess/instruments/loki/bindings.py b/packages/essinstruments/src/ess/instruments/loki/bindings.py index 55e73d00..b2f82723 100644 --- a/packages/essinstruments/src/ess/instruments/loki/bindings.py +++ b/packages/essinstruments/src/ess/instruments/loki/bindings.py @@ -12,7 +12,7 @@ """ from collections.abc import Mapping -from functools import reduce +from functools import cache, reduce from operator import or_ from pathlib import Path from typing import Any @@ -79,9 +79,23 @@ """The name of the mask of the angle ranges of a record.""" +@cache +def _base_workflow() -> sciline.Pipeline: + """The workflow of esssans, built once per process since that takes about 0.7 s. + + Requests must not change it, but change a copy, see :func:`_workflow`. + """ + return LokiAtLarmorWorkflow() + + +def _workflow() -> sciline.Pipeline: + """A copy of the workflow of esssans, for one request to change.""" + return _base_workflow().copy() + + def _run_workflow(run: Path) -> sciline.Pipeline: """The workflow of the detector of one run, with no beam-centre correction.""" - workflow = LokiAtLarmorWorkflow() + workflow = _workflow() workflow[Filename[SampleRun]] = str(run) workflow[BeamCenter] = _NO_CORRECTION return workflow @@ -111,7 +125,7 @@ def compute_masks(params: MasksParams) -> MasksOutputs: raise ValueError(f'mask files with the same name: {sorted(names)}') masks: dict[str, sc.Variable] = {} if params.mask_files: - workflow = LokiAtLarmorWorkflow() + workflow = _workflow() workflow[DetectorIDs] = params.detector.counts.coords['detector_number'] # esssans names each mask by the path of its file, which differs by host. by_path = sans.with_pixel_mask_filenames( @@ -126,7 +140,7 @@ def find_beam_centre(params: BeamCentreParams) -> BeamCentreOutputs: # The finder raises its cutoff until pixels are left, so without any it never ends. if not counts.sum().value > 0: raise ValueError('no counts outside the masks to find a beam centre from') - workflow = LokiAtLarmorWorkflow() + workflow = _workflow() workflow[Position[snx.NXsample, SampleRun]] = counts.coords['sample_position'] workflow[Position[snx.NXsource, SampleRun]] = counts.coords['source_position'] # The finder sums the dimensions that the positions lack, and else expects events. @@ -205,7 +219,7 @@ def _transmission( def _iofq_pipeline(fixed: Mapping[str, Any]) -> sciline.Pipeline: - pipeline = LokiAtLarmorWorkflow() + pipeline = _workflow() for provider in ( _detector_masks, _beam_center, From 387b4e07508603da6cbfea68e93374468feade1a Mon Sep 17 00:00:00 2001 From: Simon Heybrock Date: Fri, 13 Nov 2026 05:57:55 +0000 Subject: [PATCH 2/2] docs: say when sharing the LoKI workflow stops being worth it The constructor is slow because sciline keeps each generic key as a node of a networkx graph and rebuilds the graph at each assignment. sciline plans to handle generics differently, so the comment says to measure again then. Co-Authored-By: Claude Fable 5.2 --- packages/essinstruments/src/ess/instruments/loki/bindings.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/essinstruments/src/ess/instruments/loki/bindings.py b/packages/essinstruments/src/ess/instruments/loki/bindings.py index b2f82723..c2609fc8 100644 --- a/packages/essinstruments/src/ess/instruments/loki/bindings.py +++ b/packages/essinstruments/src/ess/instruments/loki/bindings.py @@ -85,6 +85,10 @@ def _base_workflow() -> sciline.Pipeline: Requests must not change it, but change a copy, see :func:`_workflow`. """ + # sciline inserts each generic key as a node of a networkx graph, about 1000 + # here, and rebuilds the graph at each assignment. Sharing one workflow is only + # worth it while that holds: measure the constructor when sciline handles + # generics another way, and build a workflow per request if it is cheap. return LokiAtLarmorWorkflow()