Improve remote runtime cache logging

This commit is contained in:
2026-05-10 22:56:29 +00:00
parent 61f6d37960
commit 7de42e10df
5 changed files with 113 additions and 12 deletions
+59 -10
View File
@@ -75,9 +75,9 @@ class GitZoneIdeElectronShell {
const serverVersion = plugins.electron.app.getVersion();
progress('Staging remote runtime payload.');
const runtime = await createLocalEphemeralRuntime(serverVersion);
progress(`Runtime hash ${runtime.contentHash.slice(0, 12)} staged for ${runtime.remoteRoot}.`);
progress(`Runtime hash ${runtime.contentHash.slice(0, 12)} staged: ${runtime.fileCount} files, ${formatBytes(runtime.totalBytes)} unpacked.`);
try {
progress('Checking remote runtime cache.');
progress(`Checking remote /tmp cache at ${runtime.remoteRoot}.`);
const cacheCheckCommand = plugins.ideServerInstaller.createRemoteEphemeralRuntimeCacheCheckCommand({
runtimeRoot: runtime.remoteRoot,
runtimeSha256: runtime.contentHash,
@@ -87,17 +87,31 @@ class GitZoneIdeElectronShell {
batchMode: input.batchMode ?? true,
});
if (cacheCheckResult.exitCode === 0) {
progress('Remote runtime cache hit; skipping upload.');
progress(`Remote runtime hash matches; skipping copy for ${runtime.contentHash.slice(0, 12)}.`);
} else {
progress('Remote runtime cache miss; uploading payload.');
progress(`Remote runtime cache miss; copying ${formatBytes(runtime.totalBytes)} to ${runtime.remoteRoot}.`);
const reportUploadProgress = createUploadProgressReporter(progress, runtime.totalBytes);
const uploadResult = await plugins.ideSsh.uploadDirectoryToRemote(target, runtime.localRoot, runtime.remoteRoot, {
timeoutMs: 300000,
batchMode: input.batchMode ?? true,
onProgress: (uploadProgress) => reportUploadProgress(uploadProgress.bytesUploaded),
});
if (uploadResult.exitCode !== 0) {
throw new Error(uploadResult.stderr || `Remote runtime upload failed with ${uploadResult.exitCode}`);
}
progress('Remote runtime upload complete.');
progress('Remote runtime files copied; writing cache marker.');
const markCommand = plugins.ideServerInstaller.createRemoteEphemeralRuntimeMarkCommand({
runtimeRoot: runtime.remoteRoot,
runtimeSha256: runtime.contentHash,
});
const markResult = await plugins.ideSsh.runSshCommand(target, markCommand, {
timeoutMs: 30000,
batchMode: input.batchMode ?? true,
});
if (markResult.exitCode !== 0) {
throw new Error(markResult.stderr || `Remote runtime marker write failed with ${markResult.exitCode}`);
}
progress(`Remote runtime upload complete; cache marker ${runtime.contentHash.slice(0, 12)} stored.`);
}
progress('Starting remote Theia runtime.');
@@ -267,6 +281,25 @@ const createProgressEmitter = (webContents: { isDestroyed(): boolean; send(chann
};
};
const createUploadProgressReporter = (progress: (message: string) => void, unpackedBytes: number) => {
const startedAt = Date.now();
let lastReportedAt = 0;
let lastReportedBytes = 0;
return (bytesUploaded: number) => {
const now = Date.now();
if (bytesUploaded - lastReportedBytes < 4 * 1024 * 1024 && now - lastReportedAt < 2000) {
return;
}
lastReportedAt = now;
lastReportedBytes = bytesUploaded;
const elapsedSeconds = Math.max((now - startedAt) / 1000, 0.001);
const uploadRate = bytesUploaded / elapsedSeconds;
progress(`Copying runtime: ${formatBytes(bytesUploaded)} compressed sent at ${formatBytes(uploadRate)}/s (${formatBytes(unpackedBytes)} unpacked).`);
};
};
const createLocalEphemeralRuntime = async (serverVersion: string) => {
const stageId = `gitzone-ide-stage-${sanitizeRuntimePart(serverVersion)}-${Date.now()}-${plugins.crypto.randomBytes(4).toString('hex')}`;
const localRoot = path.join(os.tmpdir(), stageId);
@@ -283,11 +316,10 @@ const createLocalEphemeralRuntime = async (serverVersion: string) => {
await fs.copyFile(nodeBinary, targetNodeBinary);
await fs.chmod(targetNodeBinary, 0o755);
await copyNodeSharedLibraries(nodeBinary, path.join(localRoot, 'node', 'lib'));
const contentHash = await hashLocalRuntimeDirectory(localRoot);
await fs.writeFile(path.join(localRoot, runtimeMarkerFileName), `${contentHash}\n`, 'utf8');
const remoteRoot = `/tmp/gitzone-ide-${sanitizeRuntimePart(serverVersion)}-${contentHash}`;
const runtimeHash = await hashLocalRuntimeDirectory(localRoot);
const remoteRoot = `/tmp/gitzone-ide-${sanitizeRuntimePart(serverVersion)}-${runtimeHash.contentHash}`;
return { localRoot, remoteRoot, contentHash };
return { localRoot, remoteRoot, ...runtimeHash };
};
const copyNodeSharedLibraries = async (nodeBinary: string, targetDirectory: string) => {
@@ -311,6 +343,8 @@ const copyNodeSharedLibraries = async (nodeBinary: string, targetDirectory: stri
const hashLocalRuntimeDirectory = async (rootDirectory: string) => {
const hash = plugins.crypto.createHash('sha256');
const filePaths = await listLocalRuntimeFiles(rootDirectory);
let fileCount = 0;
let totalBytes = 0;
for (const filePath of filePaths) {
const relativePath = path.relative(rootDirectory, filePath).split(path.sep).join('/');
@@ -321,6 +355,8 @@ const hashLocalRuntimeDirectory = async (rootDirectory: string) => {
const stats = await fs.lstat(filePath);
if (stats.isSymbolicLink()) {
const linkTarget = await fs.readlink(filePath);
fileCount++;
totalBytes += Buffer.byteLength(linkTarget);
hash.update(`link\0${relativePath}\0${linkTarget}\0`);
continue;
}
@@ -329,12 +365,14 @@ const hashLocalRuntimeDirectory = async (rootDirectory: string) => {
continue;
}
fileCount++;
totalBytes += stats.size;
hash.update(`file\0${relativePath}\0${stats.mode & 0o111 ? 'x' : '-'}\0${stats.size}\0`);
await updateHashFromFile(hash, filePath);
hash.update('\0');
}
return hash.digest('hex');
return { contentHash: hash.digest('hex'), fileCount, totalBytes };
};
const listLocalRuntimeFiles = async (rootDirectory: string) => {
@@ -385,6 +423,17 @@ const resolveLocalNodeBinary = async () => {
const sanitizeRuntimePart = (value: string) => value.replace(/[^a-zA-Z0-9._-]/g, '-');
const formatBytes = (bytes: number) => {
const units = ['B', 'KiB', 'MiB', 'GiB'];
let value = bytes;
let unitIndex = 0;
while (value >= 1024 && unitIndex < units.length - 1) {
value /= 1024;
unitIndex++;
}
return `${value.toFixed(unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`;
};
const waitForHttpUrl = async (url: string, timeoutMs: number) => {
const startedAt = Date.now();
let lastError: unknown;