For illustrated IDE workflows, see VS Code CPU profiling and Bartman frame profiling; the latter also covers installing the Copperline fork independently of the upstream extension.
The profile.start method on the control protocol captures
per-frame performance data for external profilers, analysis tools, and scripts.
Only one profile capture can run at a time; call profile.stop on an active
capture before starting a new one. path names the capture directory; it
defaults to a new copperline-profile-<timestamp> directory in the
traces folder.
profile.start {"path": "out/profile", "frames": 500, "slots": true,
"memory": true, "screenshots": "last", "pc_samples": true}
# A deferred trigger starts without a RAM baseline:
profile.start {"path": "out/triggered", "frames": 500, "slots": true,
"trigger": {"busy_cck_over": 60000}}
# Precise instruction sampling (registers are optional):
profile.start {"path": "out/profile", "frames": 60, "samples": true,
"registers": true,
"unwind": {"base": "0x123400", "table": "<base64>"},
"relocation_bases": ["0x123400", "0x20a000"],
"code_ranges": [{"base": "0x123400", "size": "0x6000"}]}
# Guest code coverage (no call stacks, no per-frame sidecars):
profile.start {"path": "out/coverage", "frames": 100000, "coverage": true,
"relocation_bases": ["0x123400", "0x20a000"],
"code_ranges": [{"base": "0x123400", "size": "0x6000"}]}
profile.stop
profile.statusEach committed emulated frame appends a JSON object to profile.jsonl in the
output directory. When the capture completes or profile.stop is called, a
profile.json summary is written beside it. Because the records are streamed,
profile.jsonl keeps what was recorded even if emulation stops unexpectedly.
The frames parameter defaults to 500 (about 10 seconds of PAL) and is
capped at 100,000. When the frame budget is reached, recording stops by itself
(profile.status reports done).
Instruction sampling buffers at most 262,144 pending records (about 42 MiB on
64-bit hosts), including interrupt records. If execution outpaces profile
draining, the earliest pending records are kept and later samples are dropped
until the buffer is drained. Copperline logs one warning per capture.
profile.status, the profile.stop result, and profile.json expose
samples_buffer_limit and the cumulative samples_dropped count; sampled
profile.jsonl records also carry the cumulative drop count. A nonzero count
means the instruction profile is incomplete, including its timing totals.
The count survives buffer drains and the frame limit stopping the sampler;
starting a new capture resets it. Bartman binary export fails on overflow
instead of publishing an incomplete capture.
This buffer limit also applies during long debugger steps and runs to a PC, which currently do not drain profiles or advance their capture frame limit. Use normal frame execution for instruction profiles, or stop profiling before those debugger operations.
registers and unwind require "samples": true, and relocation_bases and
code_ranges require samples or coverage. A coverage capture cannot run
while a --coverage run is already counting.
An optional trigger keeps the profiler armed but does not write records until
an absolute emulated frame is reached or a completed frame’s busy colour-clock
count exceeds busy_cck_over. Busy clocks are the frame length minus uaelib
idle markers when the guest supplies them; otherwise the traced frame length is
used. profile.status reports triggered and triggered_at.
Because a deferred trigger omits the preceding slot writes, memory: true
cannot be combined with trigger: an offline consumer would not have a RAM
baseline aligned with the first recorded frame.
A profile capture arms the Frame Analyzer’s bus tracing: cheap owner tracing,
promoted to full per-colour-clock records by "slots": true. Run-ahead input
latency reduction is suspended while the tracing is armed. Tracing is shared
with the Frame Analyzer pane: closing the pane does not interrupt an active
profile capture, and stopping a profile capture leaves tracing enabled if the
pane is still open.
profile.jsonl¶
One JSON object per committed frame:
| Field | Meaning |
|---|---|
frame, seconds | Emulated timeline position. |
idle_cck | Colour clocks declared idle by guest code via uaelib trap markers (null if unused). |
retired | 68k instructions retired during the frame. |
pc | Program counter sample at the frame boundary (pc_samples: true only). |
traced | True if chip-bus tracing covered the frame. |
rows, line_cck, cck_length | Raster geometry: scanline count, clocks per line, and total clocks per frame. |
owner_cck | Clocks granted per chip-bus owner (refresh, bitplane, sprite, disk, audio, copper, blitter, cpu, idle). |
blitter | busy_cck (clocks blitter requested bus) and starve_cck (breakdown of owners that stalled it). |
blits | The blits the frame references, including one still running from the previous frame (at most 64): control words, size, pointers, and start/end beam positions. |
cpu | The CPU’s side of the arbitration: wait_cck (colour clocks the CPU asked for the chip bus and was denied), wait_by (those clocks by denier: refresh, bitplane, sprite, disk, audio, copper, blitter with BLTPRI clear, blitter_nasty with BLTPRI set including its warm-up fence, and port for the 020+ chip port’s own turnaround), wait_by_kind (by the pending access: read, which includes the 68000’s opcode prefetches since the CPU core issues them as plain word reads; fetch for immediate and extension words read outside the prefetch queue; write; custom for custom-register accesses), stall_pcs (up to 16 {"pc", "cck"} entries, the instructions that waited longest, longest first), stall_pcs_distinct and stall_pcs_other (clocks pooled once 4096 distinct PCs are kept). Zero entries are omitted from the maps. |
partial | True if tracing was enabled mid-frame. |
registers | Frame-start snapshot: all 256 custom-register words, chipset_flags, and the AGA palette’s 256 high and low nibbles. Present on every traced frame. |
slots | When "slots": true: run-length encoded per-clock owner grid for each scanline ("12R3B497."). Codes match vAmiga DMA debugger (R refresh, B bitplane, S sprite, D disk, A audio, C copper, L blitter, P CPU, . idle). |
cpu_wait | When "slots": true: the CPU wait grid per scanline in the same run-length encoding. Codes are the denier’s owner letter (R, B, S, D, A, C, L for the blitter with BLTPRI clear), N for the blitter with BLTPRI set, p for the port turnaround, and . where the CPU was not waiting. |
slots_file, slots_record_bytes | When "slots": true: raw full-record sidecar filename and its fixed 24-byte stride. |
instantaneous_slots | When "slots": true and zero-time floppy turbo DMA occurred: ordered {vpos, hpos, ...record} entries that cannot share the one-record-per-clock sidecar slot. |
screenshot, digest | When "screenshots": "every": frame screenshot PNG filename and FNV-1a64 hash digest. |
samples, samples_meta | With "samples": true, the filenames of this frame’s compact instruction stream and Copperline timing metadata. |
sample_count, samples_total, irq_cck | Encoded samples in this frame, cumulative encoded samples, and interrupt-dispatch colour clocks in this frame. |
stall_pcs names the instruction that was executing when each wait began.
On the precise CPU loop that is the current instruction; under [cpu] jit
the PC is republished once per batch, so the attribution is per batch.
If the timeline moves backward (a state load or a reverse step), a
{"marker": "reposition", "frame": N} line is written and the instruction
counters are rebaselined.
profile.json¶
Written when the profile stops. It contains version, the machine
configuration, the capture options, the chip-bus owner names (owners) and
CPU wait classes (cpu_wait_classes), the started/ended timeline points,
frames_written, samples_total, irq_cck, triggered_at, the coverage
summary of a coverage capture, and a snapshot of the registered uaelib
resources (matching debug.resources) for address labelling. rom_symbols
snapshots the running guest’s ROM ranges, resident modules, and live
library/device LVO targets at capture stop; this makes conversion deterministic
while still reflecting SetFunction() patches. It also records systemStackLower,
systemStackUpper, stackLower, and stackUpper, read from ExecBase and
ThisTask when a valid AmigaOS task is running. An unreadable ExecBase makes
all four fields null; an unavailable or implausible ThisTask leaves the two
system fields intact and reports zero for the two task fields.
With "memory": true, chip-ram.bin and slow-ram.bin are written once,
at capture start. Together with the per-slot write records these let an offline
consumer reconstruct memory at any later colour clock without running the
emulator.
Full chip-bus slot records¶
"slots": true writes one slots-NNNNNN-frame-MMMMMM.bin per recorded frame,
where the first number is a monotonic capture sequence and the second is the
emulated frame. The sequence keeps repeated frames distinct after rewind or
state loading. Records are in raster order (VPOS * line_cck + HPOS) and use
this packed, little-endian 24-byte layout:
| Offset | Type | Field |
|---|---|---|
| 0 | u16 | reg: custom-register offset; bit 0x1000 marks a CPU access (0x1000 alone is ordinary CPU memory, while 0x1000 | offset is a CPU custom-register access); 0xffff when not applicable |
| 2 | u8 | kind: idle 0, refresh 1, CPU 2, Copper 3, audio 4, blitter 5, bitplane 6, sprite 7, disk 8, conflict 9 |
| 3 | u8 | subtype: CPU code/data; Copper move/wait/skip; audio 0-3; bitplane 1-8; sprite 0-7; blitter A-D plus fill bit 0x10 and line bit 0x20 |
| 4 | u8 | size: transferred bytes (1 through 4 for CPU/CIA, 2 for ordinary DMA, 4 or 8 for grouped AGA fetches; zero when no data transfer is attached) |
| 5 | u8 | ipl: CPU-visible interrupt level at this slot |
| 6 | u16 | flags: bit 0 write; bit 1 CIA access valid; bit 2 CIA-B; bits 8-11 CIA register; bits 12-14 E-clock phase |
| 8 | u32 | addr: chip/CPU address |
| 12 | u64 | data: transfer data, including 32/64-bit AGA FMODE fetch groups |
| 20 | u32 | events: hardware-edge bits listed below |
The same fields are available live from frame.slots {"row": V} and in the
Frame Analyzer’s selected-slot readout. In the JSON, data is a fixed-width
hexadecimal string so all 64 bits remain exact.
Each frame’s blits array also carries a stable blit ID, start/end frame and
beam positions, direction, fill/line mode, enabled channels, all four
pointers/modulos, A/B shifts, A masks, minterm and formula, effective A/B/C
constant inputs (including BLTBDAT’s write-time-shifted hold latch),
captured-word counts, and clocks used versus stalled. An in-flight blit is
referenced from both adjacent frame records and finalized in both when it
ends. The full DMA words remain in the live trace for blit.render; the
profile keeps their bounded counts while the slot sidecar is the lossless
offline transfer stream.
Floppy turbo DMA deliberately consumes no emulated time, so several transfers
can occur at one beam position and cannot occupy the single raster record for
that colour clock. Those transfers remain zero-time while tracing and are
exported in order as instantaneous_slots in the frame’s JSONL record, with
vpos, hpos, and the same record fields above. Apply them after the ordinary
record at that position when replaying memory. frame.slots exposes the same
ordered entries for its requested row as instantaneous_records.
| Bit | Event |
|---|---|
| 0 | blitter interrupt request |
| 1 | blitter final D write |
| 2 | blitter start or finish (the streamed event name distinguishes the edge) |
| 3 | bitplane fetch/update |
| 4 | Copper WAIT wake |
| 5 | CPU interrupt recognition |
| 6 | INTREQ set |
| 7 | Copper wanted a fixed-DMA-owned slot |
| 8 | no requester received the slot |
| 9 | blitter denied by CPU |
| 10 | CPU denied by BLTPRI blitter |
| 11 | Copper SKIP taken |
| 12-14 | DDFSTRT, DDFSTOP, hard DDF stop |
| 15 | CIA register access |
| 16-26 | VB, VS, LOF, LOL, HBS, HBE, HDIWS, HDIWE, VDIW, HSS, HSE |
| 27-28 | CIA-A and CIA-B IRQ pin assertions |
| 29-30 | CPU STOP entered and STOP IPL wake |
VB, VS, and VDIW are signal-state markers on the first refresh slot of every line; LOF and LOL similarly mark the second refresh slot. HBS/HBE, HSS/HSE, HDIWS/HDIWE, and DDF events mark their programmable comparator edges.
registers.chipset_flags uses bit 0 for AGA, bit 1 for ECS Agnus or newer,
bit 2 for NTSC, bit 3 for interlace, and bit 4 for the LOF field.
When precise sampling or coverage is enabled, the summary’s sampling object
also records cck_per_cpu_cycle, every loaded hunk base and executable range,
the unwind text base and size, and the sidecar layouts. Samples use colour
clocks (CCK), Copperline’s native chipset time unit; cck_per_cpu_cycle
converts the configured CPU clock to that unit.
Precise CPU samples and unwinding¶
"samples": true moves a JIT-configured CPU temporarily onto the precise
per-instruction path. It does not change the emulated timeline. Every retired
instruction records its PC and colour-clock cost. "registers": true appends
D0-D7, A0-A7 and SR. Interrupt entry is a distinct [IRQ] sample; its metadata
contains the interrupt level and exception vector rather than inferring them
from the cost.
Each samples-SSSSSS-frame-NNNNNN.bin is a little-endian u32 stream compatible with
vscode-amiga-debug/WinUAE: leaf-to-root call-stack PCs, 0xffffffff - cck,
then the 17 optional register words. PCs in the supplied text range are
relative to its base; Kickstart PCs in $F80000..$FFFFFF remain absolute.
Samples longer than 65535 CCK are split so their cost word cannot be mistaken
for a PC by existing parsers.
The optional live unwind table has one six-byte row for every two bytes of
text: (cfa_register << 12) | cfa_offset, saved-A5 offset, return-address
offset, all little-endian i16 words. CFA register 13 is A5 and 15 is A7. The
emulator keeps expanded offsets as i32, follows the return address at sample
time, and stops when it leaves the supplied text. copperline-ctl --dap
builds this table directly from the already-loaded DWARF call-frame
information; no objdump process is involved.
samples-SSSSSS-frame-NNNNNN.meta starts with CLSM, u32 version 1, and a u32 row count.
Each row is five little-endian u32 values: total CCK, instruction CCK,
chip-bus-wait CCK, IRQ level, and IRQ vector. The latter two are 0xffffffff
for ordinary instructions. This parallel file lets Copperline reports expose
[Bus wait] below the responsible function while leaving the main stream
compatible with Bartman’s reader.
SSSSSS is a monotonic capture sequence, so revisiting the same emulated
frame through reverse execution or state loading never overwrites an earlier
sidecar. relocation_bases is ordered by hunk and lets the offline converter
map absolute samples from every code hunk. code_ranges lets the live compact
unwinder retain a leaf or caller from a code hunk outside its hunk-0 table while
still stopping at external code. Older captures without relocation data fall
back to the unwind table’s hunk-0 base.
Converting to a CPU profile¶
Convert an offline capture with the same hunk executable and, when applicable, its ELF debug sibling:
copperline-ctl profile-report out/profile --program hello \
--elf hello.elf --out hello.cpuprofileThe default is one merged Chrome DevTools .cpuprofile. Add --per-frame for
one numbered file per captured frame, --format bartman for Bartman’s $amiga
annotations, or repeat --source-map FROM=TO to rewrite recorded build paths.
Functions, source lines, and optimized inline frames come from Copperline’s
native debug-info reader. VS Code opens .cpuprofile files directly; its CPU
profile flame-chart extension adds the graphical flame view. ROM samples use
the captured live names, for example [Kick]exec/AllocMem; older captures
without rom_symbols retain the generic [Kickstart] label.
Guest code coverage (lcov)¶
Line and function coverage of a guest program is written in lcov’s .info
format, so genhtml, VS Code’s coverage extensions and CI coverage services
consume it directly. See VS Code for the
editor workflow.
Collecting¶
Coverage is the lightweight sibling of precise sampling. "coverage": true
on profile.start keeps one saturating hit counter per instruction word
inside the capture’s code_ranges (the program’s code hunks) and a single
total for everything retired outside them: Kickstart, libraries, other
tasks. Nothing is written per frame; the histogram is written once as
coverage.bin when the capture stops (or reaches its frame budget), and
profile.json summarizes it under coverage. relocation_bases is
recorded as for samples, so the offline converter can relocate the
program’s debug information. Without code_ranges every address is counted
in a bounded sparse map (1M distinct addresses), which is enough for a small
program launched by hand. "coverage": true and "samples": true combine
freely; a coverage-only capture needs no unwind table.
Like the sampler, the counter observes retired instructions and feeds
nothing back: the emulated timeline is identical with and without it. It
forces the precise per-instruction loop while armed (a [cpu] jit machine
logs its fallback once) and blocks run-ahead, whose speculative frames
would be counted twice. Reverse steps and state loads are not undone: an
instruction re-executed after a rewind counts again.
coverage.bin starts with CLCV and a little-endian u32 version (1), then
a u32 range count and, per range, base, byte size, word count and that many
u32 counters; then a u32 sparse-entry count and (address, count) pairs;
then u64 outside and total instruction counts.
Converting¶
copperline-ctl profile-report out/coverage --program hello \
--elf hello.elf --format lcov --out hello.info \
--source-map /build/src=/home/me/src
genhtml hello.info -o coverage-html--format lcov reads coverage.bin; a capture made with "samples": true
but without coverage is converted from its precise samples’ leaf PCs
instead. Executed addresses map through the program’s own debug information
(vasm LINE hunks, amiga-gcc DWARF, an ELF sibling; see
Debug information) to one record block per
source file: FN/FNDA for functions (DWARF subprograms, or the code
hunks’ symbols when there is no DWARF), DA for every line that has code,
including lines never executed, and the LF/LH/FNF/FNH totals.
--source-map FROM=TO rewrites the recorded paths. TN names the program.
A line’s count is gcov-like: over each contiguous run of line-table rows for
the line, the highest hit count of any instruction in the run, summed across
the runs, so a for header split into an initialization run and a test run
counts each pass once. A function’s count is the hit count of its entry
instruction (or of its most executed instruction when control only entered
mid-body).
Instructions that map to no source line are never dropped silently. The
file starts with # comment lines, which every lcov reader ignores, that
account for every retired instruction, and the converter prints the same
summary:
# Copperline guest coverage: hello
# 1234567 instruction(s) retired: 4321 on 26 source line(s) in 1 file(s)
# 12 instruction(s) at 3 program address(es) without line information
# 1230234 instruction(s) outside the program (Kickstart, libraries, other tasks)
# lines 24/26 hit, functions 3/3 hit“Without line information” covers a startup stub assembled without
-linedebug, code in a hunk the debug information does not describe, and
data hunks executed by mistake. A program built without any line
information yields only these totals.
--run PROG --coverage FILE¶
The whole loop in one flag:
copperline --factory --noaudio --run build/hello --coverage build/lcov.infoCopperline boots, watches the guest’s LoadSeg() results for the program,
relocates its debug information by the segments the loader reports (a
PROG.elf beside the executable is used automatically), counts from the
program’s first instruction to its return, and writes the lcov file when the
boot script’s completion marker shows the program exited. Counting stops the
moment the program’s CLI drops its seglist (cli_Module), so the command the
script runs next is not counted even when the loader places it over the
program’s freed hunks, as the AROS ROM does with its top-of-memory LoadSeg
allocations.
On its own the flag makes a headless capture run like --screenshot-after:
unpaced, windowless, and ending with the program (or after 60 emulated seconds
if it never loads, leaving an all-zero file that still lists every line and
function). Combine it with --screenshot-after and friends to bound a program
that does not exit; the file is then written when the run ends, or, with
--control-gui/--gdb-gui, when the windowed session closes. While the
program runs, the file is rewritten every 250 frames (five seconds of PAL), so
an interrupted run leaves a current one behind. --coverage-source-map FROM=TO
(repeatable) rewrites source paths as --source-map does. The
DAP adapter’s coverage launch argument passes
the same flag.
Storage overhead¶
Enabling slots keeps one 24-byte record per colour clock live (about 1.6 MiB
for a 313x227 PAL frame) and writes about the same amount per frame, plus the
roughly 2-20 KB run-length encoded grids. Setting "memory": true adds one
copy each of chip and slow RAM. Setting "screenshots": "every" produces 50
PNG images per emulated second in PAL. Precise sampling is larger: without
registers each sample is the call stack plus one word; registers add 68 bytes
per sample. Coverage keeps four bytes per instruction word of the code ranges
in memory and writes them once. All of these options are off by default.
Profiling a saved machine offline¶
copperline-ctl profile boots no guest volume and needs no running server:
copperline-ctl profile scene.clstate --frames 2 --out out/scene
copperline-ctl profile scene.uss --rom kickstart.rom --frames 2 --out out/scene-uss
copperline-ctl profile scene.uss --rom kickstart.rom --frames 2 \
--format bartman --out out/scene.profileThe default --format native writes the native capture directory with
instruction/register samples, DMA slots, replay memory and a screenshot of
every frame. --frames accepts 1-100 (default 1). A native state supplies its
own ROM; a USS file requires the matching ROM and discards a reconstructed
frame before profiling. See
USS coverage and limitations.
Here --format bartman selects the legacy binary file documented under
GDB compatibility. The separate
profile-report --format bartman command still writes an annotated JSON
.cpuprofile from an existing native capture.
For interoperability checks, compile the upstream extension (npm ci,
npx tsc -p .) and run its real parser against the exported file:
node tools/check-bartman-profile.cjs /path/to/vscode-amiga-debug scene.profileStatic size profile¶
For static footprint rather than runtime cost, copperline-ctl size-report PROG [--elf PROG.elf] [--out FILE] emits a .cpuprofile weighted in bytes.
Its hierarchy is hunk, section, and function, with unattributed bytes kept as
an explicit node. The VS Code extension’s Profile File Size command runs
the same converter and opens the result.