1 |
|
2 |
|
3 | const summary = require('summary')
|
4 | const tf = require('@tensorflow/tfjs-core')
|
5 | const HMM = require('hidden-markov-model-tf')
|
6 | const util = require('util')
|
7 |
|
8 | // Disable warning about that `require('@tensorflow/tfjs-node')` is
|
9 | // recommended. There is no/minimal performance penalty and we avoid a
|
10 | // native addon.
|
11 | // NOTE: This is not a documented API.
|
12 | tf.ENV.set('IS_NODE', false)
|
13 |
|
14 | // There is no truth here. This parameter might need more tuning.
|
15 | const SEPARATION_THRESHOLD = 1
|
16 | /* eslint-disable no-loss-of-precision */
|
17 | const HHM_SEED = 0xa74b9cbd4047b4bbe79f365a9f247886ac0a8a9c23ef8c5c45d98badb8
|
18 | /* eslint-enable no-loss-of-precision */
|
19 | function performanceIssue (issue) {
|
20 | return issue ? 'performance' : 'none'
|
21 | }
|
22 |
|
23 | async function analyseCPU (systemInfo, processStatSubset, traceEventSubset) {
|
24 | const cpu = processStatSubset.map((d) => d.cpu)
|
25 | const summaryAll = summary(cpu)
|
26 |
|
27 | // For extremely small data, this algorithm doesn't work
|
28 | if (cpu.length < 4) {
|
29 | return 'data'
|
30 | }
|
31 |
|
32 | // The CPU graph is typically composed of two "modes". An application mode
|
33 | // and a V8 mode. In the V8 mode, extra CPU threads are running garbage
|
34 | // collection and optimization. This causes the CPU usage for the
|
35 | // entire process, to be higher in these periods. For the analysing the
|
36 | // users application for I/O issues, the CPU usage data during the V8
|
37 | // mode is not of interest.
|
38 | //
|
39 | // | .--. ..- -.-
|
40 | // cpu | .- .. . . . - . . .
|
41 | // | . -. - . . - . - .. - ..
|
42 | // +----------------------------------------
|
43 | // app v8 app v8 app v8 app
|
44 | // Unfortunately, it is quite difficult to separate out the V8 data, even
|
45 | // with the traceEvent data from V8.
|
46 | // NOTE(@AndreasMadsen): I don't entirely know why.
|
47 | //
|
48 | // Instead, the V8 mode data will be removed using a statistical approach.
|
49 | // The statistical approach is "Gausian Mixture Model" (GMM), a better model
|
50 | // would be a "Hidden Markov Model" (HMM). However, this model is a bit more
|
51 | // complex and there doesn't exists an implementation of HMM where the data
|
52 | // is continues. HMM is better because it understands that the data is a
|
53 | // time series, which GMM doesn't. There is a comparison in the docs.
|
54 | //
|
55 | const hmm = new HMM({
|
56 | states: 2,
|
57 | dimensions: 1
|
58 | })
|
59 | const data = tf.tidy(() => tf.reshape(tf.tensor1d(cpu), [1, cpu.length, 1]))
|
60 |
|
61 | // Attempt to reach 0.001, but accept 0.01
|
62 | const results = await hmm.fit(data, {
|
63 | tolerance: 0.001,
|
64 | seed: HHM_SEED
|
65 | })
|
66 |
|
67 | /* istanbul ignore if: it is not clear what causes HMM to not converge */
|
68 | if (results.tolerance >= 0.01) {
|
69 | return 'data' // has data issue
|
70 | }
|
71 |
|
72 | // Split data depending on the likelihood
|
73 | const group = [[], []]
|
74 | const state = await tf.tidy(() => tf.squeeze(hmm.inference(data))).data()
|
75 | for (let i = 0; i < cpu.length; i++) {
|
76 | group[state[i]].push(cpu[i])
|
77 | }
|
78 | const summary0 = summary(group[0])
|
79 | const summary1 = summary(group[1])
|
80 |
|
81 | // If one group is too small for a summary to be computed, just threat the
|
82 | // data as ungrouped.
|
83 | if (summary0.size() <= 1 || summary1.size() <= 1) {
|
84 | return performanceIssue(summaryAll.quartile(0.9) < 0.9)
|
85 | }
|
86 |
|
87 | // It is not always that there are two "modes". Determine if the groups are
|
88 | // separate by the separation coefficient.
|
89 | // https://en.wikipedia.org/wiki/Multimodal_distribution#Bimodal_separation
|
90 | const commonSd = 2 * (summary0.sd() + summary1.sd())
|
91 | const separation = (summary0.mean() - summary1.mean()) / commonSd
|
92 | // Threat the data as one "mode", if the separation coefficient is too small.
|
93 | if (Math.abs(separation) < SEPARATION_THRESHOLD) {
|
94 | return performanceIssue(summaryAll.quartile(0.9) < 0.9)
|
95 | }
|
96 |
|
97 | // The mode group with the highest mean is the V8 mode, the other is the
|
98 | // application mode. This is because V8 is multi-threaded, but javascript is
|
99 | // single-threaded.
|
100 | const summaryApplication = (
|
101 | summary0.mean() < summary1.mean() ? summary0 : summary1
|
102 | )
|
103 |
|
104 | // If the 90% quartile has less than 90% CPU load then the CPU is not
|
105 | // utilized optimally, likely because of some I/O delays. Highlight the
|
106 | // CPU curve in that case.
|
107 | return performanceIssue(summaryApplication.quartile(0.9) < 0.9)
|
108 | }
|
109 |
|
110 | // Wrap to be a callback function
|
111 | module.exports = util.callbackify(analyseCPU)
|
112 | module.exports[util.promisify.custom] = analyseCPU
|