Skip to content

Commit

Permalink
Fix build cache management in workspaces (#675)
Browse files Browse the repository at this point in the history
This PR changes a way we manage build caches. Previous approach was
dependent on current vscode workspace, which led to build caches not
being accessible when switching from working in the root of the
workspace to the root of the application (or doing the same in reverse).
The new approach saves this data in global storage, and adds a appRoot
identifier to the cache key.

This PR is a dependency for #663

### How Has This Been Tested: 

- Open a project in the root of the workspace and run some application
that is part of it in the IDE.
- Open IDE in the root of that application and see if the build cache is
loaded.
- repeat the proces in reverse
- open an application that had a build made before this change and check
if migration process works as expected

Verify that the build cache migration work:
- Build project without these changes
- Open the same project with no changes but with the new version of
extension – expect the cache hit for native build

---------

Co-authored-by: Krzysztof Magiera <[email protected]>
  • Loading branch information
filip131311 and kmagiera authored Dec 16, 2024
1 parent 7d0607f commit 4aa4897
Show file tree
Hide file tree
Showing 5 changed files with 58 additions and 38 deletions.
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

0 comments on commit 4aa4897

Please sign in to comment.