Skip to content

WIP feat: Android Tile compression#390

Open
abelonogov-ld wants to merge 18 commits intomainfrom
andrey/tile-compression
Open

WIP feat: Android Tile compression#390
abelonogov-ld wants to merge 18 commits intomainfrom
andrey/tile-compression

Conversation

@abelonogov-ld
Copy link
Contributor

@abelonogov-ld abelonogov-ld commented Feb 24, 2026

Summary

How did you test this change?

Are there any deployment considerations?


Note

Medium Risk
Touches core session replay capture and RRWeb export logic (frame diffing, bitmap lifecycle, event/state sequencing), so regressions could impact replay correctness or memory usage, but scope is contained to the replay module with added tests.

Overview
Session replay capture/export is refactored from emitting single full-screen CaptureEvents to emitting ExportFrames that can represent incremental tile updates (adds/removes) with keyframes.

This introduces a new capture pipeline (CaptureManager + ImageCaptureService + TileDiffManager + ExportDiffManager) with configurable ReplayOptions.compression (ScreenImage vs OverlayTiles with layer/keyframe cadence and optional backtracking/rollback).

RRWeb event generation is rewritten (RRWebEventGenerator replaces SessionReplayEventGenerator) to build full snapshots as positioned <img> nodes and subsequent updates as mutation add/remove operations, tracking node IDs and keyframe continuity; exporter logic is updated accordingly (including revised canvas-buffer-limit behavior).

Written by Cursor Bugbot for commit 425d82d. This will update automatically on new commits. Configure here.

Updated the class name from SessionReplayEventGenerator to RRWebEventGenerator for improved clarity and consistency. Adjusted the instantiation in SessionReplayExporter accordingly.
Updated the RRWebEventGenerator to improve the generation of incremental events. Introduced new constants for DOM elements and refined the state management for image nodes. The event generation now utilizes a more structured approach for adding and removing nodes, enhancing clarity and maintainability.
Modified the RRWebEventGenerator to use a constant for the image MIME type, ensuring consistency in data URL generation for incremental events. This change enhances maintainability and clarity in the codebase.
…natureManager

Updated the observability module to replace instances of CaptureEvent with the new ExportFrame class, enhancing the structure of exported frames. Introduced TileSignatureManager for improved tile-based signature computation. Adjusted related tests and export logic to accommodate these changes, ensuring consistency across the codebase.
Added CaptureManager class to facilitate capturing frames from the lowest visible window. This implementation includes functionality for emitting captured frames as ExportFrame objects, integrating session management, and applying masks for privacy. The new class enhances the observability module by providing a structured approach to frame capture, with future optimizations planned for capture quality and memory management.
Modified ExportDiffManager to accept ImageCaptureService.RawFrame instead of the previously defined RawFrame data class. This change streamlines the frame capture process and enhances integration with the ImageCaptureService, improving overall code clarity and maintainability. Additionally, removed the RawFrame data class from ExportDiffManager to reduce redundancy.
…ions

Modified the TileSignatureManager to accept separate width and height parameters for tile size during signature computation. This change enhances flexibility in tile processing and improves the accuracy of tile-based signatures. Additionally, updated the ExportDiffManager to utilize the new signature computation method, ensuring consistency across the observability module.
…roved state management

Updated the SessionReplayExporter to include a condition for forcing full captures based on canvas size and keyframe status. Refined the RRWebEventGenerator to manage node IDs and image handling more effectively, including the introduction of a new tile-based signature system. Adjusted the ExportFrame structure to utilize IntSize for original dimensions, enhancing clarity and consistency across the observability module.
…hashing and signature computation

Modified the TileSignature data class to include separate hashLo and hashHi values, enhancing the hashing mechanism. Updated the TileSignatureManager to support multiple compute methods with preferred tile dimensions and added convenience overloads for square tiles. Adjusted internal hashing logic to reduce collision probability and improved test cases to reflect changes in tile signature handling.
@abelonogov-ld abelonogov-ld requested a review from a team as a code owner February 24, 2026 04:28
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

val (node, canvasSize) = tileNode(exportFrame, image)
totalCanvasSize += canvasSize
node
}
Copy link

Choose a reason for hiding this comment

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

Missing nodeIds.clear() in generateCaptureFullEvents causes stale mappings

Medium Severity

generateCaptureFullEvents resets lastNodeId to 0 (so new node IDs restart from 1) but never clears the nodeIds map. Stale entries from previous snapshots remain, mapping old TileSignature keys to node IDs that are now reused for different tiles. When a subsequent incremental frame references one of those stale signatures in its removeImages, addCommandNodes resolves it to a wrong node, causing incorrect DOM mutations in the replay. The nodeIds.clear() call only exists in addCommandNodes for keyframe incrementals — the full-snapshot path is missing it.

Additional Locations (1)

Fix in Cursor Fix in Web

* Queue cost derived from the base64 string length; it is a cost unit, not exact bytes.
*/
override fun cost(): Int = capture.imageBase64.length
override fun cost(): Int = capture.addImages.firstOrNull()?.imageBase64?.length ?: 0
Copy link

Choose a reason for hiding this comment

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

cost() underestimates queue cost for multi-tile frames

Low Severity

cost() only accounts for the first element of addImages. With tile compression, a frame may contain multiple AddImage entries, and remove-only rollback frames report zero cost since addImages is empty. Summing all image sizes would give a more accurate queue cost estimate.

Fix in Cursor Fix in Web

(cherry picked from commit 49e4fb9)
@abelonogov-ld abelonogov-ld changed the title WIP Tile compression WIP feat: Android Tile compression Feb 24, 2026
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.

2 participants