Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(benchmarks): memory allocation per function during page-load [s… #508

Merged
merged 4 commits into from
Dec 9, 2019
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion dev-utils/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,10 @@ function getWebpackConfig(bundleType, packageType) {
minimizer: [
new UglifyJSPlugin({
sourceMap: true,
extractComments: true
extractComments: true,
uglifyOptions: {
keep_fnames: true
}
})
]
},
Expand Down
108 changes: 97 additions & 11 deletions packages/rum/test/benchmarks/analyzer.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,48 @@ const stats = require('stats-lite')
const { readFileSync } = require('fs')
const path = require('path')
const zlib = require('zlib')
const webpack = require('webpack')
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 the function names are not mangled
*/
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)
vigneshshanmugam marked this conversation as resolved.
Show resolved Hide resolved
}

return new Promise((resolve, reject) => {
webpack(config, err => {
if (err) {
reject(err)
}
console.info('custom apm build - ', filename, 'generated')
resolve()
})
})
vigneshshanmugam marked this conversation as resolved.
Show resolved Hide resolved
}

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 +114,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 +137,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 +160,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
vigneshshanmugam marked this conversation as resolved.
Show resolved Hide resolved
})
} else if (!Array.isArray(value)) {
/**
* deal with common data points
*/
result[metricName] = value
} else {
const unit = getUnit(metricName)
Expand Down Expand Up @@ -181,12 +222,57 @@ function capturePayloadInfo(payload) {
}
}

function getMemoryAllocationPerFunction({ profile }) {
const allocations = []
/**
* Whitelist built functions from the sample
*/
const whitelist = [
vigneshshanmugam marked this conversation as resolved.
Show resolved Hide resolved
'(V8 API)',
'(BYTECODE_COMPILER)',
'__webpack_require__',
'scriptId'
vigneshshanmugam marked this conversation as resolved.
Show resolved Hide resolved
]

function traverseChild(obj) {
const { callFrame, selfSize } = obj
const { functionName } = callFrame

if (
selfSize > 0 &&
functionName !== '' &&
!whitelist.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')
vigneshshanmugam marked this conversation as resolved.
Show resolved Hide resolved
/**
* 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: 24 additions & 16 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() {
const startServer = (module.exports = filename => {
vigneshshanmugam marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -93,4 +95,10 @@ module.exports = function startServer() {
resolve(server)
})
})
}
})

!(async () => {
if (require.main === module) {
vigneshshanmugam marked this conversation as resolved.
Show resolved Hide resolved
await startServer('elastic-apm-rum.umd.min.js')
}
})()