Skip to content

Commit 0f1c5ce

Browse files
update publish function
1 parent 875502b commit 0f1c5ce

File tree

1 file changed

+168
-94
lines changed
  • apps/web/client/src/server/api/routers/publish

1 file changed

+168
-94
lines changed

apps/web/client/src/server/api/routers/publish/manager.ts

Lines changed: 168 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -186,52 +186,203 @@ export class PublishManager {
186186
}
187187

188188
/**
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
190191
* @param currentDir - The directory path to serialize
191192
* @returns Record of file paths to their content (base64 for binary, utf-8 for text)
192193
*/
193194
private async serializeFiles(currentDir: string): Promise<Record<string, FreestyleFile>> {
194195
const timer = new LogTimer('File Serialization');
196+
const startTime = Date.now();
195197

196198
try {
197199
const allFilePaths = await this.getAllFilePathsFlat(currentDir);
198200
timer.log(`File discovery completed - ${allFilePaths.length} files found`);
199201

200202
const filteredPaths = allFilePaths.filter(filePath => !this.shouldSkipFile(filePath));
203+
timer.log(`Filtered to ${filteredPaths.length} files after exclusions`);
201204

202205
const { binaryFiles, textFiles } = this.categorizeFiles(filteredPaths);
206+
timer.log(`Categorized: ${textFiles.length} text files, ${binaryFiles.length} binary files`);
203207

204-
const BATCH_SIZE = 50;
205208
const files: Record<string, FreestyleFile> = {};
206-
209+
207210
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);
214214
timer.log('Text files processing completed');
215215
}
216216

217217
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);
224221
timer.log('Binary files processing completed');
225222
}
226223

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`);
228227
return files;
229228
} 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);
231232
throw error;
232233
}
233234
}
234235

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+
*/
235386
private async getAllFilePathsFlat(rootDir: string): Promise<string[]> {
236387
const allPaths: string[] = [];
237388
const dirsToProcess = [rootDir];
@@ -288,81 +439,4 @@ export class PublishManager {
288439

289440
return { binaryFiles, textFiles };
290441
}
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-
}
368442
}

0 commit comments

Comments
 (0)