Skip to content

Commit 96bbf8b

Browse files
committed
rework
1 parent 1b9c57f commit 96bbf8b

20 files changed

+650
-1838
lines changed

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## Overview
44

5-
StackImpact is a performance profiler for production applications. It gives developers continuous and historical view of application performance with line-of-code precision, which includes CPU, memory allocation and async call hot spots as well as execution bottlenecks, errors and runtime metrics. Learn more at [stackimpact.com](https://stackimpact.com/).
5+
StackImpact is a production-grade performance profiler built for both production and development environments. It gives developers continuous and historical view of application performance with line-of-code precision that is essential for locating CPU, memory allocation and async call hot spots as well as latency bottlenecks. Included runtime metric and error monitoring complement profiles for extensive performance analysis. Learn more at [stackimpact.com](https://stackimpact.com/).
66

77
![dashboard](https://stackimpact.com/wp-content/uploads/2017/09/hotspots-cpu-1.4-nodejs.png)
88

@@ -28,8 +28,8 @@ See full [documentation](https://stackimpact.com/docs/) for reference.
2828
## Supported environment
2929

3030
* Linux, OS X or Windows. Node.js v4.0.0 or higher.
31-
* **CPU profiler is disabled by default for Node.js v7.0.0 and higher due to memory leak in underlying V8s CPU profiler. To enable, add `cpuProfilerDisabled: false` to startup options.**
32-
* Allocation profiler supports Node.js v6.0.0 and higher. **The allocation profiler is disabled by default, since V8s heap sampling is still experimental and is seen to result in segmentation faults. To enable, add `allocationProfilerDisabled: false` to startup options.**
31+
* CPU profiler is disabled by default for Node.js v7.0.0 to v8.9.3 due to memory leak in underlying V8's CPU profiler. To enable, add `cpuProfilerDisabled: false` to startup options.
32+
* Allocation profiler supports Node.js v6.0.0 and higher. The allocation profiler is disabled by default, since V8's heap sampling is still experimental and is seen to result in segmentation faults. To enable, add `allocationProfilerDisabled: false` to startup options.
3333
* Async profiler supports Node.js v8.1.0 and higher.
3434

3535

@@ -80,7 +80,7 @@ All initialization options:
8080
* `includeAgentFrames` (Optional) Set to `true` to not exclude agent stack frames from profiles.
8181

8282

83-
#### Workload profiling
83+
#### Programmatic profiling
8484
*Optional*
8585

8686
Use `agent.profile()` to instruct the agent when to start and stop profiling. The agent decides if and which profiler is activated. Normally, this method should be used in repeating code, such as request or event handlers. Usage example:

examples/app.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,19 @@ function simulateHttp() {
9797
}
9898

9999

100+
function simulateProgrammaticProfiling() {
101+
setInterval(() => {
102+
let span = agent.profile();
103+
104+
for(let i = 0; i < usage * 300000; i++) {
105+
Math.random();
106+
}
107+
108+
span.stop();
109+
}, 1000);
110+
}
111+
112+
100113
function simulateExceptions() {
101114
setInterval(() => {
102115
if(Math.random() > 0.2) {
@@ -137,6 +150,7 @@ server.listen(5005, '127.0.0.1', () => {
137150
simulateCpu();
138151
simulateMemLeak();
139152
simulateHttp();
153+
simulateProgrammaticProfiling();
140154
simulateExceptions();
141155
simulateRejections();
142156
});

lib/agent.js

Lines changed: 47 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,18 @@ const Config = require('./config').Config;
88
const ConfigLoader = require('./config_loader').ConfigLoader;
99
const MessageQueue = require('./message_queue').MessageQueue;
1010
const ProcessReporter = require('./reporters/process_reporter').ProcessReporter;
11-
const CPUReporter = require('./reporters/cpu_reporter').CpuReporter;
12-
const AllocationReporter = require('./reporters/allocation_reporter').AllocationReporter;
13-
const AsyncReporter = require('./reporters/async_reporter').AsyncReporter;
11+
const ProfileReporter = require('./reporters/profile_reporter').ProfileReporter;
12+
const CpuProfiler = require('./profilers/cpu_profiler').CpuProfiler;
13+
const AllocationProfiler = require('./profilers/allocation_profiler').AllocationProfiler;
14+
const AsyncProfiler = require('./profilers/async_profiler').AsyncProfiler;
1415
const ErrorReporter = require('./reporters/error_reporter').ErrorReporter;
1516

1617
class Agent {
1718

1819
constructor() {
1920
let self = this;
2021

22+
self.AGENT_FRAME_REGEXP = /node_modules\/stackimpact\//;
2123
self.SAAS_DASHBOARD_ADDRESS = 'https://agent-api.stackimpact.com';
2224

2325
self.version = pkg.version;
@@ -36,15 +38,36 @@ class Agent {
3638
self.configLoader = new ConfigLoader(self);
3739
self.messageQueue = new MessageQueue(self);
3840
self.processReporter = new ProcessReporter(self);
39-
self.cpuReporter = new CPUReporter(self);
40-
self.allocationReporter = new AllocationReporter(self);
41-
self.asyncReporter = new AsyncReporter(self);
41+
self.cpuReporter = new ProfileReporter(self, new CpuProfiler(self), {
42+
logPrefix: "CPU profiler",
43+
maxProfileDuration: 10 * 1000,
44+
maxSpanDuration: 2 * 1000,
45+
maxSpanCount: 30,
46+
spanInterval: 16 * 1000,
47+
reportInterval: 120 * 1000,
48+
});
49+
self.allocationReporter = new ProfileReporter(self, new AllocationProfiler(self), {
50+
logPrefix: "Allocation profiler",
51+
maxProfileDuration: 20 * 1000,
52+
maxSpanDuration: 4 * 1000,
53+
maxSpanCount: 30,
54+
spanInterval: 16 * 1000,
55+
reportInterval: 120 * 1000
56+
});
57+
self.asyncReporter = new ProfileReporter(self, new AsyncProfiler(self), {
58+
logPrefix: "Async profiler",
59+
maxProfileDuration: 20 * 1000,
60+
maxSpanDuration: 4 * 1000,
61+
maxSpanCount: 30,
62+
spanInterval: 16 * 1000,
63+
reportInterval: 120 * 1000
64+
});
4265
self.errorReporter = new ErrorReporter(self);
4366

4467
self.options = undefined;
4568

4669
self.isProfiling = false;
47-
self.profilerLock = false;
70+
self.profilerActive = false;
4871

4972
self.exitHandlerFunc = undefined;
5073
}
@@ -98,18 +121,21 @@ class Agent {
98121
throw new Error('Supported Node.js version 4.0.0 or higher');
99122
}
100123

101-
// disable CPU profiler by default starting 7.0 until the memory leak is fixed.
102124
if (opts.autoProfiling === undefined) {
103125
opts.autoProfiling = true;
104126
}
105127

106-
// disable CPU profiler by default starting 7.0 until the memory leak is fixed.
107-
if (opts.cpuProfilerDisabled === undefined && self.minVersion(7, 0, 0)) {
128+
// disable CPU profiler by default for 7.0.0-8.9.3 because of the memory leak.
129+
if (opts.cpuProfilerDisabled === undefined &&
130+
((self.minVersion(7, 0, 0) && self.maxVersion(8, 9, 3)) ||
131+
(self.minVersion(9, 0, 0) && self.maxVersion(9, 2, 1)))) {
132+
self.log('CPU profiler disabled.');
108133
opts.cpuProfilerDisabled = true;
109134
}
110135

111-
// disable allocation profiler by default until it's the V8 API's are stable.
136+
// disable allocation profiler by default until it's the V8 API's are stable, no segfaults.
112137
if (opts.allocationProfilerDisabled === undefined) {
138+
self.log('Allocation profiler disabled.');
113139
opts.allocationProfilerDisabled = true;
114140
}
115141

@@ -137,11 +163,11 @@ class Agent {
137163
self.options['dashboardAddress'] = self.SAAS_DASHBOARD_ADDRESS;
138164
}
139165

140-
if (!self.options['agentKey'] && !self.options['standalone']) {
166+
if (!self.options['agentKey']) {
141167
throw new Error('missing option: agentKey');
142168
}
143169

144-
if (!self.options['appName'] && !self.options['standalone']) {
170+
if (!self.options['appName']) {
145171
throw new Error('missing option: appName');
146172
}
147173

@@ -249,7 +275,7 @@ class Agent {
249275

250276
self.isProfiling = true;
251277

252-
let rec = null;
278+
let span = null;
253279
if(self.config.isAgentEnabled() && !self.config.isProfilingDisabled()) {
254280
let reporters = [];
255281
if (self.cpuReporter.started) {
@@ -263,14 +289,14 @@ class Agent {
263289
}
264290

265291
if(reporters.length > 0) {
266-
rec = reporters[Math.floor(Math.random() * reporters.length)].record(true);
292+
span = reporters[Math.floor(Math.random() * reporters.length)].profile(true);
267293
}
268294
}
269295

270296
return {
271297
stop: function() {
272-
if (rec) {
273-
rec.stop();
298+
if (span) {
299+
span.stop();
274300
}
275301

276302
self.isProfiling = false;
@@ -328,31 +354,15 @@ class Agent {
328354
}
329355

330356

331-
readMetrics() {
332-
let self = this;
333-
334-
if (!self.getOption('standalone')) {
335-
return;
336-
}
337-
338-
let metrics = self.messageQueue.queue.filter((message) => {
339-
return message.topic === 'metric';
340-
});
341-
self.messageQueue.queue = [];
342-
343-
metrics = metrics.map((metric) => {
344-
return metric.content;
345-
});
346-
347-
return metrics;
348-
}
349-
350-
351357
minVersion(major, minor, patch) {
352358
let m = process.version.match(/v?(\d+)\.(\d+)\.(\d+)/);
353359
return parseInt(m[1]) >= major && parseInt(m[2]) >= minor && parseInt(m[3]) >= patch;
354360
}
355361

362+
maxVersion(major, minor, patch) {
363+
let m = process.version.match(/v?(\d+)\.(\d+)\.(\d+)/);
364+
return parseInt(m[1]) <= major && parseInt(m[2]) <= minor && parseInt(m[3]) <= patch;
365+
}
356366

357367
logPrefix() {
358368
let self = this;

lib/config_loader.js

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,6 @@ class ConfigLoader {
6060
load(callback) {
6161
let self = this;
6262

63-
if (self.agent.getOption('standalone')) {
64-
return callback(null);
65-
}
66-
6763
let now = Date.now();
6864
if (!self.agent.getOption('autoProfiling') && self.lastLoadTs > now - self.LOAD_INTERVAL) {
6965
return callback(null);
@@ -82,7 +78,6 @@ class ConfigLoader {
8278
self.agent.config.setAgentEnabled(false);
8379
}
8480

85-
8681
if (config['profiling_disabled'] === 'yes') {
8782
self.agent.config.setProfilingDisabled(config['profiling_disabled'] == 'yes');
8883
}

lib/message_queue.js

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,11 +93,6 @@ class MessageQueue {
9393
return m.added_at >= now - self.MESSAGE_TTL;
9494
});
9595

96-
// no upload in standalone mode
97-
if (self.agent.getOption('standalone')) {
98-
return callback(null);
99-
}
100-
10196
// read queue
10297
let outgoing = self.queue;
10398
self.queue = [];
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
'use strict';
2+
3+
const os = require('os');
4+
const Metric = require('../metric').Metric;
5+
const Breakdown = require('../metric').Breakdown;
6+
7+
8+
class AllocationProfiler {
9+
10+
constructor(agent) {
11+
let self = this;
12+
13+
self.agent = agent;
14+
self.started = false;
15+
self.profile = undefined;
16+
}
17+
18+
19+
test() {
20+
let self = this;
21+
22+
if (self.agent.getOption('allocationProfilerDisabled')) {
23+
return false;
24+
}
25+
26+
if (!self.agent.addon.checkAllocationSampler()) {
27+
return false;
28+
}
29+
30+
return true;
31+
}
32+
33+
34+
reset() {
35+
let self = this;
36+
37+
self.profile = new Breakdown(self.agent, 'root');
38+
}
39+
40+
41+
startProfiler() {
42+
let self = this;
43+
44+
self.agent.addon.startAllocationSampler();
45+
}
46+
47+
48+
stopProfiler() {
49+
let self = this;
50+
51+
let allocationProfileRoot = self.agent.addon.readAllocationProfile();
52+
self.agent.addon.stopAllocationSampler();
53+
if (allocationProfileRoot) {
54+
let includeAgentFrames = self.agent.getOption('includeAgentFrames');
55+
self.updateProfile(self.profile, allocationProfileRoot.children, includeAgentFrames);
56+
}
57+
}
58+
59+
60+
updateProfile(parent, nodes, includeAgentFrames) {
61+
let self = this;
62+
63+
nodes.forEach((node) => {
64+
// exclude/include agent frames
65+
if (node.file_name &&
66+
!includeAgentFrames &&
67+
self.agent.AGENT_FRAME_REGEXP.exec(node.file_name)) {
68+
return;
69+
}
70+
71+
let frame;
72+
if (!node.func_name && !node.file_name) {
73+
frame = 'unknown';
74+
}
75+
else if (!node.file_name) {
76+
frame = node.func_name;
77+
}
78+
else {
79+
frame = `${node.func_name} (${node.file_name}:${node.line_num}:${node.col_num})`;
80+
}
81+
82+
let child = parent.findOrAddChild(frame);
83+
84+
child.measurement += node.size;
85+
child.numSamples += node.count;
86+
87+
self.updateProfile(child, node.children, includeAgentFrames);
88+
});
89+
}
90+
91+
92+
buildProfile(duration) {
93+
let self = this;
94+
95+
self.profile.normalize(duration / 1000);
96+
self.profile.propagate();
97+
self.profile.floor();
98+
self.profile.filter(2, 1000, +Infinity);
99+
100+
return [{
101+
category: Metric.c.CATEGORY_MEMORY_PROFILE,
102+
name: Metric.c.NAME_HEAP_ALLOCATION_RATE,
103+
unit: Metric.c.UNIT_BYTE,
104+
unitInterval: 1,
105+
profile: self.profile
106+
}];
107+
}
108+
}
109+
110+
exports.AllocationProfiler = AllocationProfiler;

0 commit comments

Comments
 (0)