UNPKG

@nearform/heap-profiler

Version:

Heap dump, sample profiler and allocation timeline generator for Node.

217 lines (172 loc) 6.7 kB
'use strict' const { unlinkSync, readFileSync, statSync } = require('fs') const t = require('tap') const { spy } = require('sinon') const { tmpName } = require('tmp-promise') const { promisify } = require('util') const validateSnapshot = require('./fixtures/snapshotSchema') const validateProfile = require('./fixtures/profileSchema') const sleep = promisify(setTimeout) const logger = { info: spy(), error: spy() } const preloader = require('../src/preloader') function cleanEnvironment() { for (const key of [ 'HEAP_PROFILER_SNAPSHOT', 'HEAP_PROFILER_SNAPSHOT_RUN_GC', 'HEAP_PROFILER_DESTINATION', 'HEAP_PROFILER_SNAPSHOT_DESTINATION', 'HEAP_PROFILER_PROFILE', 'HEAP_PROFILER_PROFILE_DESTINATION', 'HEAP_PROFILER_PROFILE_INTERVAL', 'HEAP_PROFILER_TIMELINE', 'HEAP_PROFILER_TIMELINE_DESTINATION', 'HEAP_PROFILER_TIMELINE_RUN_GC', 'HEAP_PROFILER_TIMELINE_DURATION' ]) { delete process.env[key] } logger.info.resetHistory() logger.error.resetHistory() } function startPreloader() { process.removeAllListeners('SIGUSR2') preloader(logger) } async function waitForReport(spy, calls, timeout) { const startTime = process.hrtime.bigint() while (spy.callCount < calls && Number(process.hrtime.bigint() - startTime) / 1e9 < timeout) { await sleep(100) } } t.beforeEach(done => { cleanEnvironment() startPreloader() done() }) t.test('it correctly generates reports when receiving USR2 and stop the second time', async t => { const snapshotDestination = await tmpName() const profileDestination = await tmpName() const timelineDestination = await tmpName() // Set preloader variables process.env.HEAP_PROFILER_SNAPSHOT_DESTINATION = snapshotDestination process.env.HEAP_PROFILER_SNAPSHOT_RUN_GC = 'true' process.env.HEAP_PROFILER_PROFILE_DESTINATION = profileDestination process.env.HEAP_PROFILER_PROFILE_INTERVAL = 32768 process.env.HEAP_PROFILER_TIMELINE_DESTINATION = timelineDestination process.env.HEAP_PROFILER_TIMELINE_RUN_GC = 'true' // Set the signal to start process.kill(process.pid, 'SIGUSR2') // Wait for generators time to finish await waitForReport(logger.info, 4, 15) t.equal(logger.info.callCount, 4) // Stop timeline and profiler process.kill(process.pid, 'SIGUSR2') await waitForReport(logger.info, 8, 15) t.equal(logger.info.callCount, 8) const generatedSnapshot = JSON.parse(readFileSync(snapshotDestination, 'utf-8')) const generatedProfile = JSON.parse(readFileSync(profileDestination, 'utf-8')) const generatedTimeline = JSON.parse(readFileSync(timelineDestination, 'utf-8')) validateSnapshot(generatedSnapshot) t.strictSame(validateSnapshot.errors, null) validateProfile(generatedProfile) t.strictSame(validateProfile.errors, null) validateSnapshot(generatedTimeline) t.strictSame(validateSnapshot.errors, null) try { unlinkSync(snapshotDestination) unlinkSync(profileDestination) unlinkSync(timelineDestination) } catch (e) { // No-op } }) t.test('it correctly exclude reports based on environment variables', async t => { const snapshotDestination = await tmpName() const profileDestination = await tmpName() const timelineDestination = await tmpName() // Set preloader variables process.env.HEAP_PROFILER_SNAPSHOT = 'false' process.env.HEAP_PROFILER_PROFILE = 'false' process.env.HEAP_PROFILER_TIMELINE = 'false' process.env.HEAP_PROFILER_SNAPSHOT_DESTINATION = snapshotDestination process.env.HEAP_PROFILER_PROFILE_DESTINATION = profileDestination process.env.HEAP_PROFILER_TIMELINE_DESTINATION = timelineDestination process.kill(process.pid, 'SIGUSR2') // Wait for generators time to finish await waitForReport(logger.info, 3, 10) // Check the reports were never invoked t.equal(logger.info.callCount, 3) t.throws(() => statSync(snapshotDestination), { message: `ENOENT: no such file or directory, stat '${snapshotDestination}'` }) t.throws(() => statSync(profileDestination), { message: `ENOENT: no such file or directory, stat '${profileDestination}'` }) t.throws(() => statSync(timelineDestination), { message: `ENOENT: no such file or directory, stat '${timelineDestination}'` }) }) t.test('it should run continuously', async t => { const snapshotDestination = await tmpName() const profileDestination = await tmpName() // Set preloader variables process.env.HEAP_PROFILER_SNAPSHOT_DESTINATION = snapshotDestination process.env.HEAP_PROFILER_PROFILE = 'false' process.env.HEAP_PROFILER_TIMELINE = 'false' // first run - snapshot only process.kill(process.pid, 'SIGUSR2') // Wait for generators time to finish await waitForReport(logger.info, 4, 10) t.equal(logger.info.callCount, 4) // Set preloader variables cleanEnvironment() process.env.HEAP_PROFILER_PROFILE_DESTINATION = profileDestination process.env.HEAP_PROFILER_SNAPSHOT = 'false' process.env.HEAP_PROFILER_TIMELINE = 'false' // second run - profile only process.kill(process.pid, 'SIGUSR2') await sleep(100) // stop the run process.kill(process.pid, 'SIGUSR2') // Wait for generators time to finish await waitForReport(logger.info, 4, 10) // Check the report was generated t.equal(logger.info.callCount, 4) try { unlinkSync(snapshotDestination) unlinkSync(profileDestination) } catch (e) { // No-op } }) t.test('it should run continuously even when all tools are disabled', async t => { // Set preloader variables process.env.HEAP_PROFILER_SNAPSHOT = 'false' process.env.HEAP_PROFILER_PROFILE = 'false' process.env.HEAP_PROFILER_TIMELINE = 'false' // first snapshot process.kill(process.pid, 'SIGUSR2') // Wait for generators time to finish await waitForReport(logger.info, 3, 10) t.equal(logger.info.callCount, 3) // second snapshot process.kill(process.pid, 'SIGUSR2') // Wait for generators time to finish await waitForReport(logger.info, 5, 10) // Check the report was generated t.equal(logger.info.callCount, 5) }) t.test('it correctly logs generation errors', async t => { // Set preloader variables process.env.HEAP_PROFILER_SNAPSHOT_DESTINATION = '/this/doesnt/exists-1' process.env.HEAP_PROFILER_PROFILE_DESTINATION = '/this/doesnt/exists-2' process.env.HEAP_PROFILER_TIMELINE_DESTINATION = '/this/doesnt/exists-3' process.env.HEAP_PROFILER_PROFILE_INTERVAL = 32768 process.kill(process.pid, 'SIGUSR2') // Wait for generators time to finish await waitForReport(logger.info, 5, 2) await waitForReport(logger.error, 3, 2) // Check the reports were not generated t.equal(logger.info.callCount, 5) t.equal(logger.error.callCount, 3) })