UNPKG

4.4 kBJavaScriptView Raw
1'use strict'
2
3const summary = require('summary')
4const tf = require('@tensorflow/tfjs-core')
5const HMM = require('hidden-markov-model-tf')
6const 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.
12tf.ENV.set('IS_NODE', false)
13
14// There is no truth here. This parameter might need more tuning.
15const SEPARATION_THRESHOLD = 1
16/* eslint-disable no-loss-of-precision */
17const HHM_SEED = 0xa74b9cbd4047b4bbe79f365a9f247886ac0a8a9c23ef8c5c45d98badb8
18/* eslint-enable no-loss-of-precision */
19function performanceIssue (issue) {
20 return issue ? 'performance' : 'none'
21}
22
23async 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
111module.exports = util.callbackify(analyseCPU)
112module.exports[util.promisify.custom] = analyseCPU