Skip to content

Commit 628fc3f

Browse files
authored
Merge pull request #3979 from github/henrymercer/overlay-db-cleanup-size-telemetry
Measure overlay-base database size at the clear cleanup level
2 parents 9cea582 + 9cfb67b commit 628fc3f

4 files changed

Lines changed: 331 additions & 1 deletion

File tree

lib/entry-points.js

Lines changed: 40 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/database-upload.test.ts

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ import * as apiClient from "./api-client";
1111
import { createStubCodeQL } from "./codeql";
1212
import { Config } from "./config-utils";
1313
import { cleanupAndUploadDatabases } from "./database-upload";
14+
import { Feature } from "./feature-flags";
1415
import * as gitUtils from "./git-utils";
1516
import { BuiltInLanguage } from "./languages";
17+
import { OverlayDatabaseMode } from "./overlay/overlay-database-mode";
1618
import { RepositoryNwo } from "./repository";
1719
import {
1820
checkExpectedLogMessages,
@@ -24,6 +26,7 @@ import {
2426
setupTests,
2527
} from "./testing-utils";
2628
import {
29+
CleanupLevel,
2730
GitHubVariant,
2831
HTTPError,
2932
initializeEnvironment,
@@ -335,3 +338,191 @@ test.serial("Successfully uploading a database to GHEC-DR", async (t) => {
335338
);
336339
});
337340
});
341+
342+
test.serial(
343+
"Records overlay and clear cleanup sizes when uploading an overlay-base database",
344+
async (t) => {
345+
await withTmpDir(async (tmpDir) => {
346+
setupActionsVars(tmpDir, tmpDir);
347+
sinon
348+
.stub(actionsUtil, "getRequiredInput")
349+
.withArgs("upload-database")
350+
.returns("true");
351+
sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true);
352+
353+
await mockHttpRequests(201);
354+
355+
// Track the cleanup level passed to each cleanup so that the database
356+
// bundle stub can write a differently-sized bundle for each level.
357+
const cleanupLevels: CleanupLevel[] = [];
358+
let lastCleanupLevel: CleanupLevel | undefined;
359+
const overlaySizeBytes = 100;
360+
const clearSizeBytes = 50;
361+
const codeql = createStubCodeQL({
362+
async databaseCleanupCluster(_config, cleanupLevel) {
363+
cleanupLevels.push(cleanupLevel);
364+
lastCleanupLevel = cleanupLevel;
365+
},
366+
async databaseBundle(_databasePath, outputFilePath) {
367+
const sizeBytes =
368+
lastCleanupLevel === CleanupLevel.Overlay
369+
? overlaySizeBytes
370+
: clearSizeBytes;
371+
fs.writeFileSync(outputFilePath, "x".repeat(sizeBytes));
372+
},
373+
});
374+
375+
const config = getTestConfig(tmpDir);
376+
config.overlayDatabaseMode = OverlayDatabaseMode.OverlayBase;
377+
378+
const loggedMessages: LoggedMessage[] = [];
379+
const results = await cleanupAndUploadDatabases(
380+
testRepoName,
381+
codeql,
382+
config,
383+
testApiDetails,
384+
createFeatures([Feature.UploadOverlayDbToApi]),
385+
getRecordingLogger(loggedMessages),
386+
);
387+
388+
// The database should be cleaned up at the `overlay` level for the upload
389+
// and then re-cleaned at the `clear` level to measure its size.
390+
t.deepEqual(cleanupLevels, [CleanupLevel.Overlay, CleanupLevel.Clear]);
391+
392+
t.is(results.length, 1);
393+
t.is(results[0].is_overlay_base, true);
394+
t.is(results[0].zipped_upload_size_bytes, overlaySizeBytes);
395+
t.is(results[0].clear_cleanup_zipped_size_bytes, clearSizeBytes);
396+
t.is(typeof results[0].clear_cleanup_measurement_duration_ms, "number");
397+
});
398+
},
399+
);
400+
401+
test.serial(
402+
"Does not measure clear cleanup size for a regular (non-overlay-base) upload",
403+
async (t) => {
404+
await withTmpDir(async (tmpDir) => {
405+
setupActionsVars(tmpDir, tmpDir);
406+
sinon
407+
.stub(actionsUtil, "getRequiredInput")
408+
.withArgs("upload-database")
409+
.returns("true");
410+
sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true);
411+
412+
await mockHttpRequests(201);
413+
414+
const cleanupLevels: CleanupLevel[] = [];
415+
const codeql = createStubCodeQL({
416+
async databaseCleanupCluster(_config, cleanupLevel) {
417+
cleanupLevels.push(cleanupLevel);
418+
},
419+
async databaseBundle(_databasePath, outputFilePath) {
420+
fs.writeFileSync(outputFilePath, "");
421+
},
422+
});
423+
424+
const results = await cleanupAndUploadDatabases(
425+
testRepoName,
426+
codeql,
427+
getTestConfig(tmpDir),
428+
testApiDetails,
429+
createFeatures([Feature.UploadOverlayDbToApi]),
430+
getRecordingLogger([]),
431+
);
432+
433+
// A regular upload is cleaned only once, at the `clear` level.
434+
t.deepEqual(cleanupLevels, [CleanupLevel.Clear]);
435+
t.is(results[0].is_overlay_base, false);
436+
t.is(results[0].clear_cleanup_zipped_size_bytes, undefined);
437+
t.is(results[0].clear_cleanup_measurement_duration_ms, undefined);
438+
});
439+
},
440+
);
441+
442+
test.serial("Does not measure clear cleanup size in debug mode", async (t) => {
443+
await withTmpDir(async (tmpDir) => {
444+
setupActionsVars(tmpDir, tmpDir);
445+
sinon
446+
.stub(actionsUtil, "getRequiredInput")
447+
.withArgs("upload-database")
448+
.returns("true");
449+
sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true);
450+
451+
await mockHttpRequests(201);
452+
453+
const cleanupLevels: CleanupLevel[] = [];
454+
const codeql = createStubCodeQL({
455+
async databaseCleanupCluster(_config, cleanupLevel) {
456+
cleanupLevels.push(cleanupLevel);
457+
},
458+
async databaseBundle(_databasePath, outputFilePath) {
459+
fs.writeFileSync(outputFilePath, "");
460+
},
461+
});
462+
463+
const config = getTestConfig(tmpDir);
464+
config.overlayDatabaseMode = OverlayDatabaseMode.OverlayBase;
465+
config.debugMode = true;
466+
467+
const results = await cleanupAndUploadDatabases(
468+
testRepoName,
469+
codeql,
470+
config,
471+
testApiDetails,
472+
createFeatures([Feature.UploadOverlayDbToApi]),
473+
getRecordingLogger([]),
474+
);
475+
476+
// In debug mode we clean up at the `overlay` level for the upload but skip
477+
// the additional `clear` cleanup, to preserve the database for debugging.
478+
t.deepEqual(cleanupLevels, [CleanupLevel.Overlay]);
479+
t.is(results[0].is_overlay_base, true);
480+
t.is(results[0].clear_cleanup_zipped_size_bytes, undefined);
481+
t.is(results[0].clear_cleanup_measurement_duration_ms, undefined);
482+
});
483+
});
484+
485+
test.serial(
486+
"Does not record a clear cleanup duration when the clear cleanup fails",
487+
async (t) => {
488+
await withTmpDir(async (tmpDir) => {
489+
setupActionsVars(tmpDir, tmpDir);
490+
sinon
491+
.stub(actionsUtil, "getRequiredInput")
492+
.withArgs("upload-database")
493+
.returns("true");
494+
sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true);
495+
496+
await mockHttpRequests(201);
497+
498+
const codeql = createStubCodeQL({
499+
async databaseCleanupCluster(_config, cleanupLevel) {
500+
if (cleanupLevel === CleanupLevel.Clear) {
501+
throw new Error("clear cleanup failed");
502+
}
503+
},
504+
async databaseBundle(_databasePath, outputFilePath) {
505+
fs.writeFileSync(outputFilePath, "x".repeat(100));
506+
},
507+
});
508+
509+
const config = getTestConfig(tmpDir);
510+
config.overlayDatabaseMode = OverlayDatabaseMode.OverlayBase;
511+
512+
const results = await cleanupAndUploadDatabases(
513+
testRepoName,
514+
codeql,
515+
config,
516+
testApiDetails,
517+
createFeatures([Feature.UploadOverlayDbToApi]),
518+
getRecordingLogger([]),
519+
);
520+
521+
// When the `clear` cleanup fails, no size is measured, so we should not
522+
// report a measurement duration either.
523+
t.is(results[0].is_overlay_base, true);
524+
t.is(results[0].clear_cleanup_zipped_size_bytes, undefined);
525+
t.is(results[0].clear_cleanup_measurement_duration_ms, undefined);
526+
});
527+
},
528+
);

src/database-upload.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,19 @@ export interface DatabaseUploadResult {
2525
zipped_upload_size_bytes?: number;
2626
/** Whether the uploaded database is an overlay base. */
2727
is_overlay_base?: boolean;
28+
/**
29+
* For overlay-base uploads only: the size in bytes that the zipped database
30+
* would have been if it had been cleaned at the `clear` cleanup level instead
31+
* of the `overlay` level.
32+
*/
33+
clear_cleanup_zipped_size_bytes?: number;
34+
/**
35+
* For overlay-base uploads only: the time in milliseconds spent measuring the
36+
* `clear` cleanup size (cleaning up the cluster at the `clear` level and
37+
* bundling each database). This is a cluster-wide measurement, so it is the
38+
* same for every language in a run.
39+
*/
40+
clear_cleanup_measurement_duration_ms?: number;
2841
/** Time taken to upload database in milliseconds. */
2942
upload_duration_ms?: number;
3043
/** If there was an error during database upload, this is its message. */
@@ -156,9 +169,89 @@ export async function cleanupAndUploadDatabases(
156169
});
157170
}
158171
}
172+
173+
// When we upload an overlay-base database, we cleaned the databases at the `overlay` level, which
174+
// retains more data than the `clear` level used for regular uploads. Measure what the zipped size
175+
// would have been at the `clear` level too, so we can compare the storage cost of overlay-base
176+
// databases against regular databases for the same repository.
177+
//
178+
// We skip this in debug mode, where the databases are preserved and uploaded as debug artifacts,
179+
// since cleaning them up at the `clear` level would discard data that is useful for debugging.
180+
if (shouldUploadOverlayBase && !config.debugMode) {
181+
await withGroupAsync(
182+
"Measuring database size at the clear cleanup level",
183+
() => recordClearCleanupSizes(codeql, config, reports, logger),
184+
);
185+
}
186+
159187
return reports;
160188
}
161189

190+
/**
191+
* Cleans up the databases at the `clear` cleanup level and records the resulting zipped size for
192+
* each language in `clear_cleanup_zipped_size_bytes`. If the cleanup succeeds, also records the
193+
* time spent taking the measurement in `clear_cleanup_measurement_duration_ms`.
194+
*
195+
* This mutates the entries of `reports` in place. It must run only after all overlay-base uploads
196+
* have completed, since the `clear` cleanup discards overlay data that the uploaded database
197+
* depends on.
198+
*
199+
* Failures here are non-fatal: this is telemetry-only, so we log and move on rather than failing
200+
* the workflow.
201+
*/
202+
async function recordClearCleanupSizes(
203+
codeql: CodeQL,
204+
config: Config,
205+
reports: DatabaseUploadResult[],
206+
logger: Logger,
207+
): Promise<void> {
208+
// Include both the cleanup and the re-bundling to record how much time taking this measurement adds
209+
// to the run.
210+
const startTime = performance.now();
211+
212+
try {
213+
await codeql.databaseCleanupCluster(config, CleanupLevel.Clear);
214+
} catch (e) {
215+
// The cleanup didn't run, so there are no sizes to measure. Return without recording a
216+
// duration, so that we don't report a measurement duration with no accompanying sizes.
217+
logger.warning(
218+
`Failed to clean up databases at the '${CleanupLevel.Clear}' level for ` +
219+
`size measurement: ${util.getErrorMessage(e)}`,
220+
);
221+
return;
222+
}
223+
224+
for (const language of config.languages) {
225+
const report = reports.find((r) => r.language === language);
226+
if (report === undefined) {
227+
continue;
228+
}
229+
try {
230+
const bundledDb = await bundleDb(config, language, codeql, language, {
231+
includeDiagnostics: false,
232+
});
233+
report.clear_cleanup_zipped_size_bytes = fs.statSync(bundledDb).size;
234+
logger.debug(
235+
`Database for ${language} is ` +
236+
`${report.clear_cleanup_zipped_size_bytes} bytes zipped at the ` +
237+
`'${CleanupLevel.Clear}' cleanup level ` +
238+
`(vs. ${report.zipped_upload_size_bytes ?? "unknown"} bytes at the ` +
239+
`'${CleanupLevel.Overlay}' level).`,
240+
);
241+
} catch (e) {
242+
logger.warning(
243+
`Failed to measure the '${CleanupLevel.Clear}' cleanup database size ` +
244+
`for ${language}: ${util.getErrorMessage(e)}`,
245+
);
246+
}
247+
}
248+
249+
const durationMs = performance.now() - startTime;
250+
for (const report of reports) {
251+
report.clear_cleanup_measurement_duration_ms = durationMs;
252+
}
253+
}
254+
162255
/**
163256
* Uploads a bundled database to the GitHub API.
164257
*

src/util.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -712,7 +712,13 @@ export function getBaseDatabaseOidsFilePath(config: Config): string {
712712
return path.join(config.dbLocation, BASE_DATABASE_OIDS_FILE_NAME);
713713
}
714714

715-
// Create a bundle for the given DB, if it doesn't already exist
715+
/**
716+
* Bundles the database for the given language into a `.zip` file, returning the path to it.
717+
*
718+
* If a bundle for `dbName` already exists (e.g. from an earlier call), it is deleted and
719+
* re-created, so each call produces a fresh bundle reflecting the current database contents and the
720+
* given `includeDiagnostics` value.
721+
*/
716722
export async function bundleDb(
717723
config: Config,
718724
language: Language,

0 commit comments

Comments
 (0)