@xenon-device-management/xenon 2.9.2 → 2.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/package.json +1 -1
- package/lib/public/assets/{AnnotationOverlay-B87n4pUu.js → AnnotationOverlay-defSfi9B.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-CNZJH6Cs.js → ApiKeyGate-DS7dJ2bU.js} +1 -1
- package/lib/public/assets/{BugReportButton-DQ-R87ZV.js → BugReportButton-CeD8SUnQ.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-dX3e7O-w.js → DeviceMosaicView-Dgue7t7s.js} +2 -2
- package/lib/public/assets/{EmptyState-D3fG2GHY.js → EmptyState-Bo-vvxiy.js} +1 -1
- package/lib/public/assets/{FieldGroup-CUmbJZxI.js → FieldGroup-DdhTNAXZ.js} +1 -1
- package/lib/public/assets/{FilterMenu-Dm2c-UCk.js → FilterMenu-J3F_3fDw.js} +1 -1
- package/lib/public/assets/{Menu-Dl4uXMkH.js → Menu-Bxluaw2m.js} +1 -1
- package/lib/public/assets/{Modal-DNmn2RDs.js → Modal-qgMVFrqV.js} +1 -1
- package/lib/public/assets/{RecordingPage-Dn3By34o.js → RecordingPage-BUT7WgJB.js} +1 -1
- package/lib/public/assets/{RecordingsPage-lCUIM4pm.js → RecordingsPage-CY0OIgCo.js} +1 -1
- package/lib/public/assets/{SegmentedControl-BdA_mHiZ.js → SegmentedControl-Cq-fybZ2.js} +1 -1
- package/lib/public/assets/{SettingCard-Dx3WhK7w.js → SettingCard-CASWJr7h.js} +1 -1
- package/lib/public/assets/{Table-D_2lhvRM.js → Table-BSO_dZzu.js} +1 -1
- package/lib/public/assets/{activity-cNHHOKx5.js → activity-B_-MNwwR.js} +1 -1
- package/lib/public/assets/{ai-settings-t6RMrDkg.js → ai-settings-DFhemyo8.js} +1 -1
- package/lib/public/assets/{api-keys-CK_qyQvW.js → api-keys-DV1oB8Tf.js} +1 -1
- package/lib/public/assets/{apps-DCiI3UOb.js → apps-B5f-VuLN.js} +1 -1
- package/lib/public/assets/{arrow-left-CnYs8HVw.js → arrow-left-D_xIcVqg.js} +1 -1
- package/lib/public/assets/{arrow-right-Cc9qm6UG.js → arrow-right-CLGlL_Un.js} +1 -1
- package/lib/public/assets/{arrow-up-right-e7yBOb-c.js → arrow-up-right-CuIgWFj2.js} +1 -1
- package/lib/public/assets/{auth-shell-DKrtcIjG.js → auth-shell-SgAkrFQg.js} +2 -2
- package/lib/public/assets/{builds-page-CRd5oEz6.js → builds-page-h9ZJ8n74.js} +4 -4
- package/lib/public/assets/{button-C0caCjJB.js → button-DKl94fzV.js} +1 -1
- package/lib/public/assets/{calendar-BqOOkln_.js → calendar-BNLTjBsT.js} +1 -1
- package/lib/public/assets/{check-DYSXRG79.js → check-1u_3Zsx5.js} +1 -1
- package/lib/public/assets/{chevron-right-hCNu0CA-.js → chevron-right-Cn2BixQX.js} +1 -1
- package/lib/public/assets/{circle-check-DnQ263fa.js → circle-check-OymVCeGU.js} +1 -1
- package/lib/public/assets/{circle-x-BaRKZyz9.js → circle-x-D3ZyFWOd.js} +1 -1
- package/lib/public/assets/{clock-XP3iBAhg.js → clock-DwzGN-aa.js} +1 -1
- package/lib/public/assets/{copy-D8d9UzWE.js → copy-De43WY6T.js} +1 -1
- package/lib/public/assets/{device-explorer-pdM3L9TC.js → device-explorer-CtvEmCC5.js} +5 -5
- package/lib/public/assets/{download-SaN9-umX.js → download-DcSdKSvR.js} +1 -1
- package/lib/public/assets/{forgot-password-DfUhEZW5.js → forgot-password-DDjM8J7Z.js} +1 -1
- package/lib/public/assets/{index-CzaIyL9t.js → index-Bqt1dmMA.js} +2 -2
- package/lib/public/assets/{index-IefGCFQQ.js → index-CSNs0EE4.js} +1 -1
- package/lib/public/assets/{index-C8pvxG1f.css → index-DQa193-A.css} +1 -1
- package/lib/public/assets/{input-BqFzAQT3.js → input-fRGFwm1K.js} +1 -1
- package/lib/public/assets/{keycodes-DDV2OlpQ.js → keycodes-Dj4m3JV7.js} +10 -5
- package/lib/public/assets/line-chart-DHSZ8Sm4.js +1 -0
- package/lib/public/assets/list-checks-Co1gqqDn.js +11 -0
- package/lib/public/assets/{lock-Eye1CG8p.js → lock-BzutP2Yf.js} +1 -1
- package/lib/public/assets/{login-Co1IXuen.js → login-DSVZLgW2.js} +1 -1
- package/lib/public/assets/{maintenance-settings-D1fw6iqm.js → maintenance-settings-PYFNp_tc.js} +1 -1
- package/lib/public/assets/{monitor-U5Y-PxW5.js → monitor-DdKVA3ze.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-CDAwphUm.js → mouse-pointer-2-C8UpXByt.js} +1 -1
- package/lib/public/assets/{network-BP_U4iDZ.js → network-BkzHrr2L.js} +1 -1
- package/lib/public/assets/overview-0YFo6f97.js +6 -0
- package/lib/public/assets/{page-header-ZdzNEJp9.js → page-header-DkmHEtLJ.js} +1 -1
- package/lib/public/assets/{plus-1k0sxXyU.js → plus-BH6Y7G7y.js} +1 -1
- package/lib/public/assets/{profile-page-B_GWKRy3.js → profile-page-C1IDBP4l.js} +1 -1
- package/lib/public/assets/{reset-password-DizCTkdw.js → reset-password-yNMsl5fZ.js} +1 -1
- package/lib/public/assets/{runbook-page-DWzRiOLZ.js → runbook-page--fH5nToN.js} +1 -1
- package/lib/public/assets/{select-CiRW6v17.js → select-DqjhZ6h2.js} +1 -1
- package/lib/public/assets/selector-detail-redirect-CrYIFrUj.js +1 -0
- package/lib/public/assets/selector-health-page-BnRUmiS2.js +11 -0
- package/lib/public/assets/session-detail-page-BNh_QfHv.js +4 -0
- package/lib/public/assets/{settings-DhwZoWv7.js → settings-BcxYFxnE.js} +1 -1
- package/lib/public/assets/stat-tile-DxVLBgA7.js +1 -0
- package/lib/public/assets/{tablet-CPJV3dBq.js → tablet-C_uKWfUK.js} +1 -1
- package/lib/public/assets/{teams-D0IBa2Ov.js → teams-C9G2WisD.js} +1 -1
- package/lib/public/assets/time-D-gjmE1o.js +1 -0
- package/lib/public/assets/{trash-2-u8MlXUBD.js → trash-2-C4cJO0CG.js} +1 -1
- package/lib/public/assets/{upload-Cq8L3kpw.js → upload-BN3PMMvP.js} +1 -1
- package/lib/public/assets/use-builds-data-CtelVuo_.js +8 -0
- package/lib/public/assets/{use-password-reset-mode-Dbr2HEfw.js → use-password-reset-mode-CClcmiRh.js} +1 -1
- package/lib/public/assets/{users-DxLHYmFm.js → users-BK7fwGGA.js} +1 -1
- package/lib/public/assets/{users-CZqCE511.js → users-OdnOb63Y.js} +1 -1
- package/lib/public/assets/{video-off-mh1EEzP9.js → video-off-DUAYoXTQ.js} +1 -1
- package/lib/public/assets/view-state-83Z1gFME.js +1 -0
- package/lib/public/assets/{webhook-settings-BYuOib-Q.js → webhook-settings-BRyvDWHI.js} +1 -1
- package/lib/public/assets/{zap-BOpUur0M.js → zap-BiceX7DC.js} +1 -1
- package/lib/public/index.html +2 -2
- package/lib/schema.json +5 -0
- package/lib/src/app/routers/dashboard.js +86 -40
- package/lib/src/app/routers/selector-health.js +75 -0
- package/lib/src/app/swagger-docs.js +77 -8
- package/lib/src/dashboard/event-manager.js +15 -99
- package/lib/src/generated/client/edge.js +27 -3
- package/lib/src/generated/client/index-browser.js +24 -0
- package/lib/src/generated/client/index.d.ts +8207 -5283
- package/lib/src/generated/client/index.js +27 -3
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +49 -9
- package/lib/src/generated/client/wasm.js +24 -0
- package/lib/src/prisma.js +2 -0
- package/lib/src/services/CleanupService.js +1 -0
- package/lib/src/services/NotificationService.js +3 -4
- package/lib/src/services/SelectorStateService.js +151 -113
- package/lib/src/services/SelectorVerificationJob.js +64 -14
- package/lib/src/services/metrics/AndroidMetricsSampler.js +94 -0
- package/lib/src/services/metrics/IOSMetricsSampler.js +120 -0
- package/lib/src/services/metrics/SessionMetricsService.js +238 -0
- package/lib/src/services/metrics/androidMetrics.js +94 -0
- package/lib/src/services/metrics/iosMetrics.js +26 -0
- package/lib/src/services/metrics/metricsBody.js +37 -0
- package/lib/src/services/metrics/types.js +19 -0
- package/lib/src/services/selector-health/access.js +66 -0
- package/lib/src/services/selector-health/healingTrend.js +49 -0
- package/lib/src/services/selector-health/people.js +27 -0
- package/lib/src/services/selector-health/selectorDetail.js +214 -0
- package/lib/src/services/selector-health/selectorKeys.js +36 -0
- package/lib/src/services/selector-health/selectorList.js +333 -0
- package/lib/src/services/selector-health/stateView.js +67 -0
- package/lib/test/helpers/scratch-database.js +19 -3
- package/lib/test/helpers/selector-health-fixture.js +216 -0
- package/lib/test/integration/team-visibility-sessions.spec.js +1 -0
- package/lib/test/unit/CleanupService.spec.js +28 -2
- package/lib/test/unit/dashboard-events-metrics.spec.js +146 -0
- package/lib/test/unit/healing-no-cost.spec.js +134 -0
- package/lib/test/unit/healing-state-endpoints.spec.js +70 -0
- package/lib/test/unit/healing-trend.spec.js +118 -0
- package/lib/test/unit/selector-event-model.spec.js +47 -0
- package/lib/test/unit/selector-health-actions.spec.js +106 -0
- package/lib/test/unit/selector-health-detail.spec.js +188 -0
- package/lib/test/unit/selector-health-list.spec.js +185 -0
- package/lib/test/unit/selector-state-service.spec.js +74 -1
- package/lib/test/unit/selector-verification-job.spec.js +41 -0
- package/lib/test/unit/selector-verification-missed-regression.spec.js +132 -0
- package/lib/test/unit/session-log-indexes.spec.js +147 -0
- package/lib/test/unit/session-metrics-parse.spec.js +138 -0
- package/lib/test/unit/session-metrics-route.spec.js +140 -0
- package/lib/test/unit/session-metrics-samplers.spec.js +248 -0
- package/lib/test/unit/session-metrics-service.spec.js +215 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/prisma/migrations/20261002120000_session_metric/migration.sql +17 -0
- package/prisma/migrations/20261002130000_selector_event/migration.sql +13 -0
- package/prisma/migrations/20261002180000_session_log_indexes/migration.sql +5 -0
- package/prisma/migrations/20261002190000_log_profiling_session_indexes/migration.sql +5 -0
- package/prisma/schema.prisma +49 -9
- package/schema.json +5 -0
- package/lib/public/assets/chevron-left-BSG6mcpo.js +0 -6
- package/lib/public/assets/funnel-OefHE-54.js +0 -6
- package/lib/public/assets/layers-a3GNz2DA.js +0 -6
- package/lib/public/assets/list-checks-DMrMAFDt.js +0 -6
- package/lib/public/assets/overview-CsPyPv10.js +0 -1
- package/lib/public/assets/selector-detail-page-pA913STP.js +0 -1
- package/lib/public/assets/selector-health-BlICzZlk.css +0 -1
- package/lib/public/assets/selector-health-CHLX8KqY.js +0 -16
- package/lib/public/assets/selector-health-page-vVkg8Mii.js +0 -16
- package/lib/public/assets/session-detail-page-BCD49zDi.js +0 -4
- package/lib/public/assets/stat-tile-BaE76cnK.js +0 -8
- package/lib/public/assets/time-B8FIgwPE.js +0 -1
- package/lib/src/profiling/AndroidAppProfiler.js +0 -213
- /package/lib/public/assets/{session-detail-page-Bzzc3bMl.css → line-chart-Bzzc3bMl.css} +0 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
const chai_1 = require("chai");
|
|
16
|
+
const events_1 = require("events");
|
|
17
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
18
|
+
const AndroidMetricsSampler_1 = require("../../src/services/metrics/AndroidMetricsSampler");
|
|
19
|
+
const IOSMetricsSampler_1 = require("../../src/services/metrics/IOSMetricsSampler");
|
|
20
|
+
const types_1 = require("../../src/services/metrics/types");
|
|
21
|
+
const reading = (jiffies, idle, app) => [
|
|
22
|
+
`cpu ${jiffies - idle} 0 0 ${idle} 0 0 0 0 0 0`,
|
|
23
|
+
'MemTotal: 5755748 kB',
|
|
24
|
+
'MemAvailable: 2839168 kB',
|
|
25
|
+
'pid=1404',
|
|
26
|
+
`stat=${app} 0`,
|
|
27
|
+
'VmRSS:\t 334480 kB',
|
|
28
|
+
].join('\n');
|
|
29
|
+
describe('session metrics: AndroidMetricsSampler', () => {
|
|
30
|
+
let clock;
|
|
31
|
+
let samples;
|
|
32
|
+
let gaveUp;
|
|
33
|
+
const hooks = () => ({
|
|
34
|
+
onSample: (s) => samples.push(s),
|
|
35
|
+
onGiveUp: (r) => gaveUp.push(r),
|
|
36
|
+
});
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
clock = sinon_1.default.useFakeTimers({ now: 1000000 });
|
|
39
|
+
samples = [];
|
|
40
|
+
gaveUp = [];
|
|
41
|
+
});
|
|
42
|
+
afterEach(() => clock.restore());
|
|
43
|
+
it("samples the session's app every 2 s, with CPU from the second sample", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
44
|
+
const commands = [];
|
|
45
|
+
let n = 0;
|
|
46
|
+
const s = new AndroidMetricsSampler_1.AndroidMetricsSampler({
|
|
47
|
+
appPackage: 'com.acme.shop',
|
|
48
|
+
hooks: hooks(),
|
|
49
|
+
shell: (c) => __awaiter(void 0, void 0, void 0, function* () {
|
|
50
|
+
commands.push(c);
|
|
51
|
+
n += 1;
|
|
52
|
+
return reading(1000 * n, 800 * n, 50 * n);
|
|
53
|
+
}),
|
|
54
|
+
});
|
|
55
|
+
s.start();
|
|
56
|
+
yield clock.tickAsync(0);
|
|
57
|
+
yield clock.tickAsync(types_1.SAMPLE_INTERVAL_MS);
|
|
58
|
+
yield s.stop();
|
|
59
|
+
(0, chai_1.expect)(commands).to.have.length(2);
|
|
60
|
+
(0, chai_1.expect)(commands.every((c) => c.includes('pidof com.acme.shop'))).to.equal(true);
|
|
61
|
+
(0, chai_1.expect)(samples.map((x) => x.deviceCpuPct)).to.deep.equal([null, 20]);
|
|
62
|
+
(0, chai_1.expect)(samples.map((x) => x.appCpuPct)).to.deep.equal([null, 5]);
|
|
63
|
+
(0, chai_1.expect)(samples[1].appId).to.equal('com.acme.shop');
|
|
64
|
+
}));
|
|
65
|
+
it('with no appPackage, samples the foreground app, looked up once per 10 s', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
66
|
+
const commands = [];
|
|
67
|
+
const s = new AndroidMetricsSampler_1.AndroidMetricsSampler({
|
|
68
|
+
appPackage: undefined,
|
|
69
|
+
hooks: hooks(),
|
|
70
|
+
shell: (c) => __awaiter(void 0, void 0, void 0, function* () {
|
|
71
|
+
commands.push(c);
|
|
72
|
+
return c.startsWith('dumpsys')
|
|
73
|
+
? ' topResumedActivity=ActivityRecord{a1 u0 com.acme.shop/.Main t3}'
|
|
74
|
+
: reading(1000, 800, 50);
|
|
75
|
+
}),
|
|
76
|
+
});
|
|
77
|
+
s.start();
|
|
78
|
+
yield clock.tickAsync(0);
|
|
79
|
+
yield clock.tickAsync(types_1.SAMPLE_INTERVAL_MS);
|
|
80
|
+
yield clock.tickAsync(types_1.FOREGROUND_REFRESH_MS);
|
|
81
|
+
yield s.stop();
|
|
82
|
+
const lookups = commands.filter((c) => c.startsWith('dumpsys'));
|
|
83
|
+
(0, chai_1.expect)(lookups).to.have.length(2);
|
|
84
|
+
(0, chai_1.expect)(commands.filter((c) => c.includes('pidof com.acme.shop')).length).to.be.greaterThan(2);
|
|
85
|
+
}));
|
|
86
|
+
it("never puts an appPackage that isn't a package name into a command", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
87
|
+
const commands = [];
|
|
88
|
+
const s = new AndroidMetricsSampler_1.AndroidMetricsSampler({
|
|
89
|
+
appPackage: 'com.acme;reboot',
|
|
90
|
+
hooks: hooks(),
|
|
91
|
+
shell: (c) => __awaiter(void 0, void 0, void 0, function* () {
|
|
92
|
+
commands.push(c);
|
|
93
|
+
return c.startsWith('dumpsys') ? '' : reading(1000, 800, 50);
|
|
94
|
+
}),
|
|
95
|
+
});
|
|
96
|
+
s.start();
|
|
97
|
+
yield clock.tickAsync(0);
|
|
98
|
+
yield s.stop();
|
|
99
|
+
(0, chai_1.expect)(commands.some((c) => c.includes('reboot'))).to.equal(false);
|
|
100
|
+
(0, chai_1.expect)(samples[0].appId).to.equal(null);
|
|
101
|
+
}));
|
|
102
|
+
it('stops itself after 5 failures in a row, and says so once', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
103
|
+
let calls = 0;
|
|
104
|
+
const s = new AndroidMetricsSampler_1.AndroidMetricsSampler({
|
|
105
|
+
appPackage: 'com.acme.shop',
|
|
106
|
+
hooks: hooks(),
|
|
107
|
+
shell: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
108
|
+
calls += 1;
|
|
109
|
+
throw new Error('device offline');
|
|
110
|
+
}),
|
|
111
|
+
});
|
|
112
|
+
s.start();
|
|
113
|
+
yield clock.tickAsync(0);
|
|
114
|
+
yield clock.tickAsync(types_1.SAMPLE_INTERVAL_MS * (types_1.MAX_CONSECUTIVE_FAILURES + 3));
|
|
115
|
+
(0, chai_1.expect)(calls).to.equal(types_1.MAX_CONSECUTIVE_FAILURES);
|
|
116
|
+
(0, chai_1.expect)(gaveUp).to.deep.equal(['device offline']);
|
|
117
|
+
}));
|
|
118
|
+
it('starts counting failures again after a good sample', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
119
|
+
let n = 0;
|
|
120
|
+
const s = new AndroidMetricsSampler_1.AndroidMetricsSampler({
|
|
121
|
+
appPackage: 'com.acme.shop',
|
|
122
|
+
hooks: hooks(),
|
|
123
|
+
shell: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
124
|
+
n += 1;
|
|
125
|
+
if (n % 4 === 0)
|
|
126
|
+
return reading(1000 * n, 800 * n, 50 * n);
|
|
127
|
+
throw new Error('flaky');
|
|
128
|
+
}),
|
|
129
|
+
});
|
|
130
|
+
s.start();
|
|
131
|
+
yield clock.tickAsync(0);
|
|
132
|
+
yield clock.tickAsync(types_1.SAMPLE_INTERVAL_MS * 12);
|
|
133
|
+
yield s.stop();
|
|
134
|
+
(0, chai_1.expect)(gaveUp).to.deep.equal([]);
|
|
135
|
+
(0, chai_1.expect)(samples.length).to.equal(3);
|
|
136
|
+
}));
|
|
137
|
+
it('reports nothing that was in flight when it stopped', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
138
|
+
let release = () => undefined;
|
|
139
|
+
const s = new AndroidMetricsSampler_1.AndroidMetricsSampler({
|
|
140
|
+
appPackage: 'com.acme.shop',
|
|
141
|
+
hooks: hooks(),
|
|
142
|
+
shell: () => new Promise((r) => (release = r)),
|
|
143
|
+
});
|
|
144
|
+
s.start();
|
|
145
|
+
yield clock.tickAsync(0);
|
|
146
|
+
yield s.stop();
|
|
147
|
+
release(reading(1000, 800, 50));
|
|
148
|
+
yield clock.tickAsync(types_1.SAMPLE_INTERVAL_MS * 3);
|
|
149
|
+
(0, chai_1.expect)(samples).to.deep.equal([]);
|
|
150
|
+
}));
|
|
151
|
+
});
|
|
152
|
+
class FakeChild extends events_1.EventEmitter {
|
|
153
|
+
constructor() {
|
|
154
|
+
super(...arguments);
|
|
155
|
+
this.stdout = new events_1.EventEmitter();
|
|
156
|
+
this.stderr = new events_1.EventEmitter();
|
|
157
|
+
this.pid = 4321;
|
|
158
|
+
this.killed = [];
|
|
159
|
+
}
|
|
160
|
+
kill(sig) {
|
|
161
|
+
this.killed.push(sig);
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
describe('session metrics: IOSMetricsSampler', () => {
|
|
166
|
+
let clock;
|
|
167
|
+
let samples;
|
|
168
|
+
let gaveUp;
|
|
169
|
+
let children;
|
|
170
|
+
let envs;
|
|
171
|
+
let borrows;
|
|
172
|
+
const line = (load) => JSON.stringify({ msg: 'received CPU usage data', enabled_cpus: 6, cpu_total_load: load }) +
|
|
173
|
+
'\n';
|
|
174
|
+
function sampler() {
|
|
175
|
+
return new IOSMetricsSampler_1.IOSMetricsSampler({
|
|
176
|
+
udid: 'iphone-1',
|
|
177
|
+
tunnels: {
|
|
178
|
+
borrow: () => __awaiter(this, void 0, void 0, function* () {
|
|
179
|
+
borrows += 1;
|
|
180
|
+
return 12100;
|
|
181
|
+
}),
|
|
182
|
+
envFor: () => ({ GO_IOS_AGENT_PORT: '12100' }),
|
|
183
|
+
},
|
|
184
|
+
spawnSysmontap: (env) => {
|
|
185
|
+
envs.push(env);
|
|
186
|
+
const c = new FakeChild();
|
|
187
|
+
children.push(c);
|
|
188
|
+
return c;
|
|
189
|
+
},
|
|
190
|
+
hooks: { onSample: (s) => samples.push(s), onGiveUp: (r) => gaveUp.push(r) },
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
beforeEach(() => {
|
|
194
|
+
clock = sinon_1.default.useFakeTimers({ now: 1000000 });
|
|
195
|
+
samples = [];
|
|
196
|
+
gaveUp = [];
|
|
197
|
+
children = [];
|
|
198
|
+
envs = [];
|
|
199
|
+
borrows = 0;
|
|
200
|
+
});
|
|
201
|
+
afterEach(() => clock.restore());
|
|
202
|
+
it("runs sysmontap through the phone's tunnel and reports device CPU every 2 s", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
203
|
+
const s = sampler();
|
|
204
|
+
s.start();
|
|
205
|
+
yield clock.tickAsync(0);
|
|
206
|
+
(0, chai_1.expect)(envs).to.deep.equal([{ GO_IOS_AGENT_PORT: '12100' }]);
|
|
207
|
+
children[0].stderr.emit('data', line(60) + line(120));
|
|
208
|
+
yield clock.tickAsync(types_1.SAMPLE_INTERVAL_MS);
|
|
209
|
+
yield s.stop();
|
|
210
|
+
(0, chai_1.expect)(samples).to.have.length(1);
|
|
211
|
+
(0, chai_1.expect)(samples[0]).to.include({
|
|
212
|
+
deviceCpuPct: 20,
|
|
213
|
+
deviceMemMb: null,
|
|
214
|
+
appCpuPct: null,
|
|
215
|
+
appMemMb: null,
|
|
216
|
+
appId: null,
|
|
217
|
+
});
|
|
218
|
+
(0, chai_1.expect)(children[0].killed).to.deep.equal(['SIGTERM']);
|
|
219
|
+
}));
|
|
220
|
+
it('reports nothing when no reading came for 4 s', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
221
|
+
const s = sampler();
|
|
222
|
+
s.start();
|
|
223
|
+
yield clock.tickAsync(0);
|
|
224
|
+
children[0].stdout.emit('data', line(60));
|
|
225
|
+
yield clock.tickAsync(types_1.SAMPLE_INTERVAL_MS * 4);
|
|
226
|
+
yield s.stop();
|
|
227
|
+
(0, chai_1.expect)(samples.length).to.be.lessThan(3);
|
|
228
|
+
}));
|
|
229
|
+
it('asks for the tunnel again every 30 s', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
230
|
+
const s = sampler();
|
|
231
|
+
s.start();
|
|
232
|
+
yield clock.tickAsync(0);
|
|
233
|
+
yield clock.tickAsync(types_1.TUNNEL_RENEW_MS * 2);
|
|
234
|
+
yield s.stop();
|
|
235
|
+
(0, chai_1.expect)(borrows).to.equal(3);
|
|
236
|
+
}));
|
|
237
|
+
it('starts sysmontap again when it exits, and gives up after 5 exits with no reading', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
238
|
+
const s = sampler();
|
|
239
|
+
s.start();
|
|
240
|
+
for (let i = 0; i < types_1.MAX_CONSECUTIVE_FAILURES; i += 1) {
|
|
241
|
+
yield clock.tickAsync(0);
|
|
242
|
+
children[children.length - 1].emit('exit', 1);
|
|
243
|
+
yield clock.tickAsync(30000);
|
|
244
|
+
}
|
|
245
|
+
(0, chai_1.expect)(children).to.have.length(types_1.MAX_CONSECUTIVE_FAILURES);
|
|
246
|
+
(0, chai_1.expect)(gaveUp).to.deep.equal(['ios sysmontap kept exiting']);
|
|
247
|
+
}));
|
|
248
|
+
});
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
require("reflect-metadata");
|
|
16
|
+
const chai_1 = require("chai");
|
|
17
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
18
|
+
const SessionMetricsService_1 = require("../../src/services/metrics/SessionMetricsService");
|
|
19
|
+
const types_1 = require("../../src/services/metrics/types");
|
|
20
|
+
class FakeSampler {
|
|
21
|
+
constructor(hooks) {
|
|
22
|
+
this.hooks = hooks;
|
|
23
|
+
this.started = false;
|
|
24
|
+
this.stopped = false;
|
|
25
|
+
}
|
|
26
|
+
start() {
|
|
27
|
+
this.started = true;
|
|
28
|
+
}
|
|
29
|
+
stop() {
|
|
30
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
31
|
+
this.stopped = true;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
class TestMetrics extends SessionMetricsService_1.SessionMetricsService {
|
|
36
|
+
constructor() {
|
|
37
|
+
super(...arguments);
|
|
38
|
+
this.args = {};
|
|
39
|
+
this.written = [];
|
|
40
|
+
this.failWrites = 0;
|
|
41
|
+
this.samplers = [];
|
|
42
|
+
this.caps = [];
|
|
43
|
+
}
|
|
44
|
+
context() {
|
|
45
|
+
return {
|
|
46
|
+
pluginArgs: Object.assign({ bindHostOrIp: '127.0.0.1' }, this.args),
|
|
47
|
+
port: 4723,
|
|
48
|
+
nodeId: 'node-1',
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
writeSamples(sessionId, samples) {
|
|
52
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
53
|
+
if (this.failWrites > 0) {
|
|
54
|
+
this.failWrites -= 1;
|
|
55
|
+
throw new Error('FOREIGN KEY constraint failed');
|
|
56
|
+
}
|
|
57
|
+
this.written.push({ sessionId, ats: samples.map((s) => s.at) });
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
samplerFor(_id, _d, caps, hooks) {
|
|
61
|
+
this.caps.push(caps);
|
|
62
|
+
const f = new FakeSampler(hooks);
|
|
63
|
+
this.samplers.push(f);
|
|
64
|
+
return f;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const phone = (over = {}) => (Object.assign({ udid: 'phone-1', platform: 'android', realDevice: true, host: 'http://127.0.0.1:4723', nodeId: 'node-1' }, over));
|
|
68
|
+
const sample = (at) => ({
|
|
69
|
+
at,
|
|
70
|
+
deviceCpuPct: 10,
|
|
71
|
+
deviceMemMb: 1,
|
|
72
|
+
deviceMemTotalMb: 2,
|
|
73
|
+
appCpuPct: null,
|
|
74
|
+
appMemMb: null,
|
|
75
|
+
appId: null,
|
|
76
|
+
});
|
|
77
|
+
describe('SessionMetricsService', () => {
|
|
78
|
+
let clock;
|
|
79
|
+
let m;
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
clock = sinon_1.default.useFakeTimers({ now: 1000000 });
|
|
82
|
+
m = new TestMetrics();
|
|
83
|
+
});
|
|
84
|
+
afterEach(() => clock.restore());
|
|
85
|
+
it("samples this server's Android phones (emulators too) and iPhones, not simulators", () => {
|
|
86
|
+
(0, chai_1.expect)(m.appliesTo(phone())).to.equal(true);
|
|
87
|
+
(0, chai_1.expect)(m.appliesTo(phone({ realDevice: false }))).to.equal(true);
|
|
88
|
+
(0, chai_1.expect)(m.appliesTo(phone({ platform: 'ios' }))).to.equal(true);
|
|
89
|
+
(0, chai_1.expect)(m.appliesTo(phone({ platform: 'ios', realDevice: false }))).to.equal(false);
|
|
90
|
+
(0, chai_1.expect)(m.appliesTo(undefined)).to.equal(false);
|
|
91
|
+
});
|
|
92
|
+
it("doesn't sample another server's phone, or anything when sessionMetrics is off", () => {
|
|
93
|
+
(0, chai_1.expect)(m.appliesTo(phone({ nodeId: 'node-2' }))).to.equal(false);
|
|
94
|
+
m.args = { sessionMetrics: false };
|
|
95
|
+
(0, chai_1.expect)(m.appliesTo(phone())).to.equal(false);
|
|
96
|
+
});
|
|
97
|
+
it('writes the samples every 10 s, and the rest when the session stops', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
98
|
+
m.start({ sessionId: 's1', device: phone(), capabilities: { 'appium:appPackage': 'com.a.b' } });
|
|
99
|
+
const hooks = m.samplers[0].hooks;
|
|
100
|
+
(0, chai_1.expect)(m.samplers[0].started).to.equal(true);
|
|
101
|
+
(0, chai_1.expect)(m.caps).to.deep.equal([{ 'appium:appPackage': 'com.a.b' }]);
|
|
102
|
+
[1, 2, 3].forEach((t) => hooks.onSample(sample(t)));
|
|
103
|
+
yield clock.tickAsync(types_1.FLUSH_INTERVAL_MS);
|
|
104
|
+
[4, 5].forEach((t) => hooks.onSample(sample(t)));
|
|
105
|
+
yield m.stop('s1');
|
|
106
|
+
(0, chai_1.expect)(m.written).to.deep.equal([
|
|
107
|
+
{ sessionId: 's1', ats: [1, 2, 3] },
|
|
108
|
+
{ sessionId: 's1', ats: [4, 5] },
|
|
109
|
+
]);
|
|
110
|
+
(0, chai_1.expect)(m.samplers[0].stopped).to.equal(true);
|
|
111
|
+
}));
|
|
112
|
+
it('keeps samples whose write failed, in order, for the next write', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
113
|
+
m.start({ sessionId: 's1', device: phone(), capabilities: {} });
|
|
114
|
+
const hooks = m.samplers[0].hooks;
|
|
115
|
+
m.failWrites = 1;
|
|
116
|
+
[1, 2].forEach((t) => hooks.onSample(sample(t)));
|
|
117
|
+
yield clock.tickAsync(types_1.FLUSH_INTERVAL_MS);
|
|
118
|
+
hooks.onSample(sample(3));
|
|
119
|
+
yield clock.tickAsync(types_1.FLUSH_INTERVAL_MS);
|
|
120
|
+
yield m.stop('s1');
|
|
121
|
+
(0, chai_1.expect)(m.written).to.deep.equal([{ sessionId: 's1', ats: [1, 2, 3] }]);
|
|
122
|
+
}));
|
|
123
|
+
it('keeps only the newest samples while writes keep failing', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
124
|
+
m.start({ sessionId: 's1', device: phone(), capabilities: {} });
|
|
125
|
+
const hooks = m.samplers[0].hooks;
|
|
126
|
+
m.failWrites = 1;
|
|
127
|
+
for (let t = 1; t <= types_1.MAX_BUFFERED_SAMPLES + 5; t += 1)
|
|
128
|
+
hooks.onSample(sample(t));
|
|
129
|
+
yield clock.tickAsync(types_1.FLUSH_INTERVAL_MS);
|
|
130
|
+
yield m.stop('s1');
|
|
131
|
+
const ats = m.written[0].ats;
|
|
132
|
+
(0, chai_1.expect)(ats).to.have.length(types_1.MAX_BUFFERED_SAMPLES);
|
|
133
|
+
(0, chai_1.expect)(ats[0]).to.equal(6);
|
|
134
|
+
(0, chai_1.expect)(ats[ats.length - 1]).to.equal(types_1.MAX_BUFFERED_SAMPLES + 5);
|
|
135
|
+
}));
|
|
136
|
+
it('drops a sample that arrives after the session stopped', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
137
|
+
m.start({ sessionId: 's1', device: phone(), capabilities: {} });
|
|
138
|
+
const hooks = m.samplers[0].hooks;
|
|
139
|
+
yield m.stop('s1');
|
|
140
|
+
hooks.onSample(sample(9));
|
|
141
|
+
yield clock.tickAsync(types_1.FLUSH_INTERVAL_MS * 2);
|
|
142
|
+
(0, chai_1.expect)(m.written).to.deep.equal([]);
|
|
143
|
+
}));
|
|
144
|
+
it('stops twice, or a session it never sampled, without complaint', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
145
|
+
m.start({ sessionId: 's1', device: phone(), capabilities: {} });
|
|
146
|
+
yield m.stop('s1');
|
|
147
|
+
yield m.stop('s1');
|
|
148
|
+
yield m.stop('never-started');
|
|
149
|
+
(0, chai_1.expect)(m.samplers).to.have.length(1);
|
|
150
|
+
}));
|
|
151
|
+
it('says whether it is sampling a session, stopped after giving up, or not at all', () => {
|
|
152
|
+
(0, chai_1.expect)(m.recordingState('s1')).to.equal('off');
|
|
153
|
+
m.start({ sessionId: 's1', device: phone(), capabilities: {} });
|
|
154
|
+
(0, chai_1.expect)(m.recordingState('s1')).to.equal('sampling');
|
|
155
|
+
m.samplers[0].hooks.onGiveUp('device offline');
|
|
156
|
+
(0, chai_1.expect)(m.recordingState('s1')).to.equal('stopped');
|
|
157
|
+
});
|
|
158
|
+
it("doesn't start a sampler for a phone it doesn't apply to", () => {
|
|
159
|
+
m.start({ sessionId: 's2', device: phone({ nodeId: 'node-2' }), capabilities: {} });
|
|
160
|
+
(0, chai_1.expect)(m.samplers).to.deep.equal([]);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
/** The real Android wiring, down to the adb process it runs. */
|
|
164
|
+
class WiredMetrics extends SessionMetricsService_1.SessionMetricsService {
|
|
165
|
+
constructor() {
|
|
166
|
+
super(...arguments);
|
|
167
|
+
this.calls = [];
|
|
168
|
+
}
|
|
169
|
+
context() {
|
|
170
|
+
return { pluginArgs: { bindHostOrIp: '127.0.0.1' }, port: 4723, nodeId: 'node-1' };
|
|
171
|
+
}
|
|
172
|
+
writeSamples() {
|
|
173
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
174
|
+
// nothing stored
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
adbCommand(udid) {
|
|
178
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
179
|
+
return { path: '/sdk/platform-tools/adb', base: ['-P', '5037', '-s', udid] };
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
execAdb(path, args) {
|
|
183
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
184
|
+
this.calls.push([path, ...args]);
|
|
185
|
+
return args.includes('dumpsys activity activities')
|
|
186
|
+
? ''
|
|
187
|
+
: 'cpu 100 0 0 50 0 0 0 0 0 0\nMemTotal: 1024 kB\nMemAvailable: 512 kB\n';
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
describe("SessionMetricsService: the Android sampler's adb calls", () => {
|
|
192
|
+
let clock;
|
|
193
|
+
beforeEach(() => {
|
|
194
|
+
clock = sinon_1.default.useFakeTimers({ now: 1000000 });
|
|
195
|
+
});
|
|
196
|
+
afterEach(() => clock.restore());
|
|
197
|
+
it('names the phone in every adb call, so a server with several phones reaches the right one', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
198
|
+
const m = new WiredMetrics();
|
|
199
|
+
m.start({ sessionId: 's-adb', device: phone(), capabilities: {} });
|
|
200
|
+
yield clock.tickAsync(0);
|
|
201
|
+
yield clock.tickAsync(types_1.SAMPLE_INTERVAL_MS);
|
|
202
|
+
yield m.stop('s-adb');
|
|
203
|
+
(0, chai_1.expect)(m.calls.length).to.be.greaterThan(1);
|
|
204
|
+
for (const call of m.calls) {
|
|
205
|
+
(0, chai_1.expect)(call.slice(0, 6)).to.deep.equal([
|
|
206
|
+
'/sdk/platform-tools/adb',
|
|
207
|
+
'-P',
|
|
208
|
+
'5037',
|
|
209
|
+
'-s',
|
|
210
|
+
'phone-1',
|
|
211
|
+
'shell',
|
|
212
|
+
]);
|
|
213
|
+
}
|
|
214
|
+
}));
|
|
215
|
+
});
|