Try using the wasm base64 decoder
This commit is contained in:
@@ -9,6 +9,7 @@
|
|||||||
import { IApcHandler, IImageAddonOptions, IResetHandler, ITerminalExt } from '../Types';
|
import { IApcHandler, IImageAddonOptions, IResetHandler, ITerminalExt } from '../Types';
|
||||||
import { ImageRenderer } from '../ImageRenderer';
|
import { ImageRenderer } from '../ImageRenderer';
|
||||||
import { ImageStorage, CELL_SIZE_DEFAULT } from '../ImageStorage';
|
import { ImageStorage, CELL_SIZE_DEFAULT } from '../ImageStorage';
|
||||||
|
import Base64Decoder from 'xterm-wasm-parts/lib/base64/Base64Decoder.wasm';
|
||||||
import {
|
import {
|
||||||
KittyAction,
|
KittyAction,
|
||||||
KittyFormat,
|
KittyFormat,
|
||||||
@@ -22,6 +23,13 @@ import {
|
|||||||
parseKittyCommand
|
parseKittyCommand
|
||||||
} from './KittyGraphicsTypes';
|
} from './KittyGraphicsTypes';
|
||||||
|
|
||||||
|
// Memory limit for base64 decoder (4MB, same as IIPHandler)
|
||||||
|
const DECODER_KEEP_DATA = 4194304;
|
||||||
|
|
||||||
|
// Base64 shard size for chunked decoding (1MB encoded → ~768KB decoded)
|
||||||
|
// This prevents memory pressure with large images
|
||||||
|
const BASE64_SHARD_SIZE = 1048576;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Kitty graphics protocol handler.
|
* Kitty graphics protocol handler.
|
||||||
*
|
*
|
||||||
@@ -84,22 +92,22 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Called for each chunk of data in the APC sequence.
|
* Called for each chunk of data in the APC sequence.
|
||||||
* Accumulates codepoints as a string.
|
* Accumulates codepoints as a string for control data parsing.
|
||||||
|
* The base64 payload is decoded later using the wasm decoder.
|
||||||
*/
|
*/
|
||||||
public put(data: Uint32Array, start: number, end: number): void {
|
public put(data: Uint32Array, start: number, end: number): void {
|
||||||
if (this._aborted) return;
|
if (this._aborted) return;
|
||||||
|
|
||||||
// Check size limit
|
// Check size limit
|
||||||
if (this._data.length + (end - start) > this._opts.kittySizeLimit) {
|
if (this._data.length + (end - start) > this._opts.kittySizeLimit) {
|
||||||
console.warn('[KittyHandler] Data exceeds size limit, aborting');
|
console.warn('[KittyGraphicsHandler] Data exceeds size limit, aborting');
|
||||||
this._aborted = true;
|
this._aborted = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert Uint32Array codepoints to string
|
// Convert Uint32Array codepoints to string for control data parsing
|
||||||
// TODO: This atob + charCodeAt pattern has bad runtime. Consider using the wasm-based
|
// Note: We accumulate as string because we need to parse control data (before semicolon).
|
||||||
// base64 decoder from xterm-wasm-parts which supports chunked data ingestion.
|
// The base64 payload is decoded efficiently using the wasm-based decoder in _decodeImage().
|
||||||
// For now, we accumulate as string and decode at the end.
|
|
||||||
for (let i = start; i < end; i++) {
|
for (let i = start; i < end; i++) {
|
||||||
this._data += String.fromCodePoint(data[i]);
|
this._data += String.fromCodePoint(data[i]);
|
||||||
}
|
}
|
||||||
@@ -244,11 +252,7 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const binaryString = atob(payload);
|
const bytes = this._decodeBase64(payload);
|
||||||
const bytes = new Uint8Array(binaryString.length);
|
|
||||||
for (let i = 0; i < binaryString.length; i++) {
|
|
||||||
bytes[i] = binaryString.charCodeAt(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
const format = cmd.format || KittyFormat.RGBA;
|
const format = cmd.format || KittyFormat.RGBA;
|
||||||
|
|
||||||
@@ -375,25 +379,21 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Decode base64 image data into an ImageBitmap.
|
* Decode base64 image data into an ImageBitmap.
|
||||||
|
* Uses the wasm-based decoder from xterm-wasm-parts for better performance.
|
||||||
*/
|
*/
|
||||||
private async _decodeImage(image: IKittyImageData): Promise<ImageBitmap> {
|
private async _decodeImage(image: IKittyImageData): Promise<ImageBitmap> {
|
||||||
const format = image.format;
|
const format = image.format;
|
||||||
const base64Data = image.data;
|
const base64Data = image.data;
|
||||||
|
|
||||||
// TODO: This atob + charCodeAt loop has bad runtime and creates memory pressure with large
|
// TODO: Get this confirmed!
|
||||||
// images. Consider using the wasm-based base64 decoder from xterm-wasm-parts.
|
let bytes = this._decodeBase64(base64Data);
|
||||||
const binaryString = atob(base64Data);
|
|
||||||
let bytes = new Uint8Array(binaryString.length);
|
|
||||||
for (let i = 0; i < binaryString.length; i++) {
|
|
||||||
bytes[i] = binaryString.charCodeAt(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (image.compression === KittyCompression.ZLIB) {
|
if (image.compression === KittyCompression.ZLIB) {
|
||||||
bytes = await this._decompressZlib(bytes) as Uint8Array<ArrayBuffer>;
|
bytes = await this._decompressZlib(bytes) as Uint8Array<ArrayBuffer>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (format === KittyFormat.PNG) {
|
if (format === KittyFormat.PNG) {
|
||||||
const blob = new Blob([bytes], { type: 'image/png' });
|
const blob = new Blob([new Uint8Array(bytes)], { type: 'image/png' });
|
||||||
// Safari fallback pattern (from IIPHandler)
|
// Safari fallback pattern (from IIPHandler)
|
||||||
if (!window.createImageBitmap) {
|
if (!window.createImageBitmap) {
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
@@ -485,6 +485,75 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode base64 string using the wasm-based decoder for better performance.
|
||||||
|
* Uses shard-based decoding (1MB chunks) to prevent memory pressure with large images.
|
||||||
|
*/
|
||||||
|
private _decodeBase64(base64Data: string): Uint8Array {
|
||||||
|
const length = base64Data.length;
|
||||||
|
|
||||||
|
// For small data, decode directly without sharding
|
||||||
|
if (length <= BASE64_SHARD_SIZE) {
|
||||||
|
return this._decodeBase64Shard(base64Data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For large data, decode in 1MB shards and accumulate
|
||||||
|
// Base64 must be decoded in multiples of 4 chars, so align shard boundaries
|
||||||
|
const shardSize = BASE64_SHARD_SIZE - (BASE64_SHARD_SIZE % 4);
|
||||||
|
const chunks: Uint8Array[] = [];
|
||||||
|
let totalLength = 0;
|
||||||
|
|
||||||
|
for (let offset = 0; offset < length; offset += shardSize) {
|
||||||
|
const end = Math.min(offset + shardSize, length);
|
||||||
|
const shard = base64Data.substring(offset, end);
|
||||||
|
const decoded = this._decodeBase64Shard(shard);
|
||||||
|
chunks.push(decoded);
|
||||||
|
totalLength += decoded.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine all chunks into final result
|
||||||
|
const result = new Uint8Array(totalLength);
|
||||||
|
let writeOffset = 0;
|
||||||
|
for (const chunk of chunks) {
|
||||||
|
result.set(chunk, writeOffset);
|
||||||
|
writeOffset += chunk.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode a single base64 shard using the wasm decoder.
|
||||||
|
*/
|
||||||
|
private _decodeBase64Shard(base64Data: string): Uint8Array {
|
||||||
|
// Calculate exact decoded size from base64 string
|
||||||
|
// Formula: (encoded_length * 3) / 4 - padding_count
|
||||||
|
let padding = 0;
|
||||||
|
if (base64Data.endsWith('==')) padding = 2;
|
||||||
|
else if (base64Data.endsWith('=')) padding = 1;
|
||||||
|
const decodedSize = Math.floor(base64Data.length * 3 / 4) - padding;
|
||||||
|
|
||||||
|
// Convert string to Uint32Array of codepoints for the wasm decoder
|
||||||
|
const codepoints = new Uint32Array(base64Data.length);
|
||||||
|
for (let i = 0; i < base64Data.length; i++) {
|
||||||
|
codepoints[i] = base64Data.charCodeAt(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
const decoder = new Base64Decoder(DECODER_KEEP_DATA);
|
||||||
|
decoder.init(decodedSize);
|
||||||
|
decoder.put(codepoints, 0, codepoints.length);
|
||||||
|
|
||||||
|
if (decoder.end()) {
|
||||||
|
decoder.release();
|
||||||
|
throw new Error('Base64 decode failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the decoded data before releasing
|
||||||
|
const result = new Uint8Array(decoder.data8);
|
||||||
|
decoder.release();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get stored images (for testing/debugging).
|
* Get stored images (for testing/debugging).
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user