Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Evict old files from the cache prior to saving #270

Merged
merged 5 commits into from
Dec 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -240,3 +240,15 @@ jobs:
else
ls -l $(which gcc) | grep -v $(which ccache)
fi

test_option_evict:
runs-on: ubuntu-latest
strategy:
matrix:
evict: ['job', '30s', '']
steps:
- uses: actions/checkout@v4
- name: Run ccache-action
uses: ./
with:
evict-old-files: ${{ matrix.evict }}
35 changes: 34 additions & 1 deletion __tests__/common.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,40 @@
import * as common from '../src/common';

import * as core from "@actions/core";

describe('ccache common', () => {
test('parse evict age parameter in seconds', () => {
const age = '42s';
const [time, unit] = common.parseEvictAgeParameter(age);
expect(time).toEqual(42);
expect(unit).toEqual(common.AgeUnit.Seconds);
});

test('parse evict age parameter in days', () => {
const age = '28d';
const [time, unit] = common.parseEvictAgeParameter(age);
expect(time).toEqual(28);
expect(unit).toEqual(common.AgeUnit.Days);
});

test('parse evict age parameter - job', () => {
const age = 'job';
const [, unit] = common.parseEvictAgeParameter(age);
expect(unit).toEqual(common.AgeUnit.Job);
});

test('get duration of job in seconds', () => {
const stateMock = jest.spyOn(core, "getState");
const expectedAgeInSeconds = 1234;
const startTimeMs = 1734258917128;
const endTimeMs = startTimeMs + expectedAgeInSeconds * 1000;
stateMock.mockImplementationOnce(() => startTimeMs.toString());
jest.useFakeTimers().setSystemTime(new Date(endTimeMs));

const age = common.getJobDurationInSeconds();
expect(stateMock).toHaveBeenCalledWith("startTimestamp");
expect(age).toBe(expectedAgeInSeconds);
});

test('parse version string from ccache output', () => {
const ccacheOutput = `ccache version 4.10.2
Features: avx2 file-storage http-storage redis+unix-storage redis-storage
Expand Down
23 changes: 23 additions & 0 deletions __tests__/save.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {AgeUnit} from "../src/common";
import * as save from '../src/save';
import * as exec from "@actions/exec";

jest.mock("@actions/exec");

describe('ccache save', () => {
test('evict old files from the cache by age in seconds', async () => {
const proc = jest.spyOn(exec, "exec");

const ageInSeconds = 42;
await save.evictOldFiles(ageInSeconds, AgeUnit.Seconds);
expect(proc).toHaveBeenCalledWith(`ccache --evict-older-than ${ageInSeconds}s`);
});

test('evict old files from the cache by age in days', async () => {
const proc = jest.spyOn(exec, "exec");

const ageInDays = 3;
await save.evictOldFiles(ageInDays, AgeUnit.Days);
expect(proc).toHaveBeenCalledWith(`ccache --evict-older-than ${ageInDays}d`);
});
});
5 changes: 5 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ inputs:
description: "Publish stats as part of the job summary. Set to the title of the job summary section, or to the
empty string to disable this feature. Requires CCache 4.10+"
default: 'CCache Statistics'
evict-old-files:
description: "Corresponds to the ccache --evict-older-than AGE option, where AGE is the number of seconds or days
followed by the 's' or 'd' suffix respectively. Also supports the special value 'job' which represents the time
since the job started, which evicts all cache files that were not touched during the job run."
default: ''
runs:
using: "node20"
main: "dist/restore/index.js"
Expand Down
78 changes: 52 additions & 26 deletions dist/restore/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -58819,7 +58819,7 @@ var external_os_default = /*#__PURE__*/__nccwpck_require__.n(external_os_);
var external_path_ = __nccwpck_require__(6928);
var external_path_default = /*#__PURE__*/__nccwpck_require__.n(external_path_);
// EXTERNAL MODULE: ./node_modules/@actions/core/lib/core.js
var core = __nccwpck_require__(7484);
var lib_core = __nccwpck_require__(7484);
// EXTERNAL MODULE: ./node_modules/@actions/io/lib/io.js
var io = __nccwpck_require__(4994);
// EXTERNAL MODULE: ./node_modules/@actions/exec/lib/exec.js
Expand All @@ -58830,6 +58830,30 @@ const external_process_namespaceObject = require("process");
var cache = __nccwpck_require__(5116);
;// CONCATENATED MODULE: ./src/common.ts


var AgeUnit;
(function (AgeUnit) {
AgeUnit["Seconds"] = "s";
AgeUnit["Days"] = "d";
AgeUnit["Job"] = "job";
})(AgeUnit || (AgeUnit = {}));
function getJobDurationInSeconds() {
const startTime = Number.parseInt(core.getState("startTimestamp"));
return Math.floor((Date.now() - startTime) * 0.001);
}
function parseEvictAgeParameter(age) {
const expr = /([0-9]+)([sd])|job/;
const result = age.match(expr);
if (result) {
if (result[0] !== "job") {
return [Number.parseInt(result[1]), result[2]];
}
else {
return [null, AgeUnit.Job];
}
}
throw new Error(`age parameter ${age} was not valid`);
}
/**
* Parse the output of ccache --version to extract the semantic version components
* @param ccacheOutput
Expand Down Expand Up @@ -58886,44 +58910,44 @@ const SELF_CI = external_process_namespaceObject.env["CCACHE_ACTION_CI"] === "tr
// based on https://cristianadam.eu/20200113/speeding-up-c-plus-plus-github-actions-using-ccache/
async function restore(ccacheVariant) {
const inputs = {
primaryKey: core.getInput("key"),
primaryKey: lib_core.getInput("key"),
// https://github.com/actions/cache/blob/73cb7e04054996a98d39095c0b7821a73fb5b3ea/src/utils/actionUtils.ts#L56
restoreKeys: core.getInput("restore-keys").split("\n").map(s => s.trim()).filter(x => x !== "")
restoreKeys: lib_core.getInput("restore-keys").split("\n").map(s => s.trim()).filter(x => x !== "")
};
const keyPrefix = ccacheVariant + "-";
const primaryKey = inputs.primaryKey ? keyPrefix + inputs.primaryKey + "-" : keyPrefix;
const restoreKeys = inputs.restoreKeys.map(k => keyPrefix + k + "-");
const paths = [cacheDir(ccacheVariant)];
core.saveState("primaryKey", primaryKey);
const shouldRestore = core.getBooleanInput("restore");
lib_core.saveState("primaryKey", primaryKey);
const shouldRestore = lib_core.getBooleanInput("restore");
if (!shouldRestore) {
core.info("Restore set to false, skip restoring cache.");
lib_core.info("Restore set to false, skip restoring cache.");
return;
}
const restoredWith = await cache.restoreCache(paths, primaryKey, restoreKeys);
if (restoredWith) {
core.info(`Restored from cache key "${restoredWith}".`);
lib_core.info(`Restored from cache key "${restoredWith}".`);
if (SELF_CI) {
core.setOutput("test-cache-hit", true);
lib_core.setOutput("test-cache-hit", true);
}
}
else {
core.info("No cache found.");
lib_core.info("No cache found.");
if (SELF_CI) {
core.setOutput("test-cache-hit", false);
lib_core.setOutput("test-cache-hit", false);
}
}
}
async function configure(ccacheVariant, platform) {
const maxSize = core.getInput('max-size');
const maxSize = lib_core.getInput('max-size');
if (ccacheVariant === "ccache") {
await execShell(`ccache --set-config=cache_dir='${cacheDir(ccacheVariant)}'`);
await execShell(`ccache --set-config=max_size='${maxSize}'`);
await execShell(`ccache --set-config=compression=true`);
if (platform === "darwin") {
await execShell(`ccache --set-config=compiler_check=content`);
}
if (core.getBooleanInput("create-symlink")) {
if (lib_core.getBooleanInput("create-symlink")) {
const ccache = await io.which("ccache");
await execShell(`ln -s ${ccache} /usr/local/bin/gcc`);
await execShell(`ln -s ${ccache} /usr/local/bin/g++`);
Expand All @@ -58934,7 +58958,7 @@ async function configure(ccacheVariant, platform) {
await execShell(`ln -s ${ccache} /usr/local/bin/emcc`);
await execShell(`ln -s ${ccache} /usr/local/bin/em++`);
}
core.info("Cccache config:");
lib_core.info("Cccache config:");
await execShell("ccache -p");
}
else {
Expand Down Expand Up @@ -58993,7 +59017,7 @@ async function installSccacheFromGitHub(version, artifactName, binSha256, binDir
const binPath = external_path_default().join(binDir, binName);
await downloadAndExtract(url, `*/${binName}`, binPath);
checkSha256Sum(binPath, binSha256);
core.addPath(binDir);
lib_core.addPath(binDir);
await execShell(`chmod +x '${binPath}'`);
}
async function downloadAndExtract(url, srcFile, dstFile) {
Expand Down Expand Up @@ -59022,13 +59046,15 @@ function checkSha256Sum(path, expectedSha256) {
}
}
async function runInner() {
const ccacheVariant = core.getInput("variant");
core.saveState("ccacheVariant", ccacheVariant);
core.saveState("shouldSave", core.getBooleanInput("save"));
core.saveState("appendTimestamp", core.getBooleanInput("append-timestamp"));
const ccacheVariant = lib_core.getInput("variant");
lib_core.saveState("startTimestamp", Date.now());
lib_core.saveState("ccacheVariant", ccacheVariant);
lib_core.saveState("evictOldFiles", lib_core.getInput("evict-old-files"));
lib_core.saveState("shouldSave", lib_core.getBooleanInput("save"));
lib_core.saveState("appendTimestamp", lib_core.getBooleanInput("append-timestamp"));
let ccachePath = await io.which(ccacheVariant);
if (!ccachePath) {
core.startGroup(`Install ${ccacheVariant}`);
lib_core.startGroup(`Install ${ccacheVariant}`);
const installer = {
["ccache,linux"]: installCcacheLinux,
["ccache,darwin"]: installCcacheMac,
Expand All @@ -59041,24 +59067,24 @@ async function runInner() {
throw Error(`Unsupported platform: ${external_process_namespaceObject.platform}`);
}
await installer();
core.info(await io.which(ccacheVariant + ".exe"));
lib_core.info(await io.which(ccacheVariant + ".exe"));
ccachePath = await io.which(ccacheVariant, true);
core.endGroup();
lib_core.endGroup();
}
core.startGroup("Restore cache");
lib_core.startGroup("Restore cache");
await restore(ccacheVariant);
core.endGroup();
core.startGroup(`Configure ${ccacheVariant}, ${external_process_namespaceObject.platform}`);
lib_core.endGroup();
lib_core.startGroup(`Configure ${ccacheVariant}, ${external_process_namespaceObject.platform}`);
await configure(ccacheVariant, external_process_namespaceObject.platform);
await execShell(`${ccacheVariant} -z`);
core.endGroup();
lib_core.endGroup();
}
async function run() {
try {
await runInner();
}
catch (error) {
core.setFailed(`Restoring cache failed: ${error}`);
lib_core.setFailed(`Restoring cache failed: ${error}`);
}
}
run();
Expand Down
49 changes: 48 additions & 1 deletion dist/save/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -58803,7 +58803,8 @@ __nccwpck_require__.r(__webpack_exports__);

// EXPORTS
__nccwpck_require__.d(__webpack_exports__, {
"default": () => (/* binding */ save)
"default": () => (/* binding */ save),
evictOldFiles: () => (/* binding */ evictOldFiles)
});

// EXTERNAL MODULE: ./node_modules/@actions/core/lib/core.js
Expand All @@ -58817,6 +58818,30 @@ var external_path_ = __nccwpck_require__(6928);
var external_path_default = /*#__PURE__*/__nccwpck_require__.n(external_path_);
;// CONCATENATED MODULE: ./src/common.ts


var AgeUnit;
(function (AgeUnit) {
AgeUnit["Seconds"] = "s";
AgeUnit["Days"] = "d";
AgeUnit["Job"] = "job";
})(AgeUnit || (AgeUnit = {}));
function getJobDurationInSeconds() {
const startTime = Number.parseInt(core.getState("startTimestamp"));
return Math.floor((Date.now() - startTime) * 0.001);
}
function parseEvictAgeParameter(age) {
const expr = /([0-9]+)([sd])|job/;
const result = age.match(expr);
if (result) {
if (result[0] !== "job") {
return [Number.parseInt(result[1]), result[2]];
}
else {
return [null, AgeUnit.Job];
}
}
throw new Error(`age parameter ${age} was not valid`);
}
/**
* Parse the output of ccache --version to extract the semantic version components
* @param ccacheOutput
Expand Down Expand Up @@ -58863,6 +58888,7 @@ function cacheDir(ccacheVariant) {




async function ccacheIsEmpty(ccacheVariant, ccacheKnowsVerbosityFlag) {
if (ccacheVariant === "ccache") {
if (ccacheKnowsVerbosityFlag) {
Expand Down Expand Up @@ -58907,6 +58933,14 @@ async function hasJsonStats(ccacheVariant) {
const version = parseCCacheVersion(result.stdout);
return version != null && version[0] >= 4 && version[1] >= 10;
}
async function evictOldFiles(age, unit) {
try {
await exec.exec(`ccache --evict-older-than ${age}${unit}`);
}
catch (error) {
core.warning(`Error occurred evicting old cache files: ${error}`);
}
}
async function run(earlyExit) {
try {
const ccacheVariant = core.getState("ccacheVariant");
Expand Down Expand Up @@ -58939,6 +58973,19 @@ async function run(earlyExit) {
core.info("Not saving cache because 'save' is set to 'false'.");
return;
}
const evictByAge = core.getState("evictOldFiles");
if (evictByAge && ccacheVariant === "ccache") {
const [time, unit] = parseEvictAgeParameter(evictByAge);
if (unit === AgeUnit.Job) {
const duration = getJobDurationInSeconds();
core.debug(`Evicting cache files older than ${duration} seconds`);
await evictOldFiles(duration, AgeUnit.Seconds);
}
else {
core.debug(`Evicting cache files older than ${time}${unit}`);
await evictOldFiles(time, unit);
}
}
if (await ccacheIsEmpty(ccacheVariant, ccacheKnowsVerbosityFlag)) {
core.info("Not saving cache because no objects are cached.");
}
Expand Down
26 changes: 26 additions & 0 deletions src/common.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,34 @@
import path from "path";
import {SummaryTableRow} from "@actions/core/lib/summary";
import * as core from "@actions/core";

type Version = [number,number,number];

export enum AgeUnit {
Seconds = "s",
Days = "d",
Job = "job"
}

export function getJobDurationInSeconds() : number {
const startTime = Number.parseInt(core.getState("startTimestamp"));
return Math.floor((Date.now() - startTime) * 0.001);
}

export function parseEvictAgeParameter(age: string): [number | null, AgeUnit] {
const expr = /([0-9]+)([sd])|job/
const result = age.match(expr);
if (result) {
if (result[0] !== "job") {
return [Number.parseInt(result[1]), result[2] as AgeUnit];
} else {
return [null, AgeUnit.Job];
}
}

throw new Error(`age parameter ${age} was not valid`);
}

/**
* Parse the output of ccache --version to extract the semantic version components
* @param ccacheOutput
Expand Down
2 changes: 2 additions & 0 deletions src/restore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,9 @@ function checkSha256Sum (path : string, expectedSha256 : string) {

async function runInner() : Promise<void> {
const ccacheVariant = core.getInput("variant");
core.saveState("startTimestamp", Date.now());
core.saveState("ccacheVariant", ccacheVariant);
core.saveState("evictOldFiles", core.getInput("evict-old-files"));
core.saveState("shouldSave", core.getBooleanInput("save"));
core.saveState("appendTimestamp", core.getBooleanInput("append-timestamp"));
let ccachePath = await io.which(ccacheVariant);
Expand Down
Loading
Loading