Skip to content

Commit

Permalink
chore(benchmarks): memory allocation per function during page-load [s… (
Browse files Browse the repository at this point in the history
elastic#508)

* chore(benchmarks): memory allocation per function during page-load [skip ci]

* add diff bundle name for apm build [skip ci]

* fix server start [skip ci]

* address review comments [skip ci]
  • Loading branch information
vigneshshanmugam authored and jahtalab committed Dec 9, 2019
1 parent fc57b7c commit af20d35
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 31 deletions.
123 changes: 112 additions & 11 deletions packages/rum/test/benchmarks/analyzer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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') -
Expand All @@ -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
}

/**
Expand All @@ -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)
Expand Down Expand Up @@ -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
}
1 change: 1 addition & 0 deletions packages/rum/test/benchmarks/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down
26 changes: 23 additions & 3 deletions packages/rum/test/benchmarks/profiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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)
})
}
Expand Down
11 changes: 9 additions & 2 deletions packages/rum/test/benchmarks/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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
Expand Down
40 changes: 25 additions & 15 deletions packages/rum/test/benchmarks/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
/**
Expand All @@ -75,15 +75,17 @@ 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) => {
res.setHeader('cache-control', 'no-cache, no-store, must-revalidate')
const { port } = server.address()
const images = generateImageUrls(port, noOfImages)
res.render('heavy', {
apmBundleContent: getRandomBundleContent(),
apmBundleContent: getRandomBundleContent(apmBundle),
images
})
})
Expand All @@ -94,3 +96,11 @@ module.exports = function startServer() {
})
})
}

module.exports = startServer

!(async () => {
if (require.main === module) {
await startServer('elastic-apm-rum.umd.min.js')
}
})()

0 comments on commit af20d35

Please sign in to comment.