diff --git a/lib/constants.js b/lib/constants.js new file mode 100644 index 00000000..b45db6fa --- /dev/null +++ b/lib/constants.js @@ -0,0 +1,5 @@ +export const JobType = { + CPU_PROFILE: 'cpuProfile', + HEAP_SNAPSHOT: 'heapSnapshot', + ALLOCATION_PROFILE: 'allocationProfile' +}; diff --git a/lib/jobs.js b/lib/jobs.js index a7e93606..9396799d 100644 --- a/lib/jobs.js +++ b/lib/jobs.js @@ -1,3 +1,5 @@ +import { JobType } from './constants'; + let Jobs = Kadira.Jobs = {}; Jobs.getAsync = function (id, callback) { @@ -21,5 +23,79 @@ Jobs.setAsync = function (id, changes, callback) { }); }; -Jobs.set = Kadira._wrapAsync(Jobs.setAsync); -Jobs.get = Kadira._wrapAsync(Jobs.getAsync); +Jobs.set = Meteor.wrapAsync(Jobs.setAsync); +Jobs.get = Meteor.wrapAsync(Jobs.getAsync); + + +let queuePromise = Promise.resolve(); + +export function queueJob (job) { + queuePromise = queuePromise.finally(() => runJob(job)); + + return queuePromise; +} + +async function runJob (job) { + if (!job || !job._id) { + // eslint-disable-next-line no-console + console.log(`Monti APM: Invalid job: ${JSON.stringify(job)}`); + return; + } + + // eslint-disable-next-line no-console + console.log('Monti APM: Starting job', job.type, job.id); + + try { + if (!job.type) { + throw new Error('Invalid job: missing type'); + } + + let runner = JobRunners[job.type]; + + if (!runner) { + throw new Error(`Unrecognized job type: ${job.type}. You might need to update montiapm:agent`); + } + + await runner(job); + + // eslint-disable-next-line no-console + console.log('Monti APM: Finished job', job.type, job._id); + } catch (error) { + // eslint-disable-next-line no-console + console.log(`Monti APM: Error while running job: ${error}`); + Jobs.set(job._id, { state: 'errored', data: { errorMessage: error.message || 'Unknown error' } }); + } +} + +const JobRunners = { + [JobType.CPU_PROFILE] (job) { + const ProfilerPackage = Package['montiapm:profiler']; + + if (!ProfilerPackage) { + throw new Error('Please install the montiapm:profiler package'); + } + + const MontiProfiler = ProfilerPackage.MontiProfiler; + + if (!MontiProfiler || !MontiProfiler._remoteCpuProfile) { + throw new Error('Please update the montiapm:profiler package'); + } + + const {_id, data} = job; + + return MontiProfiler._remoteCpuProfile(data.duration, _id); + }, + async [JobType.HEAP_SNAPSHOT] (job) { + const ProfilerPackage = Package['montiapm:profiler']; + + if (!ProfilerPackage) { + throw new Error('Please install the montiapm:profiler package'); + } + + if (!ProfilerPackage.MontiProfiler._remoteHeapSnapshot) { + throw new Error('Please update montiapm:profiler package'); + } + + await ProfilerPackage.MontiProfiler._remoteHeapSnapshot(job._id); + } +}; diff --git a/lib/kadira.js b/lib/kadira.js index 492c4d70..afebea93 100644 --- a/lib/kadira.js +++ b/lib/kadira.js @@ -17,14 +17,17 @@ import { Ntp } from './ntp'; import { getClientVersions } from './utils'; import { handleApiResponse } from './sourcemaps'; import { TrackMeteorDebug, TrackUncaughtExceptions, TrackUnhandledRejections } from './hijack/error'; +import { queueJob } from './jobs'; const hostname = Npm.require('os').hostname(); const logger = Npm.require('debug')('kadira:apm'); const Fibers = Npm.require('fibers'); -const KadiraCore = Npm.require('monti-apm-core').Kadira; +const { CoreEvent, Kadira: KadiraCore, Feature } = Npm.require('monti-apm-core'); const DEBUG_PAYLOAD_SIZE = process.env.MONTI_DEBUG_PAYLOAD_SIZE === 'true'; +const WEBSOCKETS_SUPPORTED = parseInt(process.versions.node.split('.')[0], 10) > 4; + Kadira.models = {}; Kadira.options = {}; Kadira.env = { @@ -116,6 +119,20 @@ Kadira.connect = function (appId, appSecret, options) { console.log('Monti APM: Connected'); Kadira._sendAppStats(); Kadira._schedulePayloadSend(); + + if ( + WEBSOCKETS_SUPPORTED && + Kadira.coreApi.featureSupported(Feature.WEBSOCKETS) + ) { + logger('websockets supported'); + Kadira.coreApi._initWebSocket(); + + Kadira.coreApi.on(CoreEvent.JOB_CREATED, Meteor.bindEnvironment(job => { + queueJob(job); + })); + } else { + logger('websockets not supported'); + } }) .catch(function (err) { if (err.message === 'Unauthorized') { diff --git a/package.js b/package.js index 0d628779..94dad69a 100644 --- a/package.js +++ b/package.js @@ -2,14 +2,14 @@ Package.describe({ summary: 'Performance Monitoring for Meteor', - version: '2.49.3', + version: '2.50.0-beta.2', git: 'https://github.com/monti-apm/monti-apm-agent.git', name: 'montiapm:agent' }); let npmModules = { debug: '0.8.1', - 'monti-apm-core': '1.7.5', + 'monti-apm-core': '1.8.0-beta.5', 'lru-cache': '5.1.1', 'json-stringify-safe': '5.0.1', 'monti-apm-sketches-js': '0.0.3', @@ -137,7 +137,7 @@ function configurePackage (api, isTesting) { api.use('meteorhacks:zones@1.2.1', { weak: true }); api.use('simple:json-routes@2.1.0', { weak: true }); api.use('zodern:meteor-package-versions@0.2.0'); - api.use('zodern:types@1.0.11'); + api.use('zodern:types@1.0.13'); api.use([ 'minimongo', 'mongo', 'ddp', 'ejson', 'ddp-common',