TASK-1.2: Implement Real Frame Payloads
Status update 2026-04-27: Passes 1 and 2 are merged (Frame fields, SimulatorProfile fields, HighFrameRate seed, SimulatedCamera real
byte[]allocation,MainViewModel.CurrentFrameWriteableBitmap,Get-GcPauseP95/Get-LohAllocRateAvgextraction helpers). Pass 3 (10-minute HighFrameRate capture) was deferred pending SLICE-1.6 FlaUI rig. Pass 3 captured 2026-04-27 under the SLICE-1.6 FlaUI rig — see theslice-1-2-real-frame-payloadsrow indocs/reviews/phase-1-measurements.mdanddocs/captures/slice-1-2-high-fps-2026-04-27.csv.
- Status: All passes complete — SLICE-1.2 closed
- Date: 2026-04-27
- Spec: SLICE-1.2: Real Frame Payloads
- Depends on: TASK-1.1: Implement Multi-Tag Telemetry
Objective
Make Frame.PreviewPayload a real byte[] sized by SimulatorProfile.FrameWidth × FrameHeight × BytesPerPixel, allocated and filled per frame in SimulatedCamera. Bind a WriteableBitmap in the UI so the preview actually renders. Add a HighFrameRate simulator profile (2 MP × 8-bit @ 30 fps), extend the measurement extraction with gc-pause-p95 and LOH-alloc-rate avg, and capture the slice-1-2-real-frame-payloads row.
Scope
- extend
FramewithWidth,Height,BytesPerPixel - extend
SimulatorProfilewith the same three fields and validate them - add the
HighFrameRateprofile to seedappsettings.json - update
SimulatedCamera.ProduceFramesAsyncto allocate the per-framebyte[]and fill it with the deterministic gradient pattern from the spec - expose
WriteableBitmap CurrentFrameonMainViewModel; bind it fromMainWindow.xaml; marshal frame updates onto the dispatcher - add
Get-GcPauseP95andGet-LohAllocRateAvghelpers totools/MeasurementExtraction.psm1; haveConvertTo-MeasurementRowemit an 18-metric block when the captured CSV contains the new metrics - add §4.3 (HighFrameRate soak) to
docs/runbook/capturing-measurements.md - run a 10-minute HighFrameRate capture under
MultiTagSoakScenario --profile HighFrameRate, append the 18-metric row block todocs/reviews/phase-1-measurements.md - add tests for:
Framefield round-trip,SimulatedCamerapayload size,SimulatorProfilesValidatorrejecting each invalid case,MeasurementExtractionhelpers against a fixture CSV
Non-Scope
- buffer pooling (
ArrayPool<byte>, slab allocator, etc.) — Phase 2 - moving frame data out of
AppState.LatestFrame - compression, encoding, or any non-raw payload format
- ROI / crop / downscale / image processing
- defect-marker pixels in the frame content (defects remain a metadata field)
- new
InspectionPrototypecounters (the slice's metric story uses existing System.Runtime counters only) - a new
IScenario—MultiTagSoakScenariowith--profile HighFrameRateis the capture path - adapting
NormalorMultiTagprofiles to use larger frames — they ship640 × 480 × 1so existing rows 0/0a/0b/slice-1-1 remain reproducible
Touched Projects
src/InspectionPrototype.Domain—Framerecord fieldssrc/InspectionPrototype.Application— nothing (the consumer doesn't care about size)src/InspectionPrototype.Infrastructure—SimulatorProfile,SimulatorProfilesOptions,SimulatorProfilesValidator,SimulatedCamera.ProduceFramesAsyncsrc/InspectionPrototype.App—appsettings.json(HighFrameRate profile + new fields on existing profiles)src/InspectionPrototype.Presentation—MainViewModel.CurrentFrame, dispatcher marshalling, frame→bitmap conversionsrc/InspectionPrototype.App—MainWindow.xamladds the<Image>elementtests/InspectionPrototype.Tests—Framefield tests,SimulatedCamerapayload-size tests, validator tests,MainViewModelbitmap-update testtools/MeasurementExtraction.psm1— two new helpers,ConvertTo-MeasurementRowemits 18-metric blocktests/Tools/MeasurementExtraction.Tests.ps1— Pester tests for the new helpersdocs/runbook/capturing-measurements.md— §4.3docs/reviews/phase-1-measurements.md— new row blockdocs/captures/— the new CSV evidence
AI Tool Guidance
Three Copilot passes; one-pass-per-session protocol as in TASK-1.5.
- Domain + simulator — Frame fields, SimulatorProfile fields + validator, HighFrameRate seed config, SimulatedCamera payload generation. Tests for the simulator path. NO UI work, NO measurement extraction work.
- UI + measurement extraction — MainViewModel CurrentFrame property, MainWindow Image binding, dispatcher marshalling. Pester-tested MeasurementExtraction helpers + 18-metric ConvertTo-MeasurementRow output. NO captures.
- Capture + row block + runbook — Run the 10-min HighFrameRate scenario with sleep disabled, append the row block, write §4.3, update CLAUDE.md / roadmap-progress.md. NO code changes.
Acceptance Criteria Mapping
The implementation must satisfy all acceptance criteria from SLICE-1.2:
- Pass 1 covers criteria 1, 2, 3, 4, 6 (frames.dropped, frames.ingested counts) and the simulator/validator portions of 10 and 11
- Pass 2 covers criterion 5, criterion 9, and the extraction-helper portion of 10
- Pass 3 covers criteria 7 and 8
Copilot Agent Prompts
Pass 1 — Domain + simulator
You are implementing Pass 1 of TASK-1.2 in this repository: extend Frame and
SimulatorProfile with Width/Height/BytesPerPixel, add the HighFrameRate seed
profile, and update SimulatedCamera.ProduceFramesAsync to allocate and fill
real byte[] payloads. NO UI work, NO measurement-extraction work.
## Authoritative references
Read these before making changes:
- docs/specs/SLICE-1.2-real-frame-payloads.md (the requirements)
- docs/tasks/TASK-1.2-implement-real-frame-payloads.md (this task)
- src/InspectionPrototype.Domain/Contracts/Frame.cs
- src/InspectionPrototype.Application/State/SimulatorProfile.cs
- src/InspectionPrototype.Infrastructure/Simulator/SimulatorProfilesOptions.cs
- src/InspectionPrototype.Infrastructure/Simulator/SimulatorProfilesValidator.cs (or wherever profile validation lives)
- src/InspectionPrototype.Infrastructure/Simulator/SimulatedCamera.cs
- src/InspectionPrototype.App/appsettings.json
Spec acceptance criteria 1, 2, 3, 4, parts of 6, and the simulator/validator
portions of 10 and 11 are the definition of done for this pass.
## Scope of this pass
Domain record extension, profile extension + validation, seed config, simulator
allocation/fill, simulator tests. NO MainViewModel changes, NO MainWindow.xaml
changes, NO MeasurementExtraction.psm1 changes.
## Deliverables
1. Frame record (src/InspectionPrototype.Domain/Contracts/Frame.cs):
- add three properties to the record:
int Width
int Height
byte BytesPerPixel
- keep PreviewPayload as byte[]? (no nullability change)
- update any existing test or stub that constructs Frame inline; pick small
defaults (16 x 16 x 1) so tests don't allocate large arrays
2. SimulatorProfile (src/InspectionPrototype.Application/State/SimulatorProfile.cs):
- add three properties:
int FrameWidth (default 640)
int FrameHeight (default 480)
byte BytesPerPixel (default 1)
- update the corresponding SimulatorProfilesOptions / serialization shape
so JSON binding picks them up
- existing profiles in appsettings.json (Normal, MultiTag) inherit the
defaults (640 x 480 x 1) — explicit values are fine but not required
3. Validator (SimulatorProfilesValidator):
- reject any profile where:
FrameWidth < 1 OR FrameHeight < 1
BytesPerPixel not in {1, 2, 3, 4}
checked((long)FrameWidth * FrameHeight * BytesPerPixel) > int.MaxValue
- validation messages name the offending profile name and field
- register via services.AddOptions<SimulatorProfilesOptions>().Validate(...)
with ValidateOnStart() — match the existing pattern
4. appsettings.json:
- add a new profile entry "HighFrameRate":
FrameIntervalMs: 33
FrameWidth: 2048
FrameHeight: 1024
BytesPerPixel: 1
TelemetryIntervalMs: 100
MotionSpeedUnitsPerSecond: 50.0
DefectProbabilityPerFrame: 0.05
ConnectionFailureProbability: 0.05
- existing Normal and MultiTag profiles do NOT need to be edited — defaults
of 640 / 480 / 1 will apply
5. SimulatedCamera.ProduceFramesAsync:
- read FrameWidth/FrameHeight/BytesPerPixel from
_profileProvider.CurrentProfile per tick (so profile changes are picked
up on the next frame)
- allocate: var payload = new byte[width * height * bpp]
- fill via the deterministic gradient described in the spec:
for (int y = 0; y < height; y++)
for (int x = 0; x < width; x++) {
byte v = (byte)((x + y + (int)frame.Sequence) & 0xFF);
int o = (y * width + x) * bpp;
for (int b = 0; b < bpp; b++) payload[o + b] = v;
}
- construct Frame with Width/Height/BytesPerPixel and PreviewPayload set
- the existing channel write path is unchanged
6. Tests under tests/InspectionPrototype.Tests/:
- FrameTests.cs (or extend if exists): a Frame constructed with
Width=16, Height=16, BytesPerPixel=1 reports those values; payload of
length 256 round-trips
- SimulatorProfilesValidatorTests.cs (or extend): each of the four
reject cases produces ValidateOptionsResult.Fail naming the profile
- SimulatedCameraPayloadTests.cs (new):
* Construct a SimulatedCamera with a FakeSimulatorProfileProvider
returning a profile with width=8, height=8, bpp=1
* Start streaming, await one frame off the channel
* Assert frame.Width == 8, frame.Height == 8, frame.BytesPerPixel == 1
* Assert frame.PreviewPayload is non-null with Length == 64
* Assert the gradient pattern: payload[0] == (byte)(0 + 0 + sequence),
payload[7] == (byte)(7 + 0 + sequence) (where sequence is the
frame's actual Sequence value)
## Constraints
- Do NOT introduce ArrayPool, MemoryPool, or any pooling. The allocation
pressure is what this slice exists to expose.
- Do NOT add WPF references to the Domain or Infrastructure projects.
- Do NOT rename PreviewPayload or change its type from byte[]?.
- Do NOT touch MainViewModel, MainWindow.xaml, or any view-side code.
- Do NOT touch tools/ or docs/ in this pass.
- Test fixture sizes must be tiny (<=16 x 16) — do not introduce 2 MP
allocations into the test process; that is what criterion 11 asserts.
## Verification before you report done
dotnet build --configuration Release
dotnet test --configuration Release
Manual smoke test:
- launch interactively (no --scenario flag)
- in a fresh dev window, attach dotnet-counters monitor and watch
frames.ingested tick over once you click Connect → Start Run
- Confirm: app does not crash, frames.ingested increments. Preview is still
blank — UI rendering lands in Pass 2.
- Confirm: switching to the HighFrameRate profile via the existing profile
selector does not throw. frames.ingested rate jumps from ~2 fps (Normal,
FrameIntervalMs=500) to ~30 fps (HighFrameRate, FrameIntervalMs=33).
## Report format when finished
- files created and modified
- confirmation that all existing tests pass plus new payload/validator tests
- a single commit hash
- commit message: "feat(sim): add Frame.{Width,Height,BytesPerPixel}, HighFrameRate profile, real byte[] payloads (pass 1/3 of TASK-1.2)"Pass 2 — UI + measurement extraction
You are implementing Pass 2 of TASK-1.2. Pass 1 (Frame fields, SimulatorProfile
fields, HighFrameRate seed, SimulatedCamera payload generation) is already
merged. This pass adds the UI render path (MainViewModel.CurrentFrame +
MainWindow.xaml binding) and the measurement-extraction helpers
(Get-GcPauseP95, Get-LohAllocRateAvg) so captures produce 18-metric blocks.
## Authoritative references
Read these before making changes:
- docs/specs/SLICE-1.2-real-frame-payloads.md (criteria 5, 9; verification on dispatcher work)
- src/InspectionPrototype.Presentation/ViewModels/MainViewModel.cs
- src/InspectionPrototype.App/MainWindow.xaml
- src/InspectionPrototype.Application/Abstractions/IAppStateStore.cs
- tools/MeasurementExtraction.psm1 (existing ConvertTo-MeasurementRow)
- tests/Tools/MeasurementExtraction.Tests.ps1 (existing Pester pattern)
- docs/captures/demo-baseline-2026-04-23.csv (a fixture CSV containing dotnet.gc.pause.time and dotnet.gc.heap.size rows for the helpers' test)
Pass 1 must be merged. Confirm by inspecting Frame and a fresh build of
SimulatedCamera before starting.
## Scope of this pass
UI binding + dispatcher marshalling, measurement-extraction helpers, Pester
tests. NO captures, NO scenario or simulator changes.
## Deliverables
1. MainViewModel.cs:
- add a property:
public WriteableBitmap? CurrentFrame
{
get => _currentFrame;
private set { _currentFrame = value; OnPropertyChanged(); }
}
(use the existing OnPropertyChanged pattern in the file)
- subscribe to IAppStateStore.StateChanged in the constructor (or wherever
other state-driven properties are projected). On every state change with
a non-null state.LatestFrame.PreviewPayload, marshal to the dispatcher:
Application.Current.Dispatcher.InvokeAsync(() => UpdateCurrentFrame(frame));
- UpdateCurrentFrame(Frame frame):
* Compute PixelFormat from BytesPerPixel:
1 -> PixelFormats.Gray8
2 -> PixelFormats.Gray16
3 -> PixelFormats.Bgr24
4 -> PixelFormats.Bgra32
* If CurrentFrame is null OR its PixelWidth/PixelHeight/Format does not
match the new frame, allocate a NEW WriteableBitmap with the right
shape and assign to CurrentFrame
* Otherwise reuse the existing WriteableBitmap
* Lock the bitmap, call WritePixels(new Int32Rect(0, 0, w, h),
frame.PreviewPayload, stride: w * bpp, offset: 0), unlock
* Do NOT call OnPropertyChanged after WritePixels — WriteableBitmap
marks itself dirty and the Image control rebinds automatically
- in scenario mode (no main window) the dispatcher is still spinning;
subscriptions must NOT be skipped — the dispatcher work is the cost
this slice exists to measure
2. MainWindow.xaml:
- locate the existing preview area (placeholder or empty panel)
- add (or replace the placeholder with):
<Image Source="{Binding CurrentFrame}" Stretch="Uniform" />
- if the area has fixed dimensions, leave them — the bitmap will scale
- DO NOT add any other XAML changes. Existing layout is preserved exactly.
3. tools/MeasurementExtraction.psm1:
- add and export Get-GcPauseP95:
function Get-GcPauseP95 {
[CmdletBinding()]
param([Parameter(Mandatory)][object[]] $Csv)
$pauseRows = $Csv | Where-Object {
$_.'Counter Name' -match 'dotnet\.gc\.pause\.time'
} | ForEach-Object { [double]$_.'Mean/Increment' }
if ($pauseRows.Count -eq 0) { return 0.0 }
$sorted = $pauseRows | Sort-Object
$idx = [int][math]::Ceiling($sorted.Count * 0.95) - 1
if ($idx -lt 0) { $idx = 0 }
return [math]::Round($sorted[$idx] * 1000.0, 2) # seconds -> ms
}
- add and export Get-LohAllocRateAvg:
function Get-LohAllocRateAvg {
[CmdletBinding()]
param([Parameter(Mandatory)][object[]] $Csv)
# dotnet.gc.last_collection.heap.size[gc.heap.generation=loh] is a Metric (gauge),
# not a Rate. Average byte/sec is computed from successive deltas
# over their inter-sample times.
$samples = $Csv | Where-Object {
$_.'Counter Name' -match 'dotnet\.gc\.last_collection\.heap\.size.*generation=loh'
} | ForEach-Object {
[pscustomobject]@{
Time = [datetime]::Parse($_.Timestamp)
Bytes = [double]$_.'Mean/Increment'
}
} | Sort-Object Time
if ($samples.Count -lt 2) { return 0.0 }
$totalBytes = 0.0; $totalSec = 0.0
for ($i = 1; $i -lt $samples.Count; $i++) {
$delta = $samples[$i].Bytes - $samples[$i-1].Bytes
# only count GROWTH; collections cause negative deltas which are
# not "allocations"
if ($delta -gt 0) { $totalBytes += $delta }
$totalSec += ($samples[$i].Time - $samples[$i-1].Time).TotalSeconds
}
if ($totalSec -le 0) { return 0.0 }
return [math]::Round($totalBytes / $totalSec, 0)
}
- update ConvertTo-MeasurementRow to call both helpers and append two rows
to the markdown table:
| gc-pause-p95 (ms) | <Get-GcPauseP95 result> |
| LOH-alloc-rate avg (B/s)| <Get-LohAllocRateAvg result>|
- guard against absent rows: if neither helper finds matching CSV rows
(older captures predate this work), emit "—" instead of "0" so the
reader can tell "absent" from "actually-measured-zero"
4. tests/Tools/MeasurementExtraction.Tests.ps1:
- extend the existing test file with five new tests:
Test "GcPauseP95_OnFixtureWithKnownPauses_ReturnsCorrectP95":
use a synthetic Csv array with 100 dotnet.gc.pause.time rows whose
Mean/Increment values are 0.001..0.100 in 0.001 increments. Expect
P95 = 0.095 seconds = 95 ms. Allow ±0.5 ms tolerance for ceiling.
Test "GcPauseP95_OnEmptyCsv_ReturnsZero":
pass [], expect 0.0.
Test "LohAllocRateAvg_OnGrowingHeap_ReturnsAverageGrowthPerSecond":
synthetic 10 rows over 10 s with heap growing 1 MB/s. Expect ~1 048 576.
Test "LohAllocRateAvg_OnShrinkingHeap_ReturnsZero":
synthetic 10 rows where heap value strictly decreases. Expect 0.0
(no growth = no allocations counted).
Test "ConvertTo-MeasurementRow_AppendsTwoNewMetrics_WhenCsvHasGcRows":
feed a fixture CSV that has both dotnet.gc.pause.time and the LOH
heap size counter; assert the markdown output contains both
"gc-pause-p95" and "LOH-alloc-rate avg" rows
Test "ConvertTo-MeasurementRow_EmitsDashWhenGcMetricsAbsent":
feed a fixture with only InspectionPrototype rows; assert the two
new rows show "—" rather than "0"
## Constraints
- Do NOT change the IAppStateStore subscription pattern. MainViewModel already
subscribes to StateChanged; this pass adds work to that subscription, not
a new subscription.
- Do NOT capture frames in any field other than the per-update local; do NOT
hold a reference to Frame.PreviewPayload after WritePixels. The byte[]
is owned by the producer once-written; the consumer copies into the
WriteableBitmap and lets the byte[] be garbage-collected.
- Do NOT introduce a new InspectionPrototype counter. The slice's metric
surface is entirely System.Runtime.
- Do NOT modify Capture-Measurements.ps1 in this pass beyond what's
required for ConvertTo-MeasurementRow's new output. The orchestrator
already calls the function; an updated function automatically flows
through.
- Tests must NOT actually launch the WPF app or instantiate Application.
WriteableBitmap can be constructed in xUnit with [Fact] decorated as
[STAThread] only if absolutely required — otherwise just unit-test
the byte[] -> bitmap-stride math separately and skip the WriteableBitmap
step in tests.
## Verification before you report done
dotnet build --configuration Release
dotnet test --configuration Release
Plus:
- Pester: Invoke-Pester tests/Tools/MeasurementExtraction.Tests.ps1
All five new tests pass plus the five existing partial-CSV tests.
- Manual smoke test: launch the app interactively, switch to the
HighFrameRate profile, click Connect → Home → Start Run, observe the
preview area showing a continuously updating gradient pattern. The
pattern should visibly change frame-to-frame (because the Sequence
offset shifts the gradient).
- Run a 60-second smoke capture under the existing automation:
tools/Capture-Measurements.ps1 -Scenario MultiTagSoak `
-Duration 60 -Profile HighFrameRate `
-OutputCsv docs/captures/_smoke.csv `
-CommitHash $(git rev-parse --short HEAD) -AllowDirty
Verify the printed row block has 18 metrics, including gc-pause-p95
and LOH-alloc-rate. Delete the smoke CSV before commit.
## Report format when finished
- files created and modified
- confirmation all C# tests + Pester tests pass
- the smoke-capture stdout (the 18-metric markdown block included as evidence)
- a single commit hash
- commit message: "feat(ui,tools): bind WriteableBitmap preview, add gc-pause-p95 + LOH-alloc-rate to capture (pass 2/3 of TASK-1.2)"Pass 3 — Capture + row block + runbook §4.3
You are implementing Pass 3 of TASK-1.2, the final pass. Passes 1 and 2 are
merged. This pass runs the 10-min HighFrameRate capture, appends the row
block, writes runbook §4.3, and updates session-handoff documents. NO code
changes — Passes 1 and 2 own those.
## Authoritative references
Read these before making changes:
- docs/specs/SLICE-1.2-real-frame-payloads.md (criteria 7, 8)
- docs/runbook/capturing-measurements.md (existing §3a, §4.1, §4.2 to mirror for §4.3)
- docs/reviews/phase-1-measurements.md (slice-1-1-multi-tag-telemetry row to mirror)
- CLAUDE.md, docs/reviews/roadmap-progress.md
- tools/Capture-Measurements.ps1 (the orchestrator that runs the capture)
## Scope of this pass
Capture, table edit, runbook §4.3, session-handoff updates.
## Deliverables
1. Disable system sleep BEFORE capturing (TASK-1.5.1 follow-up runbook
discipline). On the capture machine:
powercfg /change standby-timeout-ac 0
powercfg /change monitor-timeout-ac 0
Note the previous values in the session-log entry so they can be restored
after the capture.
2. Run the 10-minute HighFrameRate capture:
$date = Get-Date -Format 'yyyy-MM-dd'
tools/Capture-Measurements.ps1 -Scenario MultiTagSoak `
-Duration 600 -Profile HighFrameRate `
-OutputCsv "docs/captures/slice-1-2-high-fps-$date.csv" `
-CommitHash $(git rev-parse --short HEAD) `
-OperatorDelayMs 0 `
-SliceTag slice-1-2-real-frame-payloads
Confirm:
* app exit code 0
* frames.dropped == 0 (any non-zero is a failure of criterion 6)
* frames.ingested >= 17 500 (criterion 6)
* 18 metrics in the printed row block, including gc-pause-p95 and
LOH-alloc-rate avg (criterion 7)
* tags.active == 50 in the CSV (regression check vs the SLICE-1.1
workdir bug)
If any of these fails, STOP — do not proceed to the table edit. Capture
the failure mode, file an issue, hand off.
3. Append row block to docs/reviews/phase-1-measurements.md:
- new "### Row — slice-1-2-real-frame-payloads" header under "## Phase 1 rows"
and AFTER the existing "### Row — slice-1-1-multi-tag-telemetry" subsection
- mirror the SLICE-1.1 row's header (Scenario / Capture / Per-tag rate /
Commit / Date), but the per-tag-rate file is irrelevant for this slice;
replace that line with a one-line "**Profile:** HighFrameRate (2 MP × 8-bit grayscale @ 30 fps)"
- 18-metric table:
Slice = "slice-1-2-real-frame-payloads"
Baseline = row 0b values for the 16 metrics in row 0/0a/0b's
set + "—" for gc-pause-p95 and LOH-alloc-rate avg (row 0b
predates these metrics)
After = the captured values
Delta = after − baseline (or after ÷ baseline for rates), "—" where
Baseline is "—"
- "### Notes on slice-1-2-real-frame-payloads" subsection with three points:
(a) Why row 0b is the baseline reference and what the deltas isolate.
(b) The new gc-pause-p95 and LOH-alloc-rate-avg metrics — establishing
the LOH-pressure baseline that future allocation-pooling slices
will delta against.
(c) Whether frames.dropped stayed at 0 throughout. If anything else
surprised — runs.completed throughput change, working-set growth
pattern under sustained 60 MB/s LOH, etc — note it here.
4. Add §4.3 to docs/runbook/capturing-measurements.md:
- title: "### 4.3 High-frame-rate soak — slice-1.2, `HighFrameRate` profile"
- placement: between the existing §4.2 "Multi-tag soak" and "§4.3+ — pending
Phase 1 scenarios". Update §4.3+'s entry to remove "real frame payloads"
since this slice now occupies that slot; the placeholder for §4.4
(encoder-rate motion) and §4.5 (storm/soak) stays
- content covers:
* one-paragraph rationale (links back to SLICE-1.2 spec)
* 10-minute step list mirroring §4.2 but with profile = HighFrameRate
and an explicit "10:00" stopwatch mark instead of 30 minutes
* sanity check: tags.active == 50 (the workdir bug regression check),
frames.ingested rate ≈ 30 fps in the steady-state portion of the
run, frames.dropped == 0
* the row block is 18-metric, not 16 — name the two new fields and
where they come from in the CSV (System.Runtime provider, already
collected)
* "Implemented by: MultiTagSoakScenario with --profile HighFrameRate"
(no new IScenario class)
5. Update CLAUDE.md "Current position" block:
- Phase: 1 (Simulator to scale) — SLICE-1.2 complete
- Last completed slice: TASK-1.2 Pass 3 — captured 10-min HighFrameRate
soak (2 MP × 8-bit @ 30 fps), 18-metric row block appended, runbook
§4.3 added; commit <hash>
- Next action: SLICE-1.3 (Encoder-rate motion); spec needs to be written
- Blocked on: nothing
- Last updated: <today's date>
6. Append a session-log entry to docs/reviews/roadmap-progress.md under
today's date naming the CSV path, gc-pause-p95 ms value, LOH-alloc-rate-avg
value, runs.completed count, and the commit hash. Mark SLICE-1.2 as
Completed in the progress table.
7. Restore powercfg settings after capture finishes:
powercfg /change standby-timeout-ac <previous_value_in_minutes>
powercfg /change monitor-timeout-ac <previous_value_in_minutes>
## Constraints
- Do NOT make any code or test changes in this pass.
- Do NOT modify the SLICE-1.2 spec — the row block is the slice's exit gate
evidence, not an opportunity to amend the spec.
- Do NOT skip the 10-minute capture. A 60-second smoke run does not satisfy
criterion 6 — that needs frames.ingested >= 17 500, which requires the full
duration.
- Do NOT capture without disabling system sleep first (deliverable 1). If
your machine sleeps mid-capture, the capture is invalid (per the
TASK-1.5.1 follow-up note).
## Verification before you report done
dotnet build --configuration Release
dotnet test --configuration Release
Plus:
- the docs/captures/slice-1-2-high-fps-<date>.csv file exists and is committed
- the row block is in docs/reviews/phase-1-measurements.md with all 18
metrics filled, frames.dropped = 0
- §4.3 renders correctly (no broken markdown tables or links)
- CLAUDE.md current-position block reflects SLICE-1.2 closure
## Report format when finished
- files created and modified
- the captured row block (the 18-metric markdown table) included in the report
- gc-pause-p95 and LOH-alloc-rate-avg values, with one-sentence interpretation
(e.g. "p95 GC pause was X ms across the 10-min run; LOH alloc rate averaged
Y MB/s, consistent with 2 MB × 30 fps = 60 MB/s")
- a single commit hash
- commit message: "feat(measurements): close SLICE-1.2 with high-fps row block; runbook §4.3 (pass 3/3 of TASK-1.2)"Operator notes
- One pass per Copilot session. Same protocol as TASK-1.5.
- Pass 1's allocation pattern is intentionally unflattering. Plain
new byte[]per frame, no pooling. The agent may "helpfully" suggestArrayPool<byte>— refuse. The slice's whole point is to expose unfiltered LOH pressure so a future slice has evidence justifying the pool. - Pass 2's WriteableBitmap reuse is the load-bearing optimization. A naive implementation re-allocates the bitmap on every frame; that creates a parallel allocation stream that masks the simulator's allocation rate in the captured metrics. Verify by inspecting
MainViewModel.UpdateCurrentFrame— exactly onenew WriteableBitmap(...)per profile change, not one per frame. - Pass 3 must run with system sleep disabled. Without that discipline, the LOH-alloc-rate-avg metric is diluted by any sleep duration, masking what the slice is trying to measure. Skip the capture and re-run if the machine sleeps mid-run.
- The HighFrameRate profile is not a "production" profile — it's a measurement profile. Existing scenarios (
DemoBaseline,MultiTagSoak) continue with their 640 × 480 × 1 defaults so rows 0/0a/0b/slice-1-1 stay reproducible. - Update the index files only at the end of the phase, not per-slice. Same rationale as earlier tasks.