UNPKG

@typespec/compiler

Version:

TypeSpec compiler and standard library

266 lines 11.4 kB
import { formatLog } from "../core/logger/index.js"; import { compile as compileProgram, normalizePath, } from "../index.js"; import { debugLoggers } from "./debug.js"; import { trackActionFunc } from "./server-track-action-task.js"; /** * This class purely manages compilations triggered and the caches used underneath. It doesn't have or care about any extra knowledge beyond compile itself. * Instead compiler service would have more knowledge about the lsp scenarios to provide higher level service. It will be responsible to make sure compilation request * is built properly (i.e. figure out the correct entrypoint file, double check whether compilation result covers given document, and so on) and then trigger the actual * compilation with proper options through this class. */ export class ServerCompileManager { updateManager; compilerHost; log; // We may want a ttl for this trackerCache = new CompileCache((msg) => this.logDebug(msg)); compileId = 0; logDebug; constructor(updateManager, compilerHost, log) { this.updateManager = updateManager; this.compilerHost = compilerHost; this.log = log; const debug = debugLoggers.serverCompile; this.logDebug = debug.enabled ? (msg) => this.log({ level: "debug", message: msg }) : () => { }; } async compile(mainFile, compileOptions = {}, serverCompileOptions) { let cache; const curId = this.compileId++; const err = new Error(); const lines = err.stack?.split("\n") ?? []; // log where the compiler is triggered, skip the first 2 frame and only log the next 2 if exists const stackLines = lines.slice(3, 5).join("\n"); this.logDebug(`Server compile #${curId}: Triggered, version=${this.updateManager.docChangedVersion}, mode=${serverCompileOptions.mode}, mainFile=${mainFile}, from\n${stackLines}`); if (!serverCompileOptions.skipCache) { cache = this.trackerCache.get(mainFile, compileOptions, false /*hasCompleted*/, serverCompileOptions.mode); if (cache && cache.isUpToDate()) { this.logDebug(`Server compile #${curId}: Return cache at #${cache.getCompileId()}(${cache.getMode()})`); return cache; } else { this.logDebug(`Server compile #${curId}: Cache miss because ${cache ? `it's outdated with version ${cache.getVersion()}` : "no cache available"}`); } } let oldProgram = undefined; if (!serverCompileOptions.skipOldProgramFromCache) { const completedTracker = this.trackerCache.get(mainFile, compileOptions, true /* hasCompleted */, serverCompileOptions.mode); oldProgram = await completedTracker?.getCompileResult(); this.logDebug(`Server compile #${curId}: Use old program from cache: ${oldProgram ? `from #${completedTracker?.getCompileId() ?? "undefined"}` : "n/a"}`); } const tracker = CompileTracker.compile(curId, this.updateManager, this.compilerHost, mainFile, compileOptions, serverCompileOptions.mode, oldProgram, (msg) => this.logDebug(msg)); this.trackerCache.set(mainFile, compileOptions, tracker); return tracker; } } class CompileCache { log; coreCache; fullCache; constructor(log) { this.log = log; this.coreCache = new CompileCacheInternal(log); this.fullCache = new CompileCacheInternal(log); } get(entrypoint, compileOption, completedOnly, mode) { switch (mode) { case "core": // full cache can also be used for core, just return the latest one const core = this.coreCache.get(entrypoint, compileOption, completedOnly); const full = this.fullCache.get(entrypoint, compileOption, completedOnly); // only consider using full when it's already completed, otherwise, full compilation may take longer time if (core && full && full.isCompleted()) { if (full.getVersion() > core.getVersion()) { this.log(`Server compile: Using full cache (version ${full.getVersion()}) over core cache (version ${core.getVersion()})`); return full; } else { return core; } } else if (core) { return core; } else if (full && full.isCompleted()) { this.log(`Server compile: Using full cache (version ${full.getVersion()}) over core cache which is unavailable`); return full; } else { return undefined; } case "full": return this.fullCache.get(entrypoint, compileOption, completedOnly); default: // not expected, just in case, and we don't want to terminate because of cache in prod if (process.env.NODE_ENV === "development") { throw new Error(`Unexpected compile mode: ${mode}`); } return undefined; } } set(entrypoint, compileOption, tracker) { const mode = tracker.getMode(); switch (mode) { case "core": this.coreCache.set(entrypoint, compileOption, tracker); break; case "full": this.fullCache.set(entrypoint, compileOption, tracker); break; default: if (process.env.NODE_ENV === "development") { throw new Error(`Unexpected compile mode: ${mode}`); } return undefined; } } } class CompileCacheInternal { log; cacheLatest = new Map(); /** Cache for completed compilation which is needed when we need an old program but the latest one is still in progress */ cacheCompleted = new Map(); constructor(log) { this.log = log; } getCacheKey(entrypoint, compileOption) { const normalizedEntrypoint = normalizePath(entrypoint); const normalizedOptions = { ...compileOption, outputDir: undefined, }; return `${normalizedEntrypoint}\n${normalizedOptions}`; } /** Get the latest completed compilation */ get(entrypoint, compileOption, /** * Whether to only return completed compilations */ completedOnly) { const key = this.getCacheKey(entrypoint, compileOption); const tracker = this.cacheLatest.get(key); // completed cache is from latest cache, so if latest is undefined, no need to check completed cache any more if (!completedOnly || !tracker) { return tracker; } if (tracker.isCompleted() === true) { return tracker; } return this.cacheCompleted.get(key); } set(entrypoint, compileOption, tracker) { const key = this.getCacheKey(entrypoint, compileOption); this.cacheLatest.set(key, tracker); const onComplete = () => { const cur = this.cacheCompleted.get(key); if (!cur || cur.getVersion() < tracker.getVersion()) { // There may be a race condition here when two onComplete occur at the same time(both of them pass the check and try to set the cache) // But the chance is very low, the cache status is still good (just set to a newer but not latest version), and the next compile can // likely fix it, so don't do special handling here for it this.cacheCompleted.set(key, tracker); this.log(`Server compile #${tracker.getCompileId()}: Completed Cache updated ( ${cur?.getVersion() ?? "n/a"} -> ${tracker.getVersion()} )`); } }; tracker.getCompileResult().then(() => { onComplete(); }, (err) => { onComplete(); }); } } export class CompileTracker { id; updateManager; entrypoint; compileResultPromise; version; startTime; mode; logs; endTime; static compile(id, updateManager, host, mainFile, options = {}, mode, oldProgram, log) { const sLogs = []; // Clone an compilerhost instance with my logSink so that we can collect compiler logs // for each compilation to it's own tracker // Usually we don't want to send out the log because we are compiling aggressively in the lsp // but in some case like the 'emit-code', we will want to send back the logs if there is any const myHost = { ...host, logSink: { log: (log) => { const msg = formatLog(log, { excludeLogLevel: true }); const sLog = { level: log.level, message: msg, }; sLogs.push(sLog); }, trackAction: (message, finalMessage, action) => trackActionFunc((log) => sLogs.push(log), message, finalMessage, action), }, }; const myOption = mode === "core" ? { ...options, emit: [], linterRuleSet: undefined, } : { ...options, }; const version = updateManager.docChangedVersion; const startTime = new Date(); const p = compileProgram(myHost, mainFile, myOption, oldProgram); log(`Server compile #${id}: Start compilation at ${startTime.toISOString()}, version = ${version}, mainFile = ${mainFile}, mode = ${mode}`); return new CompileTracker(id, updateManager, mainFile, p, version, startTime, mode, sLogs, log); } constructor(id, updateManager, entrypoint, compileResultPromise, version, startTime, mode, logs, log) { this.id = id; this.updateManager = updateManager; this.entrypoint = entrypoint; this.compileResultPromise = compileResultPromise; this.version = version; this.startTime = startTime; this.mode = mode; this.logs = logs; this.startTime = startTime; const onComplete = () => { this.endTime = new Date(); log(`Server compile #${this.getCompileId()}: Compilation finished at ${this.endTime.toISOString()}. Duration = ${this.endTime.getTime() - this.startTime.getTime()}ms`); }; compileResultPromise.then((r) => { onComplete(); }, (err) => { onComplete(); }); } getCompileId() { return this.id; } getEntryPoint() { return this.entrypoint; } async getCompileResult() { return await this.compileResultPromise; } getVersion() { return this.version; } getStartTime() { return this.startTime; } getEndTime() { return this.endTime; } isCompleted() { return this.endTime !== undefined; } isUpToDate() { return this.version === this.updateManager.docChangedVersion; } getMode() { return this.mode; } getLogs() { return this.logs; } } //# sourceMappingURL=server-compile-manager.js.map