1 |
|
2 |
|
3 | const summary = require('summary')
|
4 | const distributions = require('distributions')
|
5 |
|
6 | function analyseHandles (processStatSubset, traceEventSubset) {
|
7 | // Healthy handle graphs tends to grow or shrink in small steps.
|
8 | //
|
9 | // handles
|
10 | // | ^ ^ /^^
|
11 | // | / \ / \ /\ / \
|
12 | // | / \^/ \/ \
|
13 | // ------------------------ time
|
14 | //
|
15 | // Unhealthy handles graphs increase and makes large drop
|
16 | //
|
17 | // handles
|
18 | // | ^^^ /^^^ /^^^
|
19 | // | / | / | /
|
20 | // | / |^^ |^^^
|
21 | // ------------------------ time
|
22 | //
|
23 | // A heuristic statistical description of such a behaviour, is that
|
24 | // healthy handle graphs are essentially random walks on a symmetric
|
25 | // distribution.
|
26 | // To test this, perform a sign change test on the differential.
|
27 | const handles = processStatSubset.map((d) => d.handles)
|
28 |
|
29 | // Check for stochasticity, otherwise the following is mathematical nonsense.
|
30 | if (summary(handles).sd() === 0) {
|
31 | return false
|
32 | }
|
33 |
|
34 | // Calculate the changes in handles (differential)
|
35 | const changes = diff(handles)
|
36 | // Determine if the change was an increase or a decrease
|
37 | const direction = changes.map(Math.sign)
|
38 | const notConstant = direction.map((v) => v !== 0)
|
39 | // Count the number of sign changes
|
40 | const signChanges = diff(direction).map((v) => v !== 0)
|
41 | const numSignChanges = summary(signChanges).sum()
|
42 |
|
43 | // In cases where the number of handles is somewhat constant, it looks
|
44 | // like there are unusually few sign changes. But this is actually fine if
|
45 | // the server doesn't do anything asynchronously at all. To not see this
|
46 | // as a handle issue, compare not the number of sign changes with
|
47 | // `processStatSubset.length` but instead with the number of observations
|
48 | // where changes were observed.
|
49 | // There can be an off-by-one error were there are more sign changes than
|
50 | // non constant observations. Simply round the number of non constant
|
51 | // observations up to fit.
|
52 | const numNotConstant = Math.max(numSignChanges, summary(notConstant).sum())
|
53 |
|
54 | // Sign changes on a symmetric distribution will follow a binomial distribution
|
55 | // where the probability of sign change equals 0.5. Thus, perform a binomial
|
56 | // test with the null hypothesis `probability >= 0.5`.
|
57 | // Note that we don't use a two-sided test because:
|
58 | // 1. It is apparently expensive and complicated to implement.
|
59 | // See: https://github.com/scipy/scipy/blob/master/scipy/stats/morestats.py
|
60 | // look for `binom_test`
|
61 | // 2. We currently have don't understand of what a consistent sign change
|
62 | // indicates. Thus we will treat `probability > 0.5` as being fine.
|
63 | const binomial = new distributions.Binomial(0.5, numNotConstant)
|
64 | const pvalue = binomial.cdf(numSignChanges)
|
65 |
|
66 | // If is is very unlikely that the sign change probability is greater than
|
67 | // 0.5, then we likely have an issue with the handles.
|
68 | const alpha = 0.001 // risk acceptance
|
69 | return pvalue < alpha
|
70 | }
|
71 |
|
72 | module.exports = analyseHandles
|
73 |
|
74 | function diff (handles) {
|
75 | const changes = []
|
76 | let lastHandles = handles[0]
|
77 | for (let i = 1; i < handles.length; i++) {
|
78 | changes.push(handles[i] - lastHandles)
|
79 | lastHandles = handles[i]
|
80 | }
|
81 | return changes
|
82 | }
|