[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
This commit is contained in:
Patrick Mueller 2023-06-30 08:42:38 -04:00 committed by GitHub
parent b12238bac8
commit 1f3426942c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 547 additions and 0 deletions

1
.github/CODEOWNERS vendored
View file

@ -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

View file

@ -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.

View file

@ -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,
}
}

View file

@ -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);
}

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 { 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;
};
}

View file

@ -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 };
}

View file

@ -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;
};
}

View file

@ -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<Session> {
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;
}
}

View file

@ -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<void, void> {
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() {}
}

View file

@ -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<any>;
type StartProfilingFn<ArgType> = (session: Session, args: ArgType) => Promise<StopProfilingFn>;
export async function handleRoute<ArgType>(
startProfiling: StartProfilingFn<ArgType>,
args: ArgType,
logger: Logger,
response: KibanaResponseFactory,
duration: number,
type: string
): Promise<IKibanaResponse> {
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"`,
},
});
}

View file

@ -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');
});
}

View file

@ -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');
});
}

View file

@ -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);
}

View file

@ -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",
]
}

View file

@ -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",

View file

@ -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"],

View file

@ -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 ""