[7.x] [kbn/optimizer][ci-stats] ship metrics separate from build (#90482) (#90659)

Co-authored-by: spalger <spalger@users.noreply.github.com>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Spencer 2021-02-08 15:40:11 -08:00 committed by GitHub
parent 4bf2f618a4
commit d3c37cde22
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 519 additions and 354 deletions

View file

@ -555,6 +555,7 @@
"@types/webpack": "^4.41.3", "@types/webpack": "^4.41.3",
"@types/webpack-env": "^1.15.3", "@types/webpack-env": "^1.15.3",
"@types/webpack-merge": "^4.1.5", "@types/webpack-merge": "^4.1.5",
"@types/webpack-sources": "^0.1.4",
"@types/write-pkg": "^3.1.0", "@types/write-pkg": "^3.1.0",
"@types/xml-crypto": "^1.4.1", "@types/xml-crypto": "^1.4.1",
"@types/xml2js": "^0.4.5", "@types/xml2js": "^0.4.5",
@ -840,6 +841,7 @@
"webpack-cli": "^3.3.12", "webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.0", "webpack-dev-server": "^3.11.0",
"webpack-merge": "^4.2.2", "webpack-merge": "^4.2.2",
"webpack-sources": "^1.4.1",
"write-pkg": "^4.0.0", "write-pkg": "^4.0.0",
"xml-crypto": "^2.0.0", "xml-crypto": "^2.0.0",
"xmlbuilder": "13.0.2", "xmlbuilder": "13.0.2",

View file

@ -7,3 +7,4 @@
*/ */
export * from './ci_stats_reporter'; export * from './ci_stats_reporter';
export * from './ship_ci_stats_cli';

View file

@ -0,0 +1,48 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import Path from 'path';
import Fs from 'fs';
import { CiStatsReporter } from './ci_stats_reporter';
import { run, createFlagError } from '../run';
export function shipCiStatsCli() {
run(
async ({ log, flags }) => {
let metricPaths = flags.metrics;
if (typeof metricPaths === 'string') {
metricPaths = [metricPaths];
} else if (!Array.isArray(metricPaths) || !metricPaths.every((p) => typeof p === 'string')) {
throw createFlagError('expected --metrics to be a string');
}
const reporter = CiStatsReporter.fromEnv(log);
for (const path of metricPaths) {
// resolve path from CLI relative to CWD
const abs = Path.resolve(path);
const json = Fs.readFileSync(abs, 'utf8');
await reporter.metrics(JSON.parse(json));
log.success('shipped metrics from', path);
}
},
{
description: 'ship ci-stats which have been written to files',
usage: `node scripts/ship_ci_stats`,
log: {
defaultLevel: 'debug',
},
flags: {
string: ['metrics'],
help: `
--metrics [path] A path to a JSON file that includes metrics which should be sent. Multiple instances supported
`,
},
}
);
}

View file

@ -12,11 +12,10 @@ import Path from 'path';
import { REPO_ROOT } from '@kbn/utils'; import { REPO_ROOT } from '@kbn/utils';
import { lastValueFrom } from '@kbn/std'; import { lastValueFrom } from '@kbn/std';
import { run, createFlagError, CiStatsReporter } from '@kbn/dev-utils'; import { run, createFlagError } from '@kbn/dev-utils';
import { logOptimizerState } from './log_optimizer_state'; import { logOptimizerState } from './log_optimizer_state';
import { OptimizerConfig } from './optimizer'; import { OptimizerConfig } from './optimizer';
import { reportOptimizerStats } from './report_optimizer_stats';
import { runOptimizer } from './run_optimizer'; import { runOptimizer } from './run_optimizer';
import { validateLimitsForAllBundles, updateBundleLimits } from './limits'; import { validateLimitsForAllBundles, updateBundleLimits } from './limits';
@ -120,17 +119,7 @@ run(
return; return;
} }
let update$ = runOptimizer(config); const update$ = runOptimizer(config);
if (reportStats) {
const reporter = CiStatsReporter.fromEnv(log);
if (!reporter.isEnabled()) {
log.warning('Unable to initialize CiStatsReporter from env');
}
update$ = update$.pipe(reportOptimizerStats(reporter, config, log));
}
await lastValueFrom(update$.pipe(logOptimizerState(log, config))); await lastValueFrom(update$.pipe(logOptimizerState(log, config)));
@ -153,7 +142,6 @@ run(
'cache', 'cache',
'profile', 'profile',
'inspect-workers', 'inspect-workers',
'report-stats',
'validate-limits', 'validate-limits',
'update-limits', 'update-limits',
], ],
@ -179,7 +167,6 @@ run(
--dist create bundles that are suitable for inclusion in the Kibana distributable, enabled when running with --update-limits --dist create bundles that are suitable for inclusion in the Kibana distributable, enabled when running with --update-limits
--scan-dir add a directory to the list of directories scanned for plugins (specify as many times as necessary) --scan-dir add a directory to the list of directories scanned for plugins (specify as many times as necessary)
--no-inspect-workers when inspecting the parent process, don't inspect the workers --no-inspect-workers when inspecting the parent process, don't inspect the workers
--report-stats attempt to report stats about this execution of the build to the kibana-ci-stats service using this name
--validate-limits validate the limits.yml config to ensure that there are limits defined for every bundle --validate-limits validate the limits.yml config to ensure that there are limits defined for every bundle
--update-limits run a build and rewrite the limits file to include the current bundle sizes +5kb --update-limits run a build and rewrite the limits file to include the current bundle sizes +5kb
`, `,

View file

@ -42,6 +42,7 @@ it('creates cache keys', () => {
"id": "bar", "id": "bar",
"manifestPath": undefined, "manifestPath": undefined,
"outputDir": "/foo/bar/target", "outputDir": "/foo/bar/target",
"pageLoadAssetSizeLimit": undefined,
"publicDirNames": Array [ "publicDirNames": Array [
"public", "public",
], ],
@ -79,6 +80,7 @@ it('parses bundles from JSON specs', () => {
"id": "bar", "id": "bar",
"manifestPath": undefined, "manifestPath": undefined,
"outputDir": "/foo/bar/target", "outputDir": "/foo/bar/target",
"pageLoadAssetSizeLimit": undefined,
"publicDirNames": Array [ "publicDirNames": Array [
"public", "public",
], ],

View file

@ -36,6 +36,8 @@ export interface BundleSpec {
readonly banner?: string; readonly banner?: string;
/** Absolute path to a kibana.json manifest file, if omitted we assume there are not dependenices */ /** Absolute path to a kibana.json manifest file, if omitted we assume there are not dependenices */
readonly manifestPath?: string; readonly manifestPath?: string;
/** Maximum allowed page load asset size for the bundles page load asset */
readonly pageLoadAssetSizeLimit?: number;
} }
export class Bundle { export class Bundle {
@ -63,6 +65,8 @@ export class Bundle {
* Every bundle mentioned in the `requiredBundles` must be built together. * Every bundle mentioned in the `requiredBundles` must be built together.
*/ */
public readonly manifestPath: BundleSpec['manifestPath']; public readonly manifestPath: BundleSpec['manifestPath'];
/** Maximum allowed page load asset size for the bundles page load asset */
public readonly pageLoadAssetSizeLimit: BundleSpec['pageLoadAssetSizeLimit'];
public readonly cache: BundleCache; public readonly cache: BundleCache;
@ -75,8 +79,9 @@ export class Bundle {
this.outputDir = spec.outputDir; this.outputDir = spec.outputDir;
this.manifestPath = spec.manifestPath; this.manifestPath = spec.manifestPath;
this.banner = spec.banner; this.banner = spec.banner;
this.pageLoadAssetSizeLimit = spec.pageLoadAssetSizeLimit;
this.cache = new BundleCache(Path.resolve(this.outputDir, '.kbn-optimizer-cache')); this.cache = new BundleCache(this.outputDir);
} }
/** /**
@ -107,6 +112,7 @@ export class Bundle {
outputDir: this.outputDir, outputDir: this.outputDir,
manifestPath: this.manifestPath, manifestPath: this.manifestPath,
banner: this.banner, banner: this.banner,
pageLoadAssetSizeLimit: this.pageLoadAssetSizeLimit,
}; };
} }
@ -222,6 +228,13 @@ export function parseBundles(json: string) {
} }
} }
const { pageLoadAssetSizeLimit } = spec;
if (pageLoadAssetSizeLimit !== undefined) {
if (!(typeof pageLoadAssetSizeLimit === 'number')) {
throw new Error('`bundles[]` must have a numeric `pageLoadAssetSizeLimit` property');
}
}
return new Bundle({ return new Bundle({
type, type,
id, id,
@ -231,6 +244,7 @@ export function parseBundles(json: string) {
outputDir, outputDir,
banner, banner,
manifestPath, manifestPath,
pageLoadAssetSizeLimit,
}); });
} }
); );

View file

@ -25,12 +25,12 @@ beforeEach(() => {
}); });
it(`doesn't complain if files are not on disk`, () => { it(`doesn't complain if files are not on disk`, () => {
const cache = new BundleCache('/foo/bar.json'); const cache = new BundleCache('/foo');
expect(cache.get()).toEqual({}); expect(cache.get()).toEqual({});
}); });
it(`updates files on disk when calling set()`, () => { it(`updates files on disk when calling set()`, () => {
const cache = new BundleCache('/foo/bar.json'); const cache = new BundleCache('/foo');
cache.set(SOME_STATE); cache.set(SOME_STATE);
expect(mockReadFileSync).not.toHaveBeenCalled(); expect(mockReadFileSync).not.toHaveBeenCalled();
expect(mockMkdirSync.mock.calls).toMatchInlineSnapshot(` expect(mockMkdirSync.mock.calls).toMatchInlineSnapshot(`
@ -46,7 +46,7 @@ it(`updates files on disk when calling set()`, () => {
expect(mockWriteFileSync.mock.calls).toMatchInlineSnapshot(` expect(mockWriteFileSync.mock.calls).toMatchInlineSnapshot(`
Array [ Array [
Array [ Array [
"/foo/bar.json", "/foo/.kbn-optimizer-cache",
"{ "{
\\"cacheKey\\": \\"abc\\", \\"cacheKey\\": \\"abc\\",
\\"files\\": [ \\"files\\": [
@ -61,7 +61,7 @@ it(`updates files on disk when calling set()`, () => {
}); });
it(`serves updated state from memory`, () => { it(`serves updated state from memory`, () => {
const cache = new BundleCache('/foo/bar.json'); const cache = new BundleCache('/foo');
cache.set(SOME_STATE); cache.set(SOME_STATE);
jest.clearAllMocks(); jest.clearAllMocks();
@ -72,7 +72,7 @@ it(`serves updated state from memory`, () => {
}); });
it('reads state from disk on get() after refresh()', () => { it('reads state from disk on get() after refresh()', () => {
const cache = new BundleCache('/foo/bar.json'); const cache = new BundleCache('/foo');
cache.set(SOME_STATE); cache.set(SOME_STATE);
cache.refresh(); cache.refresh();
jest.clearAllMocks(); jest.clearAllMocks();
@ -83,7 +83,7 @@ it('reads state from disk on get() after refresh()', () => {
expect(mockReadFileSync.mock.calls).toMatchInlineSnapshot(` expect(mockReadFileSync.mock.calls).toMatchInlineSnapshot(`
Array [ Array [
Array [ Array [
"/foo/bar.json", "/foo/.kbn-optimizer-cache",
"utf8", "utf8",
], ],
] ]
@ -91,7 +91,7 @@ it('reads state from disk on get() after refresh()', () => {
}); });
it('provides accessors to specific state properties', () => { it('provides accessors to specific state properties', () => {
const cache = new BundleCache('/foo/bar.json'); const cache = new BundleCache('/foo');
expect(cache.getModuleCount()).toBe(undefined); expect(cache.getModuleCount()).toBe(undefined);
expect(cache.getReferencedFiles()).toEqual(undefined); expect(cache.getReferencedFiles()).toEqual(undefined);

View file

@ -9,6 +9,9 @@
import Fs from 'fs'; import Fs from 'fs';
import Path from 'path'; import Path from 'path';
import webpack from 'webpack';
import { RawSource } from 'webpack-sources';
export interface State { export interface State {
optimizerCacheKey?: unknown; optimizerCacheKey?: unknown;
cacheKey?: unknown; cacheKey?: unknown;
@ -20,13 +23,17 @@ export interface State {
const DEFAULT_STATE: State = {}; const DEFAULT_STATE: State = {};
const DEFAULT_STATE_JSON = JSON.stringify(DEFAULT_STATE); const DEFAULT_STATE_JSON = JSON.stringify(DEFAULT_STATE);
const CACHE_FILENAME = '.kbn-optimizer-cache';
/** /**
* Helper to read and update metadata for bundles. * Helper to read and update metadata for bundles.
*/ */
export class BundleCache { export class BundleCache {
private state: State | undefined = undefined; private state: State | undefined = undefined;
constructor(private readonly path: string | false) {} private readonly path: string | false;
constructor(outputDir: string | false) {
this.path = outputDir === false ? false : Path.resolve(outputDir, CACHE_FILENAME);
}
refresh() { refresh() {
this.state = undefined; this.state = undefined;
@ -63,6 +70,7 @@ export class BundleCache {
set(updated: State) { set(updated: State) {
this.state = updated; this.state = updated;
if (this.path) { if (this.path) {
const directory = Path.dirname(this.path); const directory = Path.dirname(this.path);
Fs.mkdirSync(directory, { recursive: true }); Fs.mkdirSync(directory, { recursive: true });
@ -107,4 +115,16 @@ export class BundleCache {
} }
} }
} }
public writeWebpackAsset(compilation: webpack.compilation.Compilation) {
if (!this.path) {
return;
}
const source = new RawSource(JSON.stringify(this.state, null, 2));
// see https://github.com/jantimon/html-webpack-plugin/blob/33d69f49e6e9787796402715d1b9cd59f80b628f/index.js#L266
// @ts-expect-error undocumented, used to add assets to the output
compilation.emitAsset(CACHE_FILENAME, source);
}
} }

View file

@ -9,6 +9,5 @@
export { OptimizerConfig } from './optimizer'; export { OptimizerConfig } from './optimizer';
export * from './run_optimizer'; export * from './run_optimizer';
export * from './log_optimizer_state'; export * from './log_optimizer_state';
export * from './report_optimizer_stats';
export * from './node'; export * from './node';
export * from './limits'; export * from './limits';

File diff suppressed because one or more lines are too long

View file

@ -16,13 +16,7 @@ import del from 'del';
import { tap, filter } from 'rxjs/operators'; import { tap, filter } from 'rxjs/operators';
import { REPO_ROOT } from '@kbn/utils'; import { REPO_ROOT } from '@kbn/utils';
import { ToolingLog } from '@kbn/dev-utils'; import { ToolingLog } from '@kbn/dev-utils';
import { import { runOptimizer, OptimizerConfig, OptimizerUpdate, logOptimizerState } from '@kbn/optimizer';
runOptimizer,
OptimizerConfig,
OptimizerUpdate,
logOptimizerState,
readLimits,
} from '@kbn/optimizer';
import { allValuesFrom } from '../common'; import { allValuesFrom } from '../common';
@ -69,9 +63,6 @@ it('builds expected bundles, saves bundle counts to metadata', async () => {
dist: false, dist: false,
}); });
expect(config.limits).toEqual(readLimits());
(config as any).limits = '<Limits>';
expect(config).toMatchSnapshot('OptimizerConfig'); expect(config).toMatchSnapshot('OptimizerConfig');
const msgs = await allValuesFrom( const msgs = await allValuesFrom(
@ -231,6 +222,10 @@ it('prepares assets for distribution', async () => {
await allValuesFrom(runOptimizer(config).pipe(logOptimizerState(log, config))); await allValuesFrom(runOptimizer(config).pipe(logOptimizerState(log, config)));
expect(
Fs.readFileSync(Path.resolve(MOCK_REPO_DIR, 'plugins/foo/target/public/metrics.json'), 'utf8')
).toMatchSnapshot('metrics.json');
expectFileMatchesSnapshotWithCompression('plugins/foo/target/public/foo.plugin.js', 'foo bundle'); expectFileMatchesSnapshotWithCompression('plugins/foo/target/public/foo.plugin.js', 'foo bundle');
expectFileMatchesSnapshotWithCompression( expectFileMatchesSnapshotWithCompression(
'plugins/foo/target/public/foo.chunk.1.js', 'plugins/foo/target/public/foo.chunk.1.js',

View file

@ -7,12 +7,13 @@
*/ */
import Fs from 'fs'; import Fs from 'fs';
import Path from 'path';
import dedent from 'dedent'; import dedent from 'dedent';
import Yaml from 'js-yaml'; import Yaml from 'js-yaml';
import { createFailError, ToolingLog } from '@kbn/dev-utils'; import { createFailError, ToolingLog, CiStatsMetrics } from '@kbn/dev-utils';
import { OptimizerConfig, getMetrics, Limits } from './optimizer'; import { OptimizerConfig, Limits } from './optimizer';
const LIMITS_PATH = require.resolve('../limits.yml'); const LIMITS_PATH = require.resolve('../limits.yml');
const DEFAULT_BUDGET = 15000; const DEFAULT_BUDGET = 15000;
@ -33,7 +34,7 @@ export function readLimits(): Limits {
} }
export function validateLimitsForAllBundles(log: ToolingLog, config: OptimizerConfig) { export function validateLimitsForAllBundles(log: ToolingLog, config: OptimizerConfig) {
const limitBundleIds = Object.keys(config.limits.pageLoadAssetSize || {}); const limitBundleIds = Object.keys(readLimits().pageLoadAssetSize || {});
const configBundleIds = config.bundles.map((b) => b.id); const configBundleIds = config.bundles.map((b) => b.id);
const missingBundleIds = diff(configBundleIds, limitBundleIds); const missingBundleIds = diff(configBundleIds, limitBundleIds);
@ -75,15 +76,21 @@ interface UpdateBundleLimitsOptions {
} }
export function updateBundleLimits({ log, config, dropMissing }: UpdateBundleLimitsOptions) { export function updateBundleLimits({ log, config, dropMissing }: UpdateBundleLimitsOptions) {
const metrics = getMetrics(log, config); const limits = readLimits();
const metrics: CiStatsMetrics = config.bundles
.map((bundle) =>
JSON.parse(Fs.readFileSync(Path.resolve(bundle.outputDir, 'metrics.json'), 'utf-8'))
)
.flat()
.sort((a, b) => a.id.localeCompare(b.id));
const pageLoadAssetSize: NonNullable<Limits['pageLoadAssetSize']> = dropMissing const pageLoadAssetSize: NonNullable<Limits['pageLoadAssetSize']> = dropMissing
? {} ? {}
: config.limits.pageLoadAssetSize ?? {}; : limits.pageLoadAssetSize ?? {};
for (const metric of metrics.sort((a, b) => a.id.localeCompare(b.id))) { for (const metric of metrics) {
if (metric.group === 'page load bundle size') { if (metric.group === 'page load bundle size') {
const existingLimit = config.limits.pageLoadAssetSize?.[metric.id]; const existingLimit = limits.pageLoadAssetSize?.[metric.id];
pageLoadAssetSize[metric.id] = pageLoadAssetSize[metric.id] =
existingLimit != null && existingLimit >= metric.value existingLimit != null && existingLimit >= metric.value
? existingLimit ? existingLimit

View file

@ -1,118 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import Fs from 'fs';
import Path from 'path';
import { ToolingLog, CiStatsMetrics } from '@kbn/dev-utils';
import { OptimizerConfig } from './optimizer_config';
const flatten = <T>(arr: Array<T | T[]>): T[] =>
arr.reduce((acc: T[], item) => acc.concat(item), []);
interface Entry {
relPath: string;
stats: Fs.Stats;
}
const IGNORED_EXTNAME = ['.map', '.br', '.gz'];
const getFiles = (dir: string, parent?: string) =>
flatten(
Fs.readdirSync(dir).map((name): Entry | Entry[] => {
const absPath = Path.join(dir, name);
const relPath = parent ? Path.join(parent, name) : name;
const stats = Fs.statSync(absPath);
if (stats.isDirectory()) {
return getFiles(absPath, relPath);
}
return {
relPath,
stats,
};
})
).filter((file) => {
const filename = Path.basename(file.relPath);
if (filename.startsWith('.')) {
return false;
}
const ext = Path.extname(filename);
if (IGNORED_EXTNAME.includes(ext)) {
return false;
}
return true;
});
export function getMetrics(log: ToolingLog, config: OptimizerConfig) {
return flatten(
config.bundles.map((bundle) => {
// make the cache read from the cache file since it was likely updated by the worker
bundle.cache.refresh();
const outputFiles = getFiles(bundle.outputDir);
const entryName = `${bundle.id}.${bundle.type}.js`;
const entry = outputFiles.find((f) => f.relPath === entryName);
if (!entry) {
throw new Error(
`Unable to find bundle entry named [${entryName}] in [${bundle.outputDir}]`
);
}
const chunkPrefix = `${bundle.id}.chunk.`;
const asyncChunks = outputFiles.filter((f) => f.relPath.startsWith(chunkPrefix));
const miscFiles = outputFiles.filter((f) => f !== entry && !asyncChunks.includes(f));
if (asyncChunks.length) {
log.verbose(bundle.id, 'async chunks', asyncChunks);
}
if (miscFiles.length) {
log.verbose(bundle.id, 'misc files', asyncChunks);
}
const sumSize = (files: Entry[]) => files.reduce((acc: number, f) => acc + f.stats!.size, 0);
const bundleMetrics: CiStatsMetrics = [
{
group: `@kbn/optimizer bundle module count`,
id: bundle.id,
value: bundle.cache.getModuleCount() || 0,
},
{
group: `page load bundle size`,
id: bundle.id,
value: entry.stats!.size,
limit: config.limits.pageLoadAssetSize?.[bundle.id],
limitConfigPath: `packages/kbn-optimizer/limits.yml`,
},
{
group: `async chunks size`,
id: bundle.id,
value: sumSize(asyncChunks),
},
{
group: `async chunk count`,
id: bundle.id,
value: asyncChunks.length,
},
{
group: `miscellaneous assets size`,
id: bundle.id,
value: sumSize(miscFiles),
},
];
log.debug(bundle.id, 'metrics', bundleMetrics);
return bundleMetrics;
})
);
}

View file

@ -48,7 +48,12 @@ it('returns a bundle for core and each plugin', () => {
}, },
], ],
'/repo', '/repo',
'/output' '/output',
{
pageLoadAssetSize: {
box: 123,
},
}
).map((b) => b.toSpec()) ).map((b) => b.toSpec())
).toMatchInlineSnapshot(` ).toMatchInlineSnapshot(`
Array [ Array [
@ -58,6 +63,7 @@ it('returns a bundle for core and each plugin', () => {
"id": "foo", "id": "foo",
"manifestPath": <repoRoot>/plugins/foo/kibana.json, "manifestPath": <repoRoot>/plugins/foo/kibana.json,
"outputDir": <outputRoot>/plugins/foo/target/public, "outputDir": <outputRoot>/plugins/foo/target/public,
"pageLoadAssetSizeLimit": undefined,
"publicDirNames": Array [ "publicDirNames": Array [
"public", "public",
], ],
@ -70,6 +76,7 @@ it('returns a bundle for core and each plugin', () => {
"id": "baz", "id": "baz",
"manifestPath": <outsideOfRepo>/plugins/baz/kibana.json, "manifestPath": <outsideOfRepo>/plugins/baz/kibana.json,
"outputDir": <outsideOfRepo>/plugins/baz/target/public, "outputDir": <outsideOfRepo>/plugins/baz/target/public,
"pageLoadAssetSizeLimit": undefined,
"publicDirNames": Array [ "publicDirNames": Array [
"public", "public",
], ],
@ -84,6 +91,7 @@ it('returns a bundle for core and each plugin', () => {
"id": "box", "id": "box",
"manifestPath": <repoRoot>/x-pack/plugins/box/kibana.json, "manifestPath": <repoRoot>/x-pack/plugins/box/kibana.json,
"outputDir": <outputRoot>/x-pack/plugins/box/target/public, "outputDir": <outputRoot>/x-pack/plugins/box/target/public,
"pageLoadAssetSizeLimit": 123,
"publicDirNames": Array [ "publicDirNames": Array [
"public", "public",
], ],

View file

@ -9,13 +9,15 @@
import Path from 'path'; import Path from 'path';
import { Bundle } from '../common'; import { Bundle } from '../common';
import { Limits } from './optimizer_config';
import { KibanaPlatformPlugin } from './kibana_platform_plugins'; import { KibanaPlatformPlugin } from './kibana_platform_plugins';
export function getPluginBundles( export function getPluginBundles(
plugins: KibanaPlatformPlugin[], plugins: KibanaPlatformPlugin[],
repoRoot: string, repoRoot: string,
outputRoot: string outputRoot: string,
limits: Limits
) { ) {
const xpackDirSlash = Path.resolve(repoRoot, 'x-pack') + Path.sep; const xpackDirSlash = Path.resolve(repoRoot, 'x-pack') + Path.sep;
@ -39,6 +41,7 @@ export function getPluginBundles(
? `/*! Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one or more contributor license agreements. \n` + ? `/*! Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one or more contributor license agreements. \n` +
` * Licensed under the Elastic License 2.0; you may not use this file except in compliance with the Elastic License 2.0. */\n` ` * Licensed under the Elastic License 2.0; you may not use this file except in compliance with the Elastic License 2.0. */\n`
: undefined, : undefined,
pageLoadAssetSizeLimit: limits.pageLoadAssetSize?.[p.id],
}) })
); );
} }

View file

@ -14,4 +14,3 @@ export * from './watch_bundles_for_changes';
export * from './run_workers'; export * from './run_workers';
export * from './bundle_cache'; export * from './bundle_cache';
export * from './handle_optimizer_completion'; export * from './handle_optimizer_completion';
export * from './get_output_stats';

View file

@ -435,7 +435,6 @@ describe('OptimizerConfig::create()', () => {
"cache": Symbol(parsed cache), "cache": Symbol(parsed cache),
"dist": Symbol(parsed dist), "dist": Symbol(parsed dist),
"inspectWorkers": Symbol(parsed inspect workers), "inspectWorkers": Symbol(parsed inspect workers),
"limits": Symbol(limits),
"maxWorkerCount": Symbol(parsed max worker count), "maxWorkerCount": Symbol(parsed max worker count),
"plugins": Symbol(new platform plugins), "plugins": Symbol(new platform plugins),
"profileWebpack": Symbol(parsed profile webpack), "profileWebpack": Symbol(parsed profile webpack),
@ -457,7 +456,7 @@ describe('OptimizerConfig::create()', () => {
[Window], [Window],
], ],
"invocationCallOrder": Array [ "invocationCallOrder": Array [
21, 22,
], ],
"results": Array [ "results": Array [
Object { Object {
@ -480,7 +479,7 @@ describe('OptimizerConfig::create()', () => {
[Window], [Window],
], ],
"invocationCallOrder": Array [ "invocationCallOrder": Array [
24, 25,
], ],
"results": Array [ "results": Array [
Object { Object {
@ -498,13 +497,14 @@ describe('OptimizerConfig::create()', () => {
Symbol(new platform plugins), Symbol(new platform plugins),
Symbol(parsed repo root), Symbol(parsed repo root),
Symbol(parsed output root), Symbol(parsed output root),
Symbol(limits),
], ],
], ],
"instances": Array [ "instances": Array [
[Window], [Window],
], ],
"invocationCallOrder": Array [ "invocationCallOrder": Array [
22, 23,
], ],
"results": Array [ "results": Array [
Object { Object {

View file

@ -211,6 +211,7 @@ export class OptimizerConfig {
} }
static create(inputOptions: Options) { static create(inputOptions: Options) {
const limits = readLimits();
const options = OptimizerConfig.parseOptions(inputOptions); const options = OptimizerConfig.parseOptions(inputOptions);
const plugins = findKibanaPlatformPlugins(options.pluginScanDirs, options.pluginPaths); const plugins = findKibanaPlatformPlugins(options.pluginScanDirs, options.pluginPaths);
const bundles = [ const bundles = [
@ -223,10 +224,11 @@ export class OptimizerConfig {
sourceRoot: options.repoRoot, sourceRoot: options.repoRoot,
contextDir: Path.resolve(options.repoRoot, 'src/core'), contextDir: Path.resolve(options.repoRoot, 'src/core'),
outputDir: Path.resolve(options.outputRoot, 'src/core/target/public'), outputDir: Path.resolve(options.outputRoot, 'src/core/target/public'),
pageLoadAssetSizeLimit: limits.pageLoadAssetSize?.core,
}), }),
] ]
: []), : []),
...getPluginBundles(plugins, options.repoRoot, options.outputRoot), ...getPluginBundles(plugins, options.repoRoot, options.outputRoot, limits),
]; ];
return new OptimizerConfig( return new OptimizerConfig(
@ -239,8 +241,7 @@ export class OptimizerConfig {
options.maxWorkerCount, options.maxWorkerCount,
options.dist, options.dist,
options.profileWebpack, options.profileWebpack,
options.themeTags, options.themeTags
readLimits()
); );
} }
@ -254,8 +255,7 @@ export class OptimizerConfig {
public readonly maxWorkerCount: number, public readonly maxWorkerCount: number,
public readonly dist: boolean, public readonly dist: boolean,
public readonly profileWebpack: boolean, public readonly profileWebpack: boolean,
public readonly themeTags: ThemeTags, public readonly themeTags: ThemeTags
public readonly limits: Limits
) {} ) {}
getWorkerConfig(optimizerCacheKey: unknown): WorkerConfig { getWorkerConfig(optimizerCacheKey: unknown): WorkerConfig {

View file

@ -1,46 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { materialize, mergeMap, dematerialize } from 'rxjs/operators';
import { CiStatsReporter, ToolingLog } from '@kbn/dev-utils';
import { OptimizerUpdate$ } from './run_optimizer';
import { OptimizerConfig, getMetrics } from './optimizer';
import { pipeClosure } from './common';
export function reportOptimizerStats(
reporter: CiStatsReporter,
config: OptimizerConfig,
log: ToolingLog
) {
return pipeClosure((update$: OptimizerUpdate$) =>
update$.pipe(
materialize(),
mergeMap(async (n) => {
if (n.kind === 'C') {
const metrics = getMetrics(log, config);
await reporter.metrics(metrics);
for (const metric of metrics) {
if (metric.limit != null && metric.value > metric.limit) {
const value = metric.value.toLocaleString();
const limit = metric.limit.toLocaleString();
log.warning(
`Metric [${metric.group}] for [${metric.id}] of [${value}] over the limit of [${limit}]`
);
}
}
}
return n;
}),
dematerialize()
)
);
}

View file

@ -0,0 +1,108 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import Path from 'path';
import webpack from 'webpack';
import { RawSource } from 'webpack-sources';
import { CiStatsMetrics } from '@kbn/dev-utils';
import { Bundle } from '../common';
interface Asset {
name: string;
size: number;
}
const IGNORED_EXTNAME = ['.map', '.br', '.gz'];
export class BundleMetricsPlugin {
constructor(private readonly bundle: Bundle) {}
public apply(compiler: webpack.Compiler) {
const { bundle } = this;
compiler.hooks.emit.tap('BundleMetricsPlugin', (compilation) => {
const assets = Object.entries(compilation.assets)
.map(
([name, source]: [string, any]): Asset => ({
name,
size: source.size(),
})
)
.filter((asset) => {
const filename = Path.basename(asset.name);
if (filename.startsWith('.')) {
return false;
}
const ext = Path.extname(filename);
if (IGNORED_EXTNAME.includes(ext)) {
return false;
}
return true;
});
const entryName = `${bundle.id}.${bundle.type}.js`;
const entry = assets.find((a) => a.name === entryName);
if (!entry) {
throw new Error(
`Unable to find bundle entry named [${entryName}] in [${bundle.outputDir}]`
);
}
const chunkPrefix = `${bundle.id}.chunk.`;
const asyncChunks = assets.filter((a) => a.name.startsWith(chunkPrefix));
const miscFiles = assets.filter((a) => a !== entry && !asyncChunks.includes(a));
const sumSize = (files: Asset[]) => files.reduce((acc: number, a) => acc + a.size, 0);
const moduleCount = bundle.cache.getModuleCount();
if (moduleCount === undefined) {
throw new Error(`moduleCount wasn't populated by PopulateBundleCachePlugin`);
}
const bundleMetrics: CiStatsMetrics = [
{
group: `@kbn/optimizer bundle module count`,
id: bundle.id,
value: moduleCount,
},
{
group: `page load bundle size`,
id: bundle.id,
value: entry.size,
limit: bundle.pageLoadAssetSizeLimit,
limitConfigPath: `packages/kbn-optimizer/limits.yml`,
},
{
group: `async chunks size`,
id: bundle.id,
value: sumSize(asyncChunks),
},
{
group: `async chunk count`,
id: bundle.id,
value: asyncChunks.length,
},
{
group: `miscellaneous assets size`,
id: bundle.id,
value: sumSize(miscFiles),
},
];
const metricsSource = new RawSource(JSON.stringify(bundleMetrics, null, 2));
// see https://github.com/jantimon/html-webpack-plugin/blob/33d69f49e6e9787796402715d1b9cd59f80b628f/index.js#L266
// @ts-expect-error undocumented, used to add assets to the output
compilation.emitAsset('metrics.json', metricsSource);
});
}
}

View file

@ -0,0 +1,34 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import Fs from 'fs';
import Path from 'path';
import webpack from 'webpack';
import { Bundle } from '../common';
export class EmitStatsPlugin {
constructor(private readonly bundle: Bundle) {}
public apply(compiler: webpack.Compiler) {
compiler.hooks.done.tap(
{
name: 'EmitStatsPlugin',
// run at the very end, ensure that it's after clean-webpack-plugin
stage: 10,
},
(stats) => {
Fs.writeFileSync(
Path.resolve(this.bundle.outputDir, 'stats.json'),
JSON.stringify(stats.toJson())
);
}
);
}
}

View file

@ -0,0 +1,132 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import webpack from 'webpack';
import Path from 'path';
import { inspect } from 'util';
import { Bundle, WorkerConfig, ascending, parseFilePath } from '../common';
import { BundleRefModule } from './bundle_ref_module';
import {
isExternalModule,
isNormalModule,
isIgnoredModule,
isConcatenatedModule,
getModulePath,
} from './webpack_helpers';
/**
* sass-loader creates about a 40% overhead on the overall optimizer runtime, and
* so this constant is used to indicate to assignBundlesToWorkers() that there is
* extra work done in a bundle that has a lot of scss imports. The value is
* arbitrary and just intended to weigh the bundles so that they are distributed
* across mulitple workers on machines with lots of cores.
*/
const EXTRA_SCSS_WORK_UNITS = 100;
export class PopulateBundleCachePlugin {
constructor(private readonly workerConfig: WorkerConfig, private readonly bundle: Bundle) {}
public apply(compiler: webpack.Compiler) {
const { bundle, workerConfig } = this;
compiler.hooks.emit.tap(
{
name: 'PopulateBundleCachePlugin',
before: ['BundleMetricsPlugin'],
},
(compilation) => {
const bundleRefExportIds: string[] = [];
const referencedFiles = new Set<string>();
let moduleCount = 0;
let workUnits = compilation.fileDependencies.size;
if (bundle.manifestPath) {
referencedFiles.add(bundle.manifestPath);
}
for (const module of compilation.modules) {
if (isNormalModule(module)) {
moduleCount += 1;
const path = getModulePath(module);
const parsedPath = parseFilePath(path);
if (!parsedPath.dirs.includes('node_modules')) {
referencedFiles.add(path);
if (path.endsWith('.scss')) {
workUnits += EXTRA_SCSS_WORK_UNITS;
for (const depPath of module.buildInfo.fileDependencies) {
referencedFiles.add(depPath);
}
}
continue;
}
const nmIndex = parsedPath.dirs.lastIndexOf('node_modules');
const isScoped = parsedPath.dirs[nmIndex + 1].startsWith('@');
referencedFiles.add(
Path.join(
parsedPath.root,
...parsedPath.dirs.slice(0, nmIndex + 1 + (isScoped ? 2 : 1)),
'package.json'
)
);
continue;
}
if (module instanceof BundleRefModule) {
bundleRefExportIds.push(module.ref.exportId);
continue;
}
if (isConcatenatedModule(module)) {
moduleCount += module.modules.length;
continue;
}
if (isExternalModule(module) || isIgnoredModule(module)) {
continue;
}
throw new Error(`Unexpected module type: ${inspect(module)}`);
}
const files = Array.from(referencedFiles).sort(ascending((p) => p));
const mtimes = new Map(
files.map((path): [string, number | undefined] => {
try {
return [path, compiler.inputFileSystem.statSync(path)?.mtimeMs];
} catch (error) {
if (error?.code === 'ENOENT') {
return [path, undefined];
}
throw error;
}
})
);
bundle.cache.set({
bundleRefExportIds: bundleRefExportIds.sort(ascending((p) => p)),
optimizerCacheKey: workerConfig.optimizerCacheKey,
cacheKey: bundle.createCacheKey(files, mtimes),
moduleCount,
workUnits,
files,
});
// write the cache to the compilation so that it isn't cleaned by clean-webpack-plugin
bundle.cache.writeWebpackAsset(compilation);
}
);
}
}

View file

@ -8,46 +8,16 @@
import 'source-map-support/register'; import 'source-map-support/register';
import Fs from 'fs';
import Path from 'path';
import { inspect } from 'util';
import webpack, { Stats } from 'webpack'; import webpack, { Stats } from 'webpack';
import * as Rx from 'rxjs'; import * as Rx from 'rxjs';
import { mergeMap, map, mapTo, takeUntil } from 'rxjs/operators'; import { mergeMap, map, mapTo, takeUntil } from 'rxjs/operators';
import { import { CompilerMsgs, CompilerMsg, maybeMap, Bundle, WorkerConfig, BundleRefs } from '../common';
CompilerMsgs,
CompilerMsg,
maybeMap,
Bundle,
WorkerConfig,
ascending,
parseFilePath,
BundleRefs,
} from '../common';
import { BundleRefModule } from './bundle_ref_module';
import { getWebpackConfig } from './webpack.config'; import { getWebpackConfig } from './webpack.config';
import { isFailureStats, failedStatsToErrorMessage } from './webpack_helpers'; import { isFailureStats, failedStatsToErrorMessage } from './webpack_helpers';
import {
isExternalModule,
isNormalModule,
isIgnoredModule,
isConcatenatedModule,
getModulePath,
} from './webpack_helpers';
const PLUGIN_NAME = '@kbn/optimizer'; const PLUGIN_NAME = '@kbn/optimizer';
/**
* sass-loader creates about a 40% overhead on the overall optimizer runtime, and
* so this constant is used to indicate to assignBundlesToWorkers() that there is
* extra work done in a bundle that has a lot of scss imports. The value is
* arbitrary and just intended to weigh the bundles so that they are distributed
* across mulitple workers on machines with lots of cores.
*/
const EXTRA_SCSS_WORK_UNITS = 100;
/** /**
* Create an Observable<CompilerMsg> for a specific child compiler + bundle * Create an Observable<CompilerMsg> for a specific child compiler + bundle
*/ */
@ -80,13 +50,6 @@ const observeCompiler = (
return undefined; return undefined;
} }
if (workerConfig.profileWebpack) {
Fs.writeFileSync(
Path.resolve(bundle.outputDir, 'stats.json'),
JSON.stringify(stats.toJson())
);
}
if (!workerConfig.watch) { if (!workerConfig.watch) {
process.nextTick(() => done$.next()); process.nextTick(() => done$.next());
} }
@ -97,88 +60,11 @@ const observeCompiler = (
}); });
} }
const bundleRefExportIds: string[] = []; const moduleCount = bundle.cache.getModuleCount();
const referencedFiles = new Set<string>(); if (moduleCount === undefined) {
let moduleCount = 0; throw new Error(`moduleCount wasn't populated by PopulateBundleCachePlugin`);
let workUnits = stats.compilation.fileDependencies.size;
if (bundle.manifestPath) {
referencedFiles.add(bundle.manifestPath);
} }
for (const module of stats.compilation.modules) {
if (isNormalModule(module)) {
moduleCount += 1;
const path = getModulePath(module);
const parsedPath = parseFilePath(path);
if (!parsedPath.dirs.includes('node_modules')) {
referencedFiles.add(path);
if (path.endsWith('.scss')) {
workUnits += EXTRA_SCSS_WORK_UNITS;
for (const depPath of module.buildInfo.fileDependencies) {
referencedFiles.add(depPath);
}
}
continue;
}
const nmIndex = parsedPath.dirs.lastIndexOf('node_modules');
const isScoped = parsedPath.dirs[nmIndex + 1].startsWith('@');
referencedFiles.add(
Path.join(
parsedPath.root,
...parsedPath.dirs.slice(0, nmIndex + 1 + (isScoped ? 2 : 1)),
'package.json'
)
);
continue;
}
if (module instanceof BundleRefModule) {
bundleRefExportIds.push(module.ref.exportId);
continue;
}
if (isConcatenatedModule(module)) {
moduleCount += module.modules.length;
continue;
}
if (isExternalModule(module) || isIgnoredModule(module)) {
continue;
}
throw new Error(`Unexpected module type: ${inspect(module)}`);
}
const files = Array.from(referencedFiles).sort(ascending((p) => p));
const mtimes = new Map(
files.map((path): [string, number | undefined] => {
try {
return [path, compiler.inputFileSystem.statSync(path)?.mtimeMs];
} catch (error) {
if (error?.code === 'ENOENT') {
return [path, undefined];
}
throw error;
}
})
);
bundle.cache.set({
bundleRefExportIds: bundleRefExportIds.sort(ascending((p) => p)),
optimizerCacheKey: workerConfig.optimizerCacheKey,
cacheKey: bundle.createCacheKey(files, mtimes),
moduleCount,
workUnits,
files,
});
return compilerMsgs.compilerSuccess({ return compilerMsgs.compilerSuccess({
moduleCount, moduleCount,
}); });

View file

@ -19,6 +19,9 @@ import * as UiSharedDeps from '@kbn/ui-shared-deps';
import { Bundle, BundleRefs, WorkerConfig } from '../common'; import { Bundle, BundleRefs, WorkerConfig } from '../common';
import { BundleRefsPlugin } from './bundle_refs_plugin'; import { BundleRefsPlugin } from './bundle_refs_plugin';
import { BundleMetricsPlugin } from './bundle_metrics_plugin';
import { EmitStatsPlugin } from './emit_stats_plugin';
import { PopulateBundleCachePlugin } from './populate_bundle_cache_plugin';
const BABEL_PRESET_PATH = require.resolve('@kbn/babel-preset/webpack_preset'); const BABEL_PRESET_PATH = require.resolve('@kbn/babel-preset/webpack_preset');
@ -65,6 +68,9 @@ export function getWebpackConfig(bundle: Bundle, bundleRefs: BundleRefs, worker:
plugins: [ plugins: [
new CleanWebpackPlugin(), new CleanWebpackPlugin(),
new BundleRefsPlugin(bundle, bundleRefs), new BundleRefsPlugin(bundle, bundleRefs),
new PopulateBundleCachePlugin(worker, bundle),
new BundleMetricsPlugin(bundle),
...(worker.profileWebpack ? [new EmitStatsPlugin(bundle)] : []),
...(bundle.banner ? [new webpack.BannerPlugin({ banner: bundle.banner, raw: true })] : []), ...(bundle.banner ? [new webpack.BannerPlugin({ banner: bundle.banner, raw: true })] : []),
], ],

View file

@ -74,13 +74,14 @@ it('builds a generated plugin into a viable archive', async () => {
await extract(PLUGIN_ARCHIVE, { dir: TMP_DIR }); await extract(PLUGIN_ARCHIVE, { dir: TMP_DIR });
const files = await globby(['**/*'], { cwd: TMP_DIR }); const files = await globby(['**/*'], { cwd: TMP_DIR, dot: true });
files.sort((a, b) => a.localeCompare(b)); files.sort((a, b) => a.localeCompare(b));
expect(files).toMatchInlineSnapshot(` expect(files).toMatchInlineSnapshot(`
Array [ Array [
"kibana/fooTestPlugin/common/index.js", "kibana/fooTestPlugin/common/index.js",
"kibana/fooTestPlugin/kibana.json", "kibana/fooTestPlugin/kibana.json",
"kibana/fooTestPlugin/node_modules/.yarn-integrity",
"kibana/fooTestPlugin/package.json", "kibana/fooTestPlugin/package.json",
"kibana/fooTestPlugin/server/index.js", "kibana/fooTestPlugin/server/index.js",
"kibana/fooTestPlugin/server/plugin.js", "kibana/fooTestPlugin/server/plugin.js",

View file

@ -34,9 +34,15 @@ export async function optimize({ log, plugin, sourceDir, buildDir }: BuildContex
pluginScanDirs: [], pluginScanDirs: [],
}); });
const target = Path.resolve(sourceDir, 'target');
await runOptimizer(config).pipe(logOptimizerState(log, config)).toPromise(); await runOptimizer(config).pipe(logOptimizerState(log, config)).toPromise();
// clean up unnecessary files
Fs.unlinkSync(Path.resolve(target, 'public/metrics.json'));
Fs.unlinkSync(Path.resolve(target, 'public/.kbn-optimizer-cache'));
// move target into buildDir // move target into buildDir
await asyncRename(Path.resolve(sourceDir, 'target'), Path.resolve(buildDir, 'target')); await asyncRename(target, Path.resolve(buildDir, 'target'));
log.indent(-2); log.indent(-2);
} }

10
scripts/ship_ci_stats.js Normal file
View file

@ -0,0 +1,10 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
require('../src/setup_node_env/no_transpilation');
require('@kbn/dev-utils').shipCiStatsCli();

View file

@ -6,20 +6,18 @@
* Side Public License, v 1. * Side Public License, v 1.
*/ */
import { REPO_ROOT } from '@kbn/utils'; import Path from 'path';
import { CiStatsReporter } from '@kbn/dev-utils';
import {
runOptimizer,
OptimizerConfig,
logOptimizerState,
reportOptimizerStats,
} from '@kbn/optimizer';
import { Task } from '../lib'; import { REPO_ROOT } from '@kbn/utils';
import { lastValueFrom } from '@kbn/std';
import { CiStatsMetrics } from '@kbn/dev-utils';
import { runOptimizer, OptimizerConfig, logOptimizerState } from '@kbn/optimizer';
import { Task, deleteAll, write, read } from '../lib';
export const BuildKibanaPlatformPlugins: Task = { export const BuildKibanaPlatformPlugins: Task = {
description: 'Building distributable versions of Kibana platform plugins', description: 'Building distributable versions of Kibana platform plugins',
async run(_, log, build) { async run(buildConfig, log, build) {
const config = OptimizerConfig.create({ const config = OptimizerConfig.create({
repoRoot: REPO_ROOT, repoRoot: REPO_ROOT,
outputRoot: build.resolvePath(), outputRoot: build.resolvePath(),
@ -31,12 +29,27 @@ export const BuildKibanaPlatformPlugins: Task = {
includeCoreBundle: true, includeCoreBundle: true,
}); });
const reporter = CiStatsReporter.fromEnv(log); await lastValueFrom(runOptimizer(config).pipe(logOptimizerState(log, config)));
await runOptimizer(config) const combinedMetrics: CiStatsMetrics = [];
.pipe(reportOptimizerStats(reporter, config, log), logOptimizerState(log, config)) const metricFilePaths: string[] = [];
.toPromise(); for (const bundle of config.bundles) {
const path = Path.resolve(bundle.outputDir, 'metrics.json');
const metrics: CiStatsMetrics = JSON.parse(await read(path));
combinedMetrics.push(...metrics);
metricFilePaths.push(path);
}
// write combined metrics to target
await write(
buildConfig.resolveFromTarget('optimizer_bundle_metrics.json'),
JSON.stringify(combinedMetrics, null, 2)
);
// delete all metric files
await deleteAll(metricFilePaths, log);
// delete all bundle cache files
await Promise.all(config.bundles.map((b) => b.cache.clear())); await Promise.all(config.bundles.map((b) => b.cache.clear()));
}, },
}; };

View file

@ -5,6 +5,10 @@ source "$KIBANA_DIR/src/dev/ci_setup/setup_percy.sh"
echo " -> building and extracting OSS Kibana distributable for use in functional tests" echo " -> building and extracting OSS Kibana distributable for use in functional tests"
node scripts/build --debug --oss node scripts/build --debug --oss
echo " -> shipping metrics from build to ci-stats"
node scripts/ship_ci_stats --metrics target/optimizer_bundle_metrics.json
linuxBuild="$(find "$KIBANA_DIR/target" -name 'kibana-*-linux-x86_64.tar.gz')" linuxBuild="$(find "$KIBANA_DIR/target" -name 'kibana-*-linux-x86_64.tar.gz')"
installDir="$PARENT_DIR/install/kibana" installDir="$PARENT_DIR/install/kibana"
mkdir -p "$installDir" mkdir -p "$installDir"

View file

@ -15,5 +15,8 @@ node scripts/ensure_all_tests_in_ci_group;
echo " -> building and extracting OSS Kibana distributable for use in functional tests" echo " -> building and extracting OSS Kibana distributable for use in functional tests"
node scripts/build --debug --oss node scripts/build --debug --oss
echo " -> shipping metrics from build to ci-stats"
node scripts/ship_ci_stats --metrics target/optimizer_bundle_metrics.json
mkdir -p "$WORKSPACE/kibana-build-oss" mkdir -p "$WORKSPACE/kibana-build-oss"
cp -pR build/oss/kibana-*-SNAPSHOT-linux-x86_64/. $WORKSPACE/kibana-build-oss/ cp -pR build/oss/kibana-*-SNAPSHOT-linux-x86_64/. $WORKSPACE/kibana-build-oss/

View file

@ -6,6 +6,10 @@ source "$KIBANA_DIR/src/dev/ci_setup/setup_percy.sh"
echo " -> building and extracting default Kibana distributable" echo " -> building and extracting default Kibana distributable"
cd "$KIBANA_DIR" cd "$KIBANA_DIR"
node scripts/build --debug --no-oss node scripts/build --debug --no-oss
echo " -> shipping metrics from build to ci-stats"
node scripts/ship_ci_stats --metrics target/optimizer_bundle_metrics.json
linuxBuild="$(find "$KIBANA_DIR/target" -name 'kibana-*-linux-x86_64.tar.gz')" linuxBuild="$(find "$KIBANA_DIR/target" -name 'kibana-*-linux-x86_64.tar.gz')"
installDir="$KIBANA_DIR/install/kibana" installDir="$KIBANA_DIR/install/kibana"
mkdir -p "$installDir" mkdir -p "$installDir"

View file

@ -30,6 +30,10 @@ node scripts/functional_tests --assert-none-excluded \
echo " -> building and extracting default Kibana distributable for use in functional tests" echo " -> building and extracting default Kibana distributable for use in functional tests"
cd "$KIBANA_DIR" cd "$KIBANA_DIR"
node scripts/build --debug --no-oss node scripts/build --debug --no-oss
echo " -> shipping metrics from build to ci-stats"
node scripts/ship_ci_stats --metrics target/optimizer_bundle_metrics.json
linuxBuild="$(find "$KIBANA_DIR/target" -name 'kibana-*-linux-x86_64.tar.gz')" linuxBuild="$(find "$KIBANA_DIR/target" -name 'kibana-*-linux-x86_64.tar.gz')"
installDir="$KIBANA_DIR/install/kibana" installDir="$KIBANA_DIR/install/kibana"
mkdir -p "$installDir" mkdir -p "$installDir"

View file

@ -6803,7 +6803,7 @@
dependencies: dependencies:
"@types/webpack" "*" "@types/webpack" "*"
"@types/webpack-sources@*": "@types/webpack-sources@*", "@types/webpack-sources@^0.1.4":
version "0.1.5" version "0.1.5"
resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-0.1.5.tgz#be47c10f783d3d6efe1471ff7f042611bd464a92" resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-0.1.5.tgz#be47c10f783d3d6efe1471ff7f042611bd464a92"
integrity sha512-zfvjpp7jiafSmrzJ2/i3LqOyTYTuJ7u1KOXlKgDlvsj9Rr0x7ZiYu5lZbXwobL7lmsRNtPXlBfmaUD8eU2Hu8w== integrity sha512-zfvjpp7jiafSmrzJ2/i3LqOyTYTuJ7u1KOXlKgDlvsj9Rr0x7ZiYu5lZbXwobL7lmsRNtPXlBfmaUD8eU2Hu8w==