From 1f3426942c6f2ad8cebe9791435218da5a797437 Mon Sep 17 00:00:00 2001 From: Patrick Mueller Date: Fri, 30 Jun 2023 08:42:38 -0400 Subject: [PATCH] [examples] add routes to access v8 profiling (#155956) Adds routes to run v8 profiling tools, when running the examples plugins via `--run-examples` See the included README.md for more info --- .github/CODEOWNERS | 1 + examples/v8_profiler_examples/README.md | 112 ++++++++++++++++++ examples/v8_profiler_examples/kibana.jsonc | 12 ++ examples/v8_profiler_examples/server/index.ts | 14 +++ .../server/lib/cpu_profile.ts | 34 ++++++ .../server/lib/deferred.ts | 27 +++++ .../server/lib/heap_profile.ts | 35 ++++++ .../server/lib/session.ts | 79 ++++++++++++ .../v8_profiler_examples/server/plugin.ts | 29 +++++ .../server/routes/common.ts | 84 +++++++++++++ .../server/routes/cpu_profile.ts | 37 ++++++ .../server/routes/heap_profile.ts | 41 +++++++ .../server/routes/index.ts | 17 +++ examples/v8_profiler_examples/tsconfig.json | 18 +++ package.json | 1 + tsconfig.base.json | 2 + yarn.lock | 4 + 17 files changed, 547 insertions(+) create mode 100644 examples/v8_profiler_examples/README.md create mode 100644 examples/v8_profiler_examples/kibana.jsonc create mode 100644 examples/v8_profiler_examples/server/index.ts create mode 100644 examples/v8_profiler_examples/server/lib/cpu_profile.ts create mode 100644 examples/v8_profiler_examples/server/lib/deferred.ts create mode 100644 examples/v8_profiler_examples/server/lib/heap_profile.ts create mode 100644 examples/v8_profiler_examples/server/lib/session.ts create mode 100644 examples/v8_profiler_examples/server/plugin.ts create mode 100644 examples/v8_profiler_examples/server/routes/common.ts create mode 100644 examples/v8_profiler_examples/server/routes/cpu_profile.ts create mode 100644 examples/v8_profiler_examples/server/routes/heap_profile.ts create mode 100644 examples/v8_profiler_examples/server/routes/index.ts create mode 100644 examples/v8_profiler_examples/tsconfig.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7ed90e7930b3..bfe7bdb251c6 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -742,6 +742,7 @@ packages/kbn-utility-types @elastic/kibana-core packages/kbn-utility-types-jest @elastic/kibana-operations packages/kbn-utils @elastic/kibana-operations x-pack/plugins/ux @elastic/uptime +examples/v8_profiler_examples @elastic/response-ops packages/kbn-validate-next-docs-cli @elastic/kibana-operations src/plugins/vis_default_editor @elastic/kibana-visualizations src/plugins/vis_types/gauge @elastic/kibana-visualizations diff --git a/examples/v8_profiler_examples/README.md b/examples/v8_profiler_examples/README.md new file mode 100644 index 000000000000..50c188ab0e28 --- /dev/null +++ b/examples/v8_profiler_examples/README.md @@ -0,0 +1,112 @@ +# V8 profiler examples + +Provides access to the V8 CPU Profiler and Heap Profiler, to inspect +the Kibana running this plugin. + +The endpoints are: + + /_dev/cpu_profile?duration=(seconds)&interval=(microseconds) + /_dev/heap_profile?duration=(seconds)&interval=(bytes) + +The default duration is 5 seconds. + +For more information on these V8 APIs, see: + +- https://chromedevtools.github.io/devtools-protocol/v8/Profiler/ +- https://chromedevtools.github.io/devtools-protocol/v8/HeapProfiler/ + +Note that due to bugs or limitations, +it's not possible to generate heap snapshots using the techniques used +to generate the cpu and heap profiles. + +Try them right now, assuming you started Kibana with `--run-examples`! + +- [`http://localhost:5601/_dev/cpu_profile`](http://localhost:5601/_dev/cpu_profile) +- [`http://localhost:5601/_dev/heap_profile`](http://localhost:5601/_dev/heap_profile) + + +When using curl, you can use the `-kOJ` options, which: + +- `-k --insecure`: allow HTTPS usage with self-signed certs +- `-O --remote-name`: use the server-specified name for this download +- `-J --remote-header-name`: use the `Content-Disposition` as the name of + the download + +So one of the following should work for you, to run a 10 second CPU profile +using an interval of 100μs (default: 5s / 1000μs): + +``` +curl -OJ "http://elastic:changeme@localhost:5601/_dev/cpu_profile?duration=10&interval=100" +curl -kOJ "https://elastic:changeme@localhost:5601/_dev/cpu_profile?duration=10&interval=100" +``` + +The files generated will be: + + MM-DD_hh-mm-ss.cpuprofile + MM-DD_hh-mm-ss.heapprofile + +These filetypes are the ones expected by various V8 tools that can read these. + +You can use these URLs in your browser, and the files will be saved with the +generated names. + +## profile / heap profile readers + +The traditional tools used to view these are part of Chrome Dev Tools (CDT) +and now VS Code also supports viewing these files. They provide +similar capabilities. + +There doesn't seem to be a much doc available on how to use the viewers +for these files. The Chrome Dev Tools docs are extremely old and appear +to be out-of-date with the current user interface. The VS Code +documentation [Analyzing a profile][] is more recent, but there's not +much there. + +[Analyzing a profile]: https://code.visualstudio.com/docs/nodejs/profiling#_analyzing-a-profile + +For CPU profiles, open CDT and then click on the "Performance" tab. You +should be able to drop a file right from Finder / Explorer onto the CDT +window, and then get the visualization of the profile. If you +downloaded the profile right from the browser, using the URL in the URL +bar, you can drop the download file from the download status button +right into CDT. + +For heap profiles, open CDT and then click on the "Memory" tab. Drag and +drop doesn't seem to work here, but you can load a file via a file +prompter by clicking the "Load" button at the bottom of the "Memory" +pane. You will probably need to scroll to see the button. + +VSCode now supports `.cpuprofile` files and `.heapprofile` files +directly, displaying them as a table of function timings. There is also +[an extension available to display flame charts][] installed by clicking +on the grey-ed out "flame" button on the top-right of the cpu profile +view. + +[an extension available to display flame charts]: https://marketplace.visualstudio.com/items?itemName=ms-vscode.vscode-js-profile-flame + +There seem to be problems with both CDT and the VS Code tools, at times. +The flame charts in VS Code seem to go haywire sometimes. The heap +profile tables in CDT don't expand. Etc. So, beware, and be prepared to +have to use multiple tools to analyze these files. + +An alternate view of CPU profiles, which organizes files based on +"packages", is available with the **NO**de **PRO**filer (`no-pro`) thing +available at https://pmuellr.github.io/no-pro/ . It also supports +drag-n-drop of CPU profile files. Note that you can get more +directories to show up as "packages", by bringing up CDT and running the +following code: + + localStorage['fake-packages-dirs'] = "x-pack/plugins,packages,src/core" + + +## tips / tricks + +If you're handy with Mac Finder, or other ways of auto-launching apps +based on file extensions, it's easy to associate `.cpuprofile` files +with vscode. + +Since the http endpoints are GET requests, they are easy to bookmark. +Start a profile by clicking on a bookmark. + +When these files are downloaded via Chrome, you can typically launch +them directly from the download bar, or drag the file to a viewer. diff --git a/examples/v8_profiler_examples/kibana.jsonc b/examples/v8_profiler_examples/kibana.jsonc new file mode 100644 index 000000000000..a011bd751ce8 --- /dev/null +++ b/examples/v8_profiler_examples/kibana.jsonc @@ -0,0 +1,12 @@ +{ + "type": "plugin", + "id": "@kbn/v8-profiler-examples-plugin", + "owner": "@elastic/response-ops", + "description": "Provides access to the v8 cpu profiler and heap profiler running this app", + "plugin": { + // umm, vs code says I can't use digits in the id field? + "id": "vEIGHTProfilerExamples", + "server": true, + "browser": false, + } +} diff --git a/examples/v8_profiler_examples/server/index.ts b/examples/v8_profiler_examples/server/index.ts new file mode 100644 index 000000000000..d06809f7411b --- /dev/null +++ b/examples/v8_profiler_examples/server/index.ts @@ -0,0 +1,14 @@ +/* + * 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 { PluginInitializerContext } from '@kbn/core/server'; +import { V8ProfilerExamplesPlugin } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new V8ProfilerExamplesPlugin(initializerContext); +} diff --git a/examples/v8_profiler_examples/server/lib/cpu_profile.ts b/examples/v8_profiler_examples/server/lib/cpu_profile.ts new file mode 100644 index 000000000000..358f8c7b8bbb --- /dev/null +++ b/examples/v8_profiler_examples/server/lib/cpu_profile.ts @@ -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 { Session } from './session'; + +interface StartProfilingArgs { + interval: number; +} + +// Start a new profile, resolves to a function to stop the profile and resolve +// the profile data. +export async function startProfiling( + session: Session, + args: StartProfilingArgs +): Promise<() => any> { + session.logger.info(`starting cpu profile with args: ${JSON.stringify(args)}`); + + await session.post('Profiler.enable'); + // microseconds, v8 default is 1000 + await session.post('Profiler.setSamplingInterval', args); + await session.post('Profiler.start'); + + // returned function which stops the profile and resolves to the profile data + return async function stopProfiling() { + session.logger.info('stopping cpu profile'); + const result: any = await session.post('Profiler.stop'); + return result.profile; + }; +} diff --git a/examples/v8_profiler_examples/server/lib/deferred.ts b/examples/v8_profiler_examples/server/lib/deferred.ts new file mode 100644 index 000000000000..9885b24a1691 --- /dev/null +++ b/examples/v8_profiler_examples/server/lib/deferred.ts @@ -0,0 +1,27 @@ +/* + * 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. + */ + +export function createDeferred() { + let resolver: any; + let rejecter: any; + + function resolve(...args: any[]) { + resolver(...args); + } + + function reject(...args: any[]) { + rejecter(...args); + } + + const promise = new Promise((resolve_, reject_) => { + resolver = resolve_; + rejecter = reject_; + }); + + return { promise, resolve, reject }; +} diff --git a/examples/v8_profiler_examples/server/lib/heap_profile.ts b/examples/v8_profiler_examples/server/lib/heap_profile.ts new file mode 100644 index 000000000000..16e0e160476f --- /dev/null +++ b/examples/v8_profiler_examples/server/lib/heap_profile.ts @@ -0,0 +1,35 @@ +/* + * 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 { Session } from './session'; + +interface StartProfilingArgs { + samplingInterval: number; + includeObjectsCollectedByMajorGC: boolean; + includeObjectsCollectedByMinorGC: boolean; +} + +// Start a new profile, resolves to a function to stop the profile and resolve +// the profile data. +export async function startProfiling( + session: Session, + args: StartProfilingArgs +): Promise<() => any> { + session.logger.info(`starting heap profile with args: ${JSON.stringify(args)}`); + + await session.post('Profiler.enable'); + await session.post('HeapProfiler.enable'); + await session.post('HeapProfiler.startSampling', args); + + // returned function which stops the profile and resolves to the profile data + return async function stopProfiling() { + session.logger.info('stopping heap profile'); + const result: any = await session.post('HeapProfiler.stopSampling'); + return result.profile; + }; +} diff --git a/examples/v8_profiler_examples/server/lib/session.ts b/examples/v8_profiler_examples/server/lib/session.ts new file mode 100644 index 000000000000..6da96e7d6a8b --- /dev/null +++ b/examples/v8_profiler_examples/server/lib/session.ts @@ -0,0 +1,79 @@ +/* + * 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 { Logger } from '@kbn/core/server'; + +import { createDeferred } from './deferred'; + +let inspector: any = null; +try { + inspector = require('inspector'); +} catch (err) { + // inspector will be null :-( +} + +export async function createSession(logger: Logger): Promise { + logger.debug('creating session'); + + if (inspector == null) { + throw new Error('the inspector module is not available for this version of node'); + } + + let session = null; + try { + session = new inspector.Session(); + } catch (err) { + throw new Error(`error creating inspector session: ${err.message}`); + } + + try { + session.connect(); + } catch (err) { + throw new Error(`error connecting inspector session: ${err.message}`); + } + + return new Session(logger, session); +} + +export class Session { + readonly logger: Logger; + private session: any; + + constructor(logger: Logger, session: any) { + this.logger = logger; + this.session = session; + } + + async destroy() { + this.session.disconnect(); + this.session = null; + } + + on(event: string, handler: any) { + this.session.on(event, handler); + } + + async post(method: string, args?: any) { + this.logger.debug(`posting method ${method} ${JSON.stringify(args)}`); + if (this.session == null) { + throw new Error('session disconnected'); + } + + const deferred = createDeferred(); + + this.session.post(method, args, (err: any, response: any) => { + if (err) { + this.logger.debug(`error from method ${method}: ${err.message}`); + return deferred.reject(err); + } + deferred.resolve(response); + }); + + return deferred.promise; + } +} diff --git a/examples/v8_profiler_examples/server/plugin.ts b/examples/v8_profiler_examples/server/plugin.ts new file mode 100644 index 000000000000..4ec29cf19ac0 --- /dev/null +++ b/examples/v8_profiler_examples/server/plugin.ts @@ -0,0 +1,29 @@ +/* + * 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 { Plugin, Logger, CoreSetup, PluginInitializerContext } from '@kbn/core/server'; + +import { registerRoutes } from './routes'; + +// this plugin's dependencies +export class V8ProfilerExamplesPlugin implements Plugin { + readonly logger: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + } + + public setup(core: CoreSetup) { + const router = core.http.createRouter(); + registerRoutes(this.logger, router); + } + + public start() {} + + public stop() {} +} diff --git a/examples/v8_profiler_examples/server/routes/common.ts b/examples/v8_profiler_examples/server/routes/common.ts new file mode 100644 index 000000000000..f7b753a37284 --- /dev/null +++ b/examples/v8_profiler_examples/server/routes/common.ts @@ -0,0 +1,84 @@ +/* + * 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 { Logger, IKibanaResponse, KibanaResponseFactory } from '@kbn/core/server'; +import { createSession, Session } from '../lib/session'; +import { createDeferred } from '../lib/deferred'; + +type StopProfilingFn = () => Promise; +type StartProfilingFn = (session: Session, args: ArgType) => Promise; + +export async function handleRoute( + startProfiling: StartProfilingFn, + args: ArgType, + logger: Logger, + response: KibanaResponseFactory, + duration: number, + type: string +): Promise { + let session: Session; + + try { + session = await createSession(logger); + } catch (err) { + const message = `unable to create session: ${err.message}`; + logger.error(message); + return response.badRequest({ body: message }); + } + + const deferred = createDeferred(); + let stopProfiling: any; + try { + stopProfiling = await startProfiling(session, args); + } catch (err) { + const message = `unable to start ${type} profiling: ${err.message}`; + logger.error(message); + return response.badRequest({ body: message }); + } + + setTimeout(whenDone, 1000 * duration); + + let profile; + async function whenDone() { + try { + profile = await stopProfiling(); + } catch (err) { + logger.warn(`unable to capture ${type} profile: ${err.message}`); + } + deferred.resolve(); + } + + await deferred.promise; + + try { + await session.destroy(); + } catch (err) { + logger.warn(`unable to destroy session: ${err.message}`); + } + + if (profile == null) { + const message = `unable to capture ${type} profile`; + logger.error(message); + return response.badRequest({ body: message }); + } + + const fileName = new Date() + .toISOString() + .replace('T', '_') + .replace(/\//g, '-') + .replace(/:/g, '-') + .substring(5, 19); + + return response.ok({ + body: profile, + headers: { + 'Content-Type': 'application/octet-stream', + 'Content-Disposition': `attachment; filename="${fileName}.${type}profile"`, + }, + }); +} diff --git a/examples/v8_profiler_examples/server/routes/cpu_profile.ts b/examples/v8_profiler_examples/server/routes/cpu_profile.ts new file mode 100644 index 000000000000..35eaff9e35ad --- /dev/null +++ b/examples/v8_profiler_examples/server/routes/cpu_profile.ts @@ -0,0 +1,37 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { Logger, IRouter } from '@kbn/core/server'; +import { startProfiling } from '../lib/cpu_profile'; +import { handleRoute } from './common'; + +const routeValidation = { + query: schema.object({ + // seconds to run the profile + duration: schema.number({ defaultValue: 5 }), + // microseconds, v8 default is 1000 + interval: schema.number({ defaultValue: 1000 }), + }), +}; + +const routeConfig = { + path: '/_dev/cpu_profile', + validate: routeValidation, +}; + +export function registerRoute(logger: Logger, router: IRouter): void { + router.get(routeConfig, async (context, request, response) => { + const { duration, interval } = request.query; + const args = { + interval, + }; + + return await handleRoute(startProfiling, args, logger, response, duration, 'cpu'); + }); +} diff --git a/examples/v8_profiler_examples/server/routes/heap_profile.ts b/examples/v8_profiler_examples/server/routes/heap_profile.ts new file mode 100644 index 000000000000..9f5cbcff944c --- /dev/null +++ b/examples/v8_profiler_examples/server/routes/heap_profile.ts @@ -0,0 +1,41 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { Logger, IRouter } from '@kbn/core/server'; +import { startProfiling } from '../lib/heap_profile'; +import { handleRoute } from './common'; + +const routeValidation = { + query: schema.object({ + // seconds to run the profile + duration: schema.number({ defaultValue: 5 }), + // Average sample interval in bytes. The default value is 32768 bytes. + interval: schema.number({ defaultValue: 32768 }), + includeMajorGC: schema.boolean({ defaultValue: true }), + includeMinorGC: schema.boolean({ defaultValue: true }), + }), +}; + +const routeConfig = { + path: '/_dev/heap_profile', + validate: routeValidation, +}; + +export function registerRoute(logger: Logger, router: IRouter): void { + router.get(routeConfig, async (context, request, response) => { + const { duration, interval, includeMajorGC, includeMinorGC } = request.query; + const args = { + samplingInterval: interval, + includeObjectsCollectedByMajorGC: includeMajorGC, + includeObjectsCollectedByMinorGC: includeMinorGC, + }; + + return await handleRoute(startProfiling, args, logger, response, duration, 'heap'); + }); +} diff --git a/examples/v8_profiler_examples/server/routes/index.ts b/examples/v8_profiler_examples/server/routes/index.ts new file mode 100644 index 000000000000..fbe442eebad3 --- /dev/null +++ b/examples/v8_profiler_examples/server/routes/index.ts @@ -0,0 +1,17 @@ +/* + * 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 { Logger, IRouter } from '@kbn/core/server'; + +import { registerRoute as registerRouteCpuProfile } from './cpu_profile'; +import { registerRoute as registerRouteHeapProfile } from './heap_profile'; + +export function registerRoutes(logger: Logger, router: IRouter): void { + registerRouteCpuProfile(logger, router); + registerRouteHeapProfile(logger, router); +} diff --git a/examples/v8_profiler_examples/tsconfig.json b/examples/v8_profiler_examples/tsconfig.json new file mode 100644 index 000000000000..31f0888931bc --- /dev/null +++ b/examples/v8_profiler_examples/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types" + }, + "include": [ + "index.ts", + "server/**/*.ts", + "../../typings/**/*" + ], + "exclude": [ + "target/**/*", + ], + "kbn_references": [ + "@kbn/core", + "@kbn/config-schema", + ] +} diff --git a/package.json b/package.json index d1c7fd0f769f..2fce048b2155 100644 --- a/package.json +++ b/package.json @@ -731,6 +731,7 @@ "@kbn/utility-types-jest": "link:packages/kbn-utility-types-jest", "@kbn/utils": "link:packages/kbn-utils", "@kbn/ux-plugin": "link:x-pack/plugins/ux", + "@kbn/v8-profiler-examples-plugin": "link:examples/v8_profiler_examples", "@kbn/vis-default-editor-plugin": "link:src/plugins/vis_default_editor", "@kbn/vis-type-gauge-plugin": "link:src/plugins/vis_types/gauge", "@kbn/vis-type-heatmap-plugin": "link:src/plugins/vis_types/heatmap", diff --git a/tsconfig.base.json b/tsconfig.base.json index b1a515ff31d5..610d9704b4d9 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1478,6 +1478,8 @@ "@kbn/utils/*": ["packages/kbn-utils/*"], "@kbn/ux-plugin": ["x-pack/plugins/ux"], "@kbn/ux-plugin/*": ["x-pack/plugins/ux/*"], + "@kbn/v8-profiler-examples-plugin": ["examples/v8_profiler_examples"], + "@kbn/v8-profiler-examples-plugin/*": ["examples/v8_profiler_examples/*"], "@kbn/validate-next-docs-cli": ["packages/kbn-validate-next-docs-cli"], "@kbn/validate-next-docs-cli/*": ["packages/kbn-validate-next-docs-cli/*"], "@kbn/vis-default-editor-plugin": ["src/plugins/vis_default_editor"], diff --git a/yarn.lock b/yarn.lock index 7909e5466ce7..b34388e82f10 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5714,6 +5714,10 @@ version "0.0.0" uid "" +"@kbn/v8-profiler-examples-plugin@link:examples/v8_profiler_examples": + version "0.0.0" + uid "" + "@kbn/validate-next-docs-cli@link:packages/kbn-validate-next-docs-cli": version "0.0.0" uid ""