Skip to content

Commit 311292c

Browse files
committed
Cache the output of codeql resolve languages
Repeated calls to `resolveLanguages()` will only pay the performance penalty of executing `codeql resolve languages` once.
1 parent ee70cd5 commit 311292c

3 files changed

Lines changed: 99 additions & 19 deletions

File tree

src/codeql.ts

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -731,26 +731,31 @@ async function getCodeQLForCmd(
731731
filterToLanguagesWithQueries: boolean;
732732
} = { filterToLanguagesWithQueries: false },
733733
) {
734-
const codeqlArgs = [
735-
"resolve",
736-
"languages",
737-
"--format=betterjson",
738-
"--extractor-options-verbosity=4",
739-
"--extractor-include-aliases",
740-
...(filterToLanguagesWithQueries
741-
? ["--filter-to-languages-with-queries"]
742-
: []),
743-
...getExtraOptionsFromEnv(["resolve", "languages"]),
744-
];
745-
const output = await runCli(cmd, codeqlArgs);
734+
let result = util.getCachedCodeQlResolveLanguages(cmd);
735+
if (result === undefined) {
736+
const codeqlArgs = [
737+
"resolve",
738+
"languages",
739+
"--format=betterjson",
740+
"--extractor-options-verbosity=4",
741+
"--extractor-include-aliases",
742+
...(filterToLanguagesWithQueries
743+
? ["--filter-to-languages-with-queries"]
744+
: []),
745+
...getExtraOptionsFromEnv(["resolve", "languages"]),
746+
];
747+
const output = await runCli(cmd, codeqlArgs);
746748

747-
try {
748-
return JSON.parse(output) as ResolveLanguagesOutput;
749-
} catch (e) {
750-
throw new Error(
751-
`Unexpected output from codeql resolve languages with --format=betterjson: ${e}`,
752-
);
749+
try {
750+
result = JSON.parse(output) as ResolveLanguagesOutput;
751+
} catch (e) {
752+
throw new Error(
753+
`Unexpected output from codeql resolve languages with --format=betterjson: ${e}`,
754+
);
755+
}
756+
util.cacheCodeQlResolveLanguages(cmd, result);
753757
}
758+
return result;
754759
},
755760
async resolveBuildEnvironment(
756761
workingDir: string | undefined,

src/environment.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ export enum EnvVar {
2323
*/
2424
CODEQL_VERSION_INFO = "CODEQL_ACTION_CLI_VERSION_INFO",
2525

26+
/**
27+
* `ResolveLanguagesOutput` for the CodeQL CLI, so later Actions steps can reuse it instead of
28+
* invoking `codeql resolve languages` again.
29+
*/
30+
CODEQL_RESOLVE_LANGUAGES = "CODEQL_ACTION_CLI_RESOLVE_LANGUAGES",
31+
2632
/** Whether the CodeQL Action has invoked the Go autobuilder. */
2733
DID_AUTOBUILD_GOLANG = "CODEQL_ACTION_DID_AUTOBUILD_GOLANG",
2834

src/util.ts

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import * as yaml from "js-yaml";
1010
import * as semver from "semver";
1111

1212
import * as apiCompatibility from "./api-compatibility.json";
13-
import type { CodeQL, VersionInfo } from "./codeql";
13+
import type { CodeQL, VersionInfo, ResolveLanguagesOutput } from "./codeql";
1414
import type { Pack } from "./config/db-config";
1515
import type { Config } from "./config-utils";
1616
import { EnvVar } from "./environment";
@@ -701,6 +701,75 @@ export function getCachedCodeQlVersion(cmd?: string): undefined | VersionInfo {
701701
return cachedCodeQlVersion;
702702
}
703703

704+
let cachedCodeQlResolveLanguages: undefined | ResolveLanguagesOutput =
705+
undefined;
706+
707+
interface PersistedResolveLanguagesOutput {
708+
cmd: string;
709+
output: ResolveLanguagesOutput;
710+
}
711+
712+
export function cacheCodeQlResolveLanguages(
713+
cmd: string,
714+
output: ResolveLanguagesOutput,
715+
): void {
716+
if (cachedCodeQlResolveLanguages !== undefined) {
717+
throw new Error("cacheCodeQlResolveLanguages() should be called only once");
718+
}
719+
cachedCodeQlResolveLanguages = output;
720+
// Persist the output so that subsequent Actions steps, which run in separate
721+
// processes, can reuse it rather than invoking `codeql resolve languages` again. We
722+
// record the CLI path so that a different step using a different CodeQL bundle
723+
// doesn't pick up a stale output.
724+
core.exportVariable(
725+
EnvVar.CODEQL_RESOLVE_LANGUAGES,
726+
JSON.stringify({ cmd, output }),
727+
);
728+
}
729+
730+
function isPersistedResolveLanguagesOutput(
731+
value: unknown,
732+
): value is PersistedResolveLanguagesOutput {
733+
return (
734+
typeof value === "object" &&
735+
value !== null &&
736+
typeof (value as Record<string, unknown>).cmd === "string" &&
737+
typeof (value as Record<string, unknown>).output === "object" &&
738+
(value as Record<string, unknown>).output !== null
739+
);
740+
}
741+
742+
export function getCachedCodeQlResolveLanguages(
743+
cmd?: string,
744+
): undefined | ResolveLanguagesOutput {
745+
if (cachedCodeQlResolveLanguages !== undefined) {
746+
return cachedCodeQlResolveLanguages;
747+
}
748+
// Fall back to the value persisted by an earlier Actions step, if any. This is
749+
// best-effort: any malformed or mismatched value is ignored so that the caller
750+
// invokes `codeql resolve languages` instead.
751+
const serialized = process.env[EnvVar.CODEQL_RESOLVE_LANGUAGES];
752+
if (!serialized) {
753+
return undefined;
754+
}
755+
let persisted: unknown;
756+
try {
757+
persisted = JSON.parse(serialized);
758+
} catch {
759+
return undefined;
760+
}
761+
if (
762+
!isPersistedResolveLanguagesOutput(persisted) ||
763+
(cmd !== undefined && persisted.cmd !== cmd)
764+
) {
765+
return undefined;
766+
}
767+
// Memoize the parsed value so that subsequent calls in this process don't
768+
// re-parse the environment variable.
769+
cachedCodeQlResolveLanguages = persisted.output;
770+
return cachedCodeQlResolveLanguages;
771+
}
772+
704773
export async function codeQlVersionAtLeast(
705774
codeql: CodeQL,
706775
requiredVersion: string,

0 commit comments

Comments
 (0)