Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix build cache management in workspaces #675

Merged
merged 7 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,44 +30,37 @@ export type BuildCacheInfo = {
buildResult: AndroidBuildResult | IOSBuildResult;
};

export class PlatformBuildCache {
static instances: Record<DevicePlatform, PlatformBuildCache | undefined> = {
[DevicePlatform.Android]: undefined,
[DevicePlatform.IOS]: undefined,
};

static forPlatform(platform: DevicePlatform): PlatformBuildCache {
if (!this.instances[platform]) {
this.instances[platform] = new PlatformBuildCache(platform);
}

return this.instances[platform];
}
function makeCacheKey(platform: DevicePlatform, appRoot: string) {
const keyPrefix =
platform === DevicePlatform.Android ? ANDROID_BUILD_CACHE_KEY : IOS_BUILD_CACHE_KEY;
return `${keyPrefix}:${appRoot}`;
}

private constructor(private readonly platform: DevicePlatform) {}
export class BuildCache {
private readonly cacheKey: string;

get cacheKey() {
return this.platform === DevicePlatform.Android ? ANDROID_BUILD_CACHE_KEY : IOS_BUILD_CACHE_KEY;
constructor(private readonly platform: DevicePlatform, private readonly appRoot: string) {
this.cacheKey = makeCacheKey(platform, appRoot);
}

/**
* Passed fingerprint should be calculated at the time build is started.
*/
public async storeBuild(buildFingerprint: string, build: BuildResult) {
const appPath = await getAppHash(getAppPath(build));
await extensionContext.workspaceState.update(this.cacheKey, {
await extensionContext.globalState.update(this.cacheKey, {
fingerprint: buildFingerprint,
buildHash: appPath,
buildResult: build,
});
}

public async clearCache() {
await extensionContext.workspaceState.update(this.cacheKey, undefined);
await extensionContext.globalState.update(this.cacheKey, undefined);
}

public async getBuild(currentFingerprint: string) {
const cache = extensionContext.workspaceState.get<BuildCacheInfo>(this.cacheKey);
const cache = extensionContext.globalState.get<BuildCacheInfo>(this.cacheKey);
if (!cache) {
Logger.debug("No cached build found.");
return undefined;
Expand Down Expand Up @@ -105,8 +98,7 @@ export class PlatformBuildCache {

public async isCacheStale() {
const currentFingerprint = await this.calculateFingerprint();
const { fingerprint } =
extensionContext.workspaceState.get<BuildCacheInfo>(this.cacheKey) ?? {};
const { fingerprint } = extensionContext.globalState.get<BuildCacheInfo>(this.cacheKey) ?? {};

return currentFingerprint !== fingerprint;
}
Expand Down Expand Up @@ -145,10 +137,10 @@ export class PlatformBuildCache {
const fingerprint = await runfingerprintCommand(fingerprintCommand, env);

if (!fingerprint) {
throw new Error("Failed to generate workspace fingerprint using custom script.");
throw new Error("Failed to generate application fingerprint using custom script.");
}

Logger.debug("Workspace fingerprint", fingerprint);
Logger.debug("Application fingerprint", fingerprint);
return fingerprint;
}
}
Expand All @@ -160,3 +152,23 @@ function getAppPath(build: BuildResult) {
async function getAppHash(appPath: string) {
return (await calculateMD5(appPath)).digest("hex");
}

export async function migrateOldBuildCachesToNewStorage() {
try {
const appRoot = getAppRootFolder();

for (const platform of [DevicePlatform.Android, DevicePlatform.IOS]) {
const oldKey =
platform === DevicePlatform.Android ? ANDROID_BUILD_CACHE_KEY : IOS_BUILD_CACHE_KEY;
const cache = extensionContext.workspaceState.get<BuildCacheInfo>(oldKey);
if (cache) {
await extensionContext.globalState.update(makeCacheKey(platform, appRoot), cache);
await extensionContext.workspaceState.update(oldKey, undefined);
}
}
} catch (e) {
// we ignore all potential errors in this phase as it isn't critical and it is
// better to not block the extension from starting in case of any issues when
// migrating the caches
}
}
18 changes: 10 additions & 8 deletions packages/vscode-extension/src/builders/BuildManager.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Disposable, OutputChannel, window } from "vscode";
import { PlatformBuildCache } from "./PlatformBuildCache";
import { BuildCache } from "./BuildCache";
import { AndroidBuildResult, buildAndroid } from "./buildAndroid";
import { IOSBuildResult, buildIos } from "./buildIOS";
import { DeviceInfo, DevicePlatform } from "../common/DeviceManager";
Expand All @@ -22,7 +22,10 @@ type BuildOptions = {
};

export class BuildManager {
constructor(private readonly dependencyManager: DependencyManager) {}
constructor(
private readonly dependencyManager: DependencyManager,
private readonly buildCache: BuildCache
) {}

private buildOutputChannel: OutputChannel | undefined;

Expand Down Expand Up @@ -53,10 +56,9 @@ export class BuildManager {
});

const cancelToken = new CancelToken();
const buildCache = PlatformBuildCache.forPlatform(platform);

const buildApp = async () => {
const currentFingerprint = await buildCache.calculateFingerprint();
const currentFingerprint = await this.buildCache.calculateFingerprint();

// Native build dependencies when changed, should invalidate cached build (even if the fingerprint is the same)
const buildDependenciesChanged = await this.checkBuildDependenciesChanged(deviceInfo);
Expand All @@ -68,9 +70,9 @@ export class BuildManager {
"Build cache is being invalidated",
forceCleanBuild ? "on request" : "due to build dependencies change"
);
await buildCache.clearCache();
await this.buildCache.clearCache();
} else {
const cachedBuild = await buildCache.getBuild(currentFingerprint);
const cachedBuild = await this.buildCache.getBuild(currentFingerprint);
if (cachedBuild) {
Logger.debug("Skipping native build – using cached");
getTelemetryReporter().sendTelemetryEvent("build:cache-hit", { platform });
Expand Down Expand Up @@ -122,7 +124,7 @@ export class BuildManager {
await this.dependencyManager.installPods(iOSBuildOutputChannel, cancelToken);
// Installing pods may impact the fingerprint as new pods may be created under the project directory.
// For this reason we need to recalculate the fingerprint after installing pods.
buildFingerprint = await buildCache.calculateFingerprint();
buildFingerprint = await this.buildCache.calculateFingerprint();
}
};
buildResult = await buildIos(
Expand All @@ -136,7 +138,7 @@ export class BuildManager {
);
}

await buildCache.storeBuild(buildFingerprint, buildResult);
await this.buildCache.storeBuild(buildFingerprint, buildResult);

return buildResult;
};
Expand Down
6 changes: 5 additions & 1 deletion packages/vscode-extension/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { getLaunchConfiguration } from "./utilities/launchConfiguration";
import { Project } from "./project/project";
import { findFilesInWorkspace, isWorkspaceRoot } from "./utilities/common";
import { Platform } from "./utilities/platform";
import { migrateOldBuildCachesToNewStorage } from "./builders/BuildCache";

const OPEN_PANEL_ON_ACTIVATION = "open_panel_on_activation";

Expand Down Expand Up @@ -73,7 +74,7 @@ export async function activate(context: ExtensionContext) {
enableDevModeLogging();
}

migrateOldConfiguration();
await migrateOldConfiguration();

commands.executeCommand("setContext", "RNIDE.sidePanelIsClosed", false);

Expand Down Expand Up @@ -245,6 +246,9 @@ export async function activate(context: ExtensionContext) {
}
}

// this needs to be run after app root is set
migrateOldBuildCachesToNewStorage();

extensionActivated();
}

Expand Down
4 changes: 3 additions & 1 deletion packages/vscode-extension/src/project/deviceSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { DebugSession, DebugSessionDelegate } from "../debugging/DebugSession";
import { throttle } from "../utilities/throttle";
import { DependencyManager } from "../dependency/DependencyManager";
import { getTelemetryReporter } from "../utilities/telemetry";
import { BuildCache } from "../builders/BuildCache";

type PreviewReadyCallback = (previewURL: string) => void;
type StartOptions = { cleanBuild: boolean; previewReadyCallback: PreviewReadyCallback };
Expand Down Expand Up @@ -54,10 +55,11 @@ export class DeviceSession implements Disposable {
private readonly devtools: Devtools,
private readonly metro: Metro,
readonly dependencyManager: DependencyManager,
readonly buildCache: BuildCache,
private readonly debugEventDelegate: DebugSessionDelegate,
private readonly eventDelegate: EventDelegate
) {
this.buildManager = new BuildManager(dependencyManager);
this.buildManager = new BuildManager(dependencyManager, buildCache);
this.devtools.addListener((event, payload) => {
switch (event) {
case "RNIDE_appReady":
Expand Down
10 changes: 5 additions & 5 deletions packages/vscode-extension/src/project/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
import { Logger } from "../Logger";
import { DeviceInfo } from "../common/DeviceManager";
import { DeviceAlreadyUsedError, DeviceManager } from "../devices/DeviceManager";
import { extensionContext } from "../utilities/extensionContext";
import { extensionContext, getAppRootFolder } from "../utilities/extensionContext";
import { IosSimulatorDevice } from "../devices/IosSimulatorDevice";
import { AndroidEmulatorDevice } from "../devices/AndroidEmulatorDevice";
import { DependencyManager } from "../dependency/DependencyManager";
Expand All @@ -31,7 +31,7 @@ import { DebugSessionDelegate } from "../debugging/DebugSession";
import { Metro, MetroDelegate } from "./metro";
import { Devtools } from "./devtools";
import { AppEvent, DeviceSession, EventDelegate } from "./deviceSession";
import { PlatformBuildCache } from "../builders/PlatformBuildCache";
import { BuildCache } from "../builders/BuildCache";
import { PanelLocation } from "../common/WorkspaceConfig";
import { activateDevice, getLicenseToken } from "../utilities/license";

Expand Down Expand Up @@ -632,6 +632,7 @@ export class Project
this.devtools,
this.metro,
this.dependencyManager,
new BuildCache(device.platform, getAppRootFolder()),
this,
this
);
Expand Down Expand Up @@ -669,9 +670,8 @@ export class Project
};

private checkIfNativeChanged = throttleAsync(async () => {
if (!this.isCachedBuildStale && this.projectState.selectedDevice) {
const platform = this.projectState.selectedDevice.platform;
const isCacheStale = await PlatformBuildCache.forPlatform(platform).isCacheStale();
if (!this.isCachedBuildStale && this.deviceSession) {
const isCacheStale = await this.deviceSession.buildCache.isCacheStale();

if (isCacheStale) {
this.isCachedBuildStale = true;
Expand Down
Loading