diff --git a/src/participation/cloning.service.ts b/src/participation/cloning.service.ts index 33fd386..c0bff4e 100644 --- a/src/participation/cloning.service.ts +++ b/src/participation/cloning.service.ts @@ -8,6 +8,8 @@ import * as path from "path"; import { retrieveVcsAccessToken } from "../artemis/authentication.client"; import { getWorkspaceFolder, theiaEnv } from "../theia/theia"; import { addVcsTokenToUrl } from "@shared/models/participation.model"; +import { ProgrammingExercise, ProgrammingLanguage } from "@shared/models/exercise.model"; +import { warmupGradleDaemon } from "./gradle.service"; export async function cloneUserRepo(repoUrl: string, username: string) { // get folder to clone repo into @@ -48,6 +50,13 @@ export async function cloneUserRepo(repoUrl: string, username: string) { const cloneUrlWithToken = new URL(addVcsTokenToUrl(repoUrl, username, vcsToken)); const clonePath = await cloneByGivenURL(cloneUrlWithToken, destinationPath); + // Pre-warm Gradle in the background so the student's first build is faster. + // The depth (off/daemon/deps/full) is controlled by the GRADLE_PREWARM env var. + const exercise = getState().displayedExercise; + if ((exercise as ProgrammingExercise)?.programmingLanguage === ProgrammingLanguage.JAVA) { + warmupGradleDaemon(clonePath, theiaEnv.GRADLE_PREWARM); + } + if (!theiaEnv.THEIA_FLAG) { // Prompt the user to open the cloned folder in a new workspace const openIn = await vscode.window.showInformationMessage( diff --git a/src/participation/gradle.service.ts b/src/participation/gradle.service.ts new file mode 100644 index 0000000..d980801 --- /dev/null +++ b/src/participation/gradle.service.ts @@ -0,0 +1,59 @@ +import { spawn } from "child_process"; +import * as fs from "fs"; +import * as path from "path"; +import { GradlePrewarmLevel } from "../theia/env-strategy"; + +/** + * Gradle invocation per prewarm level. Each higher level warms one more phase of + * the student's first build during session startup (see the prewarming levels L1-L4): + * - "daemon" (L1): start the Gradle daemon so it is warm and reused. + * - "deps" (L3): additionally configure the build and resolve/download dependencies. + * - "full" (L4): additionally compile, leaving the first build near-instant. + * "off" disables prewarming and is handled before this map is used. + */ +const PREWARM_ARGS: Record, string[]> = { + daemon: ["--daemon", "help"], + deps: ["--daemon", "dependencies"], + full: ["--daemon", "build", "-x", "test"], +}; + +/** + * Pre-warms the Gradle build in the background after cloning a repository, so the + * student's first build can skip the phases warmed here. Runs detached and never + * blocks the caller. + * + * Silently skips if: + * - Prewarming is disabled (`level` is "off") + * - Running on Windows (not a supported environment) + * - The project does not contain a `gradlew` file (not a Gradle project) + */ +export function warmupGradleDaemon( + projectPath: string, + level: GradlePrewarmLevel = "daemon", +): void { + if (level === "off" || process.platform === "win32") { + return; + } + + const gradlewPath = path.join(projectPath, "gradlew"); + if (!fs.existsSync(gradlewPath)) { + return; + } + + try { + fs.chmodSync(gradlewPath, 0o755); + + const child = spawn("./gradlew", PREWARM_ARGS[level], { + cwd: projectPath, + detached: true, + stdio: "ignore", + }); + // Detached failures surface asynchronously via the 'error' event, not the throw below. + child.on("error", (error) => { + console.warn(`Gradle prewarm (${level}) failed to start: ${error.message}`); + }); + child.unref(); + } catch (error: any) { + console.warn(`Gradle prewarm (${level}) failed: ${error.message}`); + } +} diff --git a/src/theia/env-strategy.ts b/src/theia/env-strategy.ts index 2b1a02f..25008b8 100644 --- a/src/theia/env-strategy.ts +++ b/src/theia/env-strategy.ts @@ -1,6 +1,16 @@ import * as vscode from "vscode"; import { exec } from "child_process"; +/** + * Controls how much of the first Gradle build is warmed up in the background at + * session startup (see the prewarming levels L1-L4): + * - "off": no prewarming. + * - "daemon": start the Gradle daemon so it is warm and reused (L1). + * - "deps": also configure the build and resolve/download dependencies (L3). + * - "full": also compile, leaving the student's first build near-instant (L4). + */ +export type GradlePrewarmLevel = "off" | "daemon" | "deps" | "full"; + export type TheiaEnv = { THEIA_FLAG: boolean; ARTEMIS_TOKEN: string | undefined; @@ -8,6 +18,7 @@ export type TheiaEnv = { GIT_URI: URL | undefined; GIT_USER: string | undefined; GIT_MAIL: string | undefined; + GRADLE_PREWARM: GradlePrewarmLevel; }; const ENV_KEYS = [ @@ -17,8 +28,28 @@ const ENV_KEYS = [ "GIT_URI", "GIT_USER", "GIT_MAIL", + "GRADLE_PREWARM", ] as const satisfies Array; +/** + * Default prewarm level when the environment variable is unset. Warming the + * daemon is cheap and benefits every Gradle session, so it is the safe default. + */ +export const DEFAULT_GRADLE_PREWARM: GradlePrewarmLevel = "daemon"; + +function parseGradlePrewarm(value: string | undefined): GradlePrewarmLevel { + const level = value?.trim().toLowerCase(); + if (level === "off" || level === "daemon" || level === "deps" || level === "full") { + return level; + } + if (level) { + console.warn( + `Unknown GRADLE_PREWARM value "${value}", falling back to "${DEFAULT_GRADLE_PREWARM}"`, + ); + } + return DEFAULT_GRADLE_PREWARM; +} + export interface TheiaEnvStrategy { load(): Promise; } @@ -32,6 +63,7 @@ function parseTheiaEnv(env: Record): TheiaEnv { GIT_URI: gitUriString ? new URL(gitUriString) : undefined, GIT_USER: env["GIT_USER"], GIT_MAIL: env["GIT_MAIL"], + GRADLE_PREWARM: parseGradlePrewarm(env["GRADLE_PREWARM"]), }; } diff --git a/src/theia/theia.ts b/src/theia/theia.ts index a126294..0446e5d 100644 --- a/src/theia/theia.ts +++ b/src/theia/theia.ts @@ -2,7 +2,7 @@ import * as vscode from "vscode"; import simpleGit, { GitConfigScope } from "simple-git"; import { hostname } from "os"; import { cloneByGivenURL } from "../participation/cloning.service"; -import { createTheiaEnvStrategy, TheiaEnv } from "./env-strategy"; +import { createTheiaEnvStrategy, DEFAULT_GRADLE_PREWARM, TheiaEnv } from "./env-strategy"; // Mutable theiaEnv that gets populated after loading export let theiaEnv: TheiaEnv = { @@ -12,6 +12,7 @@ export let theiaEnv: TheiaEnv = { GIT_URI: undefined, GIT_USER: undefined, GIT_MAIL: undefined, + GRADLE_PREWARM: DEFAULT_GRADLE_PREWARM, }; /**