1 |
|
2 |
|
3 | const summary = require('summary')
|
4 | const distributions = require('distributions')
|
5 |
|
6 | function performanceIssue (issue) {
|
7 | return issue ? 'performance' : 'none'
|
8 | }
|
9 |
|
10 | function whiteNoiseSignTest (noise) {
|
11 | // Count the number of sign changes
|
12 | const numSignChanges = nonzero(diff(sign(noise))).length
|
13 |
|
14 | // Sign changes on a symmetric distribution will follow a binomial distribution
|
15 | // where the probability of sign change equals 0.5. Thus, perform a binomial
|
16 | // test with the null hypothesis `probability >= 0.5`.
|
17 | // Note that we don't use a two-sided test because:
|
18 | // 1. It is apparently expensive and complicated to implement.
|
19 | // See: https://github.com/scipy/scipy/blob/master/scipy/stats/morestats.py
|
20 | // look for `binom_test`
|
21 | // 2. We currently have don't understand of what a consistent sign change
|
22 | // indicates. Thus we will treat `probability > 0.5` as being fine.
|
23 | const binomial = new distributions.Binomial(0.5, noise.length)
|
24 | const pvalue = binomial.cdf(numSignChanges)
|
25 |
|
26 | return pvalue
|
27 | }
|
28 |
|
29 | // Implementation of the mira skrewness tests
|
30 | // Paper:
|
31 | // Title: Distribution-free test for symmetry based on Bonferroni's Measure
|
32 | // Link: https://www.researchgate.net/publication/2783935
|
33 | function miraSkewnessTest (handleStat, handles) {
|
34 | const c = 0.5 // Distribution dependent constant, 0.5 is the best value for unknown distribution
|
35 | const n = handleStat.size()
|
36 | const median = handleStat.median()
|
37 |
|
38 | // Compute the bonferroni measure
|
39 | const bonferroni = 2 * (handleStat.mean() - median)
|
40 |
|
41 | // Compute the variance for the sqrt(n) * bonferroni measure
|
42 | const medianAbsoluteDeviation = summary(handles.map((x) => Math.abs(x - median))).mean()
|
43 | const densityInverse = (Math.pow(n, 1 / 5) / (2 * c)) * (
|
44 | handleStat.quartile(1 / 2 + 0.5 * Math.pow(n, -1 / 5)) -
|
45 | handleStat.quartile(1 / 2 - 0.5 * Math.pow(n, -1 / 5) + 1 / n)
|
46 | )
|
47 | const bonferroniVariance = 4 * handleStat.variance() +
|
48 | densityInverse * densityInverse -
|
49 | 4 * densityInverse * medianAbsoluteDeviation
|
50 |
|
51 | // Compute Z statistics
|
52 | const Z = Math.sqrt(n) * bonferroni / Math.sqrt(bonferroniVariance)
|
53 |
|
54 | // Compute two-sided p-value
|
55 | const normal = new distributions.Normal()
|
56 | const pvalue = 2 * (1 - normal.cdf(Math.abs(Z)))
|
57 |
|
58 | return pvalue
|
59 | }
|
60 |
|
61 | function analyseHandles (systemInfo, processStatSubset, traceEventSubset) {
|
62 | // Healthy handle graphs tends to grow or shrink in small steps.
|
63 | //
|
64 | // handles
|
65 | // | ^ ^ /^^
|
66 | // | / \ / \ /\ / \
|
67 | // | / \^/ \/ \
|
68 | // ------------------------ time
|
69 | //
|
70 | // Unhealthy handles graphs increase and makes large drop (sawtooth pattern)
|
71 | //
|
72 | // handles
|
73 | // | ^^^ /^^^ /^^^
|
74 | // | / | / | /
|
75 | // | / |^^ |^^^
|
76 | // ------------------------ time
|
77 | //
|
78 | // A heuristic statistical description of such a behaviour, is that
|
79 | // healthy handle graphs are essentially random walks on a symmetric
|
80 | // distribution.
|
81 | // To test this, perform a sign change test on the differential.
|
82 | const handles = processStatSubset.map((d) => d.handles)
|
83 | const handleStat = summary(handles)
|
84 | if (handles.length < 2) {
|
85 | return 'data'
|
86 | }
|
87 |
|
88 | // Check for stochasticity, otherwise the following is mathematical nonsense.
|
89 | if (handleStat.sd() === 0) {
|
90 | return 'none'
|
91 | }
|
92 |
|
93 | // 1. Assuming handles is a random-walk process, then calculate the first
|
94 | // order auto-diffrential to get the white-noise.
|
95 | // 2. Reduce the dataset to those values where the value did change, this
|
96 | // is to avoid over-sampling and ignore constant-periods.
|
97 | // NOTE: since sd() !== 0, noise is guaranteed to have some data
|
98 | const noise = nonzero(diff(handles))
|
99 |
|
100 | // Perform a sign test on the assumed-noise.
|
101 | // This is to test the sawtooth pattern.
|
102 | const signAlpha = 1e-7 // risk acceptance
|
103 | const signPvalue = whiteNoiseSignTest(noise)
|
104 | const signIssueDetected = signPvalue < signAlpha
|
105 |
|
106 | // Perform a two-sided mira-skewness-test.
|
107 | // This is to test an increasing function, that eventually flattens out
|
108 | const miraAlpha = 1e-10 // risk acceptance
|
109 | const miraPvalue = miraSkewnessTest(handleStat, handles)
|
110 | const miraIssueDetected = miraPvalue < miraAlpha
|
111 |
|
112 | return performanceIssue(signIssueDetected || miraIssueDetected)
|
113 | }
|
114 |
|
115 | module.exports = analyseHandles
|
116 |
|
117 | function diff (vec) {
|
118 | const changes = []
|
119 | let last = vec[0]
|
120 | for (let i = 1; i < vec.length; i++) {
|
121 | changes.push(vec[i] - last)
|
122 | last = vec[i]
|
123 | }
|
124 | return changes
|
125 | }
|
126 |
|
127 | function sign (vec) {
|
128 | return vec.map(Math.sign)
|
129 | }
|
130 |
|
131 | function nonzero (vec) {
|
132 | return vec.filter((v) => v !== 0)
|
133 | }
|