mirror of
https://github.com/compiler-explorer/compiler-explorer.git
synced 2025-12-27 09:23:52 -05:00
- vitest and all test types etc (test pass). Needed a few minor changes to match new behaviour of `expect().toMatchSnapshot` (trailing whitespace is now important) - `which` - read the changes and spot-checked it. They dropped <v18 - `webpack-cli` - changes don't affect our usage. Checked with a local `webpack` build and a "prod" run (`make` no dev) - `enhanced-ms` - checked and updated to use new functionality - `typescript` - all tests pass, no warnings. Tested the site locally too.
197 lines
8.2 KiB
TypeScript
197 lines
8.2 KiB
TypeScript
// Copyright (c) 2023, Compiler Explorer Authors
|
|
// All rights reserved.
|
|
//
|
|
// Redistribution and use in source and binary forms, with or without
|
|
// modification, are permitted provided that the following conditions are met:
|
|
//
|
|
// * Redistributions of source code must retain the above copyright notice,
|
|
// this list of conditions and the following disclaimer.
|
|
// * Redistributions in binary form must reproduce the above copyright
|
|
// notice, this list of conditions and the following disclaimer in the
|
|
// documentation and/or other materials provided with the distribution.
|
|
//
|
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
// POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
import {StorageClass} from '@aws-sdk/client-s3';
|
|
import {createMs, ms} from 'enhanced-ms';
|
|
|
|
import {FiledataPair} from '../types/compilation/compilation.interfaces.js';
|
|
import {CompilerOverrideOptions} from '../types/compilation/compiler-overrides.interfaces.js';
|
|
import {ConfiguredRuntimeTool} from '../types/execution/execution.interfaces.js';
|
|
import {SelectedLibraryVersion} from '../types/libraries/libraries.interfaces.js';
|
|
|
|
import {ParsedRequest} from './handlers/compile.js';
|
|
import {logger} from './logger.js';
|
|
import {PropertyGetter} from './properties.interfaces.js';
|
|
import {S3Bucket} from './s3-handler.js';
|
|
import {getHash} from './utils.js';
|
|
|
|
const formatMs = createMs({
|
|
formatOptions: {includedUnits: ['hour', 'minute', 'second', 'millisecond'], useAbbreviations: true},
|
|
});
|
|
|
|
export enum KnownBuildMethod {
|
|
Compile = 'compile',
|
|
CMake = 'cmake',
|
|
}
|
|
|
|
export interface IStatsNoter {
|
|
noteCompilation(compilerId: string, request: ParsedRequest, files: FiledataPair[], buildMethod: string): void;
|
|
}
|
|
|
|
class NullStatsNoter implements IStatsNoter {
|
|
noteCompilation(compilerId: string, request: ParsedRequest, files: FiledataPair[], buildMethod: string): void {}
|
|
}
|
|
|
|
// A type for storing only compilation information deemed non-identifying; that is, no source or execution options.
|
|
// This started out as a `Omit<ParsedRequest, ...>` but really in order to be more useful it needs to be more specialised.
|
|
type CompilationRecord = {
|
|
time: string;
|
|
compilerId: string;
|
|
sourceHash: string;
|
|
executionParamsHash: string;
|
|
options: string[];
|
|
filters: Record<string, boolean>;
|
|
backendOptions: string[];
|
|
bypassCache: boolean;
|
|
libraries: string[];
|
|
tools: string[];
|
|
overrides: string[];
|
|
runtimeTools: string[];
|
|
buildMethod: string;
|
|
};
|
|
|
|
export function filterCompilerOptions(args: string[]): string[] {
|
|
const capturableArg = /^[/-]/;
|
|
const unwantedArg = /^(([/-][DIdi])|(-$))/;
|
|
return args.filter(x => capturableArg.exec(x) && !unwantedArg.test(x));
|
|
}
|
|
|
|
// note: any type on `request` is on purpose, we cannot trust ParsedRequest to be truthful to the type as it is user input
|
|
|
|
export function makeSafe(
|
|
time: Date,
|
|
compilerId: string,
|
|
request: ParsedRequest,
|
|
files: FiledataPair[],
|
|
buildMethod: string,
|
|
): CompilationRecord {
|
|
const sourceHash = getHash(request.source + JSON.stringify(files));
|
|
return {
|
|
time: time.toISOString(),
|
|
compilerId: compilerId,
|
|
sourceHash: sourceHash,
|
|
executionParamsHash: getHash(request.executeParameters),
|
|
options: filterCompilerOptions(request.options),
|
|
filters: Object.fromEntries(
|
|
Object.entries(request.filters)
|
|
.filter(value => typeof value[1] === 'boolean')
|
|
.map(item => [item[0].toLowerCase(), item[1]]),
|
|
) as Record<string, boolean>,
|
|
backendOptions: Object.entries(
|
|
Object.fromEntries(
|
|
Object.entries(request.backendOptions)
|
|
.filter(item => item[0] !== 'overrides')
|
|
.map(item => [item[0].toLowerCase(), item[1]]),
|
|
),
|
|
).map(item => `${item[0]}=${item[1] ? '1' : '0'}`),
|
|
bypassCache: !!request.bypassCache,
|
|
libraries: (request.libraries || []).map((lib: SelectedLibraryVersion) => lib.id + '/' + lib.version),
|
|
tools: (request.tools || []).map(tool => tool.id),
|
|
overrides: ((request.backendOptions.overrides || []) as CompilerOverrideOptions)
|
|
.filter(item => item.name !== 'env' && item.value)
|
|
.map(item => `${item.name}=${item.value}`),
|
|
runtimeTools: (request.executeParameters.runtimeTools || [])
|
|
.filter((item: ConfiguredRuntimeTool) => item.name !== 'env')
|
|
.map((item: ConfiguredRuntimeTool) => item.name),
|
|
buildMethod: buildMethod,
|
|
};
|
|
}
|
|
|
|
function makeKey(now: Date): string {
|
|
return `year=${now.getUTCFullYear()}/month=${now.getUTCMonth()}/date=${now.getUTCDate()}/${now.toISOString()}.json`;
|
|
}
|
|
|
|
class StatsNoter implements IStatsNoter {
|
|
private _statsQueue: CompilationRecord[];
|
|
private readonly _flushAfterMs: number;
|
|
private _flushJob: NodeJS.Timeout | undefined;
|
|
private readonly _s3: S3Bucket;
|
|
private readonly _path: string;
|
|
|
|
constructor(bucket: string, path?: string, region?: string, flushMs?: number) {
|
|
this._statsQueue = [];
|
|
this._flushAfterMs = flushMs ?? 5 * 60 * 1000;
|
|
this._flushJob = undefined;
|
|
this._s3 = new S3Bucket(bucket, region ?? 'us-east-1');
|
|
this._path = path ?? 'compile-stats';
|
|
logger.info(`Flushing stats to ${bucket}/${this._path} every ${formatMs(this._flushAfterMs)}`);
|
|
}
|
|
|
|
private flush() {
|
|
const toFlush = this._statsQueue;
|
|
this._statsQueue = [];
|
|
if (toFlush) {
|
|
// async write to S3
|
|
const key = makeKey(new Date(Date.now()));
|
|
this._s3
|
|
.put(key, Buffer.from(toFlush.map(x => JSON.stringify(x)).join('\n')), this._path, {
|
|
redundancy: StorageClass.REDUCED_REDUNDANCY,
|
|
})
|
|
.then(() => {})
|
|
.catch(e => {
|
|
logger.warn(`Caught exception trying to log compilations to ${key}: ${e}`);
|
|
});
|
|
}
|
|
if (this._flushJob !== undefined) {
|
|
clearTimeout(this._flushJob);
|
|
this._flushJob = undefined;
|
|
}
|
|
}
|
|
|
|
noteCompilation(compilerId: string, request: ParsedRequest, files: FiledataPair[], buildMethod: string): void {
|
|
this._statsQueue.push(makeSafe(new Date(), compilerId, request, files, buildMethod));
|
|
if (!this._flushJob) this._flushJob = setTimeout(() => this.flush(), this._flushAfterMs);
|
|
}
|
|
}
|
|
|
|
export function createStatsNoter(props: PropertyGetter): IStatsNoter {
|
|
const config = props('compilationStatsNotifier', 'None()');
|
|
const match = config.match(/^([^(]+)\(([^)]*)\)$/);
|
|
if (!match) throw new Error(`Unable to parse '${config}'`);
|
|
const params = match[2].split(',');
|
|
|
|
const type = match[1];
|
|
switch (type) {
|
|
case 'None': {
|
|
if (params.length !== 1) throw new Error(`Bad params: ${config}`);
|
|
return new NullStatsNoter();
|
|
}
|
|
case 'S3': {
|
|
if (params.length === 0 || params.length > 4)
|
|
throw new Error(`Bad params: ${config} - expected S3(bucket, path?, region?, flushTime?)`);
|
|
let durationMs: number | undefined;
|
|
if (params[3]) {
|
|
const parsed = ms(params[3]);
|
|
if (!parsed)
|
|
throw new Error(
|
|
`Bad params: ${config} - expected S3(bucket, path?, region?, flushTime?), bad flush time`,
|
|
);
|
|
durationMs = parsed;
|
|
}
|
|
return new StatsNoter(params[0], params[1], params[2], durationMs);
|
|
}
|
|
}
|
|
throw new Error(`Unknown stats type '${type}'`);
|
|
}
|