Skip to content

Commit 59716e1

Browse files
committed
v1.2.5
- Bug Fixes: Fixed UI freezing during blob uploads caused by debug logging that serialized binary data as massive JSON strings. Large blob uploads now complete in under 1 second instead of 15+ seconds. - Bug Fixes: Added proper state tracking for drag-and-drop blob uploads to prevent concurrent operations and ensure UI recovery after failed uploads. - Performance: Added `Transfer` wrapper for blob operations from extension host to worker, eliminating buffer copying for large binary data. - Performance: Converted synchronous Base64 encoding to async chunked encoding with event loop yields, keeping the UI responsive during large blob serialization.
1 parent d75ada3 commit 59716e1

7 files changed

Lines changed: 170 additions & 69 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
# Changelog
22

3+
## 1.2.5
4+
5+
### Bug Fixes
6+
7+
- **Blob Upload Freeze**: Fixed UI freezing during blob uploads caused by debug logging that serialized binary data as massive JSON strings. Large blob uploads now complete in under 1 second instead of 15+ seconds.
8+
- **Upload State Management**: Added proper state tracking for drag-and-drop blob uploads to prevent concurrent operations and ensure UI recovery after failed uploads.
9+
10+
### Performance
11+
12+
- **Zero-Copy Blob Transfer**: Added `Transfer` wrapper for blob operations from extension host to worker, eliminating buffer copying for large binary data.
13+
- **Async Base64 Encoding**: Converted synchronous Base64 encoding to async chunked encoding with event loop yields, keeping the UI responsive during large blob serialization.
14+
315
## 1.2.4
416

517
### New Features

core/ui/modules/api.js

Lines changed: 61 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,49 @@ const pendingRpcCalls = new Map();
1717
// ============================================================================
1818

1919
/**
20-
* Encode Uint8Array to Base64 string.
21-
* Uses native btoa with chunked processing to avoid call stack limits on large arrays.
20+
* Encode Uint8Array to Base64 string asynchronously.
21+
* Uses chunked processing with microtask yields to prevent UI blocking.
22+
*
23+
* For small arrays (< 64KB), uses synchronous encoding for speed.
24+
* For larger arrays, yields control between chunks to keep UI responsive.
25+
*
26+
* @param {Uint8Array} bytes - Binary data to encode
27+
* @returns {Promise<string>} Base64 encoded string
28+
*/
29+
async function uint8ArrayToBase64Async(bytes) {
30+
// For small arrays, synchronous encoding is fast enough and avoids async overhead
31+
const SYNC_THRESHOLD = 65536; // 64KB
32+
if (bytes.length <= SYNC_THRESHOLD) {
33+
return uint8ArrayToBase64Sync(bytes);
34+
}
35+
36+
// For larger arrays, use chunked async encoding to prevent UI freeze
37+
// Process in chunks and yield to the event loop between chunks
38+
const CHUNK_SIZE = 32768; // 32KB per chunk
39+
let binary = '';
40+
41+
for (let i = 0; i < bytes.length; i += CHUNK_SIZE) {
42+
const chunk = bytes.subarray(i, Math.min(i + CHUNK_SIZE, bytes.length));
43+
binary += String.fromCharCode.apply(null, chunk);
44+
45+
// Yield to event loop every few chunks to allow UI updates
46+
// Using a microtask (Promise.resolve) for minimal delay while still allowing repaints
47+
if (i > 0 && (i / CHUNK_SIZE) % 4 === 0) {
48+
await new Promise(resolve => setTimeout(resolve, 0));
49+
}
50+
}
51+
52+
return btoa(binary);
53+
}
54+
55+
/**
56+
* Synchronous Base64 encoding for small arrays.
57+
* Used when async overhead isn't worth it.
2258
*
2359
* @param {Uint8Array} bytes - Binary data to encode
2460
* @returns {string} Base64 encoded string
2561
*/
26-
function uint8ArrayToBase64(bytes) {
27-
// Process in 32KB chunks to avoid call stack size limits with String.fromCharCode.apply
62+
function uint8ArrayToBase64Sync(bytes) {
2863
const CHUNK_SIZE = 32768;
2964
let binary = '';
3065
for (let i = 0; i < bytes.length; i += CHUNK_SIZE) {
@@ -54,48 +89,51 @@ function base64ToUint8Array(base64) {
5489
// ============================================================================
5590

5691
/**
57-
* Serialize a value for RPC transmission.
92+
* Serialize a value for RPC transmission (async version).
5893
* Converts Uint8Array to Base64 format for efficient transfer.
94+
* Uses async encoding to prevent UI blocking for large binary data.
5995
*
6096
* Performance: Base64 encoding is ~33% larger than binary but significantly faster
6197
* and more compact than array-of-numbers JSON serialization (which was ~300% larger).
6298
*
6399
* @param {*} value - Value to serialize
64-
* @returns {*} Serialized value
100+
* @returns {Promise<*>} Serialized value
65101
*/
66-
function serializeValue(value) {
102+
async function serializeValueAsync(value) {
67103
// Handle Uint8Array by converting to Base64 marker object
68104
if (value instanceof Uint8Array) {
69-
return { __type: 'Uint8Array', base64: uint8ArrayToBase64(value) };
105+
const base64 = await uint8ArrayToBase64Async(value);
106+
return { __type: 'Uint8Array', base64 };
70107
}
71108
// Handle other ArrayBuffer views (like DataView)
72109
if (ArrayBuffer.isView(value)) {
73110
const uint8 = new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
74-
return { __type: 'Uint8Array', base64: uint8ArrayToBase64(uint8) };
111+
const base64 = await uint8ArrayToBase64Async(uint8);
112+
return { __type: 'Uint8Array', base64 };
75113
}
76114
// Recursively serialize arrays
77115
if (Array.isArray(value)) {
78-
return value.map(serializeValue);
116+
return Promise.all(value.map(serializeValueAsync));
79117
}
80118
// Recursively serialize plain object properties only
81119
// Using Object.prototype.toString for robust object detection (handles null prototype)
82120
if (value && typeof value === 'object' && Object.prototype.toString.call(value) === '[object Object]') {
83121
const result = {};
84122
for (const key of Object.keys(value)) {
85-
result[key] = serializeValue(value[key]);
123+
result[key] = await serializeValueAsync(value[key]);
86124
}
87125
return result;
88126
}
89127
return value;
90128
}
91129

92130
/**
93-
* Serialize arguments array for RPC transmission.
131+
* Serialize arguments array for RPC transmission (async version).
94132
* @param {Array} args - Arguments to serialize
95-
* @returns {Array} Serialized arguments
133+
* @returns {Promise<Array>} Serialized arguments
96134
*/
97-
function serializeArgs(args) {
98-
return args.map(serializeValue);
135+
async function serializeArgsAsync(args) {
136+
return Promise.all(args.map(serializeValueAsync));
99137
}
100138

101139
/**
@@ -143,11 +181,16 @@ function deserializeValue(value) {
143181

144182
/**
145183
* Send an RPC request to the extension host.
184+
* Uses async serialization to prevent UI blocking during large blob encoding.
146185
*/
147-
export function sendRpcRequest(method, args) {
148-
return new Promise((resolve, reject) => {
149-
const messageId = `rpc_${++rpcMessageId}_${Date.now()}`;
186+
export async function sendRpcRequest(method, args) {
187+
const messageId = `rpc_${++rpcMessageId}_${Date.now()}`;
150188

189+
// Serialize args asynchronously to handle Uint8Array without blocking UI
190+
// This is done before setting up the timeout to ensure encoding time is included
191+
const serializedArgs = await serializeArgsAsync(args);
192+
193+
return new Promise((resolve, reject) => {
151194
const timeoutId = setTimeout(() => {
152195
if (pendingRpcCalls.has(messageId)) {
153196
pendingRpcCalls.delete(messageId);
@@ -158,8 +201,6 @@ export function sendRpcRequest(method, args) {
158201
pendingRpcCalls.set(messageId, { resolve, reject, timeoutId });
159202

160203
if (vscodeApi) {
161-
// Serialize args to handle Uint8Array and other non-JSON-safe types
162-
const serializedArgs = serializeArgs(args);
163204
vscodeApi.postMessage({
164205
channel: 'rpc',
165206
content: {

core/ui/modules/blob-inspector.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,6 @@ export class BlobInspector {
150150
const sizeMB = uint8Array.length / (1024 * 1024);
151151

152152
// Reject files larger than 50MB to prevent extension freeze
153-
// Large blobs require synchronous Base64 encoding which blocks the UI thread
154153
const MAX_BLOB_SIZE_MB = 50;
155154
if (sizeMB > MAX_BLOB_SIZE_MB) {
156155
throw new Error(`File too large (${sizeMB.toFixed(1)}MB). Maximum size is ${MAX_BLOB_SIZE_MB}MB to prevent freezing.`);

core/ui/modules/web-api.js

Lines changed: 61 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,49 @@ function getTargetOrigin() {
2626
// ============================================================================
2727

2828
/**
29-
* Encode Uint8Array to Base64 string.
30-
* Uses native btoa with chunked processing to avoid call stack limits on large arrays.
29+
* Encode Uint8Array to Base64 string asynchronously.
30+
* Uses chunked processing with microtask yields to prevent UI blocking.
31+
*
32+
* For small arrays (< 64KB), uses synchronous encoding for speed.
33+
* For larger arrays, yields control between chunks to keep UI responsive.
34+
*
35+
* @param {Uint8Array} bytes - Binary data to encode
36+
* @returns {Promise<string>} Base64 encoded string
37+
*/
38+
async function uint8ArrayToBase64Async(bytes) {
39+
// For small arrays, synchronous encoding is fast enough and avoids async overhead
40+
const SYNC_THRESHOLD = 65536; // 64KB
41+
if (bytes.length <= SYNC_THRESHOLD) {
42+
return uint8ArrayToBase64Sync(bytes);
43+
}
44+
45+
// For larger arrays, use chunked async encoding to prevent UI freeze
46+
// Process in chunks and yield to the event loop between chunks
47+
const CHUNK_SIZE = 32768; // 32KB per chunk
48+
let binary = '';
49+
50+
for (let i = 0; i < bytes.length; i += CHUNK_SIZE) {
51+
const chunk = bytes.subarray(i, Math.min(i + CHUNK_SIZE, bytes.length));
52+
binary += String.fromCharCode.apply(null, chunk);
53+
54+
// Yield to event loop every few chunks to allow UI updates
55+
// Using a microtask (Promise.resolve) for minimal delay while still allowing repaints
56+
if (i > 0 && (i / CHUNK_SIZE) % 4 === 0) {
57+
await new Promise(resolve => setTimeout(resolve, 0));
58+
}
59+
}
60+
61+
return btoa(binary);
62+
}
63+
64+
/**
65+
* Synchronous Base64 encoding for small arrays.
66+
* Used when async overhead isn't worth it.
3167
*
3268
* @param {Uint8Array} bytes - Binary data to encode
3369
* @returns {string} Base64 encoded string
3470
*/
35-
function uint8ArrayToBase64(bytes) {
36-
// Process in 32KB chunks to avoid call stack size limits with String.fromCharCode.apply
71+
function uint8ArrayToBase64Sync(bytes) {
3772
const CHUNK_SIZE = 32768;
3873
let binary = '';
3974
for (let i = 0; i < bytes.length; i += CHUNK_SIZE) {
@@ -63,48 +98,51 @@ function base64ToUint8Array(base64) {
6398
// ============================================================================
6499

65100
/**
66-
* Serialize a value for RPC transmission.
101+
* Serialize a value for RPC transmission (async version).
67102
* Converts Uint8Array to Base64 format for efficient transfer.
103+
* Uses async encoding to prevent UI blocking for large binary data.
68104
*
69105
* Performance: Base64 encoding is ~33% larger than binary but significantly faster
70106
* and more compact than array-of-numbers JSON serialization (which was ~300% larger).
71107
*
72108
* @param {*} value - Value to serialize
73-
* @returns {*} Serialized value
109+
* @returns {Promise<*>} Serialized value
74110
*/
75-
function serializeValue(value) {
111+
async function serializeValueAsync(value) {
76112
// Handle Uint8Array by converting to Base64 marker object
77113
if (value instanceof Uint8Array) {
78-
return { __type: 'Uint8Array', base64: uint8ArrayToBase64(value) };
114+
const base64 = await uint8ArrayToBase64Async(value);
115+
return { __type: 'Uint8Array', base64 };
79116
}
80117
// Handle other ArrayBuffer views (like DataView)
81118
if (ArrayBuffer.isView(value)) {
82119
const uint8 = new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
83-
return { __type: 'Uint8Array', base64: uint8ArrayToBase64(uint8) };
120+
const base64 = await uint8ArrayToBase64Async(uint8);
121+
return { __type: 'Uint8Array', base64 };
84122
}
85123
// Recursively serialize arrays
86124
if (Array.isArray(value)) {
87-
return value.map(serializeValue);
125+
return Promise.all(value.map(serializeValueAsync));
88126
}
89127
// Recursively serialize plain object properties only
90128
// Using Object.prototype.toString for robust object detection (handles null prototype)
91129
if (value && typeof value === 'object' && Object.prototype.toString.call(value) === '[object Object]') {
92130
const result = {};
93131
for (const key of Object.keys(value)) {
94-
result[key] = serializeValue(value[key]);
132+
result[key] = await serializeValueAsync(value[key]);
95133
}
96134
return result;
97135
}
98136
return value;
99137
}
100138

101139
/**
102-
* Serialize arguments array for RPC transmission.
140+
* Serialize arguments array for RPC transmission (async version).
103141
* @param {Array} args - Arguments to serialize
104-
* @returns {Array} Serialized arguments
142+
* @returns {Promise<Array>} Serialized arguments
105143
*/
106-
function serializeArgs(args) {
107-
return args.map(serializeValue);
144+
async function serializeArgsAsync(args) {
145+
return Promise.all(args.map(serializeValueAsync));
108146
}
109147

110148
/**
@@ -152,14 +190,19 @@ function deserializeValue(value) {
152190

153191
/**
154192
* Send an RPC request to the parent window.
193+
* Uses async serialization to prevent UI blocking during large blob encoding.
155194
* @param {string} method - Method name to call
156195
* @param {Array} args - Arguments for the method
157196
* @returns {Promise<*>} - Result from parent
158197
*/
159-
export function sendRpcRequest(method, args) {
160-
return new Promise((resolve, reject) => {
161-
const messageId = `rpc_${++rpcMessageId}_${Date.now()}`;
198+
export async function sendRpcRequest(method, args) {
199+
const messageId = `rpc_${++rpcMessageId}_${Date.now()}`;
200+
201+
// Serialize args asynchronously to handle Uint8Array without blocking UI
202+
// This is done before setting up the timeout to ensure encoding time is included
203+
const serializedArgs = await serializeArgsAsync(args);
162204

205+
return new Promise((resolve, reject) => {
163206
const timeoutId = setTimeout(() => {
164207
if (pendingRpcCalls.has(messageId)) {
165208
pendingRpcCalls.delete(messageId);
@@ -169,9 +212,6 @@ export function sendRpcRequest(method, args) {
169212

170213
pendingRpcCalls.set(messageId, { resolve, reject, timeoutId });
171214

172-
// Serialize args to handle Uint8Array and other non-JSON-safe types
173-
const serializedArgs = serializeArgs(args);
174-
175215
// Post message to parent window instead of VS Code API
176216
parentWindow.postMessage({
177217
channel: 'rpc',

0 commit comments

Comments
 (0)