Why cTAKES can look idle before the first note
A cTAKES JVM loads a terminology database, analysis engines, and statistical models before it can finish the first document. The wrapper separates that startup work from note processing and refuses a resource plan that does not fit the job boundary.
The run has visible phases before, during, and after note processing
| Phase | What the user may see | What is happening |
|---|---|---|
| Wrapper preflight | Fast checks and printed capacity lines. | Filename, path, versions, hashes, dictionary, WSD, CPU, memory, commit, and disk are checked. |
| JVM initialization | Java starts; no completed note yet. | Classpath and UIMA initialize; dictionary connections and Apache models load. |
| First note | The first START/COMPLETE event pair. | The fully initialized pipeline processes and commits one note. |
| Steady note window | Repeated progress updates and changing ETA. | Notes are processed through the already-loaded engines and writers. |
| Java drain | Note count is complete but Java is still running. | Analysis engines and writers close, flush, and release their runtime resources. |
| Post-Java finalization | Java has exited but the wrapper command has not returned. | CSV export, XMI provenance, sanity read-back, storage/timing receipts, and final manifest status are completed. |
Preflight finishes non-mutating checks before known outputs are cleaned
- Resolve the input, output, work, and optional SQLite paths to canonical locations.
- Reject any input/output/work nesting or SQLite target inside the input tree.
- Walk the complete input batch and enforce strict USDHUB filenames and unique normalized document identities.
- Locate Java 17, cTAKES 6.0.0, compiled wrapper helpers, and the pinned SQLite JDBC jar.
- Resolve the selected dictionary descriptor and strict manifest.
- Hash the descriptor and dictionary database, then compare release/UMLS/hash fields with the manifest.
- Resolve and validate the default WSD model and build receipt against that dictionary.
- Read CPU, memory, commit, and disk availability and calculate the write/heap/thread plan.
- Only after those checks pass, replace known prior wrapper outputs or create the new run workspace.
Read the runner gate: scripts/run_pipeline.sh and the reusable capacity probes in scripts/system_capacity.sh.
The JVM has to assemble the exact extraction system before it processes text
The wrapper creates run_pipeline.effective.piper by taking the reviewed recipe and inserting the selected writers and WSD profile. It also creates a temporary dictionary descriptor whose database path points to the protected runtime copy. Those two files are audit copies of what the JVM was asked to use.
| Initialization work | Why it can be expensive |
|---|---|
| Java/UIMA class loading | cTAKES is a large modular Java application with many analysis engine and type-system classes. |
| Dictionary open | The HSQL database and lookup tables/indexes must be opened before phrase matching. |
| POS/chunk/assertion models | Statistical model resources are loaded once per JVM. |
| Drug NER | The legacy component has shared state and is synchronized by the wrapper. |
| Relation models | Degree and location models add another initialization and inference stage. |
| WSD model open | The validated 116 MB SQLite model opens and checks its schema/completion/profile table. |
Adding more threads does not make every one of these steps parallel. It can increase heap and CAS pressure while synchronized Drug NER remains one-at-a-time.
The timing receipt separates work that earlier logs blended together
| Timing | Start | End | Interpretation |
|---|---|---|---|
| Wrapper wall | Runner begins | Runner exits after final receipts | Everything the user waited for, including preflight and post-run validation. |
| Engine wall | Java process starts | Java process exits | JVM initialization, note processing, and engine/writer shutdown. |
| Startup to first note | Java process starts | First COMPLETE event | Model/dictionary initialization plus the first document. |
| Note window | First START event | Last COMPLETE event | The observed document-processing interval after startup begins producing note events. |
| Drain | Last COMPLETE event | Java exits | Writer/engine shutdown after the final note. |
| Finalization | Java exits | Manifest becomes complete or failed | Merge/export/XMI/sanity/storage/timing and final status work. |
The receipt uses monotonic clocks for elapsed intervals, so a wall-clock adjustment cannot make a phase negative or move an event backward.
Current wrapper 2.1 correctness receipts
| Current fixture | Wrapper | Engine | Startup to first note | Note window | Result |
|---|---|---|---|---|---|
| Direct SQLite, WSD default on | 65.982 s | 43.201 s | 38.678 s | 3.930 s | 139 concepts / 14 medications / 13 abstentions / sanity pass |
| Direct standard with XMI, WSD default on | 34.861 s | 28.233 s | 24.274 s | 3.490 s | 139 concepts / 14 medications / XMI and sanity pass |
| Direct SQLite, explicit WSD off | 62.429 s | 55.588 s | 49.650 s | 5.257 s | Same 139 concepts / 14 medications / blank WSD fields / sanity pass |
| Two-shard async, WSD default on | 60.234 s | 57.269 s | 42.751 s | 12.644 s | 754 concepts / 91 medications / 30 abstentions / sanity pass |
These are literal receipts from completed wrapper 2.1 correctness runs on a busy host. They are not a benchmark and they do not prove that WSD improves speed: cache state, host load, helper compilation, and filesystem activity differed between executions. A site should measure 25–100 representative notes with the intended writer and WSD profile, alternate cold and warm starts, and size from the slower end of the observed trial distribution.
Read the timing derivation: scripts/write_timing_report.py.
WSD startup reuses one completed validation without skipping model checks
The wrapper hashes the model, verifies its build receipt and dictionary binding, opens SQLite, and runs quick_check before Java starts. It then passes three values into Java: wrapper validation is complete, the validated real model path, and the complete model SHA-256.
The annotator reuses that completed validation only when its resolved path and expected hash match. It still checks the schema, build-complete marker, and non-empty profile table. Standalone or mismatched use performs the full validation path.
| Boundary | What must be true |
|---|---|
| Before Java | Model hash, receipt, dictionary binding, SQLite integrity, schema, completion marker, and profile table all pass. |
| Validation reuse | The wrapper-validation flag, resolved model path, and full SHA-256 agree with the model Java opens. |
| Inside Java | Schema, completion marker, and non-empty profiles are checked again before scoring starts. |
| Any mismatch | The full validation path runs. The annotator does not trust a partial or path-only signal. |
This avoids repeating the same full database scan during one startup. It is a startup mechanism, not a claim that WSD makes note processing faster.
Read the WSD scoring and startup boundary, including why this shortcut cannot bypass path/hash matching.
CPU, memory, and Linux commit are all separate limits
| Probe | What it prevents |
|---|---|
| CPU affinity | Using cores the process is not allowed to schedule on. |
| cgroup CPU quota | Treating a container with a two-core quota as the full host. |
MemAvailable | Planning from total installed RAM while other processes already use it. |
| cgroup memory remaining | Crossing a container/job memory ceiling even when the host has free RAM. |
CommitLimit - Committed_AS | Requesting virtual memory the kernel cannot commit. |
| Aggregate async heap | Giving each shard a heap that fits alone but not when all JVMs run together. |
Automatic heap selection uses the smaller safe memory/commit budget, keeps a 512 MiB planning gap, applies the configured percentage, and caps the result. The initial heap is one quarter of maximum with a 512 MiB floor and 1 GiB cap, so Java does not commit the full maximum at launch.
bash scripts/run_pipeline.sh \
--input /approved/usdhub/batch \
--output /approved/usdhub/results \
--xmx 4096
An explicit heap still has to pass current RAM/commit hard guards. --no-autoscale disables automatic selection, not those safety checks.
Storage admission changes with the selected writer
| Writer profile | Source-byte factor | Persistent output |
|---|---|---|
sqlite | 24× | Canonical SQLite + concepts/drugs CSV + receipts |
csv | 96× | Compatibility name for the canonical no-XMI delivery |
standard | 256× | SQLite + CSV + full native XMI |
lean | 48× | Diagnostic per-note output, not a USDHUB delivery |
none | 16× | Pipeline diagnosis with delivery writers disabled |
The factor is an admission estimate, not an output guarantee. A 256 MiB output floor, separate SQLite/work checks, 2 GiB per concurrent async JVM, and a 10 GiB free-space reserve are applied where relevant.
When SQLite, output, work, or async shards use separate capacity checks
- A custom SQLite filesystem gets its own
8×estimate with a 128 MiB floor. For 100 MiB of notes, that is800 + 10,240 = 11,040 MiBfree on the SQLite target. - An output filesystem separate from work must pass its output estimate plus reserve:
2,400 + 10,240 = 12,640 MiBin this example. - A work filesystem separate from output must pass work plus reserve:
2,048 + 10,240 = 12,288 MiB. - Async admission adds
2,048 MiBfor every concurrent shard/JVM. For two shards and the same 100 MiB batch, the aggregate estimate is2,400 + (2 × 2,048) + 10,240 = 16,736 MiBon the checked shared target.
storage_report.json records the persistent bytes and observed work/shard footprint at measurement time. It does not replace infrastructure monitoring for true peak disk, peak RSS, I/O latency, or cgroup throttling.
Read the admission formulas in scripts/run_pipeline.sh and scripts/run_async.sh; read observed post-run categories in scripts/analyze_storage.py.
Async helps only when the notes can amortize another JVM startup
The planner starts from at least 25 notes per JVM, then caps shard count by available documents, CPU, aggregate heap, and storage. Two shards mean two dictionaries and two sets of models in memory.
- Validate the whole batch once and calculate one accepted identity set.
- Partition unique notes across shards.
- Give each child a unique
run_idwhile requiring matching release and analysis inputs. - Run each child into a separate SQLite database and retained log tree.
- Merge child databases into a new aggregate database inside a transaction.
- Regenerate aggregate CSV, XMI provenance, manifest, sanity, storage, and timing receipts.
- Delete temporary shards only after all aggregate checks pass. Preserve them on any child, merge, export, provenance, sanity, or finalization failure.
bash scripts/run_async.sh \
--input /approved/usdhub/batch \
--output /approved/usdhub/results \
--flatten-output
Use the retained events to tell slow startup from a stopped run
tail -100 /approved/usdhub/results/run_pipeline.log
python3 -m json.tool /approved/usdhub/results/timing_report.json
python3 -m json.tool /approved/usdhub/results/run_manifest.json
python3 -m json.tool /approved/usdhub/results/sanity_report.json
| Observation | Read next |
|---|---|
| No Java process and manifest failed before engine launch | Filename, dictionary, WSD, capacity, JDBC, or path error printed by preflight. |
| Java exists but no first START/COMPLETE pair | Engine startup log, dictionary/model initialization, heap/commit, and current host I/O. |
| START without matching COMPLETE | The exception or last progress event for that note; preserve the failed output. |
| All notes complete but command continues | Drain/finalization phase: writer shutdown, export, XMI receipt, sanity, storage, or timing. |
| Async failed | Each retained child run.log, child manifest, and child sanity receipt under the preserved shard tree. |
Resume is restricted because a partial canonical SQLite run cannot satisfy a full-batch run scope. A clean rerun uses a new or intentionally cleared output folder. Any resume must keep input, writer, dictionary, recipe, WSD profile, and release identity unchanged.