Map-step diagnostics¶
When the map (LinkML-Map transformation) step is slow, uses too much memory, or gets killed on real data, turn on diagnostics mode. It's opt-in and has zero cost when off.
make map-data DM_MAP_PROFILE=true DM_INPUT_DIR=... DM_SCHEMA_NAME=... DM_OUTPUT_DIR=...
DM_MAP_PROFILE=true works on any target that runs the map step (map-data,
pipeline), and can be passed to the BDC workflow the same way.
What it captures¶
For each entity, into <mapped-data>/logs/<Entity>.log and alongside it:
- CPU / wall-clock profile — a py-spy
sampling profile at
<mapped-data>/logs/<Entity>.folded. py-spy is pulled ephemerally viauv run --with py-spy, so there's no project dependency to carry; it's fetched (a prebuilt wheel) only when the flag is on. - Peak memory + OOM evidence — the container cgroup's
memory.peak(bytes) andmemory.events(whoseoom_killcounter increments when the container's memory limit — not the instance — kills the process). These land as[diag ...]lines in the entity log.
The exit-code handling is unchanged: a kill still surfaces as a signal exit (137 = SIGKILL/OOM); the cgroup line just confirms why.
Reading the flamegraph¶
<Entity>.folded is collapsed/folded stacks. Easiest viewers, no install:
- speedscope.app — drag the
.foldedfile in. flamegraph.pl <Entity>.folded > flame.svg(Brendan Gregg's FlameGraph), oruv run --with inferno-cli inferno-flamegraph < <Entity>.folded > flame.svg.
Wide plateaus are where wall-clock goes. (In the OOM investigation this is where
the per-file SchemaView rebuild showed up.)
Note: with parallel entities (-j), memory.peak is the whole container's
peak, not per-entity — profile one entity at a time when memory attribution
matters. py-spy output is always per-process.
Runbook: ad-hoc internal tracing on real data (rare)¶
The diagnostics above are external — they answer "where's the time / memory, did it get killed" without touching linkml-map internals, which is why they're safe to keep around. They cannot answer "which rows vanished where inside the engine." That question needs internal instrumentation, and internal instrumentation is throwaway by nature: the useful part is where you hook, and that's specific to the bug you're chasing — you rebuild it per incident, you don't keep a suite (a kept suite goes stale on the probe points and on linkml-map's internals, and projects coverage it doesn't have).
So this is not code we keep — it's the pattern, so a rebuild starts from a known-safe starting line instead of scratch:
-
Reach for it last. First exhaust the external signals (py-spy, cgroup), the exit code (see the map exit-code guard), and output-size comparisons. Only trace internals when those don't localize it and you can't reproduce off a small synthetic input — i.e. you must run on the real data.
-
Inject from a dm-bip-side wrapper, don't fork. A short script that imports linkml-map, monkey-patches the seams you care about (the transform loop in
linkml_map.transformer.engine, the loaders inlinkml_map.loaders), then invokes the CLI. Gate every hook behind an env var so it's inert unless asked. -
Data safety is non-negotiable on protected data. Log only counts, table/column names, and value types — never cell values. This is the rule that makes running on real BDC data acceptable.
-
Capture before the kill. An OOM
SIGKILLcannot be caught, so don't rely on a clean exit: flush counters incrementally and registeratexitplus aSIGTERM/SIGINThandler that dumps current state, so you still get something when the run dies.
The historical implementation of this pattern is archived as the linkml-map tag
archive/probe-ff846709 (row-flow counters + RSS-vs-tracemalloc memory
attribution) — excavate with git checkout tags/archive/probe-ff846709. It's a
reference for the shape, not something to re-adopt wholesale.