From b5317ea6e9851d60fcbc1b68f750619f422e73b2 Mon Sep 17 00:00:00 2001 From: = Date: Mon, 9 Mar 2026 17:09:41 +0100 Subject: [PATCH 1/2] add gradle service --- src/participation/cloning.service.ts | 8 +++++++ src/participation/gradle.service.ts | 35 ++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 src/participation/gradle.service.ts diff --git a/src/participation/cloning.service.ts b/src/participation/cloning.service.ts index 33fd386..7b15b7c 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,12 @@ 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 daemon in background to avoid ~3s init on first build + const exercise = getState().displayedExercise; + if ((exercise as ProgrammingExercise)?.programmingLanguage === ProgrammingLanguage.JAVA) { + warmupGradleDaemon(clonePath); + } + 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..bbcb913 --- /dev/null +++ b/src/participation/gradle.service.ts @@ -0,0 +1,35 @@ +import { spawn } from "child_process"; +import * as fs from "fs"; +import * as path from "path"; + +/** + * Pre-warms the Gradle daemon in the background after cloning a repository. + * This avoids the ~3s daemon initialization overhead on the first build. + * + * Silently skips if: + * - Running on Windows (not a supported environment) + * - The project does not contain a `gradlew` file (not a Gradle project) + */ +export function warmupGradleDaemon(projectPath: string): void { + if (process.platform === "win32") { + return; + } + + const gradlewPath = path.join(projectPath, "gradlew"); + if (!fs.existsSync(gradlewPath)) { + return; + } + + try { + fs.chmodSync(gradlewPath, 0o755); + + const child = spawn("./gradlew", ["--daemon"], { + cwd: projectPath, + detached: true, + stdio: "ignore", + }); + child.unref(); + } catch (error: any) { + console.warn(`Gradle daemon warmup failed: ${error.message}`); + } +} From 7969fd37c0efa13edd3e6f0b627bb88a1c1878dd Mon Sep 17 00:00:00 2001 From: = Date: Wed, 1 Jul 2026 17:37:17 +0200 Subject: [PATCH 2/2] add more granular gradle prewarmin --- src/participation/cloning.service.ts | 5 ++-- src/participation/gradle.service.ts | 36 +++++++++++++++++++++++----- src/theia/env-strategy.ts | 32 +++++++++++++++++++++++++ src/theia/theia.ts | 3 ++- 4 files changed, 67 insertions(+), 9 deletions(-) diff --git a/src/participation/cloning.service.ts b/src/participation/cloning.service.ts index 7b15b7c..c0bff4e 100644 --- a/src/participation/cloning.service.ts +++ b/src/participation/cloning.service.ts @@ -50,10 +50,11 @@ 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 daemon in background to avoid ~3s init on first build + // 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); + warmupGradleDaemon(clonePath, theiaEnv.GRADLE_PREWARM); } if (!theiaEnv.THEIA_FLAG) { diff --git a/src/participation/gradle.service.ts b/src/participation/gradle.service.ts index bbcb913..d980801 100644 --- a/src/participation/gradle.service.ts +++ b/src/participation/gradle.service.ts @@ -1,17 +1,37 @@ import { spawn } from "child_process"; import * as fs from "fs"; import * as path from "path"; +import { GradlePrewarmLevel } from "../theia/env-strategy"; /** - * Pre-warms the Gradle daemon in the background after cloning a repository. - * This avoids the ~3s daemon initialization overhead on the first build. + * 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): void { - if (process.platform === "win32") { +export function warmupGradleDaemon( + projectPath: string, + level: GradlePrewarmLevel = "daemon", +): void { + if (level === "off" || process.platform === "win32") { return; } @@ -23,13 +43,17 @@ export function warmupGradleDaemon(projectPath: string): void { try { fs.chmodSync(gradlewPath, 0o755); - const child = spawn("./gradlew", ["--daemon"], { + 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 daemon warmup failed: ${error.message}`); + 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, }; /**