Skip to content

Conversation

@cto-new
Copy link

@cto-new cto-new bot commented Jan 2, 2026

This PR was automatically generated. Git text generation failed after retries. The changes in this PR were made by the engine, but we were unable to generate a detailed description.

Powered by CTO.new

@coderabbitai
Copy link

coderabbitai bot commented Jan 2, 2026

Summary by CodeRabbit

  • New Features

    • Added snapshot functionality to capture and restore the complete state of a project, including targets, variables, monitors, and execution context.
  • Tests

    • Added comprehensive unit tests validating snapshot capture and restoration.

✏️ Tip: You can customize this high-level summary in your review settings.

Walkthrough

This PR introduces a VM snapshot system enabling serialization and deserialization of Scratch VM runtime state—including targets, threads, monitors, and IO devices. It adds a snapshot module with serialize/restore functionality and three public VirtualMachine methods (takeSnapshot, readSnapshot, loadSnapshot) for capturing and restoring VM state. Comprehensive unit tests validate snapshot roundtrip behavior with clones, variables, and thread contexts.

Changes

Cohort / File(s) Change Summary
Snapshot Serialization Module
src/serialization/snapshot.js
New module providing snapshot serialization and deserialization. Exports SNAPSHOT_TYPE, SNAPSHOT_VERSION constants, and three functions: serialize() to capture VM state (targets, threads, monitors, IO, project), restore() to apply snapshot state back to VM (with validation and optional project restoration), and validateSnapshot() for snapshot validation. Handles complex type conversions (Set, Map, Timer, immutable.js objects) and per-target/thread state collection.
Virtual Machine Integration
src/virtual-machine.js
Adds three public methods to VirtualMachine class: takeSnapshot(options) to serialize current VM state and emit SNAPSHOT_TAKEN; readSnapshot(snapshotData) to normalize and validate snapshot data (handles JSON strings and binary formats); loadSnapshot(snapshotData, options) to apply snapshot state and emit SNAPSHOT_LOADED. Enables public API for snapshot capture and restoration.
Snapshot Unit Tests
test/unit/vm_snapshot_state.js
New comprehensive test validating snapshot roundtrip functionality. Constructs project with stage and sprite, creates clone with custom state, captures snapshot, mutates runtime, loads snapshot, and asserts full restoration of targets, variables, monitors, IO device states, and thread execution context (including nested Timer instances).

Sequence Diagrams

sequenceDiagram
    actor User
    participant VM as VirtualMachine
    participant Snap as snapshotSerializer
    participant Runtime as Runtime State

    User->>VM: takeSnapshot(options)
    VM->>Snap: serialize(vm, options)
    Snap->>Runtime: Access targets, threads, monitors, IO
    Runtime-->>Snap: State data
    Snap->>Snap: Convert complex types<br/>(Set, Map, Timer, etc.)
    Snap-->>VM: snapshot object
    VM->>VM: emit SNAPSHOT_TAKEN
    VM-->>User: snapshot data
Loading
sequenceDiagram
    actor User
    participant VM as VirtualMachine
    participant Snap as snapshotSerializer
    participant Runtime as Runtime State

    User->>VM: loadSnapshot(snapshotData, options)
    VM->>VM: readSnapshot(snapshotData)
    VM->>Snap: validateSnapshot(snapshot)
    Snap-->>VM: validation result
    VM->>Snap: restore(vm, snapshot, options)
    Snap->>Snap: Deserialize & reconstruct<br/>complex types
    Snap->>Runtime: Apply targets, clones,<br/>variables, monitors, IO
    Runtime-->>Snap: State updated
    Snap-->>VM: restoration complete
    VM->>VM: emit SNAPSHOT_LOADED
    VM-->>User: completion
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly Related Issues

  • VM Snapshots #26: This PR directly implements the VM snapshot API and serialization module specified in the main feature issue, including the three public methods (takeSnapshot/readSnapshot/loadSnapshot) and complete state capture/restoration logic for targets, threads, monitors, and IO devices.

Poem

🐰 A snapshot, a moment in time,
Frozen in bytes, preserved in its prime!
With threads and targets all tucked away,
We'll hop back and forth any day!
Time-travel debugging, oh what a dream—
The future of Scratch, the ultimate scheme!

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Description check ❓ Inconclusive The description is vague and generic, stating that the PR was auto-generated with failed text generation, without conveying any meaningful information about the actual changeset. Provide a clear description of the snapshot API functionality, explaining what it enables (e.g., time-travel debugging) and the key components being serialized/restored.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat-vm-snapshot-api' is concise and directly reflects the main change: adding a VM snapshot API for serialization and deserialization of runtime state.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@supervoidcoder
Copy link
Member

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Jan 2, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai bot mentioned this pull request Jan 2, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (7)
src/serialization/snapshot.js (5)

25-28: Consider more robust immutable.js detection.

The current check relies on the presence of toJS and toJSON methods, which could match non-immutable objects. Consider using Immutable.isImmutable() if available, or check for a more specific marker.

🔎 Suggested improvement
+const Immutable = require('immutable');
+
 // immutable.js
-if (value && typeof value.toJS === 'function' && typeof value.toJSON === 'function') {
+if (Immutable.isImmutable && Immutable.isImmutable(value)) {
     return {__type: 'immutable', value: value.toJS()};
 }

117-125: Clone detection logic may miss edge cases.

The clone detection checks !target.isStage && !target.isOriginal && target.sprite.clones[0], but clones[0] being truthy doesn't guarantee it's a different target than the current one. If the original was deleted but clones remain, clones[0] might be the clone itself.

🔎 Safer clone detection
 const isClone = (
     !target.isStage &&
     !target.isOriginal &&
     target.sprite &&
     target.sprite.clones &&
-    target.sprite.clones[0]
+    target.sprite.clones[0] &&
+    target.sprite.clones[0] !== target
 );

210-244: Reliance on private IO device properties.

The code accesses internal properties like _keysPressed, _clientX, _buttons, etc. While necessary for complete state capture, these are implementation details that could change. Consider adding version guards or defensive checks.


458-464: warpTimer restoration uses Date.now() directly, inconsistent with other timer handling.

The warpTimer is created with Date.now() as the time source, while elsewhere in the code (line 77-86 in deserializeAny), timers can be restored with either Date or runtime.currentMSecs. For consistency, consider using the same pattern.

🔎 Consistent timer restoration
         if (state.warpTimer && typeof state.warpTimer.elapsed === 'number') {
-            const timer = new Timer();
-            timer.startTime = Date.now() - state.warpTimer.elapsed;
+            const timer = new Timer({now: () => runtime.currentMSecs});
+            timer.startTime = runtime.currentMSecs - state.warpTimer.elapsed;
             thread.warpTimer = timer;
         } else {
             thread.warpTimer = null;
         }

605-607: Linear search in reordering loop has O(n²) complexity.

Using reorderedTargets.includes(t) inside a loop over runtime.targets results in O(n²) complexity. For large numbers of targets, consider using a Set for O(1) lookups.

🔎 Use Set for O(n) reordering
+        const reorderedSet = new Set(reorderedTargets);
         for (const t of runtime.targets) {
-            if (!reorderedTargets.includes(t)) reorderedTargets.push(t);
+            if (!reorderedSet.has(t)) reorderedTargets.push(t);
         }
test/unit/vm_snapshot_state.js (1)

155-163: Consider adding thread target verification robustness.

The test asserts restoredThread.target.id is 'clone', but doesn't verify that this is actually the restored clone (not the original that was mutated). Consider adding a check that the clone is a distinct target.

🔎 Enhanced thread target verification
     // Threads
     t.equal(vm.runtime.threads.length, 1);
     const restoredThread = vm.runtime.threads[0];
     t.equal(restoredThread.target.id, 'clone');
+    t.equal(restoredThread.target.isOriginal, false, 'thread target is a clone');
     t.same(restoredThread.stack, ['top', 'next']);
src/virtual-machine.js (1)

737-746: JSON parsing errors are not handled.

JSON.parse at lines 739 and 741 can throw SyntaxError for malformed input. The caller would need to handle this, but it might be cleaner to wrap and rethrow with a more descriptive error message.

🔎 Add error handling for JSON parsing
     readSnapshot (snapshotData) {
+        try {
             if (typeof snapshotData === 'string') {
                 snapshotData = JSON.parse(snapshotData);
             } else if (snapshotData instanceof ArrayBuffer || ArrayBuffer.isView(snapshotData)) {
                 snapshotData = JSON.parse(Buffer.from(snapshotData).toString());
             }
+        } catch (e) {
+            throw new Error(`Invalid snapshot data: ${e.message}`);
+        }

         snapshotSerializer.validateSnapshot(snapshotData);
         return snapshotData;
     }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fb00bb6 and ba74280.

📒 Files selected for processing (3)
  • src/serialization/snapshot.js
  • src/virtual-machine.js
  • test/unit/vm_snapshot_state.js
🧰 Additional context used
🧬 Code graph analysis (2)
src/virtual-machine.js (1)
src/serialization/snapshot.js (1)
  • snapshot (492-515)
test/unit/vm_snapshot_state.js (2)
src/serialization/snapshot.js (11)
  • require (1-1)
  • MonitorRecord (3-3)
  • Thread (4-4)
  • Timer (5-5)
  • t (19-19)
  • t (68-68)
  • target (413-413)
  • thread (416-416)
  • timer (79-83)
  • timer (459-459)
  • snapshot (492-515)
src/virtual-machine.js (7)
  • require (20-20)
  • require (24-24)
  • require (25-25)
  • require (26-26)
  • require (28-28)
  • Sprite (17-17)
  • Variable (21-21)
🔇 Additional comments (9)
src/serialization/snapshot.js (3)

1-9: LGTM on imports and constants.

The module structure is clean with well-defined constants for snapshot type and version, enabling future versioning support.


472-485: LGTM on validation logic.

Clear, descriptive error messages for invalid snapshots. The validation appropriately checks type, version, and runtimeState presence.


102-104: No changes needed. The deserialization behavior is correct.

The __type: 'immutable' handler returns plain JS objects (value.value), which is appropriate because _customState, _edgeActivatedHatValues, and extensionStorage are always initialized and used as plain objects throughout the codebase (initialized as {} in engine/target.js and accessed via bracket notation, never immutable methods). The serialization/deserialization conversion from immutable.js structures to plain JS is intentional and consistent with how these fields are actually used—consumers do not expect immutable methods on these deserialized fields.

Likely an incorrect or invalid review comment.

test/unit/vm_snapshot_state.js (3)

12-29: LGTM on test setup.

Good coverage of minimal project setup with stage and sprite. The test properly initializes the VM runtime with targets.


30-55: Good coverage of target state variations.

The test sets up distinct values for the clone vs. original, enabling proper verification that each target's state is restored independently.


76-92: LGTM on thread state setup.

Good coverage of thread serialization including custom Timer in executionContext. This exercises the Timer deserialization path with a runtime-based time source.

src/virtual-machine.js (3)

30-30: LGTM on import.

Clean import of the snapshot serializer module.


715-729: LGTM on takeSnapshot implementation.

Clean API with proper event emission. The JSDoc clearly documents the purpose and options.


748-763: LGTM on loadSnapshot implementation.

Clean async implementation with proper event emission. The method correctly delegates to readSnapshot for validation and returns a Promise for the async restore operation.

Comment on lines +274 to +279
if (typeof ioState.video.skinId === 'number' && video._skinId !== -1) {
video._skinId = ioState.video.skinId;
}
if (typeof ioState.video.drawable === 'number' && video._drawable !== -1) {
video._drawable = ioState.video.drawable;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Video skinId/drawable restoration may cause inconsistencies.

The code only sets skinId and drawable if they're not -1, but the stored values from a different session may not correspond to valid renderer resources. This could lead to invalid drawable references.

🔎 Consider skipping video resource restoration entirely
     if (ioState.video) {
         const video = runtime.ioDevices.video;
         video._ghost = +ioState.video.ghost || 0;
         video._forceTransparentPreview = !!ioState.video.forceTransparentPreview;
-        // Do not attempt to set skinId/drawable unless they already exist,
-        // as these are renderer-specific resources.
-        if (typeof ioState.video.skinId === 'number' && video._skinId !== -1) {
-            video._skinId = ioState.video.skinId;
-        }
-        if (typeof ioState.video.drawable === 'number' && video._drawable !== -1) {
-            video._drawable = ioState.video.drawable;
-        }
+        // Skip restoring skinId/drawable as they are renderer-session-specific
         if (typeof video.setPreviewGhost === 'function') {
             video.setPreviewGhost(video._ghost);
         }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (typeof ioState.video.skinId === 'number' && video._skinId !== -1) {
video._skinId = ioState.video.skinId;
}
if (typeof ioState.video.drawable === 'number' && video._drawable !== -1) {
video._drawable = ioState.video.drawable;
}
if (ioState.video) {
const video = runtime.ioDevices.video;
video._ghost = +ioState.video.ghost || 0;
video._forceTransparentPreview = !!ioState.video.forceTransparentPreview;
// Skip restoring skinId/drawable as they are renderer-session-specific
if (typeof video.setPreviewGhost === 'function') {
video.setPreviewGhost(video._ghost);
}
}
🤖 Prompt for AI Agents
In src/serialization/snapshot.js around lines 274 to 279, restoring
video._skinId and video._drawable directly from ioState can create invalid
references; instead validate the stored IDs against the renderer/resource
manager before assigning: query the appropriate resource lookup (e.g.
hasSkin/hasDrawable or getSkin/getDrawable) and only assign if the resource
exists and is compatible, otherwise set the property to -1 (or a safe default)
and optionally log a warning; alternatively, if restoration of renderer
resources is undesirable, skip restoring these fields entirely and leave them at
their runtime defaults.

Comment on lines +590 to +591
runtime.addTarget(newClone);
newClone.id = targetState.id;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Clone ID reassignment after addTarget may cause inconsistencies.

The clone is added to runtime with its auto-generated ID, then the ID is changed to match the snapshot. This could cause issues if addTarget uses the ID for internal indexing (e.g., in maps or caches).

🔎 Set ID before adding to runtime
             const newClone = original.makeClone();
             if (!newClone) {
                 log.warn('Could not restore clone: makeClone() returned null', targetState);
                 continue;
             }

+            newClone.id = targetState.id;
             runtime.addTarget(newClone);
-            newClone.id = targetState.id;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
runtime.addTarget(newClone);
newClone.id = targetState.id;
newClone.id = targetState.id;
runtime.addTarget(newClone);
🤖 Prompt for AI Agents
In src/serialization/snapshot.js around lines 590-591, the code assigns
newClone.id after calling runtime.addTarget which can cause runtime indexing
inconsistencies; set newClone.id to targetState.id before calling
runtime.addTarget (or construct the clone with the correct id) so the runtime
receives the object with the intended id, and verify no duplicate-id errors
occur (handle or assert if runtime already contains that id).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant