diff --git a/packages/rum/test/benchmarks/analyzer.js b/packages/rum/test/benchmarks/analyzer.js index a1c61fc0c..0006a78c2 100644 --- a/packages/rum/test/benchmarks/analyzer.js +++ b/packages/rum/test/benchmarks/analyzer.js @@ -28,19 +28,63 @@ const stats = require('stats-lite') const { readFileSync } = require('fs') const path = require('path') const zlib = require('zlib') +const webpack = require('webpack') +const UglifyJSPlugin = require('uglifyjs-webpack-plugin') +const { + getWebpackConfig, + BUNDLE_TYPES +} = require('../../../../dev-utils/build') const { runs, noOfImages } = require('./config') const dist = path.join(__dirname, '../../dist') -function getMinifiedApmBundle() { - return readFileSync( - path.join(dist, 'bundles/elastic-apm-rum.umd.min.js'), - 'utf-8' - ) +function customApmBuild(filename) { + /** + * Match it with the default webpack prod build of elasticApm + * expect function names are not mangled and source map is not generated + */ + const config = { + entry: path.join(__dirname, '../../src/index.js'), + output: { + filename, + path: path.join(dist, 'bundles'), + library: '[name]', + libraryTarget: 'umd' + }, + ...getWebpackConfig(BUNDLE_TYPES.BROWSER_ESM_PROD), + ...{ + devtool: false, + optimization: { + minimizer: [ + new UglifyJSPlugin({ + sourceMap: false, + extractComments: true, + uglifyOptions: { + keep_fnames: true + } + }) + ] + } + } + } + + return new Promise((resolve, reject) => { + webpack(config, err => { + if (err) { + reject(err) + } + console.info('custom apm build - ', filename, 'generated') + resolve() + }) + }) +} + +function getMinifiedApmBundle(filename) { + return readFileSync(path.join(dist, 'bundles', filename), 'utf-8') } function getApmBundleSize() { - const content = getMinifiedApmBundle() + const content = getMinifiedApmBundle('elastic-apm-rum.umd.min.js') /** * To match the level with our bundlesize check */ @@ -85,7 +129,7 @@ function getUnit(metricName) { } async function analyzeMetrics(metric, resultMap) { - const { cpu, payload, navigation, measure, url, scenario } = metric + const { cpu, payload, navigation, measure, url, scenario, memory } = metric const loadTime = getFromEntries(navigation, url, 'loadEventEnd') - @@ -108,7 +152,8 @@ async function analyzeMetrics(metric, resultMap) { 'rum-cpu-time': cpu.cpuTimeFiltered, 'payload-size': payload.size, transactions: payload.transactions, - spans: payload.spans + spans: payload.spans, + memory } /** @@ -130,9 +175,20 @@ function calculateResults(resultMap) { Object.keys(metricObj).forEach(metricName => { const value = metricObj[metricName] /** - * deal with common data points + * Add consumed memory in bytes per each function to the result */ - if (!Array.isArray(value)) { + if (metricName === 'memory') { + const reducedValue = value.reduce((acc, curr) => { + acc.push(...curr) + return acc + }, []) + reducedValue.forEach(obj => { + result[`memory.${obj.name}.bytes`] = obj.size + }) + } else if (!Array.isArray(value)) { + /** + * deal with common data points + */ result[metricName] = value } else { const unit = getUnit(metricName) @@ -181,12 +237,57 @@ function capturePayloadInfo(payload) { } } +function getMemoryAllocationPerFunction({ profile }) { + const allocations = [] + /** + * exclude Function names from the memory sample + */ + const excludeFunctionNames = [ + '(V8 API)', + '(BYTECODE_COMPILER)', + '__webpack_require__', + 'scriptId' + ] + + function traverseChild(obj) { + const { callFrame, selfSize } = obj + const { functionName } = callFrame + + if ( + selfSize > 0 && + functionName !== '' && + !excludeFunctionNames.includes(functionName) + ) { + allocations.push({ + name: functionName, + size: selfSize + }) + } + + if (Array.isArray(obj.children)) { + obj.children.forEach(child => traverseChild(child)) + } + } + /** + * Build the allocation tree starting from the 'root' + */ + profile.head.children.forEach(c => { + traverseChild(c) + }) + + allocations.sort((a, b) => b.size - a.size) + + return allocations +} + module.exports = { analyzeMetrics, + customApmBuild, calculateResults, filterCpuMetrics, capturePayloadInfo, getMinifiedApmBundle, getApmBundleSize, - getCommonFields + getCommonFields, + getMemoryAllocationPerFunction } diff --git a/packages/rum/test/benchmarks/config.js b/packages/rum/test/benchmarks/config.js index e8abf64e0..4a1c749cd 100644 --- a/packages/rum/test/benchmarks/config.js +++ b/packages/rum/test/benchmarks/config.js @@ -38,6 +38,7 @@ module.exports = { * https://chromedevtools.github.io/devtools-protocol/tot/Profiler#method-setSamplingInterval */ cpuSamplingInterval: 200, + memorySamplingInterval: 10, launchOptions: { headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox'] diff --git a/packages/rum/test/benchmarks/profiler.js b/packages/rum/test/benchmarks/profiler.js index 2971d044b..1c9a40800 100644 --- a/packages/rum/test/benchmarks/profiler.js +++ b/packages/rum/test/benchmarks/profiler.js @@ -25,7 +25,11 @@ const puppeteer = require('puppeteer') const { chrome } = require('./config') -const { filterCpuMetrics, capturePayloadInfo } = require('./analyzer') +const { + filterCpuMetrics, + capturePayloadInfo, + getMemoryAllocationPerFunction +} = require('./analyzer') async function launchBrowser() { return await puppeteer.launch(chrome.launchOptions) @@ -41,6 +45,7 @@ function gatherRawMetrics(browser, url) { */ await client.send('Page.enable') await client.send('Profiler.enable') + await client.send('HeapProfiler.enable') /** * Tune the CPU sampler to get control the * number of samples generated @@ -63,11 +68,18 @@ function gatherRawMetrics(browser, url) { * the apm server */ const result = await client.send('Profiler.stop') + const sample = await client.send('HeapProfiler.stopSampling') + + const memoryMetrics = getMemoryAllocationPerFunction(sample) const filteredCpuMetrics = filterCpuMetrics(result.profile, url) const response = request.postData() const payload = capturePayloadInfo(response) - Object.assign(metrics, { cpu: filteredCpuMetrics, payload }) + Object.assign(metrics, { + cpu: filteredCpuMetrics, + payload, + memory: memoryMetrics + }) /** * Resolve the promise once we measure the size * of the payload to APM Server @@ -91,9 +103,17 @@ function gatherRawMetrics(browser, url) { Object.assign(metrics, timings) }) /** - * Start the profiler before navigating to the URL + * Perform a garbage collection to do a clean check everytime + */ + await client.send('HeapProfiler.collectGarbage') + /** + * Start the CPU and Memory profiler before navigating to the URL */ await client.send('Profiler.start') + await client.send('HeapProfiler.startSampling', { + interval: chrome.memorySamplingInterval + }) + await page.goto(url) }) } diff --git a/packages/rum/test/benchmarks/run.js b/packages/rum/test/benchmarks/run.js index 9aa14a426..a6f8a3a66 100644 --- a/packages/rum/test/benchmarks/run.js +++ b/packages/rum/test/benchmarks/run.js @@ -29,7 +29,8 @@ const { gatherRawMetrics, launchBrowser } = require('./profiler') const { analyzeMetrics, calculateResults, - getCommonFields + getCommonFields, + customApmBuild } = require('./analyzer') const { runs, port, scenarios } = require('./config') const startServer = require('./server') @@ -38,7 +39,13 @@ const REPORTS_DIR = join(__dirname, '../../reports') !(async function run() { try { - const server = await startServer() + /** + * Generate custom apm build + */ + const filename = 'apm-rum-with-name.umd.min.js' + await customApmBuild(filename) + + const server = await startServer(filename) /** * object cache holding the metrics accumlated in each run and * helps in processing the overall results diff --git a/packages/rum/test/benchmarks/server.js b/packages/rum/test/benchmarks/server.js index d56a3f65b..b2ae6c163 100644 --- a/packages/rum/test/benchmarks/server.js +++ b/packages/rum/test/benchmarks/server.js @@ -39,28 +39,28 @@ function generateImageUrls(port, number) { return urls } -/** - * Strip license and sourcemap url - */ -const APM_BUNDLE = getMinifiedApmBundle() - .replace( - '/*! For license information please see elastic-apm-rum.umd.min.js.LICENSE */', - '' - ) - .replace('//# sourceMappingURL=elastic-apm-rum.umd.min.js.map', '') - /** * Adding a random value at the end of the script text prevents * Chrome from caching the parsed/JITed script */ -function getRandomBundleContent() { - let content = APM_BUNDLE +function getRandomBundleContent(apmBundle) { + let content = apmBundle content += `var scriptId = ${Date.now()};` return content } -module.exports = function startServer() { +function startServer(filename) { return new Promise(resolve => { + /** + * Strip license and sourcemap url + */ + const apmBundle = getMinifiedApmBundle(filename) + .replace( + `/*! For license information please see ${filename}.LICENSE */`, + '' + ) + .replace(`//# sourceMappingURL=${filename}.map`, '') + const app = express() let server /** @@ -75,7 +75,9 @@ module.exports = function startServer() { app.get('/basic', (req, res) => { res.setHeader('cache-control', 'no-cache, no-store, must-revalidate') - res.render('basic', { apmBundleContent: getRandomBundleContent() }) + res.render('basic', { + apmBundleContent: getRandomBundleContent(apmBundle) + }) }) app.get('/heavy', (req, res) => { @@ -83,7 +85,7 @@ module.exports = function startServer() { const { port } = server.address() const images = generateImageUrls(port, noOfImages) res.render('heavy', { - apmBundleContent: getRandomBundleContent(), + apmBundleContent: getRandomBundleContent(apmBundle), images }) }) @@ -94,3 +96,11 @@ module.exports = function startServer() { }) }) } + +module.exports = startServer + +!(async () => { + if (require.main === module) { + await startServer('elastic-apm-rum.umd.min.js') + } +})()