Skip to content

Commit 9cfe82a

Browse files
ctruedenclaude
andcommitted
Document gaps and tradeoffs of the resident workers
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent d3c83a2 commit 9cfe82a

1 file changed

Lines changed: 171 additions & 0 deletions

File tree

‎GAPS.md‎

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
# Known gaps and tradeoffs
2+
3+
The resident-worker prototype (`_internal` package) trades some simplicity
4+
for speed. This file records what it gives up, what is still missing, and
5+
what would fix each item properly, so that the tradeoffs are chosen on
6+
purpose when the code moves to its permanent homes (see [Where the code
7+
goes](#where-the-code-goes)).
8+
9+
## Concurrency: one worker per environment
10+
11+
**Now:** Runs on the same *worker* execute one at a time, because Appose
12+
reports worker output without saying which task produced it. Serializing
13+
runs per worker is the only way to attribute output correctly.
14+
`ResidentWorkerService` gives each environment exactly one worker, so in
15+
practice runs on the same environment are serialized too. Runs on different
16+
environments proceed in parallel.
17+
18+
**Why not a pool:** `LazyEnvironment` and `ResidentWorker` already allow
19+
several workers per environment. The one-worker rule is only a policy in
20+
`ResidentWorkerService`. It stays because a pool has costs that users would
21+
notice:
22+
23+
- Each worker holds its own copy of whatever a script loads. Two workers
24+
running a GPU model need twice the GPU memory.
25+
- State kept with `task.export(...)` lives in one worker. A run that lands
26+
on a different worker loads the model again, so run times would vary
27+
unpredictably.
28+
29+
**Proper fix:** Have Appose attribute output to tasks, e.g. by having the
30+
Python worker redirect `sys.stdout`/`sys.stderr` per task thread and tag
31+
each line with the task's UUID. Then a worker could run tasks concurrently,
32+
and serialization would be needed only when the script itself is not
33+
thread-safe (which is common for GPU models). A pool, if ever wanted, should
34+
be opt-in per environment and capped.
35+
36+
## Worker lifetime and memory
37+
38+
**Now:** A worker lives until one of these happens:
39+
40+
- the application exits;
41+
- the SciJava context is disposed;
42+
- its environment's configuration changes (the old worker is released);
43+
- it is killed by the cancel timeout (see below);
44+
- it crashes.
45+
46+
Until then it keeps all of its memory, including GPU memory.
47+
`ResidentWorkerService.releaseAll()` exists, but nothing in the UI calls it.
48+
49+
**Gaps:**
50+
51+
- **No way for a user to free memory.** A *Plugins ▸ Scripting ▸ Release
52+
Python Workers* command is a few lines, and should come first.
53+
- **Edited helper modules are not reloaded.** Each run gets a fresh
54+
namespace, but `sys.modules` persists. If a script imports a local
55+
`helpers.py` and the user edits it, the old version keeps running until the
56+
worker restarts. This will confuse people during development. The release
57+
command fixes it by hand. Checking the modification times of imported
58+
local modules could fix it automatically, but may not be worth the effort.
59+
- **No idle timeout, on purpose.** From inside the process, "idle" and
60+
"finished" look the same (scikit-ops design 0019 reaches the same
61+
conclusion). Freeing memory should be an explicit action or a rule
62+
applied before a known-heavy task, not a timer.
63+
64+
## Cancelation
65+
66+
**Now:** Canceling a run's SciJava task sends a cancel request to the Appose
67+
task. Scripts that check `task.cancel_requested` stop by themselves, and
68+
their worker stays alive. If the script has not stopped after
69+
`SciJavaTasks.CANCEL_GRACE_MILLIS` (3 s), its worker process is killed.
70+
71+
**Tradeoffs:**
72+
73+
- Killing the worker also throws away everything it held, including models
74+
kept with `task.export(...)`, so the next run starts cold. That is the
75+
right trade for a script that ignores cancelation, but users may not
76+
expect it.
77+
- The 3-second grace period is a guess. A script that checks
78+
`cancel_requested` only between slow steps (e.g. once per image of a
79+
batch) may get killed even though it would have stopped on its own.
80+
Making the grace period configurable per script (e.g. a `#@script`
81+
attribute) would be cheap.
82+
83+
**Missing:** Builds cannot be canceled at all. The build task's Cancel button
84+
does nothing. This needs Appose core support: the builders run pixi, uv or
85+
micromamba as subprocesses, which could be destroyed, but the environment
86+
directory would then need cleaning up.
87+
88+
## Environment builds
89+
90+
**Now:** `LazyEnvironment` builds each environment once per instance.
91+
Builds of same-named environments are serialized, because they share a
92+
directory: e.g. an environment file edited while its previous version is
93+
still building.
94+
95+
**Gaps:**
96+
97+
- **The lock only works within one JVM.** Two processes building the same
98+
environment at once can still collide, e.g. two Fiji instances, or Fiji
99+
and a Python front end using the same Appose directory. That needs a
100+
file lock in Appose core.
101+
- **Changes are detected by content only.** A worker is replaced when the
102+
environment file's *contents* change (compared in memory). Appose's own
103+
up-to-date check handles whether the directory on disk needs rebuilding.
104+
- **Up-to-date checks show a task too.** The first run in a session shows a
105+
"Building environment" task even when nothing needs building, because the
106+
up-to-date check happens inside the build. It usually vanishes within a
107+
second, but it flickers. Appose core could report "already up to date"
108+
before a build starts.
109+
110+
## Output routing
111+
112+
**Now:** Worker stdout and stderr reach the script's output panes by parsing
113+
Appose's debug messages (`[WORKER-n] line`, `[SERVICE-n] <INVALID> line`).
114+
Because stderr is read separately from stdout, the wrapper writes a unique
115+
end-marker line to stderr, and the engine waits up to 2 s for it before
116+
returning, so that late stderr lines are not lost or misattributed.
117+
118+
**Fragile because:** The whole mechanism depends on debug message formats,
119+
which are not API. Appose core should offer structured stdout/stderr
120+
callbacks on `Service`, ideally tagged by task (see
121+
[Concurrency](#concurrency-one-worker-per-environment)), making both the
122+
parsing and the end marker unnecessary.
123+
124+
## Shared memory
125+
126+
**Now:** Image outputs converted to another type (e.g. `Img`) are copied,
127+
then their shared memory is unlinked. By Appose convention, the Java side
128+
frees shared memory, even memory the worker allocated. Before the resident
129+
worker, every such output leaked one block.
130+
131+
**Gaps:**
132+
133+
- **Outputs kept as `NDArray` are never unlinked.** Outputs declared as
134+
`NDArray` or `Object` are handed to the caller as is. The caller must call
135+
`shm().unlinkOnClose(true)` before closing them, and nothing tells them
136+
so.
137+
- **Exported inputs pin memory.** If a script exports an object that
138+
references an input array (e.g. `task.export(img=image)`), the worker keeps
139+
that shared memory mapped after Java has unlinked it. On POSIX the memory
140+
stays valid but held; on Windows the named block stays alive. It is freed
141+
when the worker exits.
142+
- **Not verified.** The unlink fix is covered only indirectly by tests;
143+
nothing checks directly that blocks are freed. A shared memory pool in
144+
Appose core (planned) would make ownership explicit.
145+
146+
## Not yet tried
147+
148+
- **Real Fiji GUI.** All tests are headless. The build and run tasks have not
149+
been looked at in Fiji's task widget, and cancel has not been clicked
150+
there.
151+
- **Windows.** Neither the numpy init workaround with a resident worker, nor
152+
killing workers, nor the end-marker handshake has been tested there.
153+
- **Long sessions.** Memory growth over many runs (e.g. exported state,
154+
leaked blocks) has not been measured.
155+
156+
## Where the code goes
157+
158+
| Class | Destination |
159+
| --- | --- |
160+
| `BuildListener`, `LazyEnvironment`, `ResidentWorker` | Appose core |
161+
| `ResidentWorkerService`, `SciJavaTasks` | `scijava/scijava-appose` |
162+
| `NDArrayToImgConverter`, `RAIToNDArrayConverter`, ImageJ2 dependencies | `fiji/fiji-appose` |
163+
164+
Planned changes in Appose core that would retire workarounds here:
165+
166+
- structured, task-tagged worker output;
167+
- build cancelation;
168+
- a file lock for concurrent builds;
169+
- a report that an environment is already up to date, before the build
170+
starts;
171+
- a shared memory pool.

0 commit comments

Comments
 (0)