@@ -186,52 +186,203 @@ export class PublishManager {
186
186
}
187
187
188
188
/**
189
- * Serializes all files in a directory for deployment using parallel processing
189
+ * Serialize files from a directory into a record of file paths to their content
190
+ * Memory-optimized version using chunking + batching approach
190
191
* @param currentDir - The directory path to serialize
191
192
* @returns Record of file paths to their content (base64 for binary, utf-8 for text)
192
193
*/
193
194
private async serializeFiles ( currentDir : string ) : Promise < Record < string , FreestyleFile > > {
194
195
const timer = new LogTimer ( 'File Serialization' ) ;
196
+ const startTime = Date . now ( ) ;
195
197
196
198
try {
197
199
const allFilePaths = await this . getAllFilePathsFlat ( currentDir ) ;
198
200
timer . log ( `File discovery completed - ${ allFilePaths . length } files found` ) ;
199
201
200
202
const filteredPaths = allFilePaths . filter ( filePath => ! this . shouldSkipFile ( filePath ) ) ;
203
+ timer . log ( `Filtered to ${ filteredPaths . length } files after exclusions` ) ;
201
204
202
205
const { binaryFiles, textFiles } = this . categorizeFiles ( filteredPaths ) ;
206
+ timer . log ( `Categorized: ${ textFiles . length } text files, ${ binaryFiles . length } binary files` ) ;
203
207
204
- const BATCH_SIZE = 50 ;
205
208
const files : Record < string , FreestyleFile > = { } ;
206
-
209
+
207
210
if ( textFiles . length > 0 ) {
208
- timer . log ( `Processing ${ textFiles . length } text files in batches of ${ BATCH_SIZE } ` ) ;
209
- for ( let i = 0 ; i < textFiles . length ; i += BATCH_SIZE ) {
210
- const batch = textFiles . slice ( i , i + BATCH_SIZE ) ;
211
- const batchFiles = await this . processTextFilesBatch ( batch , currentDir ) ;
212
- Object . assign ( files , batchFiles ) ;
213
- }
211
+ timer . log ( `Processing ${ textFiles . length } text files using chunking + batching` ) ;
212
+ const textResults = await this . processFilesWithChunkingAndBatching ( textFiles , currentDir , false ) ;
213
+ Object . assign ( files , textResults ) ;
214
214
timer . log ( 'Text files processing completed' ) ;
215
215
}
216
216
217
217
if ( binaryFiles . length > 0 ) {
218
- timer . log ( `Processing ${ binaryFiles . length } binary files in batches of ${ BATCH_SIZE } ` ) ;
219
- for ( let i = 0 ; i < binaryFiles . length ; i += BATCH_SIZE ) {
220
- const batch = binaryFiles . slice ( i , i + BATCH_SIZE ) ;
221
- const batchFiles = await this . processBinaryFilesBatch ( batch , currentDir ) ;
222
- Object . assign ( files , batchFiles ) ;
223
- }
218
+ timer . log ( `Processing ${ binaryFiles . length } binary files using chunking + batching` ) ;
219
+ const binaryResults = await this . processFilesWithChunkingAndBatching ( binaryFiles , currentDir , true ) ;
220
+ Object . assign ( files , binaryResults ) ;
224
221
timer . log ( 'Binary files processing completed' ) ;
225
222
}
226
223
227
- timer . log ( `Serialization completed - ${ Object . keys ( files ) . length } files processed` ) ;
224
+ const endTime = Date . now ( ) ;
225
+ const totalTime = endTime - startTime ;
226
+ timer . log ( `Serialization completed - ${ Object . keys ( files ) . length } files processed in ${ totalTime } ms` ) ;
228
227
return files ;
229
228
} catch ( error ) {
230
- console . error ( `[serializeFiles] Error during serialization:` , error ) ;
229
+ const endTime = Date . now ( ) ;
230
+ const totalTime = endTime - startTime ;
231
+ console . error ( `[serializeFiles] Error during serialization after ${ totalTime } ms:` , error ) ;
231
232
throw error ;
232
233
}
233
234
}
234
235
236
+ /**
237
+ * Process files using chunking + batching approach
238
+ * 1. Split files into chunks
239
+ * 2. Process each chunk in batches
240
+ * 3. Clean up memory after each chunk
241
+ */
242
+ private async processFilesWithChunkingAndBatching (
243
+ filePaths : string [ ] ,
244
+ baseDir : string ,
245
+ isBinary : boolean ,
246
+ chunkSize = 100 ,
247
+ batchSize = 10
248
+ ) : Promise < Record < string , FreestyleFile > > {
249
+ const files : Record < string , FreestyleFile > = { } ;
250
+ const totalFiles = filePaths . length ;
251
+ let processedCount = 0 ;
252
+ const chunkStartTime = Date . now ( ) ;
253
+
254
+ for ( let i = 0 ; i < filePaths . length ; i += chunkSize ) {
255
+ const chunk = filePaths . slice ( i , i + chunkSize ) ;
256
+ const chunkNumber = Math . floor ( i / chunkSize ) + 1 ;
257
+ const totalChunks = Math . ceil ( filePaths . length / chunkSize ) ;
258
+ console . log ( `Processing chunk ${ chunkNumber } /${ totalChunks } (${ chunk . length } files)` ) ;
259
+
260
+ const chunkResults = await this . processChunkInBatches ( chunk , baseDir , isBinary , batchSize ) ;
261
+ Object . assign ( files , chunkResults ) ;
262
+
263
+ processedCount += chunk . length ;
264
+ const chunkTime = Date . now ( ) - chunkStartTime ;
265
+ console . log ( `Completed chunk ${ chunkNumber } /${ totalChunks } . Total processed: ${ processedCount } /${ totalFiles } (${ chunkTime } ms elapsed)` ) ;
266
+
267
+ if ( global . gc ) {
268
+ global . gc ( ) ;
269
+ }
270
+ }
271
+
272
+ const totalChunkTime = Date . now ( ) - chunkStartTime ;
273
+ console . log ( `Completed all chunks in ${ totalChunkTime } ms` ) ;
274
+ return files ;
275
+ }
276
+
277
+ private async processChunkInBatches (
278
+ chunk : string [ ] ,
279
+ baseDir : string ,
280
+ isBinary : boolean ,
281
+ batchSize = 10
282
+ ) : Promise < Record < string , FreestyleFile > > {
283
+ const files : Record < string , FreestyleFile > = { } ;
284
+ const batchStartTime = Date . now ( ) ;
285
+
286
+ for ( let i = 0 ; i < chunk . length ; i += batchSize ) {
287
+ const batch = chunk . slice ( i , i + batchSize ) ;
288
+ const batchNumber = Math . floor ( i / batchSize ) + 1 ;
289
+ const totalBatches = Math . ceil ( chunk . length / batchSize ) ;
290
+
291
+ const batchPromises = batch . map ( filePath =>
292
+ isBinary
293
+ ? this . processBinaryFile ( filePath , baseDir )
294
+ : this . processTextFile ( filePath , baseDir )
295
+ ) ;
296
+
297
+ const batchResults = await Promise . all ( batchPromises ) ;
298
+
299
+ // Add successful results to files
300
+ for ( const result of batchResults ) {
301
+ if ( result ) {
302
+ files [ result . path ] = result . file ;
303
+ }
304
+ }
305
+
306
+ const batchTime = Date . now ( ) - batchStartTime ;
307
+ console . log ( ` Batch ${ batchNumber } /${ totalBatches } completed (${ batch . length } files) in ${ batchTime } ms` ) ;
308
+ }
309
+
310
+ const totalBatchTime = Date . now ( ) - batchStartTime ;
311
+ console . log ( ` All batches completed in ${ totalBatchTime } ms` ) ;
312
+ return files ;
313
+ }
314
+
315
+ private async processTextFile ( fullPath : string , baseDir : string ) : Promise < { path : string ; file : FreestyleFile } | null > {
316
+ const relativePath = fullPath . replace ( baseDir + '/' , '' ) ;
317
+
318
+ try {
319
+ const textContent = await this . session . fs . readTextFile ( fullPath ) ;
320
+
321
+ if ( textContent !== null ) {
322
+ // Skip very large text files to prevent memory issues
323
+ const MAX_TEXT_SIZE = 5 * 1024 * 1024 ; // 5MB limit for text files
324
+ if ( textContent . length > MAX_TEXT_SIZE ) {
325
+ console . warn ( `[processTextFile] Skipping large text file ${ relativePath } (${ textContent . length } bytes)` ) ;
326
+ return null ;
327
+ }
328
+
329
+ return {
330
+ path : relativePath ,
331
+ file : {
332
+ content : textContent ,
333
+ encoding : 'utf-8' as const ,
334
+ }
335
+ } ;
336
+ } else {
337
+ console . warn ( `[processTextFile] Failed to read text content for ${ relativePath } ` ) ;
338
+ return null ;
339
+ }
340
+ } catch ( error ) {
341
+ console . warn ( `[processTextFile] Error processing ${ relativePath } :` , error ) ;
342
+ return null ;
343
+ }
344
+ }
345
+
346
+
347
+ private async processBinaryFile ( fullPath : string , baseDir : string ) : Promise < { path : string ; file : FreestyleFile } | null > {
348
+ const relativePath = fullPath . replace ( baseDir + '/' , '' ) ;
349
+
350
+ try {
351
+ const binaryContent = await this . session . fs . readFile ( fullPath ) ;
352
+
353
+ if ( binaryContent ) {
354
+ // For very large binary files, consider skipping or compressing ??
355
+ const MAX_BINARY_SIZE = 10 * 1024 * 1024 ; // 10MB limit
356
+ if ( binaryContent . length > MAX_BINARY_SIZE ) {
357
+ console . warn ( `[processBinaryFile] Skipping large binary file ${ relativePath } (${ binaryContent . length } bytes)` ) ;
358
+ return null ;
359
+ }
360
+
361
+ const base64String = convertToBase64 ( binaryContent ) ;
362
+
363
+ return {
364
+ path : relativePath ,
365
+ file : {
366
+ content : base64String ,
367
+ encoding : 'base64' as const ,
368
+ }
369
+ } ;
370
+ } else {
371
+ console . warn ( `[processBinaryFile] Failed to read binary content for ${ relativePath } ` ) ;
372
+ return null ;
373
+ }
374
+ } catch ( error ) {
375
+ console . warn ( `[processBinaryFile] Error processing ${ relativePath } :` , error ) ;
376
+ return null ;
377
+ }
378
+ }
379
+
380
+
381
+
382
+ /**
383
+ * Get all file paths in a directory tree using memory-efficient streaming
384
+ * Instead of accumulating all paths in memory, we yield them one by one
385
+ */
235
386
private async getAllFilePathsFlat ( rootDir : string ) : Promise < string [ ] > {
236
387
const allPaths : string [ ] = [ ] ;
237
388
const dirsToProcess = [ rootDir ] ;
@@ -288,81 +439,4 @@ export class PublishManager {
288
439
289
440
return { binaryFiles, textFiles } ;
290
441
}
291
-
292
-
293
- private async processTextFilesBatch ( filePaths : string [ ] , baseDir : string ) : Promise < Record < string , FreestyleFile > > {
294
- const promises = filePaths . map ( async ( fullPath ) => {
295
- const relativePath = fullPath . replace ( baseDir + '/' , '' ) ;
296
-
297
- try {
298
- const textContent = await this . session . fs . readTextFile ( fullPath ) ;
299
-
300
- if ( textContent !== null ) {
301
- return {
302
- path : relativePath ,
303
- file : {
304
- content : textContent ,
305
- encoding : 'utf-8' as const ,
306
- }
307
- } ;
308
- } else {
309
- console . warn ( `[processTextFilesBatch] Failed to read text content for ${ relativePath } ` ) ;
310
- return null ;
311
- }
312
- } catch ( error ) {
313
- console . warn ( `[processTextFilesBatch] Error processing ${ relativePath } :` , error ) ;
314
- return null ;
315
- }
316
- } ) ;
317
-
318
- const results = await Promise . all ( promises ) ;
319
- const files : Record < string , FreestyleFile > = { } ;
320
-
321
- for ( const result of results ) {
322
- if ( result ) {
323
- files [ result . path ] = result . file ;
324
- }
325
- }
326
-
327
- return files ;
328
- }
329
-
330
- private async processBinaryFilesBatch ( filePaths : string [ ] , baseDir : string ) : Promise < Record < string , FreestyleFile > > {
331
- const promises = filePaths . map ( async ( fullPath ) => {
332
- const relativePath = fullPath . replace ( baseDir + '/' , '' ) ;
333
-
334
- try {
335
- const binaryContent = await this . session . fs . readFile ( fullPath ) ;
336
-
337
- if ( binaryContent ) {
338
- const base64String = convertToBase64 ( binaryContent ) ;
339
-
340
- return {
341
- path : relativePath ,
342
- file : {
343
- content : base64String ,
344
- encoding : 'base64' as const ,
345
- }
346
- } ;
347
- } else {
348
- console . warn ( `[processBinaryFilesBatch] Failed to read binary content for ${ relativePath } ` ) ;
349
- return null ;
350
- }
351
- } catch ( error ) {
352
- console . warn ( `[processBinaryFilesBatch] Error processing ${ relativePath } :` , error ) ;
353
- return null ;
354
- }
355
- } ) ;
356
-
357
- const results = await Promise . all ( promises ) ;
358
- const files : Record < string , FreestyleFile > = { } ;
359
-
360
- for ( const result of results ) {
361
- if ( result ) {
362
- files [ result . path ] = result . file ;
363
- }
364
- }
365
-
366
- return files ;
367
- }
368
442
}
0 commit comments