Record label edits as delta patches - #360
Conversation
Label, relabel and unlabel now mutate labels in place and record only the changed values, instead of cloning the whole cloud and keeping a snapshot. As a consequence the pre-edit PointCloud object is mutated after these edits; tests relying on it being untouched now create a fresh cloud per case. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the snapshot undo history (raw copies of the whole cloud plus a Go-side header list) with a patch-based history: each edit pushes a patch reverting it, serialized and stored uniformly on the JS heap. For now every edit type uses replacePatch, a whole-cloud snapshot, so behavior and memory characteristics are unchanged while the pipeline (push, serialized storage, undo by revert) is in place. Follow-ups replace the snapshot fallback with cheap per-operation patches and compress what remains. The non-js history stub becomes a real implementation (historyMem), making undo behavior testable with plain go test; randomized round-trip tests assert byte-exact restoration. Undo depth semantics of max_history are unchanged (N entries = N undos). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #360 +/- ##
==========================================
- Coverage 43.36% 43.32% -0.05%
==========================================
Files 10 10
Lines 1508 1498 -10
==========================================
- Hits 654 649 -5
+ Misses 803 801 -2
+ Partials 51 48 -3 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR changes label-edit operations (label/relabel/unlabel) to mutate the point cloud in place while recording undo history as compact “delta” patches containing only the indices and prior label values that actually changed.
Changes:
- Add a new
labelPatchpatch type with encode/decode support and revert logic. - Update label-edit flows to apply in-place updates and consolidate relabel/unlabel loops via
mutateLabels. - Update/add tests to account for in-place mutation and validate label patch behavior.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| patch.go | Introduces labelPatch, plus uint32-slice encoding/decoding utilities and decode support for the new patch type. |
| editor.go | Switches label-related edits to in-place mutation and records undo as labelPatch deltas; consolidates relabel/unlabel via mutateLabels. |
| patch_test.go | Adds a labelPatch revert test and includes label patches in encode/decode roundtrip coverage. |
| command_test.go | Refactors tests to use fresh point clouds per case due to in-place mutation semantics. |
Suppressed comments (1)
editor.go:190
- mutateLabels pushes an empty labelPatch when no labels changed, creating a no-op undo step. Since this is now an in-place edit, the forced runtime.GC() is also likely unnecessary overhead.
e.push(p)
runtime.GC()
return nil
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Reject a labelPatch whose indices and oldLabels lengths differ, matching the integrity check deletePatch already has. Rewrite the length-field guards in a multiplication-free form so they hold on any int width. runtime.GC after label edits lost its purpose when the whole-cloud clone was removed; the in-place edit leaves no large garbage to collect. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
patch.go:212
- writeUint32s allocates a temporary 4*len(vs) byte slice and then copies it into the bytes.Buffer. For large label patches this adds significant peak memory overhead (and extra copying) exactly when trying to keep undo deltas small. Writing each uint32 directly to the buffer avoids the extra allocation.
func writeUint32s(buf *bytes.Buffer, vs []uint32) {
b := make([]byte, 4*len(vs))
for i, v := range vs {
binary.LittleEndian.PutUint32(b[i*4:], v)
}
buf.Write(b)
}
A failed revert used to discard the entry; a later undo would then apply an older patch to a state it was not recorded against. Keep the history intact and block undo at the broken entry instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
buf.Bytes() retains the grown capacity of the buffer, which can be nearly twice the content size and is held long-term by historyMem. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
historyMem exists only in the non-js build; go vet for GOOS=js compiles test files too and failed on the reference. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bound nFields by the minimal encoded field size so corrupted counts fail before allocating, and rewrite the viewpoint bound in the same multiplication-free form as the other guards. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pushing a replacePatch serialized the whole cloud into a Go buffer before copying it to the JS heap, transiently holding extra full-size copies in the WASM linear memory, which never shrinks. Split the patch wire form into a head and a raw payload (encodeHead/payload) and copy both straight into one Uint8Array, restoring the memory behavior of the previous direct-copy implementation for snapshots. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adapt labelPatch to the encodeHead/payload interface. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
# Conflicts: # editor.go # patch.go # record_test.go
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
# Conflicts: # editor.go # record.go # record_test.go
# Conflicts: # editor.go # record.go # record_test.go
Changes
Label edits (label / relabel / unlabel) now mutate labels in place and record only the delta.